From: Mike Bayer Date: Wed, 22 Mar 2023 15:56:04 +0000 (-0400) Subject: use clone, not constructor, in BindParameter.render_literal_execute() X-Git-Tag: rel_2_0_8~19^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=d856f640b5803d52fa702a750e504990e80d8724;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git use clone, not constructor, in BindParameter.render_literal_execute() Fixed issue where the :meth:`_sql.BindParameter.render_literal_execute` method would fail when called on a parameter that also had ORM annotations associated with it. In practice, this would be observed as a failure of SQL compilation when using some combinations of a dialect that uses "FETCH FIRST" such as Oracle along with a :class:`_sql.Select` construct that uses :meth:`_sql.Select.limit`, within some ORM contexts, including if the statement were embedded within a relationship primaryjoin expression. Fixes: #9526 Change-Id: I2f512b6760a90293d274e60b06a891f10b276ecc --- diff --git a/doc/build/changelog/unreleased_20/9526.rst b/doc/build/changelog/unreleased_20/9526.rst new file mode 100644 index 0000000000..bd714eb6dd --- /dev/null +++ b/doc/build/changelog/unreleased_20/9526.rst @@ -0,0 +1,12 @@ +.. change:: + :tags: bug, orm + :tickets: 9526 + + Fixed issue where the :meth:`_sql.BindParameter.render_literal_execute` + method would fail when called on a parameter that also had ORM annotations + associated with it. In practice, this would be observed as a failure of SQL + compilation when using some combinations of a dialect that uses "FETCH + FIRST" such as Oracle along with a :class:`_sql.Select` construct that uses + :meth:`_sql.Select.limit`, within some ORM contexts, including if the + statement were embedded within a relationship primaryjoin expression. + diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index ef4587e187..8bf834d117 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -2057,12 +2057,9 @@ class BindParameter(roles.InElementRole, KeyedColumnElement[_T]): :ref:`engine_thirdparty_caching` """ - return self.__class__( - self.key, - self.value, - type_=self.type, - literal_execute=True, - ) + c = ClauseElement._clone(self) + c.literal_execute = True + return c def _negate_in_binary(self, negated_op, original_op): if self.expand_op is original_op: diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index c66ba71c3f..a51d831a90 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -498,6 +498,7 @@ class AssertsCompiledSQL: default_schema_name=None, from_linting=False, check_param_order=True, + use_literal_execute_for_simple_int=False, ): if use_default_dialect: dialect = default.DefaultDialect() @@ -541,6 +542,9 @@ class AssertsCompiledSQL: if render_postcompile: compile_kwargs["render_postcompile"] = True + if use_literal_execute_for_simple_int: + compile_kwargs["use_literal_execute_for_simple_int"] = True + if for_executemany: kw["for_executemany"] = True diff --git a/test/sql/test_external_traversal.py b/test/sql/test_external_traversal.py index b8f6e5685a..e474e75d75 100644 --- a/test/sql/test_external_traversal.py +++ b/test/sql/test_external_traversal.py @@ -33,6 +33,7 @@ from sqlalchemy.sql import util as sql_util from sqlalchemy.sql import visitors from sqlalchemy.sql.elements import _clone from sqlalchemy.sql.expression import _from_objects +from sqlalchemy.sql.util import _deep_annotate from sqlalchemy.sql.visitors import ClauseVisitor from sqlalchemy.sql.visitors import cloned_traverse from sqlalchemy.sql.visitors import CloningVisitor @@ -508,6 +509,71 @@ class ClauseTest(fixtures.TestBase, AssertsCompiledSQL): self.assert_compile(adapted, expected) + @testing.variation("annotate", [True, False]) + def test_bindparam_render_literal_execute(self, annotate): + """test #9526""" + + bp = bindparam("some_value") + + if annotate: + bp = bp._annotate({"foo": "bar"}) + + bp = bp.render_literal_execute() + self.assert_compile( + column("q") == bp, "q = __[POSTCOMPILE_some_value]" + ) + + @testing.variation("limit_type", ["limit", "fetch"]) + @testing.variation("dialect", ["default", "oracle"]) + def test_annotated_fetch(self, limit_type: testing.Variation, dialect): + """test #9526""" + + if limit_type.limit: + stmt = select(column("q")).limit(1) + elif limit_type.fetch: + stmt = select(column("q")).fetch(1) + else: + limit_type.fail() + + stmt = _deep_annotate(stmt, {"foo": "bar"}) + + if limit_type.limit: + if dialect.default: + self.assert_compile( + stmt, + "SELECT q LIMIT :param_1", + use_literal_execute_for_simple_int=True, + dialect=dialect.name, + ) + elif dialect.oracle: + self.assert_compile( + stmt, + "SELECT q FROM DUAL FETCH FIRST " + "__[POSTCOMPILE_param_1] ROWS ONLY", + dialect=dialect.name, + ) + else: + dialect.fail() + elif limit_type.fetch: + if dialect.default: + self.assert_compile( + stmt, + "SELECT q FETCH FIRST __[POSTCOMPILE_param_1] ROWS ONLY", + use_literal_execute_for_simple_int=True, + dialect=dialect.name, + ) + elif dialect.oracle: + self.assert_compile( + stmt, + "SELECT q FROM DUAL FETCH FIRST " + "__[POSTCOMPILE_param_1] ROWS ONLY", + dialect=dialect.name, + ) + else: + dialect.fail() + else: + limit_type.fail() + @testing.combinations((null(),), (true(),)) def test_dont_adapt_singleton_elements(self, elem): """test :ticket:`6259`"""