sqlspec 0.16.1__cp311-cp311-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.
- 51ff5a9eadfdefd49f98__mypyc.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/__init__.py +92 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +77 -0
- sqlspec/_sql.py +1780 -0
- sqlspec/_typing.py +680 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +361 -0
- sqlspec/adapters/adbc/driver.py +512 -0
- sqlspec/adapters/aiosqlite/__init__.py +19 -0
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +253 -0
- sqlspec/adapters/aiosqlite/driver.py +248 -0
- sqlspec/adapters/asyncmy/__init__.py +19 -0
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +180 -0
- sqlspec/adapters/asyncmy/driver.py +274 -0
- sqlspec/adapters/asyncpg/__init__.py +21 -0
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +229 -0
- sqlspec/adapters/asyncpg/driver.py +344 -0
- sqlspec/adapters/bigquery/__init__.py +18 -0
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +298 -0
- sqlspec/adapters/bigquery/driver.py +558 -0
- sqlspec/adapters/duckdb/__init__.py +22 -0
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +504 -0
- sqlspec/adapters/duckdb/driver.py +368 -0
- sqlspec/adapters/oracledb/__init__.py +32 -0
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +317 -0
- sqlspec/adapters/oracledb/driver.py +538 -0
- sqlspec/adapters/psqlpy/__init__.py +16 -0
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +214 -0
- sqlspec/adapters/psqlpy/driver.py +530 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +426 -0
- sqlspec/adapters/psycopg/driver.py +796 -0
- sqlspec/adapters/sqlite/__init__.py +15 -0
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +240 -0
- sqlspec/adapters/sqlite/driver.py +294 -0
- sqlspec/base.py +571 -0
- sqlspec/builder/__init__.py +62 -0
- sqlspec/builder/_base.py +473 -0
- sqlspec/builder/_column.py +320 -0
- sqlspec/builder/_ddl.py +1346 -0
- sqlspec/builder/_ddl_utils.py +103 -0
- sqlspec/builder/_delete.py +76 -0
- sqlspec/builder/_insert.py +256 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/builder/_parsing_utils.py +140 -0
- sqlspec/builder/_select.py +170 -0
- sqlspec/builder/_update.py +188 -0
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/builder/mixins/_delete_operations.py +41 -0
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/builder/mixins/_join_operations.py +122 -0
- sqlspec/builder/mixins/_merge_operations.py +476 -0
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +603 -0
- sqlspec/builder/mixins/_update_operations.py +187 -0
- sqlspec/builder/mixins/_where_clause.py +621 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +395 -0
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result.py +677 -0
- sqlspec/core/splitter.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +819 -0
- sqlspec/core/statement.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +19 -0
- sqlspec/driver/_async.py +502 -0
- sqlspec/driver/_common.py +631 -0
- sqlspec/driver/_sync.py +503 -0
- sqlspec/driver/mixins/__init__.py +6 -0
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +86 -0
- sqlspec/exceptions.py +193 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +461 -0
- sqlspec/extensions/litestar/__init__.py +6 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +92 -0
- sqlspec/extensions/litestar/handlers.py +260 -0
- sqlspec/extensions/litestar/plugin.py +145 -0
- sqlspec/extensions/litestar/providers.py +454 -0
- sqlspec/loader.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/loader.py +760 -0
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +407 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +23 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +163 -0
- sqlspec/storage/backends/fsspec.py +386 -0
- sqlspec/storage/backends/obstore.py +459 -0
- sqlspec/storage/capabilities.py +102 -0
- sqlspec/storage/registry.py +239 -0
- sqlspec/typing.py +299 -0
- sqlspec/utils/__init__.py +3 -0
- sqlspec/utils/correlation.py +150 -0
- sqlspec/utils/deprecation.py +106 -0
- sqlspec/utils/fixtures.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/fixtures.py +58 -0
- sqlspec/utils/logging.py +127 -0
- sqlspec/utils/module_loader.py +89 -0
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +32 -0
- sqlspec/utils/sync_tools.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +237 -0
- sqlspec/utils/text.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +96 -0
- sqlspec/utils/type_guards.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_guards.py +1139 -0
- sqlspec-0.16.1.dist-info/METADATA +365 -0
- sqlspec-0.16.1.dist-info/RECORD +148 -0
- sqlspec-0.16.1.dist-info/WHEEL +7 -0
- sqlspec-0.16.1.dist-info/entry_points.txt +2 -0
- sqlspec-0.16.1.dist-info/licenses/LICENSE +21 -0
- 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)
|