From: Mike Bayer Date: Mon, 24 Oct 2022 23:24:11 +0000 (-0400) Subject: add Oracle-specific parameter escapes for expanding params X-Git-Tag: rel_2_0_0b3~30^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b1cd6e4295b07e01983deb2845f6e22a059f5b76;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git add Oracle-specific parameter escapes for expanding params Fixed issue where bound parameter names, including those automatically derived from similarly-named database columns, which contained characters that normally require quoting with Oracle would not be escaped when using "expanding parameters" with the Oracle dialect, causing execution errors. The usual "quoting" for bound parameters used by the Oracle dialect is not used with the "expanding parameters" architecture, so escaping for a large range of characters is used instead, now using a list of characters/escapes that are specific to Oracle. Fixes: #8708 Change-Id: I90c24e48534e1b3a4c222b3022da58159784d91a --- diff --git a/doc/build/changelog/unreleased_14/8708.rst b/doc/build/changelog/unreleased_14/8708.rst new file mode 100644 index 0000000000..bb7424faaf --- /dev/null +++ b/doc/build/changelog/unreleased_14/8708.rst @@ -0,0 +1,14 @@ +.. change:: + :tags: bug, oracle + :tickets: 8708 + + Fixed issue where bound parameter names, including those automatically + derived from similarly-named database columns, which contained characters + that normally require quoting with Oracle would not be escaped when using + "expanding parameters" with the Oracle dialect, causing execution errors. + The usual "quoting" for bound parameters used by the Oracle dialect is not + used with the "expanding parameters" architecture, so escaping for a large + range of characters is used instead, now using a list of characters/escapes + that are specific to Oracle. + + diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py index 4571f51f7b..24262c1819 100644 --- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py @@ -445,6 +445,15 @@ from ...sql._typing import is_sql_compiler _CX_ORACLE_MAGIC_LOB_SIZE = 131072 +_ORACLE_BIND_TRANSLATE_RE = re.compile(r"[%\(\):\[\]\.\/\?]") + +# Oracle bind names can't start with digits or underscores. +# currently we rely upon Oracle-specific quoting of bind names in most cases. +# however for expanding params, the escape chars are used. +# see #8708 +_ORACLE_BIND_TRANSLATE_CHARS = dict(zip("%():[]./?", "PAZCCCCCCC")) + + class _OracleInteger(sqltypes.Integer): def get_dbapi_type(self, dbapi): # see https://github.com/oracle/python-cx_Oracle/issues/ @@ -693,6 +702,10 @@ class OracleCompiler_cx_oracle(OracleCompiler): quote is True or quote is not False and self.preparer._bindparam_requires_quotes(name) + # bind param quoting for Oracle doesn't work with post_compile + # params. For those, the default bindparam_string will escape + # special chars, and the appending of a number "_1" etc. will + # take care of reserved words and not kw.get("post_compile", False) ): # interesting to note about expanding parameters - since the @@ -703,6 +716,29 @@ class OracleCompiler_cx_oracle(OracleCompiler): quoted_name = '"%s"' % name kw["escaped_from"] = name name = quoted_name + return OracleCompiler.bindparam_string(self, name, **kw) + + # TODO: we could likely do away with quoting altogether for + # Oracle parameters and use the custom escaping here + escaped_from = kw.get("escaped_from", None) + if not escaped_from: + + if _ORACLE_BIND_TRANSLATE_RE.search(name): + # not quite the translate use case as we want to + # also get a quick boolean if we even found + # unusual characters in the name + new_name = _ORACLE_BIND_TRANSLATE_RE.sub( + lambda m: _ORACLE_BIND_TRANSLATE_CHARS[m.group(0)], + name, + ) + if new_name[0].isdigit(): + new_name = "D" + new_name + kw["escaped_from"] = name + name = new_name + elif name[0].isdigit(): + new_name = "D" + name + kw["escaped_from"] = name + name = new_name return OracleCompiler.bindparam_string(self, name, **kw) diff --git a/lib/sqlalchemy/testing/suite/test_dialect.py b/lib/sqlalchemy/testing/suite/test_dialect.py index efad81930e..33e395c480 100644 --- a/lib/sqlalchemy/testing/suite/test_dialect.py +++ b/lib/sqlalchemy/testing/suite/test_dialect.py @@ -379,6 +379,8 @@ class DifficultParametersTest(fixtures.TestBase): ("par(ens)",), ("percent%(ens)yah",), ("col:ons",), + ("_starts_with_underscore",), + ("dot.s",), ("more :: %colons%",), ("/slashes/",), ("more/slashes",), @@ -414,6 +416,13 @@ class DifficultParametersTest(fixtures.TestBase): # name works as the key from cursor.description eq_(row._mapping[name], "some name") + # use expanding IN + stmt = select(t.c[name]).where( + t.c[name].in_(["some name", "some other_name"]) + ) + + row = connection.execute(stmt).first() + class ReturningGuardsTest(fixtures.TablesTest): """test that the various 'returning' flags are set appropriately"""