@@ -117,18 +117,24 @@ def test_create_table_with_complex_type(self, metadata):
117117
118118
119119class TestBindParamQuoting (DDLTestBase ):
120- """Regression tests for column names that contain characters which are not
121- legal inside a bare Databricks named-parameter marker (`:name`). Without
122- the custom ``bindparam_string`` override, a column like
123- ``col-with-hyphen`` produces SQL like ``VALUES (:col-with-hyphen)`` which
124- fails with UNBOUND_SQL_PARAMETER on the server. The fix wraps such names
125- in backticks (``VALUES (:`col-with-hyphen`)``), which the Databricks SQL
126- grammar accepts as a quoted parameter identifier.
120+ """Regression tests for bind-parameter quoting.
121+
122+ Databricks named parameter markers (``:name``) must be bare identifiers
123+ (``[A-Za-z_][A-Za-z0-9_]*``) unless wrapped in backticks. Because
124+ DataFrame-origin column names frequently contain hyphens (a character
125+ that's legal inside a backtick-quoted column identifier but not in a
126+ bare bind marker), the dialect wraps every bind name in backticks
127+ unconditionally. The backticks are SQL-side quoting only — the params
128+ dict sent to the driver keeps the original unquoted key.
129+
130+ The behavior is gated by ``DatabricksDialect.quote_bind_params`` which
131+ defaults to True; set ``?quote_bind_params=false`` in the URL to
132+ disable.
127133 """
128134
129- def _compile_insert (self , table , values ):
135+ def _compile_insert (self , table , values , engine = None ):
130136 stmt = insert (table ).values (values )
131- return stmt .compile (bind = self .engine )
137+ return stmt .compile (bind = engine or self .engine )
132138
133139 def test_hyphenated_column_renders_backticked_bind_marker (self ):
134140 metadata = MetaData ()
@@ -143,18 +149,18 @@ def test_hyphenated_column_renders_backticked_bind_marker(self):
143149 )
144150
145151 sql = str (compiled )
146- # Hyphenated name is wrapped in backticks at the marker site
152+ # Both names are backticked at the marker site
147153 assert ":`col-with-hyphen`" in sql
148- # Plain name is untouched
149- assert ":normal_col" in sql
154+ assert ":`normal_col`" in sql
150155 # The params dict sent to the driver keeps the ORIGINAL unquoted key
151156 # — this matches what the Databricks server expects (verified
152- # empirically: a backticked marker `:`name`` binds against a plain
153- # `name` key in the params dict).
157+ # empirically: a backticked marker `` :`name` `` binds against a plain
158+ # `` name` ` key in the params dict).
154159 params = compiled .construct_params ()
155160 assert params ["col-with-hyphen" ] == "x"
156161 assert params ["normal_col" ] == "y"
157162 assert "`col-with-hyphen`" not in params
163+ assert "`normal_col`" not in params
158164
159165 def test_hyphen_and_underscore_columns_do_not_collide (self ):
160166 """A table containing both ``col-name`` and ``col_name`` must produce
@@ -174,14 +180,17 @@ def test_hyphen_and_underscore_columns_do_not_collide(self):
174180
175181 sql = str (compiled )
176182 assert ":`col-name`" in sql
177- assert ":col_name" in sql
183+ assert ":` col_name` " in sql
178184
179185 params = compiled .construct_params ()
180186 assert params ["col-name" ] == "hyphen_value"
181187 assert params ["col_name" ] == "underscore_value"
182188
183- def test_plain_identifier_bind_names_are_unchanged (self ):
184- """No regression: ordinary column names must not be backticked."""
189+ def test_plain_identifier_bind_names_are_also_backticked (self ):
190+ """Every bind name is wrapped unconditionally — the Databricks SQL
191+ grammar accepts ``:`id``` identically to ``:id`` for plain names
192+ (verified against a live warehouse).
193+ """
185194 metadata = MetaData ()
186195 table = Table (
187196 "t" ,
@@ -191,15 +200,10 @@ def test_plain_identifier_bind_names_are_unchanged(self):
191200 )
192201 compiled = self ._compile_insert (table , {"id" : "1" , "name" : "n" })
193202 sql = str (compiled )
194- assert ":id" in sql
195- assert ":name" in sql
196- assert ":`id`" not in sql
197- assert ":`name`" not in sql
203+ assert ":`id`" in sql
204+ assert ":`name`" in sql
198205
199- def test_space_and_dot_in_column_name_also_backticked (self ):
200- """The bare-identifier check covers all non-[A-Za-z0-9_] characters,
201- not just hyphens — spaces, dots, etc. should also be wrapped.
202- """
206+ def test_space_and_dot_in_column_name_are_backticked (self ):
203207 metadata = MetaData ()
204208 table = Table (
205209 "t" ,
@@ -218,13 +222,42 @@ def test_space_and_dot_in_column_name_also_backticked(self):
218222 assert params ["col with space" ] == "s"
219223 assert params ["col.with.dot" ] == "d"
220224
221- def test_leading_digit_column_is_backticked (self ):
222- """Databricks bind names cannot start with a digit either."""
225+ def test_quote_bind_params_can_be_disabled (self ):
226+ """Setting ``quote_bind_params=False`` on the dialect reverts to
227+ stock SQLAlchemy bind-name rendering (the pre-fix behavior).
228+ """
229+ from databricks .sqlalchemy .base import DatabricksDialect
230+
231+ dialect = DatabricksDialect ()
232+ dialect .paramstyle = "named"
233+ dialect .quote_bind_params = False
234+
223235 metadata = MetaData ()
224- table = Table ("t" , metadata , Column ("1col " , String ()))
225- compiled = self . _compile_insert (table , { "1col " : "x" } )
236+ table = Table ("t" , metadata , Column ("id " , String ()))
237+ compiled = insert (table ). values ({ "id " : "1" }). compile ( dialect = dialect )
226238 sql = str (compiled )
227- assert ":`1col`" in sql
239+ assert ":id" in sql
240+ assert ":`id`" not in sql
228241
229- params = compiled .construct_params ()
230- assert params ["1col" ] == "x"
242+ def test_url_query_string_disables_quoting (self ):
243+ """The URL query parameter ``?quote_bind_params=false`` turns the
244+ flag off on the dialect.
245+ """
246+ from sqlalchemy import create_engine
247+
248+ engine = create_engine (
249+ "databricks://token:****@****?http_path=****&catalog=****"
250+ "&schema=****"e_bind_params=false"
251+ )
252+ # create_engine lazy-initializes; force the dialect to process the URL
253+ engine .dialect .create_connect_args (engine .url )
254+ assert engine .dialect .quote_bind_params is False
255+
256+ def test_url_query_string_defaults_to_quoting (self ):
257+ from sqlalchemy import create_engine
258+
259+ engine = create_engine (
260+ "databricks://token:****@****?http_path=****&catalog=****&schema=****"
261+ )
262+ engine .dialect .create_connect_args (engine .url )
263+ assert engine .dialect .quote_bind_params is True
0 commit comments