From 8a152ec391118a05ac54974d0f013cf0e99c7832 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 8 Dec 2022 11:00:31 -0500 Subject: [PATCH] fix construct_params() for render_postcompile; add new API The :meth:`.SQLCompiler.construct_params` method, as well as the :attr:`.SQLCompiler.params` accessor, will now return the exact parameters that correspond to a compiled statement that used the ``render_postcompile`` parameter to compile. Previously, the method returned a parameter structure that by itself didn't correspond to either the original parameters or the expanded ones. Passing a new dictionary of parameters to :meth:`.SQLCompiler.construct_params` for a :class:`.SQLCompiler` that was constructed with ``render_postcompile`` is now disallowed; instead, to make a new SQL string and parameter set for an alternate set of parameters, a new method :meth:`.SQLCompiler.construct_expanded_state` is added which will produce a new expanded form for the given parameter set, using the :class:`.ExpandedState` container which includes a new SQL statement and new parameter dictionary, as well as a positional parameter tuple. Fixes: #6114 Change-Id: I9874905bb90f86799b82b244d57369558b18fd93 --- doc/build/changelog/unreleased_20/6114.rst | 20 + doc/build/core/internals.rst | 3 + lib/sqlalchemy/sql/compiler.py | 177 +++++++-- lib/sqlalchemy/testing/assertions.py | 26 +- test/sql/test_compiler.py | 433 ++++++++++++++++++--- 5 files changed, 576 insertions(+), 83 deletions(-) create mode 100644 doc/build/changelog/unreleased_20/6114.rst diff --git a/doc/build/changelog/unreleased_20/6114.rst b/doc/build/changelog/unreleased_20/6114.rst new file mode 100644 index 0000000000..e773d92b17 --- /dev/null +++ b/doc/build/changelog/unreleased_20/6114.rst @@ -0,0 +1,20 @@ +.. change:: + :tags: bug, sql + :tickets: 6114 + + The :meth:`.SQLCompiler.construct_params` method, as well as the + :attr:`.SQLCompiler.params` accessor, will now return the + exact parameters that correspond to a compiled statement that used + the ``render_postcompile`` parameter to compile. Previously, + the method returned a parameter structure that by itself didn't correspond + to either the original parameters or the expanded ones. + + Passing a new dictionary of parameters to + :meth:`.SQLCompiler.construct_params` for a :class:`.SQLCompiler` that was + constructed with ``render_postcompile`` is now disallowed; instead, to make + a new SQL string and parameter set for an alternate set of parameters, a + new method :meth:`.SQLCompiler.construct_expanded_state` is added which + will produce a new expanded form for the given parameter set, using the + :class:`.ExpandedState` container which includes a new SQL statement + and new parameter dictionary, as well as a positional parameter tuple. + diff --git a/doc/build/core/internals.rst b/doc/build/core/internals.rst index 9a19ccc13a..e3769b3422 100644 --- a/doc/build/core/internals.rst +++ b/doc/build/core/internals.rst @@ -43,6 +43,9 @@ Some key internal constructs are listed here. .. autoclass:: sqlalchemy.engine.ExecutionContext :members: +.. autoclass:: sqlalchemy.sql.compiler.ExpandedState + :members: + .. autoclass:: sqlalchemy.sql.compiler.GenericTypeCompiler :members: diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index d7358ad3be..7aa89869e4 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -404,13 +404,53 @@ class ExpandedState(NamedTuple): will be rendered into the SQL statement at execution time, rather than being passed as separate parameters to the driver. + To create an :class:`.ExpandedState` instance, use the + :meth:`.SQLCompiler.construct_expanded_state` method on any + :class:`.SQLCompiler` instance. + """ statement: str - additional_parameters: _CoreSingleExecuteParams + """String SQL statement with parameters fully expanded""" + + parameters: _CoreSingleExecuteParams + """Parameter dictionary with parameters fully expanded. + + For a statement that uses named parameters, this dictionary will map + exactly to the names in the statement. For a statement that uses + positional parameters, the :attr:`.ExpandedState.positional_parameters` + will yield a tuple with the positional parameter set. + + """ + processors: Mapping[str, _BindProcessorType[Any]] + """mapping of bound value processors""" + positiontup: Optional[Sequence[str]] + """Sequence of string names indicating the order of positional + parameters""" + parameter_expansion: Mapping[str, List[str]] + """Mapping representing the intermediary link from original parameter + name to list of "expanded" parameter names, for those parameters that + were expanded.""" + + @property + def positional_parameters(self) -> Tuple[Any, ...]: + """Tuple of positional parameters, for statements that were compiled + using a positional paramstyle. + + """ + if self.positiontup is None: + raise exc.InvalidRequestError( + "statement does not use a positional paramstyle" + ) + return tuple(self.parameters[key] for key in self.positiontup) + + @property + def additional_parameters(self) -> _CoreSingleExecuteParams: + """synonym for :attr:`.ExpandedState.parameters`.""" + return self.parameters class _InsertManyValues(NamedTuple): @@ -956,8 +996,30 @@ class SQLCompiler(Compiled): """ whether to render out POSTCOMPILE params during the compile phase. + This attribute is used only for end-user invocation of stmt.compile(); + it's never used for actual statement execution, where instead the + dialect internals access and render the internal postcompile structure + directly. + + """ + + _post_compile_expanded_state: Optional[ExpandedState] = None + """When render_postcompile is used, the ``ExpandedState`` used to create + the "expanded" SQL is assigned here, and then used by the ``.params`` + accessor and ``.construct_params()`` methods for their return values. + + .. versionadded:: 2.0.0b5 + """ + _pre_expanded_string: Optional[str] = None + """Stores the original string SQL before 'post_compile' is applied, + for cases where 'post_compile' were used. + + """ + + _pre_expanded_positiontup: Optional[List[str]] = None + _insertmanyvalues: Optional[_InsertManyValues] = None _insert_crud_params: Optional[crud._CrudParamSequence] = None @@ -1164,7 +1226,14 @@ class SQLCompiler(Compiled): self._process_positional() if self._render_postcompile: - self._process_parameters_for_postcompile(_populate_self=True) + parameters = self.construct_params( + escape_names=False, + _no_postcompile=True, + ) + + self._process_parameters_for_postcompile( + parameters, _populate_self=True + ) @property def insert_single_values_expr(self) -> Optional[str]: @@ -1481,6 +1550,29 @@ class SQLCompiler(Compiled): def sql_compiler(self): return self + def construct_expanded_state( + self, + params: Optional[_CoreSingleExecuteParams] = None, + escape_names: bool = True, + ) -> ExpandedState: + """Return a new :class:`.ExpandedState` for a given parameter set. + + For queries that use "expanding" or other late-rendered parameters, + this method will provide for both the finalized SQL string as well + as the parameters that would be used for a particular parameter set. + + .. versionadded:: 2.0.0b5 + + """ + parameters = self.construct_params( + params, + escape_names=escape_names, + _no_postcompile=True, + ) + return self._process_parameters_for_postcompile( + parameters, + ) + def construct_params( self, params: Optional[_CoreSingleExecuteParams] = None, @@ -1488,12 +1580,26 @@ class SQLCompiler(Compiled): escape_names: bool = True, _group_number: Optional[int] = None, _check: bool = True, + _no_postcompile: bool = False, ) -> _MutableCoreSingleExecuteParams: """return a dictionary of bind parameter keys and values""" + if self._render_postcompile and not _no_postcompile: + assert self._post_compile_expanded_state is not None + if not params: + return dict(self._post_compile_expanded_state.parameters) + else: + raise exc.InvalidRequestError( + "can't construct new parameters when render_postcompile " + "is used; the statement is hard-linked to the original " + "parameters. Use construct_expanded_state to generate a " + "new statement and parameters." + ) + has_escaped_names = escape_names and bool(self.escaped_bind_names) if extracted_parameters: + # related the bound parameters collected in the original cache key # to those collected in the incoming cache key. They will not have # matching names but they will line up positionally in the same @@ -1520,6 +1626,7 @@ class SQLCompiler(Compiled): resolved_extracted = None if params: + pd = {} for bindparam, name in self.bind_names.items(): escaped_name = ( @@ -1593,6 +1700,7 @@ class SQLCompiler(Compiled): pd[escaped_name] = value_param.effective_value else: pd[escaped_name] = value_param.value + return pd @util.memoized_instancemethod @@ -1649,7 +1757,7 @@ class SQLCompiler(Compiled): def _process_parameters_for_postcompile( self, - parameters: Optional[_MutableCoreSingleExecuteParams] = None, + parameters: _MutableCoreSingleExecuteParams, _populate_self: bool = False, ) -> ExpandedState: """handle special post compile parameters. @@ -1665,16 +1773,22 @@ class SQLCompiler(Compiled): """ - if parameters is None: - parameters = self.construct_params(escape_names=False) - expanded_parameters = {} - positiontup: Optional[List[str]] + new_positiontup: Optional[List[str]] + + pre_expanded_string = self._pre_expanded_string + if pre_expanded_string is None: + pre_expanded_string = self.string if self.positional: - positiontup = [] + new_positiontup = [] + + pre_expanded_positiontup = self._pre_expanded_positiontup + if pre_expanded_positiontup is None: + pre_expanded_positiontup = self.positiontup + else: - positiontup = None + new_positiontup = pre_expanded_positiontup = None processors = self._bind_processors single_processors = cast( @@ -1698,8 +1812,8 @@ class SQLCompiler(Compiled): numeric_positiontup: Optional[List[str]] = None - if self.positional and self.positiontup is not None: - names: Iterable[str] = self.positiontup + if self.positional and pre_expanded_positiontup is not None: + names: Iterable[str] = pre_expanded_positiontup if self._numeric_binds: numeric_positiontup = [] else: @@ -1772,16 +1886,16 @@ class SQLCompiler(Compiled): numeric_positiontup.extend( name for name, _ in to_update ) - elif positiontup is not None: + elif new_positiontup is not None: # to_update has escaped names, but that's ok since # these are new names, that aren't in the # escaped_bind_names dict. - positiontup.extend(name for name, _ in to_update) + new_positiontup.extend(name for name, _ in to_update) expanded_parameters[name] = [ expand_key for expand_key, _ in to_update ] - elif positiontup is not None: - positiontup.append(name) + elif new_positiontup is not None: + new_positiontup.append(name) def process_expanding(m): key = m.group(1) @@ -1799,11 +1913,11 @@ class SQLCompiler(Compiled): return expr statement = re.sub( - self._post_compile_pattern, process_expanding, self.string + self._post_compile_pattern, process_expanding, pre_expanded_string ) if numeric_positiontup is not None: - assert positiontup is not None + assert new_positiontup is not None param_pos = { key: f"{self._numeric_binds_identifier_char}{num}" for num, key in enumerate( @@ -1814,13 +1928,13 @@ class SQLCompiler(Compiled): statement = self._pyformat_pattern.sub( lambda m: param_pos[m.group(1)], statement ) - positiontup.extend(numeric_positiontup) + new_positiontup.extend(numeric_positiontup) expanded_state = ExpandedState( statement, parameters, new_processors, - positiontup, + new_positiontup, expanded_parameters, ) @@ -1828,24 +1942,15 @@ class SQLCompiler(Compiled): # this is for the "render_postcompile" flag, which is not # otherwise used internally and is for end-user debugging and # special use cases. + self._pre_expanded_string = pre_expanded_string + self._pre_expanded_positiontup = pre_expanded_positiontup self.string = expanded_state.statement - self._bind_processors.update(expanded_state.processors) - self.positiontup = list(expanded_state.positiontup or ()) - self.post_compile_params = frozenset() - for key in expanded_state.parameter_expansion: - bind = self.binds.pop(key) - - if TYPE_CHECKING: - assert bind.value is not None - - self.bind_names.pop(bind) - for value, expanded_key in zip( - bind.value, expanded_state.parameter_expansion[key] - ): - self.binds[expanded_key] = new_param = bind._with_value( - value - ) - self.bind_names[new_param] = expanded_key + self.positiontup = ( + list(expanded_state.positiontup or ()) + if self.positional + else None + ) + self._post_compile_expanded_state = expanded_state return expanded_state diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index 790a72ec84..5adda0dadc 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -636,10 +636,30 @@ class AssertsCompiledSQL: eq_(cc, result, "%r != %r on dialect %r" % (cc, result, dialect)) if checkparams is not None: - eq_(c.construct_params(params), checkparams) + if render_postcompile: + expanded_state = c.construct_expanded_state( + params, escape_names=False + ) + eq_(expanded_state.parameters, checkparams) + else: + eq_(c.construct_params(params), checkparams) if checkpositional is not None: - p = c.construct_params(params, escape_names=False) - eq_(tuple([p[x] for x in c.positiontup]), checkpositional) + if render_postcompile: + expanded_state = c.construct_expanded_state( + params, escape_names=False + ) + eq_( + tuple( + [ + expanded_state.parameters[x] + for x in expanded_state.positiontup + ] + ), + checkpositional, + ) + else: + p = c.construct_params(params, escape_names=False) + eq_(tuple([p[x] for x in c.positiontup]), checkpositional) if check_prefetch is not None: eq_(c.prefetch, check_prefetch) if check_literal_execute is not None: diff --git a/test/sql/test_compiler.py b/test/sql/test_compiler.py index d342b92483..39971fd766 100644 --- a/test/sql/test_compiler.py +++ b/test/sql/test_compiler.py @@ -8,8 +8,11 @@ styling and coherent test organization. """ +from __future__ import annotations + import datetime import decimal +from typing import TYPE_CHECKING from sqlalchemy import alias from sqlalchemy import and_ @@ -93,12 +96,18 @@ from sqlalchemy.testing import expect_raises from sqlalchemy.testing import expect_raises_message from sqlalchemy.testing import fixtures from sqlalchemy.testing import is_ +from sqlalchemy.testing import is_none from sqlalchemy.testing import is_true from sqlalchemy.testing import mock from sqlalchemy.testing import ne_ from sqlalchemy.testing.schema import pep435_enum from sqlalchemy.types import UserDefinedType + +if TYPE_CHECKING: + from sqlalchemy import Select + + table1 = table( "mytable", column("myid", Integer), @@ -4458,7 +4467,7 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): "WHERE t.x = '10') AS anon_1 UNION SELECT " "(SELECT 1 FROM t WHERE t.x = '10') AS anon_1) AS anon_2", ) - eq_(compiled.construct_params(), {"param_1": "10"}) + eq_(compiled.construct_params(_no_postcompile=True), {"param_1": "10"}) def test_construct_params_repeated_postcompile_params_two(self): """test for :ticket:`6202` two - same param name used twice @@ -4491,7 +4500,7 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): "FROM t WHERE t.x = '10') AS anon_1 UNION SELECT " "(SELECT 1 FROM t WHERE t.x = '10') AS anon_3) AS anon_2", ) - eq_(compiled.construct_params(), {"param_1": "10"}) + eq_(compiled.construct_params(_no_postcompile=True), {"param_1": "10"}) def test_construct_params_positional_plain_repeated(self): t = table("t", column("x")) @@ -4515,7 +4524,10 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): "UNION SELECT (SELECT 1 FROM t WHERE t.x = %s AND t.x = '12') " "AS anon_1) AS anon_2", ) - eq_(compiled.construct_params(), {"param_1": "10", "param_2": "12"}) + eq_( + compiled.construct_params(_no_postcompile=True), + {"param_1": "10", "param_2": "12"}, + ) eq_(compiled.positiontup, ["param_1", "param_1"]) def test_tuple_clauselist_in(self): @@ -4958,22 +4970,13 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): table1.c.name.in_(bindparam(paramname, value=["a", "b"])) ) - # NOTE: below the rendered params are just what - # render_postcompile will do right now - # if you run construct_params(). render_postcompile mode - # is not actually used by the execution internals, it's for - # user-facing compilation code. So this is likely a - # current limitation of construct_params() which is not - # doing the full blown postcompile; just assert that's - # what it does for now. it likely should be corrected - # to make more sense. if paramstyle.qmark: self.assert_compile( stmt, "SELECT mytable.myid FROM mytable " "WHERE mytable.name IN (?, ?)", - params={paramname: ["y", "z"]}, - checkpositional=(["y", "z"], ["y", "z"]), + params={paramname: ["y", "z", "q"]}, + checkpositional=("y", "z", "q"), dialect="sqlite", render_postcompile=True, ) @@ -4982,8 +4985,8 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): stmt, "SELECT mytable.myid FROM mytable " "WHERE mytable.name IN (:1, :2)", - params={paramname: ["y", "z"]}, - checkpositional=(["y", "z"], ["y", "z"]), + params={paramname: ["y", "z", "q"]}, + checkpositional=("y", "z", "q"), dialect=sqlite.dialect(paramstyle="numeric"), render_postcompile=True, ) @@ -4994,8 +4997,8 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): "(:%s_1, :%s_2)" % (expected, expected), params={paramname: ["y", "z"]}, checkparams={ - "%s_1" % expected: ["y", "z"], - "%s_2" % expected: ["y", "z"], + "%s_1" % expected: "y", + "%s_2" % expected: "z", }, dialect="default", render_postcompile=True, @@ -5071,16 +5074,6 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): .where(table1.c.myid == 9) ).order_by("myid") - # NOTE: below the rendered params are just what - # render_postcompile will do right now - # if you run construct_params(). render_postcompile mode - # is not actually used by the execution internals, it's for - # user-facing compilation code. So this is likely a - # current limitation of construct_params() which is not - # doing the full blown postcompile; just assert that's - # what it does for now. it likely should be corrected - # to make more sense. - if paramstyle.qmark: self.assert_compile( stmt, @@ -5095,16 +5088,7 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): "mytable.name IN (?)) " "AND mytable.myid = ? ORDER BY myid", params={"uname": ["y", "z"], "uname2": ["a"]}, - checkpositional=( - ["y", "z"], - ["y", "z"], - ["a"], - 8, - ["y", "z"], - ["y", "z"], - ["a"], - 9, - ), + checkpositional=("y", "z", "a", 8, "y", "z", "a", 9), dialect="sqlite", render_postcompile=True, ) @@ -5122,7 +5106,7 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): "mytable.name IN (:5)) " "AND mytable.myid = :2 ORDER BY myid", params={"uname": ["y", "z"], "uname2": ["a"]}, - checkpositional=(8, 9, ["y", "z"], ["y", "z"], ["a"]), + checkpositional=(8, 9, "y", "z", "a"), dialect=sqlite.dialect(paramstyle="numeric"), render_postcompile=True, ) @@ -5141,13 +5125,11 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): "AND mytable.myid = :myid_2 ORDER BY myid", params={"uname": ["y", "z"], "uname2": ["a"]}, checkparams={ - "uname": ["y", "z"], - "uname2": ["a"], - "uname_1": ["y", "z"], - "uname_2": ["y", "z"], - "uname2_1": ["a"], "myid_1": 8, "myid_2": 9, + "uname_1": "y", + "uname_2": "z", + "uname2_1": "a", }, dialect="default", render_postcompile=True, @@ -5171,6 +5153,369 @@ class BindParameterTest(AssertsCompiledSQL, fixtures.TestBase): ) +class CompileUXTest(fixtures.TestBase): + """tests focused on calling stmt.compile() directly, user cases""" + + @testing.fixture + def render_postcompile_fixture(self): + return ( + select(func.count(1)) + .where(column("q") == "x") + .where(column("z").in_([1, 2, 3])) + .where(column("z_tuple").in_([(1, "a"), (2, "b"), (3, "c")])) + .where( + column("y").op("foobar")( + bindparam( + "key", value=[("1", "2"), ("3", "4")], expanding=True + ) + ) + ) + ) + + def test_render_postcompile_default_stmt(self, render_postcompile_fixture): + stmt = render_postcompile_fixture + + compiled = stmt.compile(compile_kwargs={"render_postcompile": True}) + eq_ignore_whitespace( + compiled.string, + "SELECT count(:count_2) AS count_1 WHERE q = :q_1 AND z " + "IN (:z_1_1, :z_1_2, :z_1_3) AND z_tuple IN " + "((:z_tuple_1_1_1, :z_tuple_1_1_2), " + "(:z_tuple_1_2_1, :z_tuple_1_2_2), " + "(:z_tuple_1_3_1, :z_tuple_1_3_2)) " + "AND (y foobar ((:key_1_1, :key_1_2), (:key_2_1, :key_2_2)))", + ) + + def test_render_postcompile_named_parameters( + self, render_postcompile_fixture + ): + stmt = render_postcompile_fixture + + compiled = stmt.compile(compile_kwargs={"render_postcompile": True}) + is_none(compiled.positiontup) + eq_( + compiled.construct_params(), + { + "count_2": 1, + "q_1": "x", + "z_1_1": 1, + "z_1_2": 2, + "z_1_3": 3, + "z_tuple_1_1_1": 1, + "z_tuple_1_1_2": "a", + "z_tuple_1_2_1": 2, + "z_tuple_1_2_2": "b", + "z_tuple_1_3_1": 3, + "z_tuple_1_3_2": "c", + "key_1_1": "1", + "key_1_2": "2", + "key_2_1": "3", + "key_2_2": "4", + }, + ) + + def test_render_postcompile_no_new_params( + self, render_postcompile_fixture + ): + stmt = render_postcompile_fixture + + compiled = stmt.compile(compile_kwargs={"render_postcompile": True}) + params = {"q_1": "g"} + with expect_raises_message( + exc.InvalidRequestError, + "can't construct new parameters when render_postcompile is used; " + "the statement is hard-linked to the original parameters.", + ): + compiled.construct_params(params) + + @testing.variation("render_postcompile", [True, False]) + def test_new_expanded_state_no_params( + self, render_postcompile_fixture: Select, render_postcompile + ): + stmt = render_postcompile_fixture + + compiled = stmt.compile( + compile_kwargs={"render_postcompile": render_postcompile} + ) + is_none(compiled.positiontup) + + es = compiled.construct_expanded_state() + + is_none(compiled.positiontup) + + eq_ignore_whitespace( + es.statement, + "SELECT count(:count_2) AS count_1 WHERE q = :q_1 AND z " + "IN (:z_1_1, :z_1_2, :z_1_3) AND z_tuple IN " + "((:z_tuple_1_1_1, :z_tuple_1_1_2), " + "(:z_tuple_1_2_1, :z_tuple_1_2_2), " + "(:z_tuple_1_3_1, :z_tuple_1_3_2)) " + "AND (y foobar ((:key_1_1, :key_1_2), (:key_2_1, :key_2_2)))", + ) + + eq_( + es.parameters, + { + "count_2": 1, + "q_1": "x", + "z_1_1": 1, + "z_1_2": 2, + "z_1_3": 3, + "z_tuple_1_1_1": 1, + "z_tuple_1_1_2": "a", + "z_tuple_1_2_1": 2, + "z_tuple_1_2_2": "b", + "z_tuple_1_3_1": 3, + "z_tuple_1_3_2": "c", + "key_1_1": "1", + "key_1_2": "2", + "key_2_1": "3", + "key_2_2": "4", + }, + ) + + @testing.variation("render_postcompile", [True, False]) + @testing.variation("positional", [True, False]) + def test_accessor_no_params(self, render_postcompile, positional): + stmt = select(column("q")) + + positional_dialect = default.DefaultDialect( + paramstyle="qmark" if positional else "pyformat" + ) + compiled = stmt.compile( + dialect=positional_dialect, + compile_kwargs={"render_postcompile": render_postcompile}, + ) + if positional: + eq_(compiled.positiontup, []) + else: + is_none(compiled.positiontup) + eq_(compiled.params, {}) + eq_(compiled.construct_params(), {}) + + es = compiled.construct_expanded_state() + if positional: + eq_(es.positiontup, []) + eq_(es.positional_parameters, ()) + else: + is_none(es.positiontup) + with expect_raises_message( + exc.InvalidRequestError, + "statement does not use a positional paramstyle", + ): + es.positional_parameters + eq_(es.parameters, {}) + + eq_ignore_whitespace( + es.statement, + "SELECT q", + ) + + @testing.variation("render_postcompile", [True, False]) + def test_new_expanded_state_new_params( + self, render_postcompile_fixture: Select, render_postcompile + ): + stmt = render_postcompile_fixture + + compiled = stmt.compile( + compile_kwargs={"render_postcompile": render_postcompile} + ) + is_none(compiled.positiontup) + + es = compiled.construct_expanded_state( + { + "z_tuple_1": [("q", "z", "p"), ("g", "h", "i")], + "key": ["a", "b"], + } + ) + is_none(compiled.positiontup) + + eq_ignore_whitespace( + es.statement, + "SELECT count(:count_2) AS count_1 WHERE q = :q_1 AND z IN " + "(:z_1_1, :z_1_2, :z_1_3) AND z_tuple IN " + "((:z_tuple_1_1_1, :z_tuple_1_1_2, :z_tuple_1_1_3), " + "(:z_tuple_1_2_1, :z_tuple_1_2_2, :z_tuple_1_2_3)) AND " + "(y foobar (:key_1, :key_2))", + ) + + eq_( + es.parameters, + { + "count_2": 1, + "q_1": "x", + "z_1_1": 1, + "z_1_2": 2, + "z_1_3": 3, + "z_tuple_1_1_1": "q", + "z_tuple_1_1_2": "z", + "z_tuple_1_1_3": "p", + "z_tuple_1_2_1": "g", + "z_tuple_1_2_2": "h", + "z_tuple_1_2_3": "i", + "key_1": "a", + "key_2": "b", + }, + ) + + @testing.variation("render_postcompile", [True, False]) + @testing.variation("paramstyle", ["qmark", "numeric"]) + def test_new_expanded_state_new_positional_params( + self, + render_postcompile_fixture: Select, + render_postcompile, + paramstyle, + ): + stmt = render_postcompile_fixture + positional_dialect = default.DefaultDialect(paramstyle=paramstyle.name) + + compiled = stmt.compile( + dialect=positional_dialect, + compile_kwargs={"render_postcompile": render_postcompile}, + ) + + if render_postcompile: + eq_( + compiled.positiontup, + [ + "count_2", + "q_1", + "z_1_1", + "z_1_2", + "z_1_3", + "z_tuple_1_1_1", + "z_tuple_1_1_2", + "z_tuple_1_2_1", + "z_tuple_1_2_2", + "z_tuple_1_3_1", + "z_tuple_1_3_2", + "key_1_1", + "key_1_2", + "key_2_1", + "key_2_2", + ], + ) + else: + eq_( + compiled.positiontup, + ["count_2", "q_1", "z_1", "z_tuple_1", "key"], + ) + es = compiled.construct_expanded_state( + { + "z_tuple_1": [("q", "z", "p"), ("g", "h", "i")], + "key": ["a", "b"], + } + ) + if paramstyle.qmark: + eq_ignore_whitespace( + es.statement, + "SELECT count(?) AS count_1 WHERE q = ? " + "AND z IN (?, ?, ?) AND " + "z_tuple IN ((?, ?, ?), (?, ?, ?)) AND (y foobar (?, ?))", + ) + elif paramstyle.numeric: + eq_ignore_whitespace( + es.statement, + "SELECT count(:1) AS count_1 WHERE q = :2 AND z IN " + "(:3, :4, :5) AND z_tuple " + "IN ((:6, :7, :8), (:9, :10, :11)) AND (y foobar (:12, :13))", + ) + else: + paramstyle.fail() + + eq_( + es.parameters, + { + "count_2": 1, + "q_1": "x", + "z_1_1": 1, + "z_1_2": 2, + "z_1_3": 3, + "z_tuple_1_1_1": "q", + "z_tuple_1_1_2": "z", + "z_tuple_1_1_3": "p", + "z_tuple_1_2_1": "g", + "z_tuple_1_2_2": "h", + "z_tuple_1_2_3": "i", + "key_1": "a", + "key_2": "b", + }, + ) + eq_( + es.positiontup, + [ + "count_2", + "q_1", + "z_1_1", + "z_1_2", + "z_1_3", + "z_tuple_1_1_1", + "z_tuple_1_1_2", + "z_tuple_1_1_3", + "z_tuple_1_2_1", + "z_tuple_1_2_2", + "z_tuple_1_2_3", + "key_1", + "key_2", + ], + ) + eq_( + es.positional_parameters, + (1, "x", 1, 2, 3, "q", "z", "p", "g", "h", "i", "a", "b"), + ) + + def test_render_postcompile_positional_parameters( + self, render_postcompile_fixture + ): + stmt = render_postcompile_fixture + + positional_dialect = default.DefaultDialect(paramstyle="qmark") + compiled = stmt.compile( + dialect=positional_dialect, + compile_kwargs={"render_postcompile": True}, + ) + eq_( + compiled.construct_params(), + { + "count_2": 1, + "q_1": "x", + "z_1_1": 1, + "z_1_2": 2, + "z_1_3": 3, + "z_tuple_1_1_1": 1, + "z_tuple_1_1_2": "a", + "z_tuple_1_2_1": 2, + "z_tuple_1_2_2": "b", + "z_tuple_1_3_1": 3, + "z_tuple_1_3_2": "c", + "key_1_1": "1", + "key_1_2": "2", + "key_2_1": "3", + "key_2_2": "4", + }, + ) + eq_( + compiled.positiontup, + [ + "count_2", + "q_1", + "z_1_1", + "z_1_2", + "z_1_3", + "z_tuple_1_1_1", + "z_tuple_1_1_2", + "z_tuple_1_2_1", + "z_tuple_1_2_2", + "z_tuple_1_3_1", + "z_tuple_1_3_2", + "key_1_1", + "key_1_2", + "key_2_1", + "key_2_2", + ], + ) + + class UnsupportedTest(fixtures.TestBase): def test_unsupported_element_str_visit_name(self): from sqlalchemy.sql.expression import ClauseElement -- 2.47.2