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.
Files changed (45) hide show
  1. arrowsql/__init__.py +653 -0
  2. arrowsql/_rust_auth.py +170 -0
  3. arrowsql/_server_sqlstate.py +11 -0
  4. arrowsql/_utf8_strings.py +78 -0
  5. arrowsql/agent.py +620 -0
  6. arrowsql/aio.py +533 -0
  7. arrowsql/arrow_parallel.py +464 -0
  8. arrowsql/async_query/__init__.py +40 -0
  9. arrowsql/async_query/_connection_context.py +39 -0
  10. arrowsql/async_query/_native.py +24 -0
  11. arrowsql/async_query/async_connection.py +160 -0
  12. arrowsql/async_query/async_cursor.py +225 -0
  13. arrowsql/async_query/async_execute.py +113 -0
  14. arrowsql/async_query/async_fetch.py +153 -0
  15. arrowsql/async_query/exception_translator.py +132 -0
  16. arrowsql/auth.py +913 -0
  17. arrowsql/bulk_parallel.py +385 -0
  18. arrowsql/connection.py +2377 -0
  19. arrowsql/connection_string_builder.py +114 -0
  20. arrowsql/connection_string_parser.py +417 -0
  21. arrowsql/constants.py +701 -0
  22. arrowsql/cursor.py +4516 -0
  23. arrowsql/db_connection.py +98 -0
  24. arrowsql/ddbc_bindings.cp313-win_amd64.pyd +0 -0
  25. arrowsql/ddbc_bindings.pyi +283 -0
  26. arrowsql/decimal_config.py +98 -0
  27. arrowsql/exceptions.py +378 -0
  28. arrowsql/helpers.py +436 -0
  29. arrowsql/logging.py +700 -0
  30. arrowsql/mssql_python.pyi +579 -0
  31. arrowsql/odbc_provider.py +269 -0
  32. arrowsql/parameter_helper.py +442 -0
  33. arrowsql/perf_timer.py +286 -0
  34. arrowsql/pooling.py +147 -0
  35. arrowsql/py.typed +1 -0
  36. arrowsql/row.py +462 -0
  37. arrowsql/type.py +160 -0
  38. arrowsql-0.1.0.dist-info/METADATA +146 -0
  39. arrowsql-0.1.0.dist-info/RECORD +45 -0
  40. arrowsql-0.1.0.dist-info/WHEEL +4 -0
  41. arrowsql-0.1.0.dist-info/entry_points.txt +2 -0
  42. arrowsql-0.1.0.dist-info/licenses/LICENSE +21 -0
  43. arrowsql-0.1.0.dist-info/licenses/LICENSE.upstream-mssql-python +47 -0
  44. arrowsql-0.1.0.dist-info/sboms/ddbc_bindings.cyclonedx.json +5195 -0
  45. 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
+ )