arrowsql 0.1.0__cp313-cp313-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.
- arrowsql/__init__.py +653 -0
- arrowsql/_rust_auth.py +170 -0
- arrowsql/_server_sqlstate.py +11 -0
- arrowsql/_utf8_strings.py +78 -0
- arrowsql/agent.py +620 -0
- arrowsql/aio.py +533 -0
- arrowsql/arrow_parallel.py +464 -0
- arrowsql/async_query/__init__.py +40 -0
- arrowsql/async_query/_connection_context.py +39 -0
- arrowsql/async_query/_native.py +24 -0
- arrowsql/async_query/async_connection.py +160 -0
- arrowsql/async_query/async_cursor.py +225 -0
- arrowsql/async_query/async_execute.py +113 -0
- arrowsql/async_query/async_fetch.py +153 -0
- arrowsql/async_query/exception_translator.py +132 -0
- arrowsql/auth.py +913 -0
- arrowsql/bulk_parallel.py +385 -0
- arrowsql/connection.py +2377 -0
- arrowsql/connection_string_builder.py +114 -0
- arrowsql/connection_string_parser.py +417 -0
- arrowsql/constants.py +701 -0
- arrowsql/cursor.py +4516 -0
- arrowsql/db_connection.py +98 -0
- arrowsql/ddbc_bindings.cp313-win_amd64.pyd +0 -0
- arrowsql/ddbc_bindings.pyi +283 -0
- arrowsql/decimal_config.py +98 -0
- arrowsql/exceptions.py +378 -0
- arrowsql/helpers.py +436 -0
- arrowsql/logging.py +700 -0
- arrowsql/mssql_python.pyi +579 -0
- arrowsql/odbc_provider.py +269 -0
- arrowsql/parameter_helper.py +442 -0
- arrowsql/perf_timer.py +286 -0
- arrowsql/pooling.py +147 -0
- arrowsql/py.typed +1 -0
- arrowsql/row.py +462 -0
- arrowsql/type.py +160 -0
- arrowsql-0.1.0.dist-info/METADATA +146 -0
- arrowsql-0.1.0.dist-info/RECORD +45 -0
- arrowsql-0.1.0.dist-info/WHEEL +4 -0
- arrowsql-0.1.0.dist-info/entry_points.txt +2 -0
- arrowsql-0.1.0.dist-info/licenses/LICENSE +21 -0
- arrowsql-0.1.0.dist-info/licenses/LICENSE.upstream-mssql-python +47 -0
- arrowsql-0.1.0.dist-info/sboms/ddbc_bindings.cyclonedx.json +5195 -0
- mssql_python/__init__.py +39 -0
|
@@ -0,0 +1,442 @@
|
|
|
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
|
+
import logging
|
|
16
|
+
from typing import Dict, List, Tuple, Any, Union
|
|
17
|
+
from arrowsql.ddbc_bindings import (
|
|
18
|
+
has_unquoted_qmark as _native_has_unquoted_qmark,
|
|
19
|
+
scan_pyformat as _native_scan_pyformat,
|
|
20
|
+
)
|
|
21
|
+
from arrowsql.logging import logger
|
|
22
|
+
from arrowsql.exceptions import ProgrammingError
|
|
23
|
+
|
|
24
|
+
# Distinctive marker for escaped percent signs during pyformat conversion
|
|
25
|
+
# Uses a unique prefix/suffix that's extremely unlikely to appear in real SQL
|
|
26
|
+
_ESCAPED_PERCENT_MARKER = "__MSSQL_PYFORMAT_ESCAPED_PERCENT_PLACEHOLDER__"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# RUST-PORT: a statement mixing %(name)s and ? is both a ProgrammingError (what
|
|
30
|
+
# drivers raised when the server rejected it) and the client-side TypeError the
|
|
31
|
+
# other parameter style mismatches raise.
|
|
32
|
+
class ParameterStyleMismatchError(ProgrammingError, TypeError): # type: ignore[misc]
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_CLOSERS = {"'": "'", '"': '"', "[": "]"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _skip_quoted_context(sql: str, i: int, length: int) -> int:
|
|
40
|
+
"""
|
|
41
|
+
If position i starts a SQL quoted context, skip past it and return the new position.
|
|
42
|
+
Returns -1 if no quoted context starts at position i.
|
|
43
|
+
|
|
44
|
+
Handles:
|
|
45
|
+
- Single-line comments: -- ... (to end of line)
|
|
46
|
+
- Multi-line comments: /* ... */ (to closing delimiter)
|
|
47
|
+
- Single-quoted string literals: '...' (with '' escape handling)
|
|
48
|
+
- Double-quoted identifiers: "..."
|
|
49
|
+
- Bracketed identifiers: [...]
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
sql: Full SQL query string
|
|
53
|
+
i: Current scan position
|
|
54
|
+
length: Length of sql (len(sql))
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
New position after the quoted context, or -1 if position i
|
|
58
|
+
does not start a quoted context.
|
|
59
|
+
"""
|
|
60
|
+
ch = sql[i]
|
|
61
|
+
|
|
62
|
+
# Single-line comment: skip to end of line
|
|
63
|
+
if ch == "-" and i + 1 < length and sql[i + 1] == "-":
|
|
64
|
+
i += 2
|
|
65
|
+
while i < length and sql[i] != "\n":
|
|
66
|
+
i += 1
|
|
67
|
+
return i
|
|
68
|
+
|
|
69
|
+
# Multi-line comment: skip to closing */
|
|
70
|
+
# SQL Server supports nested block comments, so we track nesting depth.
|
|
71
|
+
if ch == "/" and i + 1 < length and sql[i + 1] == "*":
|
|
72
|
+
i += 2
|
|
73
|
+
depth = 1
|
|
74
|
+
while i < length and depth > 0:
|
|
75
|
+
if i + 1 < length and sql[i] == "/" and sql[i + 1] == "*":
|
|
76
|
+
depth += 1
|
|
77
|
+
i += 2
|
|
78
|
+
elif i + 1 < length and sql[i] == "*" and sql[i + 1] == "/":
|
|
79
|
+
depth -= 1
|
|
80
|
+
i += 2
|
|
81
|
+
else:
|
|
82
|
+
i += 1
|
|
83
|
+
return min(i, length) # already past final */, or at end if unterminated
|
|
84
|
+
|
|
85
|
+
# Quoted literal / identifier: skip to the closing delimiter; a doubled
|
|
86
|
+
# delimiter ('' "" ]]) is an escape inside it.
|
|
87
|
+
close = _CLOSERS.get(ch)
|
|
88
|
+
if close:
|
|
89
|
+
i += 1
|
|
90
|
+
while True:
|
|
91
|
+
i = sql.find(close, i)
|
|
92
|
+
if i == -1:
|
|
93
|
+
return length
|
|
94
|
+
if i + 1 < length and sql[i + 1] == close:
|
|
95
|
+
i += 2
|
|
96
|
+
continue
|
|
97
|
+
return i + 1
|
|
98
|
+
|
|
99
|
+
return -1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _has_unquoted_question_marks(sql: str) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Check if SQL contains ? characters that are actual qmark parameter placeholders.
|
|
105
|
+
|
|
106
|
+
Uses _skip_quoted_context to skip ? characters that appear inside
|
|
107
|
+
bracketed identifiers, string literals, quoted identifiers, and comments.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
sql: SQL query string to check
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True if the SQL contains at least one unquoted/unbracketed ? character
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
>>> _has_unquoted_question_marks("SELECT * FROM t WHERE id = ?")
|
|
117
|
+
True
|
|
118
|
+
|
|
119
|
+
>>> _has_unquoted_question_marks("SELECT [q?marks] FROM t")
|
|
120
|
+
False
|
|
121
|
+
|
|
122
|
+
>>> _has_unquoted_question_marks("SELECT 'what?' FROM t")
|
|
123
|
+
False
|
|
124
|
+
"""
|
|
125
|
+
return _native_has_unquoted_qmark(sql)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def parse_pyformat_params(sql: str) -> List[str]:
|
|
129
|
+
"""
|
|
130
|
+
Extract %(name)s parameter names from SQL string.
|
|
131
|
+
|
|
132
|
+
Uses context-aware scanning to skip %(name)s patterns inside SQL
|
|
133
|
+
string literals, quoted identifiers, bracketed identifiers, and comments.
|
|
134
|
+
Only %(name)s patterns in executable SQL are detected as parameters.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
sql: SQL query string with %(name)s placeholders
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
List of parameter names in order of appearance (with duplicates if reused)
|
|
141
|
+
|
|
142
|
+
Examples:
|
|
143
|
+
>>> parse_pyformat_params("SELECT * FROM users WHERE id = %(id)s")
|
|
144
|
+
['id']
|
|
145
|
+
|
|
146
|
+
>>> parse_pyformat_params("WHERE name = %(name)s OR email = %(name)s")
|
|
147
|
+
['name', 'name']
|
|
148
|
+
|
|
149
|
+
>>> parse_pyformat_params("SELECT * FROM %(table)s WHERE id = %(id)s")
|
|
150
|
+
['table', 'id']
|
|
151
|
+
"""
|
|
152
|
+
debug = logger.isEnabledFor(logging.DEBUG)
|
|
153
|
+
if debug:
|
|
154
|
+
logger.debug(
|
|
155
|
+
"parse_pyformat_params: Starting parse - sql_length=%d, sql_preview=%s",
|
|
156
|
+
len(sql),
|
|
157
|
+
sql[:100] if len(sql) > 100 else sql,
|
|
158
|
+
)
|
|
159
|
+
params = [name for name, _start, _end in _scan_pyformat(sql)]
|
|
160
|
+
if debug:
|
|
161
|
+
logger.debug(
|
|
162
|
+
"parse_pyformat_params: Completed - found %d parameters: %s",
|
|
163
|
+
len(params),
|
|
164
|
+
params,
|
|
165
|
+
)
|
|
166
|
+
return params
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _scan_pyformat(sql: str) -> List[Tuple[str, int, int]]:
|
|
170
|
+
"""RUST-PORT: (name, start, end) of every executable %(name)s placeholder."""
|
|
171
|
+
return _native_scan_pyformat(sql)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def convert_pyformat_to_qmark(sql: str, param_dict: Dict[str, Any]) -> Tuple[str, Tuple[Any, ...]]:
|
|
175
|
+
"""
|
|
176
|
+
Convert pyformat-style query to qmark-style for ODBC execution.
|
|
177
|
+
|
|
178
|
+
Validates that all required parameters are present and builds a positional
|
|
179
|
+
parameter tuple. Supports parameter reuse (same parameter appearing multiple times).
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
sql: SQL query with %(name)s placeholders
|
|
183
|
+
param_dict: Dictionary of parameter values
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Tuple of (rewritten_sql_with_?, positional_params_tuple)
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
KeyError: If required parameter is missing from param_dict
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
>>> convert_pyformat_to_qmark(
|
|
193
|
+
... "SELECT * FROM users WHERE id = %(id)s",
|
|
194
|
+
... {"id": 42}
|
|
195
|
+
... )
|
|
196
|
+
("SELECT * FROM users WHERE id = ?", (42,))
|
|
197
|
+
|
|
198
|
+
>>> convert_pyformat_to_qmark(
|
|
199
|
+
... "WHERE name = %(name)s OR email = %(name)s",
|
|
200
|
+
... {"name": "alice"}
|
|
201
|
+
... )
|
|
202
|
+
("WHERE name = ? OR email = ?", ("alice", "alice"))
|
|
203
|
+
"""
|
|
204
|
+
rewritten_sql, positional_params, _names = convert_pyformat_to_qmark_with_names(sql, param_dict)
|
|
205
|
+
return rewritten_sql, positional_params
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def convert_pyformat_to_qmark_with_names(
|
|
209
|
+
sql: str, param_dict: Dict[str, Any]
|
|
210
|
+
) -> Tuple[str, Tuple[Any, ...], List[str]]:
|
|
211
|
+
"""convert_pyformat_to_qmark that also returns the placeholder names, so
|
|
212
|
+
callers needing the order (executemany) do not scan the SQL again."""
|
|
213
|
+
debug = logger.isEnabledFor(logging.DEBUG)
|
|
214
|
+
if debug:
|
|
215
|
+
logger.debug(
|
|
216
|
+
"convert_pyformat_to_qmark: Starting conversion - sql_length=%d, param_count=%d",
|
|
217
|
+
len(sql),
|
|
218
|
+
len(param_dict),
|
|
219
|
+
)
|
|
220
|
+
logger.debug(
|
|
221
|
+
"convert_pyformat_to_qmark: SQL preview: %s",
|
|
222
|
+
sql[:200] if len(sql) > 200 else sql,
|
|
223
|
+
)
|
|
224
|
+
logger.debug(
|
|
225
|
+
"convert_pyformat_to_qmark: Parameters provided: %s",
|
|
226
|
+
list(param_dict.keys()),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Support %% escaping - replace %% with a placeholder before parsing
|
|
230
|
+
# This allows users to have literal % in their SQL
|
|
231
|
+
escaped_sql = sql.replace("%%", _ESCAPED_PERCENT_MARKER)
|
|
232
|
+
|
|
233
|
+
if debug and "%%" in sql:
|
|
234
|
+
logger.debug(
|
|
235
|
+
"convert_pyformat_to_qmark: Detected %d escaped percent sequences (%%%%)",
|
|
236
|
+
sql.count("%%"),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Scan once: names give the parameter order, offsets drive the rewrite.
|
|
240
|
+
scanned = _scan_pyformat(escaped_sql)
|
|
241
|
+
param_names = [name for name, _start, _end in scanned]
|
|
242
|
+
|
|
243
|
+
if not param_names:
|
|
244
|
+
if debug:
|
|
245
|
+
logger.debug(
|
|
246
|
+
"convert_pyformat_to_qmark: No pyformat parameters found - returning SQL as-is"
|
|
247
|
+
)
|
|
248
|
+
# No parameters found - restore escaped %% and return as-is
|
|
249
|
+
restored_sql = escaped_sql.replace(_ESCAPED_PERCENT_MARKER, "%")
|
|
250
|
+
return restored_sql, (), param_names
|
|
251
|
+
|
|
252
|
+
if debug:
|
|
253
|
+
logger.debug(
|
|
254
|
+
"convert_pyformat_to_qmark: Extracted %d parameter references (with duplicates): %s",
|
|
255
|
+
len(param_names),
|
|
256
|
+
param_names,
|
|
257
|
+
)
|
|
258
|
+
logger.debug(
|
|
259
|
+
"convert_pyformat_to_qmark: Unique parameters needed: %s",
|
|
260
|
+
sorted(set(param_names)),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Validate all required parameters are present
|
|
264
|
+
missing = set(param_names) - set(param_dict.keys())
|
|
265
|
+
if missing:
|
|
266
|
+
# Provide helpful error message
|
|
267
|
+
missing_list = sorted(missing)
|
|
268
|
+
required_list = sorted(set(param_names))
|
|
269
|
+
provided_list = sorted(param_dict.keys())
|
|
270
|
+
|
|
271
|
+
logger.error(
|
|
272
|
+
"convert_pyformat_to_qmark: Missing parameters - required=%s, provided=%s, missing=%s",
|
|
273
|
+
required_list,
|
|
274
|
+
provided_list,
|
|
275
|
+
missing_list,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
error_msg = (
|
|
279
|
+
f"Missing required parameter(s): {', '.join(repr(p) for p in missing_list)}. "
|
|
280
|
+
f"Query requires: {required_list}, provided: {provided_list}"
|
|
281
|
+
)
|
|
282
|
+
raise KeyError(error_msg)
|
|
283
|
+
|
|
284
|
+
# Build positional parameter tuple (with duplicates if param reused)
|
|
285
|
+
positional_params = tuple([param_dict[name] for name in param_names])
|
|
286
|
+
|
|
287
|
+
# RUST-PORT: rewrite by scanned position so a %(name)s inside a string
|
|
288
|
+
# literal, comment or quoted identifier stays untouched.
|
|
289
|
+
pieces = []
|
|
290
|
+
last = 0
|
|
291
|
+
for _name, start, end in scanned:
|
|
292
|
+
pieces.append(escaped_sql[last:start])
|
|
293
|
+
pieces.append("?")
|
|
294
|
+
last = end
|
|
295
|
+
pieces.append(escaped_sql[last:])
|
|
296
|
+
rewritten_sql = "".join(pieces)
|
|
297
|
+
|
|
298
|
+
# Restore escaped %% back to %
|
|
299
|
+
if _ESCAPED_PERCENT_MARKER in rewritten_sql:
|
|
300
|
+
rewritten_sql = rewritten_sql.replace(_ESCAPED_PERCENT_MARKER, "%")
|
|
301
|
+
|
|
302
|
+
if debug:
|
|
303
|
+
logger.debug(
|
|
304
|
+
"convert_pyformat_to_qmark: Conversion complete - result_sql_length=%d, param_count=%d",
|
|
305
|
+
len(rewritten_sql),
|
|
306
|
+
len(positional_params),
|
|
307
|
+
)
|
|
308
|
+
logger.debug(
|
|
309
|
+
"convert_pyformat_to_qmark: Result SQL preview: %s",
|
|
310
|
+
rewritten_sql[:200] if len(rewritten_sql) > 200 else rewritten_sql,
|
|
311
|
+
)
|
|
312
|
+
logger.debug(
|
|
313
|
+
"Converted pyformat to qmark: params=%s, positional=%s",
|
|
314
|
+
list(param_dict.keys()),
|
|
315
|
+
positional_params,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return rewritten_sql, positional_params, param_names
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def detect_and_convert_parameters(
|
|
322
|
+
sql: str, parameters: Union[None, Tuple, List, Dict]
|
|
323
|
+
) -> Tuple[str, Union[None, Tuple, List]]:
|
|
324
|
+
"""
|
|
325
|
+
Auto-detect parameter style and convert to qmark if needed.
|
|
326
|
+
|
|
327
|
+
Detects parameter style based on the type of parameters:
|
|
328
|
+
- None: No parameters
|
|
329
|
+
- Tuple/List: qmark style (?) - pass through unchanged
|
|
330
|
+
- Dict: pyformat style (%(name)s) - convert to qmark
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
sql: SQL query string
|
|
334
|
+
parameters: Parameters in any supported format
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Tuple of (sql, parameters) where parameters are in qmark format
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
TypeError: If parameters type doesn't match placeholders in SQL
|
|
341
|
+
KeyError: If required pyformat parameter is missing
|
|
342
|
+
|
|
343
|
+
Examples:
|
|
344
|
+
>>> detect_and_convert_parameters(
|
|
345
|
+
... "SELECT * FROM users WHERE id = ?",
|
|
346
|
+
... (42,)
|
|
347
|
+
... )
|
|
348
|
+
("SELECT * FROM users WHERE id = ?", (42,))
|
|
349
|
+
|
|
350
|
+
>>> detect_and_convert_parameters(
|
|
351
|
+
... "SELECT * FROM users WHERE id = %(id)s",
|
|
352
|
+
... {"id": 42}
|
|
353
|
+
... )
|
|
354
|
+
("SELECT * FROM users WHERE id = ?", (42,))
|
|
355
|
+
"""
|
|
356
|
+
logger.debug(
|
|
357
|
+
"detect_and_convert_parameters: Starting - sql_length=%d, parameters_type=%s",
|
|
358
|
+
len(sql),
|
|
359
|
+
type(parameters).__name__ if parameters is not None else "None",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# No parameters
|
|
363
|
+
if parameters is None:
|
|
364
|
+
logger.debug("detect_and_convert_parameters: No parameters provided - returning as-is")
|
|
365
|
+
return sql, None
|
|
366
|
+
|
|
367
|
+
# Qmark style - tuple or list
|
|
368
|
+
if isinstance(parameters, (tuple, list)):
|
|
369
|
+
logger.debug(
|
|
370
|
+
"detect_and_convert_parameters: Detected qmark-style parameters (%s) - count=%d",
|
|
371
|
+
type(parameters).__name__,
|
|
372
|
+
len(parameters),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Check if SQL has pyformat placeholders
|
|
376
|
+
param_names = parse_pyformat_params(sql) if "%(" in sql else []
|
|
377
|
+
if param_names:
|
|
378
|
+
logger.error(
|
|
379
|
+
"detect_and_convert_parameters: Parameter style mismatch - SQL has pyformat placeholders %s but received %s",
|
|
380
|
+
param_names,
|
|
381
|
+
type(parameters).__name__,
|
|
382
|
+
)
|
|
383
|
+
# SQL has %(name)s but user passed tuple/list
|
|
384
|
+
raise TypeError(
|
|
385
|
+
f"Parameter style mismatch: query uses named placeholders (%(name)s), "
|
|
386
|
+
f"but {type(parameters).__name__} was provided. "
|
|
387
|
+
f"Use dict for named parameters. Example: "
|
|
388
|
+
f'cursor.execute(sql, {{"param1": value1, "param2": value2}})'
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Valid qmark style - pass through
|
|
392
|
+
logger.debug("detect_and_convert_parameters: Valid qmark style - passing through unchanged")
|
|
393
|
+
return sql, parameters
|
|
394
|
+
|
|
395
|
+
# Pyformat style - dict
|
|
396
|
+
if isinstance(parameters, dict):
|
|
397
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
398
|
+
logger.debug(
|
|
399
|
+
"detect_and_convert_parameters: Detected pyformat-style parameters (dict) - count=%d, keys=%s",
|
|
400
|
+
len(parameters),
|
|
401
|
+
list(parameters.keys()),
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Check if SQL appears to have qmark placeholders
|
|
405
|
+
# Fast short-circuit: skip the O(n) context-aware scan if no ? exists at all
|
|
406
|
+
# Then use context-aware check that ignores ? inside brackets, quotes, and comments
|
|
407
|
+
# RUST-PORT: a dict with both ? and %(name)s is also a style mismatch
|
|
408
|
+
if "?" in sql and _has_unquoted_question_marks(sql) and parse_pyformat_params(sql):
|
|
409
|
+
raise ParameterStyleMismatchError(
|
|
410
|
+
"Parameter style mismatch: query mixes named placeholders (%(name)s) "
|
|
411
|
+
"and positional placeholders (?). Use one style.",
|
|
412
|
+
"",
|
|
413
|
+
)
|
|
414
|
+
if "?" in sql and _has_unquoted_question_marks(sql):
|
|
415
|
+
logger.error(
|
|
416
|
+
"detect_and_convert_parameters: Parameter style mismatch - SQL has ? placeholders but received dict"
|
|
417
|
+
)
|
|
418
|
+
# SQL has ? but user passed dict and no %(name)s found
|
|
419
|
+
raise TypeError(
|
|
420
|
+
f"Parameter style mismatch: query uses positional placeholders (?), "
|
|
421
|
+
f"but dict was provided. "
|
|
422
|
+
f"Use tuple/list for positional parameters. Example: "
|
|
423
|
+
f"cursor.execute(sql, (value1, value2))"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
logger.debug("detect_and_convert_parameters: Valid pyformat style - converting to qmark")
|
|
427
|
+
# Convert pyformat to qmark
|
|
428
|
+
converted_sql, qmark_params = convert_pyformat_to_qmark(sql, parameters)
|
|
429
|
+
logger.debug(
|
|
430
|
+
"detect_and_convert_parameters: Conversion complete - qmark_param_count=%d",
|
|
431
|
+
len(qmark_params) if qmark_params else 0,
|
|
432
|
+
)
|
|
433
|
+
return converted_sql, qmark_params
|
|
434
|
+
|
|
435
|
+
# Unsupported type
|
|
436
|
+
logger.error(
|
|
437
|
+
"detect_and_convert_parameters: Unsupported parameter type - %s",
|
|
438
|
+
type(parameters).__name__,
|
|
439
|
+
)
|
|
440
|
+
raise TypeError(
|
|
441
|
+
f"Parameters must be tuple, list, dict, or None. " f"Got {type(parameters).__name__}"
|
|
442
|
+
)
|