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