Skip to content

Commit 6b9c659

Browse files
committed
Cover IN-clause expansion at render_postcompile=True time too
The __init__ bindtemplate swap covered execute-time IN expansion but missed two adjacent paths: 1. compile_kwargs={'render_postcompile': True} — fires inside super's __init__, before a post-super subclass override can take effect. 2. construct_expanded_state() called directly on a compiled stmt. Both paths funnel through SQLCompiler._literal_execute_expanding_parameter, which reads self.bindtemplate (or compilation_bindtemplate for numeric paramstyles) once into a local variable and uses it to render every expanded marker. Override that single method to swap both templates to the backticked form for the duration of the super call, then restore. This removes the __init__ template swap entirely — the override on _literal_execute_expanding_parameter is the single point that covers all three expansion call sites (execute-time, render_postcompile=True compile-time, construct_expanded_state). Adds a regression test exercising both the render_postcompile=True and construct_expanded_state paths. Co-authored-by: Isaac
1 parent 3b5d8af commit 6b9c659

2 files changed

Lines changed: 72 additions & 21 deletions

File tree

src/databricks/sqlalchemy/_ddl.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,24 +110,22 @@ class DatabricksStatementCompiler(compiler.SQLCompiler):
110110
``bindparam_string`` via ``self.process(statement)``. Oracle
111111
overrides this same method (``cx_oracle.py:781``) to quote-wrap
112112
names, and we do the same here.
113-
* **Execute-time IN expansion** — SQLAlchemy's
113+
* **IN-clause expansion** — SQLAlchemy's
114114
``_literal_execute_expanding_parameter`` builds expanded markers
115115
(``:col-name_1, :col-name_2, ...``) directly from
116-
``self.bindtemplate``, bypassing ``bindparam_string``. We swap
117-
``bindtemplate`` after super's ``__init__`` to ensure that path
118-
also emits backticked markers.
116+
``self.bindtemplate``, bypassing ``bindparam_string``. This method
117+
is called from three sites: at execute time
118+
(``default.py::_execute_context``), during compile time when the
119+
user passes ``compile_kwargs={'render_postcompile': True}``, and
120+
from ``construct_expanded_state``. We intercept by overriding the
121+
method itself rather than swapping ``bindtemplate`` in
122+
``__init__``, because the ``render_postcompile=True`` path fires
123+
inside super's own ``__init__`` — before a subclass ``__init__``
124+
post-super override would take effect.
119125
"""
120126

121127
_BACKTICKED_BIND_TEMPLATE = ":`%(name)s`"
122128

123-
def __init__(self, *args, **kwargs):
124-
super().__init__(*args, **kwargs)
125-
# Super sets self.bindtemplate from BIND_TEMPLATES[paramstyle]
126-
# near the end of its __init__ (for execute-time use, including
127-
# IN-clause expansion). Override it here so the expansion path
128-
# renders backticked markers too.
129-
self.bindtemplate = self._BACKTICKED_BIND_TEMPLATE
130-
131129
def bindparam_string(self, name, **kw):
132130
# Fall through to super for the specialized render paths it
133131
# already handles (POSTCOMPILE placeholder; escape-map translation
@@ -154,6 +152,28 @@ def bindparam_string(self, name, **kw):
154152
ret = self.render_bind_cast(bindparam_type, type_impl, ret)
155153
return ret
156154

155+
def _literal_execute_expanding_parameter(self, name, parameter, values):
156+
# Super reads ``self.bindtemplate`` (or ``compilation_bindtemplate``
157+
# for numeric paramstyles) once into a local variable and uses it to
158+
# render every expanded marker. Swap both to our backticked template
159+
# for the duration of the call, then restore, so any later read sees
160+
# the original values. This covers execute-time expansion, the
161+
# ``render_postcompile=True`` compile-kwarg path that fires inside
162+
# super's ``__init__``, and ``construct_expanded_state``.
163+
saved_bt = getattr(self, "bindtemplate", None)
164+
saved_cbt = getattr(self, "compilation_bindtemplate", None)
165+
self.bindtemplate = self._BACKTICKED_BIND_TEMPLATE
166+
self.compilation_bindtemplate = self._BACKTICKED_BIND_TEMPLATE
167+
try:
168+
return super()._literal_execute_expanding_parameter(
169+
name, parameter, values
170+
)
171+
finally:
172+
if saved_bt is not None:
173+
self.bindtemplate = saved_bt
174+
if saved_cbt is not None:
175+
self.compilation_bindtemplate = saved_cbt
176+
157177
def limit_clause(self, select, **kw):
158178
"""Identical to the default implementation of SQLCompiler.limit_clause except it writes LIMIT ALL instead of LIMIT -1,
159179
since Databricks SQL doesn't support the latter.

tests/test_local/test_ddl.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -337,19 +337,50 @@ def test_multivalues_insert_disambiguates_with_backticked_markers(self):
337337
assert ":`col-name_m0`" in sql
338338
assert ":`col-name_m1`" in sql
339339

340-
def test_in_clause_with_hyphenated_column_falls_through_to_postcompile(self):
341-
"""IN clauses use ``post_compile`` params which our override skips
342-
(the rendered ``__[POSTCOMPILE_...]`` marker is not a bind name).
343-
The anonymized bind SQLAlchemy assigns to the IN parameter does
344-
still get backticked because it contains a hyphen (``col_name_1``
345-
would be fine, but the column name slug can leak hyphens).
340+
def test_in_clause_with_hyphenated_column_compiles_to_postcompile(self):
341+
"""The initial compilation leaves an IN clause as a POSTCOMPILE
342+
placeholder. The placeholder itself isn't a bind marker so no
343+
quoting is needed at this stage — the actual expanded markers
344+
(``:\\`col-name_1_1\\``, …) are rendered at expansion time by our
345+
``_literal_execute_expanding_parameter`` override (see
346+
``test_in_clause_expansion_renders_backticked_markers``).
346347
"""
347348
from sqlalchemy import select
348349

349350
metadata = MetaData()
350351
table = Table("t", metadata, Column("col-name", String()))
351352
stmt = select(table).where(table.c["col-name"].in_(["a", "b"]))
353+
sql = str(stmt.compile(bind=self.engine))
354+
assert "POSTCOMPILE_col-name_1" in sql
355+
356+
def test_in_clause_expansion_renders_backticked_markers(self):
357+
"""Exercise the three sites that invoke
358+
``_literal_execute_expanding_parameter``:
359+
360+
* normal execute-time expansion via ``construct_expanded_state``
361+
* ``compile_kwargs={'render_postcompile': True}`` — which fires
362+
inside super's ``__init__``, before any post-super subclass
363+
init would take effect
364+
"""
365+
from sqlalchemy import select
366+
367+
metadata = MetaData()
368+
table = Table("t", metadata, Column("col-name", String()))
369+
stmt = select(table).where(table.c["col-name"].in_(["a", "b", "c"]))
370+
371+
# (1) render_postcompile=True at compile time — fires inside super __init__
372+
rendered = str(
373+
stmt.compile(bind=self.engine, compile_kwargs={"render_postcompile": True})
374+
)
375+
assert ":`col-name_1_1`" in rendered
376+
assert ":`col-name_1_2`" in rendered
377+
assert ":`col-name_1_3`" in rendered
378+
379+
# (2) construct_expanded_state at execute time
352380
compiled = stmt.compile(bind=self.engine)
353-
# The POSTCOMPILE marker goes through super() — just make sure we
354-
# didn't crash and the SQL is well-formed.
355-
assert "POSTCOMPILE" in str(compiled) or "IN (" in str(compiled)
381+
expanded = compiled.construct_expanded_state(
382+
{"col-name_1": ["a", "b", "c"]}
383+
)
384+
assert ":`col-name_1_1`" in expanded.statement
385+
assert ":`col-name_1_2`" in expanded.statement
386+
assert ":`col-name_1_3`" in expanded.statement

0 commit comments

Comments
 (0)