mssql-python 1.16.0__cp315-cp315-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mssql_python/__init__.py +662 -0
- mssql_python/async_query/__init__.py +40 -0
- mssql_python/async_query/_connection_context.py +39 -0
- mssql_python/async_query/_native.py +23 -0
- mssql_python/async_query/async_connection.py +160 -0
- mssql_python/async_query/async_cursor.py +230 -0
- mssql_python/async_query/async_execute.py +115 -0
- mssql_python/async_query/async_fetch.py +134 -0
- mssql_python/async_query/exception_translator.py +114 -0
- mssql_python/auth.py +913 -0
- mssql_python/connection.py +2357 -0
- mssql_python/connection_string_builder.py +114 -0
- mssql_python/connection_string_parser.py +424 -0
- mssql_python/constants.py +701 -0
- mssql_python/cursor.py +4188 -0
- mssql_python/db_connection.py +98 -0
- mssql_python/ddbc_bindings.cp315-amd64.pyd +0 -0
- mssql_python/ddbc_bindings.py +215 -0
- mssql_python/decimal_config.py +114 -0
- mssql_python/exceptions.py +588 -0
- mssql_python/helpers.py +416 -0
- mssql_python/logging.py +714 -0
- mssql_python/mssql_python.pyi +481 -0
- mssql_python/msvcp140.dll +0 -0
- mssql_python/odbc_provider.py +251 -0
- mssql_python/parameter_helper.py +488 -0
- mssql_python/perf_timer.py +286 -0
- mssql_python/pooling.py +151 -0
- mssql_python/py.typed +1 -0
- mssql_python/row.py +398 -0
- mssql_python/type.py +160 -0
- mssql_python-1.16.0.dist-info/METADATA +102 -0
- mssql_python-1.16.0.dist-info/RECORD +36 -0
- mssql_python-1.16.0.dist-info/WHEEL +5 -0
- mssql_python-1.16.0.dist-info/licenses/LICENSE +47 -0
- mssql_python-1.16.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Microsoft Corporation.
|
|
3
|
+
Licensed under the MIT license.
|
|
4
|
+
|
|
5
|
+
Parameter style conversion helpers for mssql-python.
|
|
6
|
+
|
|
7
|
+
Supports both qmark (?) and pyformat (%(name)s) parameter styles.
|
|
8
|
+
Includes context-aware scanning for qmark and pyformat detection,
|
|
9
|
+
skipping characters inside bracketed identifiers, string literals,
|
|
10
|
+
quoted identifiers, and SQL comments.
|
|
11
|
+
|
|
12
|
+
Reference: https://www.python.org/dev/peps/pep-0249/#paramstyle
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Dict, List, Tuple, Any, Union
|
|
16
|
+
from mssql_python.logging import logger
|
|
17
|
+
|
|
18
|
+
# Distinctive marker for escaped percent signs during pyformat conversion
|
|
19
|
+
# Uses a unique prefix/suffix that's extremely unlikely to appear in real SQL
|
|
20
|
+
_ESCAPED_PERCENT_MARKER = "__MSSQL_PYFORMAT_ESCAPED_PERCENT_PLACEHOLDER__"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _skip_quoted_context(sql: str, i: int, length: int) -> int:
|
|
24
|
+
"""
|
|
25
|
+
If position i starts a SQL quoted context, skip past it and return the new position.
|
|
26
|
+
Returns -1 if no quoted context starts at position i.
|
|
27
|
+
|
|
28
|
+
Handles:
|
|
29
|
+
- Single-line comments: -- ... (to end of line)
|
|
30
|
+
- Multi-line comments: /* ... */ (to closing delimiter)
|
|
31
|
+
- Single-quoted string literals: '...' (with '' escape handling)
|
|
32
|
+
- Double-quoted identifiers: "..."
|
|
33
|
+
- Bracketed identifiers: [...]
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
sql: Full SQL query string
|
|
37
|
+
i: Current scan position
|
|
38
|
+
length: Length of sql (len(sql))
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
New position after the quoted context, or -1 if position i
|
|
42
|
+
does not start a quoted context.
|
|
43
|
+
"""
|
|
44
|
+
ch = sql[i]
|
|
45
|
+
|
|
46
|
+
# Single-line comment: skip to end of line
|
|
47
|
+
if ch == "-" and i + 1 < length and sql[i + 1] == "-":
|
|
48
|
+
i += 2
|
|
49
|
+
while i < length and sql[i] != "\n":
|
|
50
|
+
i += 1
|
|
51
|
+
return i
|
|
52
|
+
|
|
53
|
+
# Multi-line comment: skip to closing */
|
|
54
|
+
# SQL Server supports nested block comments, so we track nesting depth.
|
|
55
|
+
if ch == "/" and i + 1 < length and sql[i + 1] == "*":
|
|
56
|
+
i += 2
|
|
57
|
+
depth = 1
|
|
58
|
+
while i < length and depth > 0:
|
|
59
|
+
if i + 1 < length and sql[i] == "/" and sql[i + 1] == "*":
|
|
60
|
+
depth += 1
|
|
61
|
+
i += 2
|
|
62
|
+
elif i + 1 < length and sql[i] == "*" and sql[i + 1] == "/":
|
|
63
|
+
depth -= 1
|
|
64
|
+
i += 2
|
|
65
|
+
else:
|
|
66
|
+
i += 1
|
|
67
|
+
return min(i, length) # already past final */, or at end if unterminated
|
|
68
|
+
|
|
69
|
+
# Single-quoted string literal: skip to closing '
|
|
70
|
+
# Handles escaped quotes ('') inside strings
|
|
71
|
+
if ch == "'":
|
|
72
|
+
i += 1
|
|
73
|
+
while i < length:
|
|
74
|
+
if sql[i] == "'":
|
|
75
|
+
if i + 1 < length and sql[i + 1] == "'":
|
|
76
|
+
i += 2 # skip escaped quote
|
|
77
|
+
continue
|
|
78
|
+
break
|
|
79
|
+
i += 1
|
|
80
|
+
return min(i + 1, length) # skip closing quote
|
|
81
|
+
|
|
82
|
+
# Double-quoted identifier: skip to closing "
|
|
83
|
+
# Handles escaped quotes ("") inside identifiers
|
|
84
|
+
if ch == '"':
|
|
85
|
+
i += 1
|
|
86
|
+
while i < length:
|
|
87
|
+
if sql[i] == '"':
|
|
88
|
+
if i + 1 < length and sql[i + 1] == '"':
|
|
89
|
+
i += 2 # skip escaped quote
|
|
90
|
+
continue
|
|
91
|
+
break
|
|
92
|
+
i += 1
|
|
93
|
+
return min(i + 1, length) # skip closing quote
|
|
94
|
+
|
|
95
|
+
# Bracketed identifier: skip to closing ]
|
|
96
|
+
# Handles escaped brackets (]]) inside identifiers
|
|
97
|
+
if ch == "[":
|
|
98
|
+
i += 1
|
|
99
|
+
while i < length:
|
|
100
|
+
if sql[i] == "]":
|
|
101
|
+
if i + 1 < length and sql[i + 1] == "]":
|
|
102
|
+
i += 2 # skip escaped bracket
|
|
103
|
+
continue
|
|
104
|
+
break
|
|
105
|
+
i += 1
|
|
106
|
+
return min(i + 1, length) # skip closing bracket
|
|
107
|
+
|
|
108
|
+
return -1
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _has_unquoted_question_marks(sql: str) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Check if SQL contains ? characters that are actual qmark parameter placeholders.
|
|
114
|
+
|
|
115
|
+
Uses _skip_quoted_context to skip ? characters that appear inside
|
|
116
|
+
bracketed identifiers, string literals, quoted identifiers, and comments.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
sql: SQL query string to check
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
True if the SQL contains at least one unquoted/unbracketed ? character
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
>>> _has_unquoted_question_marks("SELECT * FROM t WHERE id = ?")
|
|
126
|
+
True
|
|
127
|
+
|
|
128
|
+
>>> _has_unquoted_question_marks("SELECT [q?marks] FROM t")
|
|
129
|
+
False
|
|
130
|
+
|
|
131
|
+
>>> _has_unquoted_question_marks("SELECT 'what?' FROM t")
|
|
132
|
+
False
|
|
133
|
+
"""
|
|
134
|
+
i = 0
|
|
135
|
+
length = len(sql)
|
|
136
|
+
|
|
137
|
+
while i < length:
|
|
138
|
+
# Skip any quoted context (brackets, strings, comments)
|
|
139
|
+
skipped = _skip_quoted_context(sql, i, length)
|
|
140
|
+
if skipped >= 0:
|
|
141
|
+
i = skipped
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
# Unquoted question mark — this is a real placeholder
|
|
145
|
+
if sql[i] == "?":
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
i += 1
|
|
149
|
+
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def parse_pyformat_params(sql: str) -> List[str]:
|
|
154
|
+
"""
|
|
155
|
+
Extract %(name)s parameter names from SQL string.
|
|
156
|
+
|
|
157
|
+
Uses context-aware scanning to skip %(name)s patterns inside SQL
|
|
158
|
+
string literals, quoted identifiers, bracketed identifiers, and comments.
|
|
159
|
+
Only %(name)s patterns in executable SQL are detected as parameters.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
sql: SQL query string with %(name)s placeholders
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
List of parameter names in order of appearance (with duplicates if reused)
|
|
166
|
+
|
|
167
|
+
Examples:
|
|
168
|
+
>>> parse_pyformat_params("SELECT * FROM users WHERE id = %(id)s")
|
|
169
|
+
['id']
|
|
170
|
+
|
|
171
|
+
>>> parse_pyformat_params("WHERE name = %(name)s OR email = %(name)s")
|
|
172
|
+
['name', 'name']
|
|
173
|
+
|
|
174
|
+
>>> parse_pyformat_params("SELECT * FROM %(table)s WHERE id = %(id)s")
|
|
175
|
+
['table', 'id']
|
|
176
|
+
"""
|
|
177
|
+
logger.debug(
|
|
178
|
+
"parse_pyformat_params: Starting parse - sql_length=%d, sql_preview=%s",
|
|
179
|
+
len(sql),
|
|
180
|
+
sql[:100] if len(sql) > 100 else sql,
|
|
181
|
+
)
|
|
182
|
+
params = []
|
|
183
|
+
i = 0
|
|
184
|
+
length = len(sql)
|
|
185
|
+
|
|
186
|
+
while i < length:
|
|
187
|
+
# Skip any quoted context (brackets, strings, comments)
|
|
188
|
+
skipped = _skip_quoted_context(sql, i, length)
|
|
189
|
+
if skipped >= 0:
|
|
190
|
+
i = skipped
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
# Look for %(
|
|
194
|
+
if i + 2 < length and sql[i] == "%" and sql[i + 1] == "(":
|
|
195
|
+
# Find the closing )
|
|
196
|
+
j = i + 2
|
|
197
|
+
while j < length and sql[j] != ")":
|
|
198
|
+
j += 1
|
|
199
|
+
|
|
200
|
+
# Check if we found ) and it's followed by 's'
|
|
201
|
+
if j < length and sql[j] == ")":
|
|
202
|
+
if j + 1 < length and sql[j + 1] == "s":
|
|
203
|
+
# Extract parameter name
|
|
204
|
+
param_name = sql[i + 2 : j]
|
|
205
|
+
params.append(param_name)
|
|
206
|
+
logger.debug(
|
|
207
|
+
"parse_pyformat_params: Found parameter '%s' at position %d",
|
|
208
|
+
param_name,
|
|
209
|
+
i,
|
|
210
|
+
)
|
|
211
|
+
i = j + 2
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
i += 1
|
|
215
|
+
|
|
216
|
+
logger.debug(
|
|
217
|
+
"parse_pyformat_params: Completed - found %d parameters: %s",
|
|
218
|
+
len(params),
|
|
219
|
+
params,
|
|
220
|
+
)
|
|
221
|
+
return params
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def convert_pyformat_to_qmark(sql: str, param_dict: Dict[str, Any]) -> Tuple[str, Tuple[Any, ...]]:
|
|
225
|
+
"""
|
|
226
|
+
Convert pyformat-style query to qmark-style for ODBC execution.
|
|
227
|
+
|
|
228
|
+
Validates that all required parameters are present and builds a positional
|
|
229
|
+
parameter tuple. Supports parameter reuse (same parameter appearing multiple times).
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
sql: SQL query with %(name)s placeholders
|
|
233
|
+
param_dict: Dictionary of parameter values
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Tuple of (rewritten_sql_with_?, positional_params_tuple)
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
KeyError: If required parameter is missing from param_dict
|
|
240
|
+
|
|
241
|
+
Examples:
|
|
242
|
+
>>> convert_pyformat_to_qmark(
|
|
243
|
+
... "SELECT * FROM users WHERE id = %(id)s",
|
|
244
|
+
... {"id": 42}
|
|
245
|
+
... )
|
|
246
|
+
("SELECT * FROM users WHERE id = ?", (42,))
|
|
247
|
+
|
|
248
|
+
>>> convert_pyformat_to_qmark(
|
|
249
|
+
... "WHERE name = %(name)s OR email = %(name)s",
|
|
250
|
+
... {"name": "alice"}
|
|
251
|
+
... )
|
|
252
|
+
("WHERE name = ? OR email = ?", ("alice", "alice"))
|
|
253
|
+
"""
|
|
254
|
+
logger.debug(
|
|
255
|
+
"convert_pyformat_to_qmark: Starting conversion - sql_length=%d, param_count=%d",
|
|
256
|
+
len(sql),
|
|
257
|
+
len(param_dict),
|
|
258
|
+
)
|
|
259
|
+
logger.debug(
|
|
260
|
+
"convert_pyformat_to_qmark: SQL preview: %s",
|
|
261
|
+
sql[:200] if len(sql) > 200 else sql,
|
|
262
|
+
)
|
|
263
|
+
logger.debug(
|
|
264
|
+
"convert_pyformat_to_qmark: Parameters provided: %s",
|
|
265
|
+
list(param_dict.keys()),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Support %% escaping - replace %% with a placeholder before parsing
|
|
269
|
+
# This allows users to have literal % in their SQL
|
|
270
|
+
escaped_sql = sql.replace("%%", _ESCAPED_PERCENT_MARKER)
|
|
271
|
+
|
|
272
|
+
if "%%" in sql:
|
|
273
|
+
logger.debug(
|
|
274
|
+
"convert_pyformat_to_qmark: Detected %d escaped percent sequences (%%%%)",
|
|
275
|
+
sql.count("%%"),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Extract parameter names in order
|
|
279
|
+
param_names = parse_pyformat_params(escaped_sql)
|
|
280
|
+
|
|
281
|
+
if not param_names:
|
|
282
|
+
logger.debug(
|
|
283
|
+
"convert_pyformat_to_qmark: No pyformat parameters found - returning SQL as-is"
|
|
284
|
+
)
|
|
285
|
+
# No parameters found - restore escaped %% and return as-is
|
|
286
|
+
restored_sql = escaped_sql.replace(_ESCAPED_PERCENT_MARKER, "%")
|
|
287
|
+
return restored_sql, ()
|
|
288
|
+
|
|
289
|
+
logger.debug(
|
|
290
|
+
"convert_pyformat_to_qmark: Extracted %d parameter references (with duplicates): %s",
|
|
291
|
+
len(param_names),
|
|
292
|
+
param_names,
|
|
293
|
+
)
|
|
294
|
+
logger.debug(
|
|
295
|
+
"convert_pyformat_to_qmark: Unique parameters needed: %s",
|
|
296
|
+
sorted(set(param_names)),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Validate all required parameters are present
|
|
300
|
+
missing = set(param_names) - set(param_dict.keys())
|
|
301
|
+
if missing:
|
|
302
|
+
# Provide helpful error message
|
|
303
|
+
missing_list = sorted(missing)
|
|
304
|
+
required_list = sorted(set(param_names))
|
|
305
|
+
provided_list = sorted(param_dict.keys())
|
|
306
|
+
|
|
307
|
+
logger.error(
|
|
308
|
+
"convert_pyformat_to_qmark: Missing parameters - required=%s, provided=%s, missing=%s",
|
|
309
|
+
required_list,
|
|
310
|
+
provided_list,
|
|
311
|
+
missing_list,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
error_msg = (
|
|
315
|
+
f"Missing required parameter(s): {', '.join(repr(p) for p in missing_list)}. "
|
|
316
|
+
f"Query requires: {required_list}, provided: {provided_list}"
|
|
317
|
+
)
|
|
318
|
+
raise KeyError(error_msg)
|
|
319
|
+
|
|
320
|
+
# Build positional parameter tuple (with duplicates if param reused)
|
|
321
|
+
positional_params = tuple(param_dict[name] for name in param_names)
|
|
322
|
+
|
|
323
|
+
logger.debug(
|
|
324
|
+
"convert_pyformat_to_qmark: Built positional params tuple - length=%d",
|
|
325
|
+
len(positional_params),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Replace %(name)s with ? using simple string replacement
|
|
329
|
+
# We replace each unique parameter name to avoid issues with overlapping names
|
|
330
|
+
rewritten_sql = escaped_sql
|
|
331
|
+
unique_params = set(param_names)
|
|
332
|
+
logger.debug(
|
|
333
|
+
"convert_pyformat_to_qmark: Replacing %d unique parameter placeholders with ?",
|
|
334
|
+
len(unique_params),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
for param_name in unique_params: # Use set to avoid duplicate replacements
|
|
338
|
+
pattern = f"%({param_name})s"
|
|
339
|
+
occurrences = rewritten_sql.count(pattern)
|
|
340
|
+
rewritten_sql = rewritten_sql.replace(pattern, "?")
|
|
341
|
+
logger.debug(
|
|
342
|
+
"convert_pyformat_to_qmark: Replaced parameter '%s' (%d occurrences)",
|
|
343
|
+
param_name,
|
|
344
|
+
occurrences,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Restore escaped %% back to %
|
|
348
|
+
if _ESCAPED_PERCENT_MARKER in rewritten_sql:
|
|
349
|
+
marker_count = rewritten_sql.count(_ESCAPED_PERCENT_MARKER)
|
|
350
|
+
rewritten_sql = rewritten_sql.replace(_ESCAPED_PERCENT_MARKER, "%")
|
|
351
|
+
logger.debug(
|
|
352
|
+
"convert_pyformat_to_qmark: Restored %d escaped percent markers to %%",
|
|
353
|
+
marker_count,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
logger.debug(
|
|
357
|
+
"convert_pyformat_to_qmark: Conversion complete - result_sql_length=%d, param_count=%d",
|
|
358
|
+
len(rewritten_sql),
|
|
359
|
+
len(positional_params),
|
|
360
|
+
)
|
|
361
|
+
logger.debug(
|
|
362
|
+
"convert_pyformat_to_qmark: Result SQL preview: %s",
|
|
363
|
+
rewritten_sql[:200] if len(rewritten_sql) > 200 else rewritten_sql,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
logger.debug(
|
|
367
|
+
"Converted pyformat to qmark: params=%s, positional=%s",
|
|
368
|
+
list(param_dict.keys()),
|
|
369
|
+
positional_params,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return rewritten_sql, positional_params
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def detect_and_convert_parameters(
|
|
376
|
+
sql: str, parameters: Union[None, Tuple, List, Dict]
|
|
377
|
+
) -> Tuple[str, Union[None, Tuple, List]]:
|
|
378
|
+
"""
|
|
379
|
+
Auto-detect parameter style and convert to qmark if needed.
|
|
380
|
+
|
|
381
|
+
Detects parameter style based on the type of parameters:
|
|
382
|
+
- None: No parameters
|
|
383
|
+
- Tuple/List: qmark style (?) - pass through unchanged
|
|
384
|
+
- Dict: pyformat style (%(name)s) - convert to qmark
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
sql: SQL query string
|
|
388
|
+
parameters: Parameters in any supported format
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Tuple of (sql, parameters) where parameters are in qmark format
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
TypeError: If parameters type doesn't match placeholders in SQL
|
|
395
|
+
KeyError: If required pyformat parameter is missing
|
|
396
|
+
|
|
397
|
+
Examples:
|
|
398
|
+
>>> detect_and_convert_parameters(
|
|
399
|
+
... "SELECT * FROM users WHERE id = ?",
|
|
400
|
+
... (42,)
|
|
401
|
+
... )
|
|
402
|
+
("SELECT * FROM users WHERE id = ?", (42,))
|
|
403
|
+
|
|
404
|
+
>>> detect_and_convert_parameters(
|
|
405
|
+
... "SELECT * FROM users WHERE id = %(id)s",
|
|
406
|
+
... {"id": 42}
|
|
407
|
+
... )
|
|
408
|
+
("SELECT * FROM users WHERE id = ?", (42,))
|
|
409
|
+
"""
|
|
410
|
+
logger.debug(
|
|
411
|
+
"detect_and_convert_parameters: Starting - sql_length=%d, parameters_type=%s",
|
|
412
|
+
len(sql),
|
|
413
|
+
type(parameters).__name__ if parameters is not None else "None",
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# No parameters
|
|
417
|
+
if parameters is None:
|
|
418
|
+
logger.debug("detect_and_convert_parameters: No parameters provided - returning as-is")
|
|
419
|
+
return sql, None
|
|
420
|
+
|
|
421
|
+
# Qmark style - tuple or list
|
|
422
|
+
if isinstance(parameters, (tuple, list)):
|
|
423
|
+
logger.debug(
|
|
424
|
+
"detect_and_convert_parameters: Detected qmark-style parameters (%s) - count=%d",
|
|
425
|
+
type(parameters).__name__,
|
|
426
|
+
len(parameters),
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Check if SQL has pyformat placeholders
|
|
430
|
+
param_names = parse_pyformat_params(sql)
|
|
431
|
+
if param_names:
|
|
432
|
+
logger.error(
|
|
433
|
+
"detect_and_convert_parameters: Parameter style mismatch - SQL has pyformat placeholders %s but received %s",
|
|
434
|
+
param_names,
|
|
435
|
+
type(parameters).__name__,
|
|
436
|
+
)
|
|
437
|
+
# SQL has %(name)s but user passed tuple/list
|
|
438
|
+
raise TypeError(
|
|
439
|
+
f"Parameter style mismatch: query uses named placeholders (%(name)s), "
|
|
440
|
+
f"but {type(parameters).__name__} was provided. "
|
|
441
|
+
f"Use dict for named parameters. Example: "
|
|
442
|
+
f'cursor.execute(sql, {{"param1": value1, "param2": value2}})'
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Valid qmark style - pass through
|
|
446
|
+
logger.debug("detect_and_convert_parameters: Valid qmark style - passing through unchanged")
|
|
447
|
+
return sql, parameters
|
|
448
|
+
|
|
449
|
+
# Pyformat style - dict
|
|
450
|
+
if isinstance(parameters, dict):
|
|
451
|
+
logger.debug(
|
|
452
|
+
"detect_and_convert_parameters: Detected pyformat-style parameters (dict) - count=%d, keys=%s",
|
|
453
|
+
len(parameters),
|
|
454
|
+
list(parameters.keys()),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Check if SQL appears to have qmark placeholders
|
|
458
|
+
# Fast short-circuit: skip the O(n) context-aware scan if no ? exists at all
|
|
459
|
+
# Then use context-aware check that ignores ? inside brackets, quotes, and comments
|
|
460
|
+
if "?" in sql and _has_unquoted_question_marks(sql) and not parse_pyformat_params(sql):
|
|
461
|
+
logger.error(
|
|
462
|
+
"detect_and_convert_parameters: Parameter style mismatch - SQL has ? placeholders but received dict"
|
|
463
|
+
)
|
|
464
|
+
# SQL has ? but user passed dict and no %(name)s found
|
|
465
|
+
raise TypeError(
|
|
466
|
+
f"Parameter style mismatch: query uses positional placeholders (?), "
|
|
467
|
+
f"but dict was provided. "
|
|
468
|
+
f"Use tuple/list for positional parameters. Example: "
|
|
469
|
+
f"cursor.execute(sql, (value1, value2))"
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
logger.debug("detect_and_convert_parameters: Valid pyformat style - converting to qmark")
|
|
473
|
+
# Convert pyformat to qmark
|
|
474
|
+
converted_sql, qmark_params = convert_pyformat_to_qmark(sql, parameters)
|
|
475
|
+
logger.debug(
|
|
476
|
+
"detect_and_convert_parameters: Conversion complete - qmark_param_count=%d",
|
|
477
|
+
len(qmark_params) if qmark_params else 0,
|
|
478
|
+
)
|
|
479
|
+
return converted_sql, qmark_params
|
|
480
|
+
|
|
481
|
+
# Unsupported type
|
|
482
|
+
logger.error(
|
|
483
|
+
"detect_and_convert_parameters: Unsupported parameter type - %s",
|
|
484
|
+
type(parameters).__name__,
|
|
485
|
+
)
|
|
486
|
+
raise TypeError(
|
|
487
|
+
f"Parameters must be tuple, list, dict, or None. " f"Got {type(parameters).__name__}"
|
|
488
|
+
)
|