sqlspec 0.16.1__cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (148) hide show
  1. 51ff5a9eadfdefd49f98__mypyc.cpython-310-aarch64-linux-gnu.so +0 -0
  2. sqlspec/__init__.py +92 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +77 -0
  6. sqlspec/_sql.py +1780 -0
  7. sqlspec/_typing.py +680 -0
  8. sqlspec/adapters/__init__.py +0 -0
  9. sqlspec/adapters/adbc/__init__.py +5 -0
  10. sqlspec/adapters/adbc/_types.py +12 -0
  11. sqlspec/adapters/adbc/config.py +361 -0
  12. sqlspec/adapters/adbc/driver.py +512 -0
  13. sqlspec/adapters/aiosqlite/__init__.py +19 -0
  14. sqlspec/adapters/aiosqlite/_types.py +13 -0
  15. sqlspec/adapters/aiosqlite/config.py +253 -0
  16. sqlspec/adapters/aiosqlite/driver.py +248 -0
  17. sqlspec/adapters/asyncmy/__init__.py +19 -0
  18. sqlspec/adapters/asyncmy/_types.py +12 -0
  19. sqlspec/adapters/asyncmy/config.py +180 -0
  20. sqlspec/adapters/asyncmy/driver.py +274 -0
  21. sqlspec/adapters/asyncpg/__init__.py +21 -0
  22. sqlspec/adapters/asyncpg/_types.py +17 -0
  23. sqlspec/adapters/asyncpg/config.py +229 -0
  24. sqlspec/adapters/asyncpg/driver.py +344 -0
  25. sqlspec/adapters/bigquery/__init__.py +18 -0
  26. sqlspec/adapters/bigquery/_types.py +12 -0
  27. sqlspec/adapters/bigquery/config.py +298 -0
  28. sqlspec/adapters/bigquery/driver.py +558 -0
  29. sqlspec/adapters/duckdb/__init__.py +22 -0
  30. sqlspec/adapters/duckdb/_types.py +12 -0
  31. sqlspec/adapters/duckdb/config.py +504 -0
  32. sqlspec/adapters/duckdb/driver.py +368 -0
  33. sqlspec/adapters/oracledb/__init__.py +32 -0
  34. sqlspec/adapters/oracledb/_types.py +14 -0
  35. sqlspec/adapters/oracledb/config.py +317 -0
  36. sqlspec/adapters/oracledb/driver.py +538 -0
  37. sqlspec/adapters/psqlpy/__init__.py +16 -0
  38. sqlspec/adapters/psqlpy/_types.py +11 -0
  39. sqlspec/adapters/psqlpy/config.py +214 -0
  40. sqlspec/adapters/psqlpy/driver.py +530 -0
  41. sqlspec/adapters/psycopg/__init__.py +32 -0
  42. sqlspec/adapters/psycopg/_types.py +17 -0
  43. sqlspec/adapters/psycopg/config.py +426 -0
  44. sqlspec/adapters/psycopg/driver.py +796 -0
  45. sqlspec/adapters/sqlite/__init__.py +15 -0
  46. sqlspec/adapters/sqlite/_types.py +11 -0
  47. sqlspec/adapters/sqlite/config.py +240 -0
  48. sqlspec/adapters/sqlite/driver.py +294 -0
  49. sqlspec/base.py +571 -0
  50. sqlspec/builder/__init__.py +62 -0
  51. sqlspec/builder/_base.py +473 -0
  52. sqlspec/builder/_column.py +320 -0
  53. sqlspec/builder/_ddl.py +1346 -0
  54. sqlspec/builder/_ddl_utils.py +103 -0
  55. sqlspec/builder/_delete.py +76 -0
  56. sqlspec/builder/_insert.py +256 -0
  57. sqlspec/builder/_merge.py +71 -0
  58. sqlspec/builder/_parsing_utils.py +140 -0
  59. sqlspec/builder/_select.py +170 -0
  60. sqlspec/builder/_update.py +188 -0
  61. sqlspec/builder/mixins/__init__.py +55 -0
  62. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  63. sqlspec/builder/mixins/_delete_operations.py +41 -0
  64. sqlspec/builder/mixins/_insert_operations.py +244 -0
  65. sqlspec/builder/mixins/_join_operations.py +122 -0
  66. sqlspec/builder/mixins/_merge_operations.py +476 -0
  67. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  68. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  69. sqlspec/builder/mixins/_select_operations.py +603 -0
  70. sqlspec/builder/mixins/_update_operations.py +187 -0
  71. sqlspec/builder/mixins/_where_clause.py +621 -0
  72. sqlspec/cli.py +247 -0
  73. sqlspec/config.py +395 -0
  74. sqlspec/core/__init__.py +63 -0
  75. sqlspec/core/cache.cpython-310-aarch64-linux-gnu.so +0 -0
  76. sqlspec/core/cache.py +871 -0
  77. sqlspec/core/compiler.cpython-310-aarch64-linux-gnu.so +0 -0
  78. sqlspec/core/compiler.py +417 -0
  79. sqlspec/core/filters.cpython-310-aarch64-linux-gnu.so +0 -0
  80. sqlspec/core/filters.py +830 -0
  81. sqlspec/core/hashing.cpython-310-aarch64-linux-gnu.so +0 -0
  82. sqlspec/core/hashing.py +310 -0
  83. sqlspec/core/parameters.cpython-310-aarch64-linux-gnu.so +0 -0
  84. sqlspec/core/parameters.py +1237 -0
  85. sqlspec/core/result.cpython-310-aarch64-linux-gnu.so +0 -0
  86. sqlspec/core/result.py +677 -0
  87. sqlspec/core/splitter.cpython-310-aarch64-linux-gnu.so +0 -0
  88. sqlspec/core/splitter.py +819 -0
  89. sqlspec/core/statement.cpython-310-aarch64-linux-gnu.so +0 -0
  90. sqlspec/core/statement.py +676 -0
  91. sqlspec/driver/__init__.py +19 -0
  92. sqlspec/driver/_async.py +502 -0
  93. sqlspec/driver/_common.py +631 -0
  94. sqlspec/driver/_sync.py +503 -0
  95. sqlspec/driver/mixins/__init__.py +6 -0
  96. sqlspec/driver/mixins/_result_tools.py +193 -0
  97. sqlspec/driver/mixins/_sql_translator.py +86 -0
  98. sqlspec/exceptions.py +193 -0
  99. sqlspec/extensions/__init__.py +0 -0
  100. sqlspec/extensions/aiosql/__init__.py +10 -0
  101. sqlspec/extensions/aiosql/adapter.py +461 -0
  102. sqlspec/extensions/litestar/__init__.py +6 -0
  103. sqlspec/extensions/litestar/_utils.py +52 -0
  104. sqlspec/extensions/litestar/cli.py +48 -0
  105. sqlspec/extensions/litestar/config.py +92 -0
  106. sqlspec/extensions/litestar/handlers.py +260 -0
  107. sqlspec/extensions/litestar/plugin.py +145 -0
  108. sqlspec/extensions/litestar/providers.py +454 -0
  109. sqlspec/loader.cpython-310-aarch64-linux-gnu.so +0 -0
  110. sqlspec/loader.py +760 -0
  111. sqlspec/migrations/__init__.py +35 -0
  112. sqlspec/migrations/base.py +414 -0
  113. sqlspec/migrations/commands.py +443 -0
  114. sqlspec/migrations/loaders.py +402 -0
  115. sqlspec/migrations/runner.py +213 -0
  116. sqlspec/migrations/tracker.py +140 -0
  117. sqlspec/migrations/utils.py +129 -0
  118. sqlspec/protocols.py +407 -0
  119. sqlspec/py.typed +0 -0
  120. sqlspec/storage/__init__.py +23 -0
  121. sqlspec/storage/backends/__init__.py +0 -0
  122. sqlspec/storage/backends/base.py +163 -0
  123. sqlspec/storage/backends/fsspec.py +386 -0
  124. sqlspec/storage/backends/obstore.py +459 -0
  125. sqlspec/storage/capabilities.py +102 -0
  126. sqlspec/storage/registry.py +239 -0
  127. sqlspec/typing.py +299 -0
  128. sqlspec/utils/__init__.py +3 -0
  129. sqlspec/utils/correlation.py +150 -0
  130. sqlspec/utils/deprecation.py +106 -0
  131. sqlspec/utils/fixtures.cpython-310-aarch64-linux-gnu.so +0 -0
  132. sqlspec/utils/fixtures.py +58 -0
  133. sqlspec/utils/logging.py +127 -0
  134. sqlspec/utils/module_loader.py +89 -0
  135. sqlspec/utils/serializers.py +4 -0
  136. sqlspec/utils/singleton.py +32 -0
  137. sqlspec/utils/sync_tools.cpython-310-aarch64-linux-gnu.so +0 -0
  138. sqlspec/utils/sync_tools.py +237 -0
  139. sqlspec/utils/text.cpython-310-aarch64-linux-gnu.so +0 -0
  140. sqlspec/utils/text.py +96 -0
  141. sqlspec/utils/type_guards.cpython-310-aarch64-linux-gnu.so +0 -0
  142. sqlspec/utils/type_guards.py +1139 -0
  143. sqlspec-0.16.1.dist-info/METADATA +365 -0
  144. sqlspec-0.16.1.dist-info/RECORD +148 -0
  145. sqlspec-0.16.1.dist-info/WHEEL +7 -0
  146. sqlspec-0.16.1.dist-info/entry_points.txt +2 -0
  147. sqlspec-0.16.1.dist-info/licenses/LICENSE +21 -0
  148. sqlspec-0.16.1.dist-info/licenses/NOTICE +29 -0
@@ -0,0 +1,1237 @@
1
+ """Parameter processing system for SQL statements.
2
+
3
+ This module implements parameter processing including type conversion,
4
+ style conversion, and validation for SQL statements.
5
+
6
+ Components:
7
+ - ParameterStyle enum: Supported parameter styles
8
+ - TypedParameter: Preserves type information through processing
9
+ - ParameterInfo: Tracks parameter metadata
10
+ - ParameterValidator: Extracts and validates parameters
11
+ - ParameterConverter: Handles parameter style conversions
12
+ - ParameterProcessor: High-level coordinator with caching
13
+ - ParameterStyleConfig: Configuration for parameter processing
14
+
15
+ Features:
16
+ - Two-phase processing: SQLGlot compatibility and execution format
17
+ - Type-specific parameter wrapping
18
+ - Parameter style conversions
19
+ - Caching system for parameter extraction and conversion
20
+ - Support for multiple parameter styles and database adapters
21
+ """
22
+
23
+ import re
24
+ from collections.abc import Mapping, Sequence
25
+ from datetime import date, datetime
26
+ from decimal import Decimal
27
+ from enum import Enum
28
+ from functools import singledispatch
29
+ from typing import Any, Callable, Optional
30
+
31
+ from mypy_extensions import mypyc_attr
32
+
33
+ __all__ = (
34
+ "ParameterConverter",
35
+ "ParameterInfo",
36
+ "ParameterProcessor",
37
+ "ParameterStyle",
38
+ "ParameterStyleConfig",
39
+ "ParameterValidator",
40
+ "TypedParameter",
41
+ "is_iterable_parameters",
42
+ "wrap_with_type",
43
+ )
44
+
45
+
46
+ _PARAMETER_REGEX = re.compile(
47
+ r"""
48
+ (?P<dquote>"(?:[^"\\]|\\.)*") |
49
+ (?P<squote>'(?:[^'\\]|\\.)*') |
50
+ (?P<dollar_quoted_string>\$(?P<dollar_quote_tag_inner>\w*)?\$[\s\S]*?\$\4\$) |
51
+ (?P<line_comment>--[^\r\n]*) |
52
+ (?P<block_comment>/\*(?:[^*]|\*(?!/))*\*/) |
53
+ (?P<pg_q_operator>\?\?|\?\||\?&) |
54
+ (?P<pg_cast>::(?P<cast_type>\w+)) |
55
+ (?P<pyformat_named>%\((?P<pyformat_name>\w+)\)s) |
56
+ (?P<pyformat_pos>%s) |
57
+ (?P<positional_colon>:(?P<colon_num>\d+)) |
58
+ (?P<named_colon>:(?P<colon_name>\w+)) |
59
+ (?P<named_at>@(?P<at_name>\w+)) |
60
+ (?P<numeric>\$(?P<numeric_num>\d+)) |
61
+ (?P<named_dollar_param>\$(?P<dollar_param_name>\w+)) |
62
+ (?P<qmark>\?)
63
+ """,
64
+ re.VERBOSE | re.IGNORECASE | re.MULTILINE | re.DOTALL,
65
+ )
66
+
67
+
68
+ class ParameterStyle(str, Enum):
69
+ """Parameter style enumeration.
70
+
71
+ Supported parameter styles:
72
+ - QMARK: ? placeholders
73
+ - NUMERIC: $1, $2 placeholders
74
+ - POSITIONAL_PYFORMAT: %s placeholders
75
+ - NAMED_PYFORMAT: %(name)s placeholders
76
+ - NAMED_COLON: :name placeholders
77
+ - NAMED_AT: @name placeholders
78
+ - NAMED_DOLLAR: $name placeholders
79
+ - POSITIONAL_COLON: :1, :2 placeholders
80
+ - STATIC: Direct embedding of values in SQL
81
+ - NONE: No parameters supported
82
+ """
83
+
84
+ NONE = "none"
85
+ STATIC = "static"
86
+ QMARK = "qmark"
87
+ NUMERIC = "numeric"
88
+ NAMED_COLON = "named_colon"
89
+ POSITIONAL_COLON = "positional_colon"
90
+ NAMED_AT = "named_at"
91
+ NAMED_DOLLAR = "named_dollar"
92
+ NAMED_PYFORMAT = "pyformat_named"
93
+ POSITIONAL_PYFORMAT = "pyformat_positional"
94
+
95
+
96
+ @mypyc_attr(allow_interpreted_subclasses=True)
97
+ class TypedParameter:
98
+ """Parameter wrapper that preserves type information.
99
+
100
+ Maintains type information through SQLGlot parsing and execution
101
+ format conversion.
102
+
103
+ Use Cases:
104
+ - Preserve boolean values through SQLGlot parsing
105
+ - Maintain Decimal precision
106
+ - Handle date/datetime formatting
107
+ - Preserve array/list structures
108
+ - Handle JSON serialization for dict parameters
109
+ """
110
+
111
+ __slots__ = ("_hash", "original_type", "semantic_name", "value")
112
+
113
+ def __init__(self, value: Any, original_type: Optional[type] = None, semantic_name: Optional[str] = None) -> None:
114
+ """Initialize typed parameter wrapper.
115
+
116
+ Args:
117
+ value: The parameter value
118
+ original_type: Original type (defaults to type(value))
119
+ semantic_name: Optional semantic name for debugging
120
+ """
121
+ self.value = value
122
+ self.original_type = original_type or type(value)
123
+ self.semantic_name = semantic_name
124
+ self._hash: Optional[int] = None
125
+
126
+ def __hash__(self) -> int:
127
+ """Cached hash value with optimization."""
128
+ if self._hash is None:
129
+ # Optimize by avoiding tuple creation for common case
130
+ value_id = id(self.value)
131
+ self._hash = hash((value_id, self.original_type, self.semantic_name))
132
+ return self._hash
133
+
134
+ def __eq__(self, other: object) -> bool:
135
+ """Equality comparison for TypedParameter instances."""
136
+ if not isinstance(other, TypedParameter):
137
+ return False
138
+ return (
139
+ self.value == other.value
140
+ and self.original_type == other.original_type
141
+ and self.semantic_name == other.semantic_name
142
+ )
143
+
144
+ def __repr__(self) -> str:
145
+ """String representation for debugging."""
146
+ name_part = f", semantic_name='{self.semantic_name}'" if self.semantic_name else ""
147
+ return f"TypedParameter({self.value!r}, original_type={self.original_type.__name__}{name_part})"
148
+
149
+
150
+ @singledispatch
151
+ def _wrap_parameter_by_type(value: Any, semantic_name: Optional[str] = None) -> Any:
152
+ """Type-specific parameter wrapping using singledispatch.
153
+
154
+ Args:
155
+ value: Parameter value to potentially wrap
156
+ semantic_name: Optional semantic name for debugging
157
+
158
+ Returns:
159
+ Either the original value or TypedParameter wrapper
160
+ """
161
+ return value
162
+
163
+
164
+ @_wrap_parameter_by_type.register
165
+ def _(value: bool, semantic_name: Optional[str] = None) -> TypedParameter:
166
+ """Wrap boolean values to prevent SQLGlot parsing issues."""
167
+ return TypedParameter(value, bool, semantic_name)
168
+
169
+
170
+ @_wrap_parameter_by_type.register
171
+ def _(value: Decimal, semantic_name: Optional[str] = None) -> TypedParameter:
172
+ """Wrap Decimal values to preserve precision."""
173
+ return TypedParameter(value, Decimal, semantic_name)
174
+
175
+
176
+ @_wrap_parameter_by_type.register
177
+ def _(value: datetime, semantic_name: Optional[str] = None) -> TypedParameter:
178
+ """Wrap datetime values for database-specific formatting."""
179
+ return TypedParameter(value, datetime, semantic_name)
180
+
181
+
182
+ @_wrap_parameter_by_type.register
183
+ def _(value: date, semantic_name: Optional[str] = None) -> TypedParameter:
184
+ """Wrap date values for database-specific formatting."""
185
+ return TypedParameter(value, date, semantic_name)
186
+
187
+
188
+ @_wrap_parameter_by_type.register
189
+ def _(value: bytes, semantic_name: Optional[str] = None) -> TypedParameter:
190
+ """Wrap bytes values to prevent string conversion issues in ADBC/Arrow."""
191
+ return TypedParameter(value, bytes, semantic_name)
192
+
193
+
194
+ @mypyc_attr(allow_interpreted_subclasses=False)
195
+ class ParameterInfo:
196
+ """Information about a detected parameter in SQL.
197
+
198
+ Tracks parameter metadata for conversion:
199
+ - name: Parameter name (for named styles)
200
+ - style: Parameter style
201
+ - position: Character position in SQL string
202
+ - ordinal: Order of appearance (0-indexed)
203
+ - placeholder_text: Original text in SQL
204
+ """
205
+
206
+ __slots__ = ("name", "ordinal", "placeholder_text", "position", "style")
207
+
208
+ def __init__(
209
+ self, name: Optional[str], style: ParameterStyle, position: int, ordinal: int, placeholder_text: str
210
+ ) -> None:
211
+ """Initialize parameter information.
212
+
213
+ Args:
214
+ name: Parameter name (None for positional styles)
215
+ style: Parameter style enum
216
+ position: Character position in SQL
217
+ ordinal: Order of appearance (0-indexed)
218
+ placeholder_text: Original placeholder text
219
+ """
220
+ self.name = name
221
+ self.style = style
222
+ self.position = position
223
+ self.ordinal = ordinal
224
+ self.placeholder_text = placeholder_text
225
+
226
+ def __repr__(self) -> str:
227
+ """String representation for debugging."""
228
+ return (
229
+ f"ParameterInfo(name={self.name!r}, style={self.style!r}, "
230
+ f"position={self.position}, ordinal={self.ordinal}, "
231
+ f"placeholder_text={self.placeholder_text!r})"
232
+ )
233
+
234
+
235
+ @mypyc_attr(allow_interpreted_subclasses=False)
236
+ class ParameterStyleConfig:
237
+ """Configuration for parameter style processing.
238
+
239
+ Provides configuration for parameter processing including:
240
+ - default_parameter_style: Primary parsing style
241
+ - supported_parameter_styles: All input styles supported
242
+ - supported_execution_parameter_styles: Styles driver can execute
243
+ - default_execution_parameter_style: Target execution format
244
+ - type_coercion_map: Type conversions
245
+ - output_transformer: Final SQL/parameter transformation hook
246
+ - preserve_parameter_format: Maintain original parameter structure
247
+ - needs_static_script_compilation: Embed parameters in SQL
248
+ """
249
+
250
+ __slots__ = (
251
+ "allow_mixed_parameter_styles",
252
+ "ast_transformer",
253
+ "default_execution_parameter_style",
254
+ "default_parameter_style",
255
+ "has_native_list_expansion",
256
+ "needs_static_script_compilation",
257
+ "output_transformer",
258
+ "preserve_original_params_for_many",
259
+ "preserve_parameter_format",
260
+ "supported_execution_parameter_styles",
261
+ "supported_parameter_styles",
262
+ "type_coercion_map",
263
+ )
264
+
265
+ def __init__(
266
+ self,
267
+ default_parameter_style: ParameterStyle,
268
+ supported_parameter_styles: Optional[set[ParameterStyle]] = None,
269
+ supported_execution_parameter_styles: Optional[set[ParameterStyle]] = None,
270
+ default_execution_parameter_style: Optional[ParameterStyle] = None,
271
+ type_coercion_map: Optional[dict[type, Callable[[Any], Any]]] = None,
272
+ has_native_list_expansion: bool = False,
273
+ needs_static_script_compilation: bool = False,
274
+ allow_mixed_parameter_styles: bool = False,
275
+ preserve_parameter_format: bool = True,
276
+ preserve_original_params_for_many: bool = False,
277
+ output_transformer: Optional[Callable[[str, Any], tuple[str, Any]]] = None,
278
+ ast_transformer: Optional[Callable[[Any, Any], tuple[Any, Any]]] = None,
279
+ ) -> None:
280
+ """Initialize with complete compatibility.
281
+
282
+ Args:
283
+ default_parameter_style: Primary parameter style for parsing
284
+ supported_parameter_styles: All input styles this config supports
285
+ supported_execution_parameter_styles: Styles driver can execute
286
+ default_execution_parameter_style: Target format for execution
287
+ type_coercion_map: Driver-specific type conversions
288
+ has_native_list_expansion: Driver supports native array parameters
289
+ output_transformer: Final transformation hook
290
+ needs_static_script_compilation: Embed parameters directly in SQL
291
+ allow_mixed_parameter_styles: Support mixed styles in single query
292
+ preserve_parameter_format: Maintain original parameter structure
293
+ preserve_original_params_for_many: Return original list of tuples for execute_many
294
+ ast_transformer: AST-based transformation hook for advanced SQL/parameter manipulation
295
+ """
296
+ self.default_parameter_style = default_parameter_style
297
+ self.supported_parameter_styles = (
298
+ supported_parameter_styles if supported_parameter_styles is not None else {default_parameter_style}
299
+ )
300
+ self.supported_execution_parameter_styles = supported_execution_parameter_styles
301
+ self.default_execution_parameter_style = default_execution_parameter_style or default_parameter_style
302
+ self.type_coercion_map = type_coercion_map or {}
303
+ self.has_native_list_expansion = has_native_list_expansion
304
+ self.output_transformer = output_transformer
305
+ self.ast_transformer = ast_transformer
306
+ self.needs_static_script_compilation = needs_static_script_compilation
307
+ self.allow_mixed_parameter_styles = allow_mixed_parameter_styles
308
+ self.preserve_parameter_format = preserve_parameter_format
309
+ self.preserve_original_params_for_many = preserve_original_params_for_many
310
+
311
+ def hash(self) -> int:
312
+ """Generate hash for cache key generation.
313
+
314
+ Returns:
315
+ Hash value for cache key generation
316
+ """
317
+ hash_components = (
318
+ self.default_parameter_style.value,
319
+ frozenset(s.value for s in self.supported_parameter_styles),
320
+ (
321
+ frozenset(s.value for s in self.supported_execution_parameter_styles)
322
+ if self.supported_execution_parameter_styles
323
+ else None
324
+ ),
325
+ self.default_execution_parameter_style.value,
326
+ tuple(sorted(self.type_coercion_map.keys(), key=str)) if self.type_coercion_map else None,
327
+ self.has_native_list_expansion,
328
+ self.preserve_original_params_for_many,
329
+ bool(self.output_transformer),
330
+ self.needs_static_script_compilation,
331
+ self.allow_mixed_parameter_styles,
332
+ self.preserve_parameter_format,
333
+ bool(self.ast_transformer),
334
+ )
335
+ return hash(hash_components)
336
+
337
+
338
+ @mypyc_attr(allow_interpreted_subclasses=False)
339
+ class ParameterValidator:
340
+ """Parameter validation and extraction.
341
+
342
+ Extracts parameter information from SQL strings and determines
343
+ SQLGlot compatibility.
344
+
345
+ Features:
346
+ - Cached parameter extraction results
347
+ - Regex-based parameter detection
348
+ - Dialect-specific compatibility checking
349
+ """
350
+
351
+ __slots__ = ("_parameter_cache",)
352
+
353
+ def __init__(self) -> None:
354
+ """Initialize validator with parameter cache."""
355
+ self._parameter_cache: dict[str, list[ParameterInfo]] = {}
356
+
357
+ def extract_parameters(self, sql: str) -> "list[ParameterInfo]":
358
+ """Extract all parameters from SQL.
359
+
360
+ Args:
361
+ sql: SQL string to analyze
362
+
363
+ Returns:
364
+ List of ParameterInfo objects for each detected parameter
365
+ """
366
+ cached_result = self._parameter_cache.get(sql)
367
+ if cached_result is not None:
368
+ return cached_result
369
+
370
+ parameters: list[ParameterInfo] = []
371
+ ordinal = 0
372
+
373
+ for match in _PARAMETER_REGEX.finditer(sql):
374
+ # Fast rejection of comments and quotes
375
+ if (
376
+ match.group("dquote")
377
+ or match.group("squote")
378
+ or match.group("dollar_quoted_string")
379
+ or match.group("line_comment")
380
+ or match.group("block_comment")
381
+ or match.group("pg_q_operator")
382
+ or match.group("pg_cast")
383
+ ):
384
+ continue
385
+
386
+ position = match.start()
387
+ placeholder_text = match.group(0)
388
+ name: Optional[str] = None
389
+ style: Optional[ParameterStyle] = None
390
+
391
+ # Optimize with elif chain for better branch prediction
392
+ pyformat_named = match.group("pyformat_named")
393
+ if pyformat_named:
394
+ style = ParameterStyle.NAMED_PYFORMAT
395
+ name = match.group("pyformat_name")
396
+ else:
397
+ pyformat_pos = match.group("pyformat_pos")
398
+ if pyformat_pos:
399
+ style = ParameterStyle.POSITIONAL_PYFORMAT
400
+ else:
401
+ positional_colon = match.group("positional_colon")
402
+ if positional_colon:
403
+ style = ParameterStyle.POSITIONAL_COLON
404
+ name = match.group("colon_num")
405
+ else:
406
+ named_colon = match.group("named_colon")
407
+ if named_colon:
408
+ style = ParameterStyle.NAMED_COLON
409
+ name = match.group("colon_name")
410
+ else:
411
+ named_at = match.group("named_at")
412
+ if named_at:
413
+ style = ParameterStyle.NAMED_AT
414
+ name = match.group("at_name")
415
+ else:
416
+ numeric = match.group("numeric")
417
+ if numeric:
418
+ style = ParameterStyle.NUMERIC
419
+ name = match.group("numeric_num")
420
+ else:
421
+ named_dollar_param = match.group("named_dollar_param")
422
+ if named_dollar_param:
423
+ style = ParameterStyle.NAMED_DOLLAR
424
+ name = match.group("dollar_param_name")
425
+ elif match.group("qmark"):
426
+ style = ParameterStyle.QMARK
427
+
428
+ if style is not None:
429
+ parameters.append(
430
+ ParameterInfo(
431
+ name=name, style=style, position=position, ordinal=ordinal, placeholder_text=placeholder_text
432
+ )
433
+ )
434
+ ordinal += 1
435
+
436
+ self._parameter_cache[sql] = parameters
437
+ return parameters
438
+
439
+ def get_sqlglot_incompatible_styles(self, dialect: Optional[str] = None) -> "set[ParameterStyle]":
440
+ """Get parameter styles incompatible with SQLGlot for dialect.
441
+
442
+ Args:
443
+ dialect: SQL dialect for compatibility checking
444
+
445
+ Returns:
446
+ Set of parameter styles incompatible with SQLGlot
447
+ """
448
+ base_incompatible = {
449
+ ParameterStyle.POSITIONAL_PYFORMAT, # %s, %d - modulo operator conflict
450
+ ParameterStyle.NAMED_PYFORMAT, # %(name)s - complex format string
451
+ ParameterStyle.POSITIONAL_COLON, # :1, :2 - numbered colon parameters
452
+ }
453
+
454
+ if dialect and dialect.lower() in {"mysql", "mariadb"}:
455
+ return base_incompatible
456
+ if dialect and dialect.lower() in {"postgres", "postgresql"}:
457
+ return {ParameterStyle.POSITIONAL_COLON}
458
+ if dialect and dialect.lower() == "sqlite":
459
+ return {ParameterStyle.POSITIONAL_COLON}
460
+ if dialect and dialect.lower() in {"oracle", "bigquery"}:
461
+ return base_incompatible
462
+ return base_incompatible
463
+
464
+
465
+ @mypyc_attr(allow_interpreted_subclasses=False)
466
+ class ParameterConverter:
467
+ """Parameter style conversion.
468
+
469
+ Handles two-phase parameter processing:
470
+ - Phase 1: SQLGlot compatibility normalization
471
+ - Phase 2: Execution format conversion
472
+
473
+ Features:
474
+ - Converts incompatible styles to canonical format
475
+ - Enables SQLGlot parsing of problematic parameter styles
476
+ - Handles parameter format changes (list ↔ dict, positional ↔ named)
477
+ """
478
+
479
+ __slots__ = ("_format_converters", "_placeholder_generators", "validator")
480
+
481
+ def __init__(self) -> None:
482
+ """Initialize converter with lookup tables."""
483
+ self.validator = ParameterValidator()
484
+
485
+ self._format_converters = {
486
+ ParameterStyle.POSITIONAL_COLON: self._convert_to_positional_colon_format,
487
+ ParameterStyle.NAMED_COLON: self._convert_to_named_colon_format,
488
+ ParameterStyle.NAMED_PYFORMAT: self._convert_to_named_pyformat_format,
489
+ ParameterStyle.QMARK: self._convert_to_positional_format,
490
+ ParameterStyle.NUMERIC: self._convert_to_positional_format,
491
+ ParameterStyle.POSITIONAL_PYFORMAT: self._convert_to_positional_format,
492
+ ParameterStyle.NAMED_AT: self._convert_to_named_colon_format, # Same logic as colon
493
+ ParameterStyle.NAMED_DOLLAR: self._convert_to_named_colon_format,
494
+ }
495
+
496
+ self._placeholder_generators: dict[ParameterStyle, Callable[[Any], str]] = {
497
+ ParameterStyle.QMARK: lambda _: "?",
498
+ ParameterStyle.NUMERIC: lambda i: f"${int(i) + 1}",
499
+ ParameterStyle.NAMED_COLON: lambda name: f":{name}",
500
+ ParameterStyle.POSITIONAL_COLON: lambda i: f":{int(i) + 1}",
501
+ ParameterStyle.NAMED_AT: lambda name: f"@{name}",
502
+ ParameterStyle.NAMED_DOLLAR: lambda name: f"${name}",
503
+ ParameterStyle.NAMED_PYFORMAT: lambda name: f"%({name})s",
504
+ ParameterStyle.POSITIONAL_PYFORMAT: lambda _: "%s",
505
+ }
506
+
507
+ def normalize_sql_for_parsing(self, sql: str, dialect: Optional[str] = None) -> "tuple[str, list[ParameterInfo]]":
508
+ """Convert SQL to SQLGlot-parsable format.
509
+
510
+ Takes raw SQL with potentially incompatible parameter styles and converts
511
+ them to a canonical format that SQLGlot can parse.
512
+
513
+ Args:
514
+ sql: Raw SQL string with any parameter style
515
+ dialect: Target SQL dialect for compatibility checking
516
+
517
+ Returns:
518
+ Tuple of (parsable_sql, original_parameter_info)
519
+ """
520
+ param_info = self.validator.extract_parameters(sql)
521
+
522
+ incompatible_styles = self.validator.get_sqlglot_incompatible_styles(dialect)
523
+ needs_conversion = any(p.style in incompatible_styles for p in param_info)
524
+
525
+ if not needs_conversion:
526
+ return sql, param_info
527
+
528
+ converted_sql = self._convert_to_sqlglot_compatible(sql, param_info, incompatible_styles)
529
+ return converted_sql, param_info
530
+
531
+ def _convert_to_sqlglot_compatible(
532
+ self, sql: str, param_info: "list[ParameterInfo]", incompatible_styles: "set[ParameterStyle]"
533
+ ) -> str:
534
+ """Convert SQL to SQLGlot-compatible format."""
535
+ converted_sql = sql
536
+ for param in reversed(param_info):
537
+ if param.style in incompatible_styles:
538
+ canonical_placeholder = f":param_{param.ordinal}"
539
+ converted_sql = (
540
+ converted_sql[: param.position]
541
+ + canonical_placeholder
542
+ + converted_sql[param.position + len(param.placeholder_text) :]
543
+ )
544
+
545
+ return converted_sql
546
+
547
+ def convert_placeholder_style(
548
+ self, sql: str, parameters: Any, target_style: ParameterStyle, is_many: bool = False
549
+ ) -> "tuple[str, Any]":
550
+ """Convert SQL and parameters to execution format.
551
+
552
+ Args:
553
+ sql: SQL string (possibly from Phase 1 normalization)
554
+ parameters: Parameter values in any format
555
+ target_style: Target parameter style for execution
556
+ is_many: Whether this is for executemany() operation
557
+
558
+ Returns:
559
+ Tuple of (final_sql, execution_parameters)
560
+ """
561
+ param_info = self.validator.extract_parameters(sql)
562
+
563
+ if target_style == ParameterStyle.STATIC:
564
+ return self._embed_static_parameters(sql, parameters, param_info)
565
+
566
+ current_styles = {p.style for p in param_info}
567
+ if len(current_styles) == 1 and target_style in current_styles:
568
+ converted_parameters = self._convert_parameter_format(
569
+ parameters, param_info, target_style, parameters, preserve_parameter_format=True
570
+ )
571
+ return sql, converted_parameters
572
+
573
+ converted_sql = self._convert_placeholders_to_style(sql, param_info, target_style)
574
+ converted_parameters = self._convert_parameter_format(
575
+ parameters, param_info, target_style, parameters, preserve_parameter_format=True
576
+ )
577
+
578
+ return converted_sql, converted_parameters
579
+
580
+ def _convert_placeholders_to_style(
581
+ self, sql: str, param_info: "list[ParameterInfo]", target_style: ParameterStyle
582
+ ) -> str:
583
+ """Convert SQL placeholders to target style."""
584
+ generator = self._placeholder_generators.get(target_style)
585
+ if not generator:
586
+ msg = f"Unsupported target parameter style: {target_style}"
587
+ raise ValueError(msg)
588
+
589
+ # Optimize parameter style detection
590
+ param_styles = {p.style for p in param_info}
591
+ use_sequential_for_qmark = (
592
+ len(param_styles) == 1 and ParameterStyle.QMARK in param_styles and target_style == ParameterStyle.NUMERIC
593
+ )
594
+
595
+ # Build unique parameters mapping efficiently
596
+ unique_params: dict[str, int] = {}
597
+ for param in param_info:
598
+ param_key = (
599
+ f"{param.placeholder_text}_{param.ordinal}"
600
+ if use_sequential_for_qmark and param.style == ParameterStyle.QMARK
601
+ else param.placeholder_text
602
+ )
603
+
604
+ if param_key not in unique_params:
605
+ unique_params[param_key] = len(unique_params)
606
+
607
+ # Convert SQL with optimized string operations
608
+ converted_sql = sql
609
+ placeholder_text_len_cache: dict[str, int] = {}
610
+
611
+ for param in reversed(param_info):
612
+ # Cache placeholder text length to avoid recalculation
613
+ if param.placeholder_text not in placeholder_text_len_cache:
614
+ placeholder_text_len_cache[param.placeholder_text] = len(param.placeholder_text)
615
+ text_len = placeholder_text_len_cache[param.placeholder_text]
616
+
617
+ # Generate new placeholder based on target style
618
+ if target_style in {
619
+ ParameterStyle.QMARK,
620
+ ParameterStyle.NUMERIC,
621
+ ParameterStyle.POSITIONAL_PYFORMAT,
622
+ ParameterStyle.POSITIONAL_COLON,
623
+ }:
624
+ param_key = (
625
+ f"{param.placeholder_text}_{param.ordinal}"
626
+ if use_sequential_for_qmark and param.style == ParameterStyle.QMARK
627
+ else param.placeholder_text
628
+ )
629
+ new_placeholder = generator(unique_params[param_key])
630
+ else: # Named styles
631
+ param_name = param.name or f"param_{param.ordinal}"
632
+ new_placeholder = generator(param_name)
633
+
634
+ # Optimized string replacement
635
+ converted_sql = (
636
+ converted_sql[: param.position] + new_placeholder + converted_sql[param.position + text_len :]
637
+ )
638
+
639
+ return converted_sql
640
+
641
+ def _convert_parameter_format( # noqa: C901
642
+ self,
643
+ parameters: Any,
644
+ param_info: "list[ParameterInfo]",
645
+ target_style: ParameterStyle,
646
+ original_parameters: Any = None,
647
+ preserve_parameter_format: bool = False,
648
+ ) -> Any:
649
+ """Convert parameter format to match target style requirements.
650
+
651
+ Args:
652
+ parameters: Current parameter values
653
+ param_info: Parameter information extracted from SQL
654
+ target_style: Target parameter style for conversion
655
+ original_parameters: Original parameter container for type preservation
656
+ preserve_parameter_format: Whether to preserve the original parameter format
657
+ """
658
+ if not parameters or not param_info:
659
+ return parameters
660
+
661
+ # Determine if target style expects named or positional parameters
662
+ is_named_style = target_style in {
663
+ ParameterStyle.NAMED_COLON,
664
+ ParameterStyle.NAMED_AT,
665
+ ParameterStyle.NAMED_DOLLAR,
666
+ ParameterStyle.NAMED_PYFORMAT,
667
+ }
668
+
669
+ if is_named_style:
670
+ # Convert to dict format if needed
671
+ if isinstance(parameters, Mapping):
672
+ return parameters # Already in correct format
673
+ if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
674
+ # Convert positional to named
675
+ param_dict = {}
676
+ for i, param in enumerate(param_info):
677
+ if i < len(parameters):
678
+ name = param.name or f"param_{param.ordinal}"
679
+ param_dict[name] = parameters[i]
680
+ return param_dict
681
+ # Convert to list/tuple format if needed
682
+ elif isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
683
+ return parameters # Already in correct format
684
+ elif isinstance(parameters, Mapping):
685
+ # Convert named to positional
686
+ param_values = []
687
+
688
+ # Handle mixed parameter styles by creating a comprehensive parameter mapping
689
+ parameter_styles = {p.style for p in param_info}
690
+ has_mixed_styles = len(parameter_styles) > 1
691
+
692
+ if has_mixed_styles:
693
+ # For mixed styles, we need to create a mapping that handles both named and positional parameters
694
+ # Strategy: Map parameters based on their ordinal position in the SQL
695
+ param_keys = list(parameters.keys())
696
+
697
+ for param in param_info:
698
+ value_found = False
699
+
700
+ # First, try direct name mapping for named parameters
701
+ if param.name and param.name in parameters:
702
+ param_values.append(parameters[param.name])
703
+ value_found = True
704
+ # For numeric parameters like $1, $2, map by ordinal position
705
+ elif param.style == ParameterStyle.NUMERIC and param.name and param.name.isdigit():
706
+ # $2 means the second parameter - use ordinal position to find corresponding key
707
+ if param.ordinal < len(param_keys):
708
+ key_to_use = param_keys[param.ordinal]
709
+ param_values.append(parameters[key_to_use])
710
+ value_found = True
711
+
712
+ # Fallback to original logic if no value found yet
713
+ if not value_found:
714
+ if f"param_{param.ordinal}" in parameters:
715
+ param_values.append(parameters[f"param_{param.ordinal}"])
716
+ elif str(param.ordinal + 1) in parameters: # 1-based for some styles
717
+ param_values.append(parameters[str(param.ordinal + 1)])
718
+ else:
719
+ # Original logic for single parameter style
720
+ for param in param_info:
721
+ if param.name and param.name in parameters:
722
+ param_values.append(parameters[param.name])
723
+ elif f"param_{param.ordinal}" in parameters:
724
+ param_values.append(parameters[f"param_{param.ordinal}"])
725
+ else:
726
+ # Try to match by ordinal key
727
+ ordinal_key = str(param.ordinal + 1) # 1-based for some styles
728
+ if ordinal_key in parameters:
729
+ param_values.append(parameters[ordinal_key])
730
+
731
+ # Preserve original container type if preserve_parameter_format=True and we have the original
732
+ if preserve_parameter_format and original_parameters is not None:
733
+ if isinstance(original_parameters, tuple):
734
+ return tuple(param_values)
735
+ if isinstance(original_parameters, list):
736
+ return param_values
737
+ # For other sequence types, try to construct the same type
738
+ if hasattr(original_parameters, "__class__") and callable(original_parameters.__class__):
739
+ try:
740
+ return original_parameters.__class__(param_values)
741
+ except (TypeError, ValueError):
742
+ # Fallback to tuple if construction fails
743
+ return tuple(param_values)
744
+
745
+ # Default to list for backward compatibility
746
+ return param_values
747
+
748
+ return parameters
749
+
750
+ def _embed_static_parameters(
751
+ self, sql: str, parameters: Any, param_info: "list[ParameterInfo]"
752
+ ) -> "tuple[str, Any]":
753
+ """Embed parameters directly into SQL for STATIC style."""
754
+ if not param_info:
755
+ return sql, None
756
+
757
+ # Build a mapping of unique parameters to their ordinals
758
+ # This handles repeated parameters like $1, $2, $1 correctly, but not
759
+ # sequential positional parameters like ?, ? which should use different values
760
+ unique_params: dict[str, int] = {}
761
+ for param in param_info:
762
+ # Create a unique key for each parameter based on what makes it distinct
763
+ if param.style in {ParameterStyle.QMARK, ParameterStyle.POSITIONAL_PYFORMAT}:
764
+ # For sequential positional parameters, each occurrence gets its own value
765
+ param_key = f"{param.placeholder_text}_{param.ordinal}"
766
+ elif param.style == ParameterStyle.NUMERIC and param.name:
767
+ # For numeric parameters like $1, $2, $1, reuse based on the number
768
+ param_key = param.placeholder_text # e.g., "$1", "$2", "$1"
769
+ elif param.name:
770
+ # For named parameters like :name, :other, :name, reuse based on name
771
+ param_key = param.placeholder_text # e.g., ":name", ":other", ":name"
772
+ else:
773
+ # Fallback: treat each occurrence as unique
774
+ param_key = f"{param.placeholder_text}_{param.ordinal}"
775
+
776
+ if param_key not in unique_params:
777
+ unique_params[param_key] = len(unique_params)
778
+
779
+ static_sql = sql
780
+ for param in reversed(param_info):
781
+ # Get parameter value using unique parameter mapping
782
+ param_value = self._get_parameter_value_with_reuse(parameters, param, unique_params)
783
+
784
+ # Convert to SQL literal
785
+ if param_value is None:
786
+ literal = "NULL"
787
+ elif isinstance(param_value, str):
788
+ # Escape single quotes
789
+ escaped = param_value.replace("'", "''")
790
+ literal = f"'{escaped}'"
791
+ elif isinstance(param_value, bool):
792
+ literal = "TRUE" if param_value else "FALSE"
793
+ elif isinstance(param_value, (int, float)):
794
+ literal = str(param_value)
795
+ else:
796
+ # Convert to string and quote
797
+ literal = f"'{param_value!s}'"
798
+
799
+ # Replace placeholder with literal value
800
+ static_sql = (
801
+ static_sql[: param.position] + literal + static_sql[param.position + len(param.placeholder_text) :]
802
+ )
803
+
804
+ return static_sql, None # No parameters needed for static SQL
805
+
806
+ def _get_parameter_value(self, parameters: Any, param: ParameterInfo) -> Any:
807
+ """Extract parameter value based on parameter info and format."""
808
+ if isinstance(parameters, Mapping):
809
+ # Try by name first, then by ordinal key
810
+ if param.name and param.name in parameters:
811
+ return parameters[param.name]
812
+ if f"param_{param.ordinal}" in parameters:
813
+ return parameters[f"param_{param.ordinal}"]
814
+ if str(param.ordinal + 1) in parameters: # 1-based ordinal
815
+ return parameters[str(param.ordinal + 1)]
816
+ elif isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
817
+ if param.ordinal < len(parameters):
818
+ return parameters[param.ordinal]
819
+
820
+ return None
821
+
822
+ def _get_parameter_value_with_reuse(
823
+ self, parameters: Any, param: ParameterInfo, unique_params: "dict[str, int]"
824
+ ) -> Any:
825
+ """Extract parameter value handling parameter reuse correctly.
826
+
827
+ Args:
828
+ parameters: Parameter values in any format
829
+ param: Parameter information
830
+ unique_params: Mapping of unique placeholders to their ordinal positions
831
+
832
+ Returns:
833
+ Parameter value, correctly handling reused parameters
834
+ """
835
+ # Build the parameter key using the same logic as in _embed_static_parameters
836
+ if param.style in {ParameterStyle.QMARK, ParameterStyle.POSITIONAL_PYFORMAT}:
837
+ # For sequential positional parameters, each occurrence gets its own value
838
+ param_key = f"{param.placeholder_text}_{param.ordinal}"
839
+ elif param.style == ParameterStyle.NUMERIC and param.name:
840
+ # For numeric parameters like $1, $2, $1, reuse based on the number
841
+ param_key = param.placeholder_text # e.g., "$1", "$2", "$1"
842
+ elif param.name:
843
+ # For named parameters like :name, :other, :name, reuse based on name
844
+ param_key = param.placeholder_text # e.g., ":name", ":other", ":name"
845
+ else:
846
+ # Fallback: treat each occurrence as unique
847
+ param_key = f"{param.placeholder_text}_{param.ordinal}"
848
+
849
+ # Get the unique ordinal for this parameter key
850
+ unique_ordinal = unique_params.get(param_key)
851
+ if unique_ordinal is None:
852
+ return None
853
+
854
+ if isinstance(parameters, Mapping):
855
+ # For named parameters, try different key formats
856
+ if param.name and param.name in parameters:
857
+ return parameters[param.name]
858
+ if f"param_{unique_ordinal}" in parameters:
859
+ return parameters[f"param_{unique_ordinal}"]
860
+ if str(unique_ordinal + 1) in parameters: # 1-based ordinal
861
+ return parameters[str(unique_ordinal + 1)]
862
+ elif isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
863
+ # Use the unique ordinal to get the correct parameter value
864
+ if unique_ordinal < len(parameters):
865
+ return parameters[unique_ordinal]
866
+
867
+ return None
868
+
869
+ # Format converter methods for different parameter styles
870
+ def _convert_to_positional_format(self, parameters: Any, param_info: "list[ParameterInfo]") -> Any:
871
+ """Convert parameters to positional format (list/tuple)."""
872
+ return self._convert_parameter_format(
873
+ parameters, param_info, ParameterStyle.QMARK, parameters, preserve_parameter_format=False
874
+ )
875
+
876
+ def _convert_to_named_colon_format(self, parameters: Any, param_info: "list[ParameterInfo]") -> Any:
877
+ """Convert parameters to named colon format (dict)."""
878
+ return self._convert_parameter_format(
879
+ parameters, param_info, ParameterStyle.NAMED_COLON, parameters, preserve_parameter_format=False
880
+ )
881
+
882
+ def _convert_to_positional_colon_format(self, parameters: Any, param_info: "list[ParameterInfo]") -> Any:
883
+ """Convert parameters to positional colon format with 1-based keys."""
884
+ if isinstance(parameters, Mapping):
885
+ return parameters # Already dict format
886
+
887
+ # Convert to 1-based ordinal keys for Oracle
888
+ param_dict = {}
889
+ if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
890
+ for i, value in enumerate(parameters):
891
+ param_dict[str(i + 1)] = value
892
+
893
+ return param_dict
894
+
895
+ def _convert_to_named_pyformat_format(self, parameters: Any, param_info: "list[ParameterInfo]") -> Any:
896
+ """Convert parameters to named pyformat format (dict)."""
897
+ return self._convert_parameter_format(
898
+ parameters, param_info, ParameterStyle.NAMED_PYFORMAT, parameters, preserve_parameter_format=False
899
+ )
900
+
901
+
902
+ @mypyc_attr(allow_interpreted_subclasses=False)
903
+ class ParameterProcessor:
904
+ """HIGH-LEVEL parameter processing engine with complete pipeline.
905
+
906
+ This is the main entry point for the complete parameter pre-processing system
907
+ that coordinates Phase 1 (SQLGlot compatibility) and Phase 2 (execution format).
908
+
909
+ Processing Pipeline:
910
+ 1. Type wrapping for SQLGlot compatibility (TypedParameter)
911
+ 2. Driver-specific type coercions (type_coercion_map)
912
+ 3. Phase 1: SQLGlot normalization if needed
913
+ 4. Phase 2: Execution format conversion if needed
914
+ 5. Final output transformation (output_transformer)
915
+
916
+ Performance:
917
+ - Fast path for no parameters or no conversion needed
918
+ - Cached processing results for repeated SQL patterns
919
+ - Minimal overhead when no processing required
920
+ """
921
+
922
+ __slots__ = ("_cache", "_cache_size", "_converter", "_validator")
923
+
924
+ # Class-level constants
925
+ DEFAULT_CACHE_SIZE = 1000
926
+
927
+ def __init__(self) -> None:
928
+ """Initialize processor with caching and component coordination."""
929
+ self._cache: dict[str, tuple[str, Any]] = {}
930
+ self._cache_size = 0
931
+ self._validator = ParameterValidator()
932
+ self._converter = ParameterConverter()
933
+ # Cache size is a class-level constant
934
+
935
+ def process(
936
+ self,
937
+ sql: str,
938
+ parameters: Any,
939
+ config: ParameterStyleConfig,
940
+ dialect: Optional[str] = None,
941
+ is_many: bool = False,
942
+ ) -> "tuple[str, Any]":
943
+ """Complete parameter processing pipeline.
944
+
945
+ This method coordinates the entire parameter pre-processing workflow:
946
+ 1. Type wrapping for SQLGlot compatibility
947
+ 2. Phase 1: SQLGlot normalization if needed
948
+ 3. Phase 2: Execution format conversion
949
+ 4. Driver-specific type coercions
950
+ 5. Final output transformation
951
+
952
+ Args:
953
+ sql: Raw SQL string
954
+ parameters: Parameter values in any format
955
+ config: Parameter style configuration
956
+ dialect: SQL dialect for compatibility
957
+ is_many: Whether this is for execute_many operation
958
+
959
+ Returns:
960
+ Tuple of (final_sql, execution_parameters)
961
+ """
962
+ # 1. Cache lookup for processed results
963
+ cache_key = f"{sql}:{hash(repr(parameters))}:{config.default_parameter_style}:{is_many}:{dialect}"
964
+ if cache_key in self._cache:
965
+ return self._cache[cache_key]
966
+
967
+ # 2. Determine what transformations are needed
968
+ param_info = self._validator.extract_parameters(sql)
969
+ original_styles = {p.style for p in param_info} if param_info else set()
970
+ needs_sqlglot_normalization = self._needs_sqlglot_normalization(param_info, dialect)
971
+ needs_execution_conversion = self._needs_execution_conversion(param_info, config)
972
+
973
+ # Check for static script compilation (embed parameters directly in SQL)
974
+ # IMPORTANT: Do NOT embed parameters for execute_many operations - they need separate parameter sets
975
+ needs_static_embedding = (
976
+ config.needs_static_script_compilation and param_info and parameters and not is_many
977
+ ) # Disable static embedding for execute_many
978
+
979
+ if needs_static_embedding:
980
+ # For static script compilation, embed parameters directly and return
981
+ # Apply type coercion first if configured
982
+ coerced_params = parameters
983
+ if config.type_coercion_map and parameters:
984
+ coerced_params = self._apply_type_coercions(parameters, config.type_coercion_map, is_many)
985
+
986
+ static_sql, static_params = self._converter.convert_placeholder_style(
987
+ sql, coerced_params, ParameterStyle.STATIC, is_many
988
+ )
989
+ self._cache[cache_key] = (static_sql, static_params)
990
+ return static_sql, static_params
991
+
992
+ # 3. Fast path: Skip processing if no transformation needed
993
+ if (
994
+ not needs_sqlglot_normalization
995
+ and not needs_execution_conversion
996
+ and not config.type_coercion_map
997
+ and not config.output_transformer
998
+ ):
999
+ return sql, parameters
1000
+
1001
+ # 4. Progressive transformation pipeline
1002
+ processed_sql, processed_parameters = sql, parameters
1003
+
1004
+ # Phase A: Type wrapping for SQLGlot compatibility
1005
+ if processed_parameters:
1006
+ processed_parameters = self._apply_type_wrapping(processed_parameters)
1007
+
1008
+ # Phase B: Phase 1 - SQLGlot normalization if needed
1009
+ if needs_sqlglot_normalization:
1010
+ processed_sql, _ = self._converter.normalize_sql_for_parsing(processed_sql, dialect)
1011
+
1012
+ # Phase C: NULL parameter removal moved to compiler where AST is available
1013
+
1014
+ # Phase D: Type coercion (database-specific)
1015
+ if config.type_coercion_map and processed_parameters:
1016
+ processed_parameters = self._apply_type_coercions(processed_parameters, config.type_coercion_map, is_many)
1017
+
1018
+ # Phase E: Phase 2 - Execution format conversion
1019
+ if needs_execution_conversion or needs_sqlglot_normalization:
1020
+ # Check if we should preserve original parameters for execute_many
1021
+ if is_many and config.preserve_original_params_for_many and isinstance(parameters, (list, tuple)):
1022
+ # For execute_many with preserve flag, keep original parameter list
1023
+ # but still convert the SQL placeholders to the target style
1024
+ target_style = self._determine_target_execution_style(original_styles, config)
1025
+ processed_sql, _ = self._converter.convert_placeholder_style(
1026
+ processed_sql, processed_parameters, target_style, is_many
1027
+ )
1028
+ # Keep the original parameter list for drivers that need it (like BigQuery)
1029
+ processed_parameters = parameters
1030
+ else:
1031
+ # Normal execution format conversion
1032
+ target_style = self._determine_target_execution_style(original_styles, config)
1033
+ processed_sql, processed_parameters = self._converter.convert_placeholder_style(
1034
+ processed_sql, processed_parameters, target_style, is_many
1035
+ )
1036
+
1037
+ # Phase F: Output transformation (custom hooks)
1038
+ if config.output_transformer:
1039
+ processed_sql, processed_parameters = config.output_transformer(processed_sql, processed_parameters)
1040
+
1041
+ # 5. Cache result and return
1042
+ if self._cache_size < self.DEFAULT_CACHE_SIZE:
1043
+ self._cache[cache_key] = (processed_sql, processed_parameters)
1044
+ self._cache_size += 1
1045
+
1046
+ return processed_sql, processed_parameters
1047
+
1048
+ def _get_sqlglot_compatible_sql(
1049
+ self, sql: str, parameters: Any, config: ParameterStyleConfig, dialect: Optional[str] = None
1050
+ ) -> "tuple[str, Any]":
1051
+ """Get SQL normalized for SQLGlot parsing only (Phase 1 only).
1052
+
1053
+ This method performs only Phase 1 normalization to make SQL compatible
1054
+ with SQLGlot parsing, without converting to execution format.
1055
+
1056
+ Args:
1057
+ sql: Raw SQL string
1058
+ parameters: Parameter values
1059
+ config: Parameter style configuration
1060
+ dialect: SQL dialect for compatibility
1061
+
1062
+ Returns:
1063
+ Tuple of (sqlglot_compatible_sql, parameters)
1064
+ """
1065
+ # 1. Determine if Phase 1 normalization is needed
1066
+ param_info = self._validator.extract_parameters(sql)
1067
+
1068
+ # 2. Apply only Phase 1 normalization if needed
1069
+ if self._needs_sqlglot_normalization(param_info, dialect):
1070
+ normalized_sql, _ = self._converter.normalize_sql_for_parsing(sql, dialect)
1071
+ return normalized_sql, parameters
1072
+
1073
+ # 3. No normalization needed - return original SQL
1074
+ return sql, parameters
1075
+
1076
+ def _needs_execution_conversion(self, param_info: "list[ParameterInfo]", config: ParameterStyleConfig) -> bool:
1077
+ """Determine if execution format conversion is needed.
1078
+
1079
+ Preserves the original parameter style if it's supported by the execution environment,
1080
+ otherwise converts to the default execution style.
1081
+ """
1082
+ if not param_info:
1083
+ return False
1084
+
1085
+ current_styles = {p.style for p in param_info}
1086
+
1087
+ # Check if mixed styles are explicitly allowed AND the execution environment supports multiple styles
1088
+ if (
1089
+ config.allow_mixed_parameter_styles
1090
+ and len(current_styles) > 1
1091
+ and config.supported_execution_parameter_styles is not None
1092
+ and len(config.supported_execution_parameter_styles) > 1
1093
+ and all(style in config.supported_execution_parameter_styles for style in current_styles)
1094
+ ):
1095
+ return False
1096
+
1097
+ # Check for mixed styles - if not allowed, force conversion to single style
1098
+ if len(current_styles) > 1:
1099
+ return True
1100
+
1101
+ # If we have a single current style and it's supported by the execution environment, preserve it
1102
+ if len(current_styles) == 1:
1103
+ current_style = next(iter(current_styles))
1104
+ supported_styles = config.supported_execution_parameter_styles
1105
+ if supported_styles is None:
1106
+ return True # No supported styles defined, need conversion
1107
+ return current_style not in supported_styles
1108
+
1109
+ # Multiple styles detected - transformation needed
1110
+ return True
1111
+
1112
+ def _needs_sqlglot_normalization(self, param_info: "list[ParameterInfo]", dialect: Optional[str] = None) -> bool:
1113
+ """Check if SQLGlot normalization is needed for this SQL."""
1114
+ incompatible_styles = self._validator.get_sqlglot_incompatible_styles(dialect)
1115
+ return any(p.style in incompatible_styles for p in param_info)
1116
+
1117
+ def _determine_target_execution_style(
1118
+ self, original_styles: "set[ParameterStyle]", config: ParameterStyleConfig
1119
+ ) -> ParameterStyle:
1120
+ """Determine the target execution style based on original styles and config.
1121
+
1122
+ Logic:
1123
+ 1. If there's a single original style and it's in supported execution styles, use it
1124
+ 2. Otherwise, use the default execution style
1125
+ 3. If no default execution style, use the default parameter style
1126
+
1127
+ This preserves the original parameter style when possible, only converting
1128
+ when necessary for execution compatibility.
1129
+ """
1130
+ # If we have a single original style that's supported for execution, preserve it
1131
+ if len(original_styles) == 1 and config.supported_execution_parameter_styles is not None:
1132
+ original_style = next(iter(original_styles))
1133
+ if original_style in config.supported_execution_parameter_styles:
1134
+ return original_style
1135
+
1136
+ # Otherwise use the configured execution style or fallback to default parameter style
1137
+ return config.default_execution_parameter_style or config.default_parameter_style
1138
+
1139
+ def _apply_type_wrapping(self, parameters: Any) -> Any:
1140
+ """Apply type wrapping using singledispatch for performance."""
1141
+ if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
1142
+ # Optimize with direct iteration instead of list comprehension for better memory usage
1143
+ return [_wrap_parameter_by_type(p) for p in parameters]
1144
+ if isinstance(parameters, Mapping):
1145
+ # Optimize dict comprehension with items() iteration
1146
+ wrapped_dict = {}
1147
+ for k, v in parameters.items():
1148
+ wrapped_dict[k] = _wrap_parameter_by_type(v)
1149
+ return wrapped_dict
1150
+ return _wrap_parameter_by_type(parameters)
1151
+
1152
+ def _apply_type_coercions(
1153
+ self, parameters: Any, type_coercion_map: "dict[type, Callable[[Any], Any]]", is_many: bool = False
1154
+ ) -> Any:
1155
+ """Apply database-specific type coercions.
1156
+
1157
+ Args:
1158
+ parameters: Parameter values to coerce
1159
+ type_coercion_map: Type coercion mappings
1160
+ is_many: If True, parameters is a list of parameter sets for execute_many
1161
+ """
1162
+
1163
+ def coerce_value(value: Any) -> Any:
1164
+ # Handle TypedParameter objects - use the wrapped value and original type
1165
+ if isinstance(value, TypedParameter):
1166
+ wrapped_value = value.value
1167
+ original_type = value.original_type
1168
+ if original_type in type_coercion_map:
1169
+ coerced = type_coercion_map[original_type](wrapped_value)
1170
+ # Recursively apply coercion to elements in the coerced result if it's a sequence
1171
+ if isinstance(coerced, (list, tuple)) and not isinstance(coerced, (str, bytes)):
1172
+ coerced = [coerce_value(item) for item in coerced]
1173
+ elif isinstance(coerced, dict):
1174
+ coerced = {k: coerce_value(v) for k, v in coerced.items()}
1175
+ return coerced
1176
+ return wrapped_value
1177
+
1178
+ # Handle regular values
1179
+ value_type = type(value)
1180
+ if value_type in type_coercion_map:
1181
+ coerced = type_coercion_map[value_type](value)
1182
+ # Recursively apply coercion to elements in the coerced result if it's a sequence
1183
+ if isinstance(coerced, (list, tuple)) and not isinstance(coerced, (str, bytes)):
1184
+ coerced = [coerce_value(item) for item in coerced]
1185
+ elif isinstance(coerced, dict):
1186
+ coerced = {k: coerce_value(v) for k, v in coerced.items()}
1187
+ return coerced
1188
+ return value
1189
+
1190
+ def coerce_parameter_set(param_set: Any) -> Any:
1191
+ """Coerce a single parameter set (dict, list, tuple, or scalar)."""
1192
+ if isinstance(param_set, Sequence) and not isinstance(param_set, (str, bytes)):
1193
+ return [coerce_value(p) for p in param_set]
1194
+ if isinstance(param_set, Mapping):
1195
+ return {k: coerce_value(v) for k, v in param_set.items()}
1196
+ return coerce_value(param_set)
1197
+
1198
+ # Handle execute_many case specially - apply coercions to individual parameter values,
1199
+ # not to the parameter set tuples/lists themselves
1200
+ if is_many and isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
1201
+ return [coerce_parameter_set(param_set) for param_set in parameters]
1202
+
1203
+ # Regular single execution - apply coercions to all parameters
1204
+ if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
1205
+ return [coerce_value(p) for p in parameters]
1206
+ if isinstance(parameters, Mapping):
1207
+ return {k: coerce_value(v) for k, v in parameters.items()}
1208
+ return coerce_value(parameters)
1209
+
1210
+
1211
+ # Helper functions for parameter processing
1212
+ def is_iterable_parameters(obj: Any) -> bool:
1213
+ """Check if object is iterable parameters (not string/bytes).
1214
+
1215
+ Args:
1216
+ obj: Object to check
1217
+
1218
+ Returns:
1219
+ True if object is iterable parameters
1220
+ """
1221
+ return isinstance(obj, (list, tuple, set)) or (
1222
+ hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes, Mapping))
1223
+ )
1224
+
1225
+
1226
+ # Public API functions that preserve exact current interfaces
1227
+ def wrap_with_type(value: Any, semantic_name: Optional[str] = None) -> Any:
1228
+ """Public API for type wrapping - preserves current interface.
1229
+
1230
+ Args:
1231
+ value: Value to potentially wrap
1232
+ semantic_name: Optional semantic name
1233
+
1234
+ Returns:
1235
+ Original value or TypedParameter wrapper
1236
+ """
1237
+ return _wrap_parameter_by_type(value, semantic_name)