From a697fcc1cb87b5a4e4f0c70361bd598086f4210f Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 6 Jan 2020 14:09:01 -0500 Subject: [PATCH] Support GenericFunction.name passed as a quoted_name A function created using :class:`.GenericFunction` can now specify that the name of the function should be rendered with or without quotes by assigning the :class:`.quoted_name` construct to the .name element of the object. Prior to 1.3.4, quoting was never applied to function names, and some quoting was introduced in :ticket:`4467` but no means to force quoting for a mixed case name was available. Additionally, the :class:`.quoted_name` construct when used as the name will properly register its lowercase name in the function registry so that the name continues to be available via the ``func.`` registry. Fixes: #5079 Change-Id: I0653ab8b16e75e628ce82dbbc3d0f77f8336c407 --- doc/build/changelog/unreleased_13/5079.rst | 18 +++++++++++++ lib/sqlalchemy/sql/compiler.py | 2 ++ lib/sqlalchemy/sql/functions.py | 30 +++++++++++++++++++--- test/sql/test_functions.py | 16 ++++++++++++ 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 doc/build/changelog/unreleased_13/5079.rst diff --git a/doc/build/changelog/unreleased_13/5079.rst b/doc/build/changelog/unreleased_13/5079.rst new file mode 100644 index 0000000000..5b1a59791a --- /dev/null +++ b/doc/build/changelog/unreleased_13/5079.rst @@ -0,0 +1,18 @@ +.. change:: + :tags: usecase, sql + :tickets: 5079 + + A function created using :class:`.GenericFunction` can now specify that the + name of the function should be rendered with or without quotes by assigning + the :class:`.quoted_name` construct to the .name element of the object. + Prior to 1.3.4, quoting was never applied to function names, and some + quoting was introduced in :ticket:`4467` but no means to force quoting for + a mixed case name was available. Additionally, the :class:`.quoted_name` + construct when used as the name will properly register its lowercase name + in the function registry so that the name continues to be available via the + ``func.`` registry. + + .. seealso:: + + :class:`.GenericFunction` + diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 5333b1419d..8499484f33 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1329,6 +1329,7 @@ class SQLCompiler(Compiled): name = ( self.preparer.quote(name) if self.preparer._requires_quotes_illegal_chars(name) + or isinstance(name, elements.quoted_name) else name ) name = name + "%(expr)s" @@ -1337,6 +1338,7 @@ class SQLCompiler(Compiled): ( self.preparer.quote(tok) if self.preparer._requires_quotes_illegal_chars(tok) + or isinstance(name, elements.quoted_name) else tok ) for tok in func.packagenames diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py index 18fe42aa4f..068fc68092 100644 --- a/lib/sqlalchemy/sql/functions.py +++ b/lib/sqlalchemy/sql/functions.py @@ -50,7 +50,8 @@ def register_function(identifier, fn, package="_default"): """ reg = _registry[package] - identifier = identifier.lower() + + identifier = util.text_type(identifier).lower() # Check if a function with the same identifier is registered. if identifier in reg: @@ -544,7 +545,6 @@ class _FunctionGenerator(object): if package is not None: func = _registry[package].get(fname.lower()) - if func is not None: return func(*c, **o) @@ -707,9 +707,33 @@ class GenericFunction(util.with_metaclass(_GenericMeta, Function)): The above function will render as follows:: - >>> print func.geo.buffer() + >>> print(func.geo.buffer()) ST_Buffer() + The name will be rendered as is, however without quoting unless the name + contains special characters that require quoting. To force quoting + on or off for the name, use the :class:`.sqlalchemy.sql.quoted_name` + construct:: + + from sqlalchemy.sql import quoted_name + + class GeoBuffer(GenericFunction): + type = Geometry + package = "geo" + name = quoted_name("ST_Buffer", True) + identifier = "buffer" + + The above function will render as:: + + >>> print(func.geo.buffer()) + "ST_Buffer"() + + .. versionadded:: 1.3.13 The :class:`.quoted_name` construct is now + recognized for quoting when used with the "name" attribute of the + object, so that quoting can be forced on or off for the function + name. + + """ coerce_arguments = True diff --git a/test/sql/test_functions.py b/test/sql/test_functions.py index a46d1af548..6ee8a67b79 100644 --- a/test/sql/test_functions.py +++ b/test/sql/test_functions.py @@ -29,6 +29,7 @@ from sqlalchemy.dialects import postgresql from sqlalchemy.dialects import sqlite from sqlalchemy.sql import column from sqlalchemy.sql import functions +from sqlalchemy.sql import quoted_name from sqlalchemy.sql import table from sqlalchemy.sql.compiler import BIND_TEMPLATES from sqlalchemy.sql.functions import FunctionElement @@ -303,6 +304,21 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL): assert isinstance(func.notmyfunc(), myfunc) assert not isinstance(func.myfunc(), myfunc) + def test_custom_w_quoted_name(self): + class myfunc(GenericFunction): + name = quoted_name("NotMyFunc", quote=True) + identifier = "myfunc" + + self.assert_compile(func.myfunc(), '"NotMyFunc"()') + + def test_custom_w_quoted_name_no_identifier(self): + class myfunc(GenericFunction): + name = quoted_name("NotMyFunc", quote=True) + + # note this requires that the quoted name be lower cased for + # correct lookup + self.assert_compile(func.notmyfunc(), '"NotMyFunc"()') + def test_custom_package_namespace(self): def cls1(pk_name): class myfunc(GenericFunction): -- 2.47.2