sqlspec 0.12.1__py3-none-any.whl → 0.13.0__py3-none-any.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.
- sqlspec/_sql.py +21 -180
- sqlspec/adapters/adbc/config.py +10 -12
- sqlspec/adapters/adbc/driver.py +120 -118
- sqlspec/adapters/aiosqlite/config.py +3 -3
- sqlspec/adapters/aiosqlite/driver.py +116 -141
- sqlspec/adapters/asyncmy/config.py +3 -4
- sqlspec/adapters/asyncmy/driver.py +123 -135
- sqlspec/adapters/asyncpg/config.py +3 -7
- sqlspec/adapters/asyncpg/driver.py +98 -140
- sqlspec/adapters/bigquery/config.py +4 -5
- sqlspec/adapters/bigquery/driver.py +231 -181
- sqlspec/adapters/duckdb/config.py +3 -6
- sqlspec/adapters/duckdb/driver.py +132 -124
- sqlspec/adapters/oracledb/config.py +6 -5
- sqlspec/adapters/oracledb/driver.py +242 -259
- sqlspec/adapters/psqlpy/config.py +3 -7
- sqlspec/adapters/psqlpy/driver.py +118 -93
- sqlspec/adapters/psycopg/config.py +34 -30
- sqlspec/adapters/psycopg/driver.py +342 -214
- sqlspec/adapters/sqlite/config.py +3 -3
- sqlspec/adapters/sqlite/driver.py +150 -104
- sqlspec/config.py +0 -4
- sqlspec/driver/_async.py +89 -98
- sqlspec/driver/_common.py +52 -17
- sqlspec/driver/_sync.py +81 -105
- sqlspec/driver/connection.py +207 -0
- sqlspec/driver/mixins/_csv_writer.py +91 -0
- sqlspec/driver/mixins/_pipeline.py +38 -49
- sqlspec/driver/mixins/_result_utils.py +27 -9
- sqlspec/driver/mixins/_storage.py +149 -216
- sqlspec/driver/mixins/_type_coercion.py +3 -4
- sqlspec/driver/parameters.py +138 -0
- sqlspec/exceptions.py +10 -2
- sqlspec/extensions/aiosql/adapter.py +0 -10
- sqlspec/extensions/litestar/handlers.py +0 -1
- sqlspec/extensions/litestar/plugin.py +0 -3
- sqlspec/extensions/litestar/providers.py +0 -14
- sqlspec/loader.py +31 -118
- sqlspec/protocols.py +542 -0
- sqlspec/service/__init__.py +3 -2
- sqlspec/service/_util.py +147 -0
- sqlspec/service/base.py +1116 -9
- sqlspec/statement/builder/__init__.py +42 -32
- sqlspec/statement/builder/_ddl_utils.py +0 -10
- sqlspec/statement/builder/_parsing_utils.py +10 -4
- sqlspec/statement/builder/base.py +70 -23
- sqlspec/statement/builder/column.py +283 -0
- sqlspec/statement/builder/ddl.py +102 -65
- sqlspec/statement/builder/delete.py +23 -7
- sqlspec/statement/builder/insert.py +29 -15
- sqlspec/statement/builder/merge.py +4 -4
- sqlspec/statement/builder/mixins/_aggregate_functions.py +113 -14
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -1
- sqlspec/statement/builder/mixins/_delete_from.py +1 -1
- sqlspec/statement/builder/mixins/_from.py +10 -8
- sqlspec/statement/builder/mixins/_group_by.py +0 -1
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -1
- sqlspec/statement/builder/mixins/_insert_values.py +0 -2
- sqlspec/statement/builder/mixins/_join.py +20 -13
- sqlspec/statement/builder/mixins/_limit_offset.py +3 -3
- sqlspec/statement/builder/mixins/_merge_clauses.py +3 -4
- sqlspec/statement/builder/mixins/_order_by.py +2 -2
- sqlspec/statement/builder/mixins/_pivot.py +4 -7
- sqlspec/statement/builder/mixins/_select_columns.py +6 -5
- sqlspec/statement/builder/mixins/_unpivot.py +6 -9
- sqlspec/statement/builder/mixins/_update_from.py +2 -1
- sqlspec/statement/builder/mixins/_update_set.py +11 -8
- sqlspec/statement/builder/mixins/_where.py +61 -34
- sqlspec/statement/builder/select.py +32 -17
- sqlspec/statement/builder/update.py +25 -11
- sqlspec/statement/filters.py +39 -14
- sqlspec/statement/parameter_manager.py +220 -0
- sqlspec/statement/parameters.py +210 -79
- sqlspec/statement/pipelines/__init__.py +166 -23
- sqlspec/statement/pipelines/analyzers/_analyzer.py +22 -25
- sqlspec/statement/pipelines/context.py +35 -39
- sqlspec/statement/pipelines/transformers/__init__.py +2 -3
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +19 -187
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +667 -43
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +76 -0
- sqlspec/statement/pipelines/validators/_dml_safety.py +33 -18
- sqlspec/statement/pipelines/validators/_parameter_style.py +87 -14
- sqlspec/statement/pipelines/validators/_performance.py +38 -23
- sqlspec/statement/pipelines/validators/_security.py +39 -62
- sqlspec/statement/result.py +37 -129
- sqlspec/statement/splitter.py +0 -12
- sqlspec/statement/sql.py +885 -379
- sqlspec/statement/sql_compiler.py +140 -0
- sqlspec/storage/__init__.py +10 -2
- sqlspec/storage/backends/fsspec.py +82 -35
- sqlspec/storage/backends/obstore.py +66 -49
- sqlspec/storage/capabilities.py +101 -0
- sqlspec/storage/registry.py +56 -83
- sqlspec/typing.py +6 -434
- sqlspec/utils/cached_property.py +25 -0
- sqlspec/utils/correlation.py +0 -2
- sqlspec/utils/logging.py +0 -6
- sqlspec/utils/sync_tools.py +0 -4
- sqlspec/utils/text.py +0 -5
- sqlspec/utils/type_guards.py +892 -0
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/METADATA +1 -1
- sqlspec-0.13.0.dist-info/RECORD +150 -0
- sqlspec/statement/builder/protocols.py +0 -20
- sqlspec/statement/pipelines/base.py +0 -315
- sqlspec/statement/pipelines/result_types.py +0 -41
- sqlspec/statement/pipelines/transformers/_remove_comments.py +0 -66
- sqlspec/statement/pipelines/transformers/_remove_hints.py +0 -81
- sqlspec/statement/pipelines/validators/base.py +0 -67
- sqlspec/storage/protocol.py +0 -170
- sqlspec-0.12.1.dist-info/RECORD +0 -145
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/statement/parameters.py
CHANGED
|
@@ -21,8 +21,10 @@ if TYPE_CHECKING:
|
|
|
21
21
|
from sqlglot import exp
|
|
22
22
|
|
|
23
23
|
__all__ = (
|
|
24
|
+
"ConvertedParameters",
|
|
24
25
|
"ParameterConverter",
|
|
25
26
|
"ParameterInfo",
|
|
27
|
+
"ParameterNormalizationState",
|
|
26
28
|
"ParameterStyle",
|
|
27
29
|
"ParameterValidator",
|
|
28
30
|
"SQLParameterType",
|
|
@@ -71,7 +73,7 @@ class ParameterStyle(str, Enum):
|
|
|
71
73
|
QMARK = "qmark"
|
|
72
74
|
NUMERIC = "numeric"
|
|
73
75
|
NAMED_COLON = "named_colon"
|
|
74
|
-
POSITIONAL_COLON = "positional_colon"
|
|
76
|
+
POSITIONAL_COLON = "positional_colon"
|
|
75
77
|
NAMED_AT = "named_at"
|
|
76
78
|
NAMED_DOLLAR = "named_dollar"
|
|
77
79
|
NAMED_PYFORMAT = "pyformat_named"
|
|
@@ -88,9 +90,9 @@ class ParameterStyle(str, Enum):
|
|
|
88
90
|
|
|
89
91
|
# Define SQLGlot incompatible styles after ParameterStyle enum
|
|
90
92
|
SQLGLOT_INCOMPATIBLE_STYLES: Final = {
|
|
91
|
-
ParameterStyle.POSITIONAL_PYFORMAT,
|
|
92
|
-
ParameterStyle.NAMED_PYFORMAT,
|
|
93
|
-
ParameterStyle.POSITIONAL_COLON,
|
|
93
|
+
ParameterStyle.POSITIONAL_PYFORMAT,
|
|
94
|
+
ParameterStyle.NAMED_PYFORMAT,
|
|
95
|
+
ParameterStyle.POSITIONAL_COLON,
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
|
|
@@ -147,6 +149,55 @@ class NormalizationInfo(TypedDict, total=False):
|
|
|
147
149
|
original_styles: list[ParameterStyle]
|
|
148
150
|
|
|
149
151
|
|
|
152
|
+
@dataclass
|
|
153
|
+
class ParameterNormalizationState:
|
|
154
|
+
"""Encapsulates all information about parameter normalization.
|
|
155
|
+
|
|
156
|
+
This class provides a single source of truth for parameter style conversions,
|
|
157
|
+
making it easier to track and reverse normalizations applied for SQLGlot compatibility.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
was_normalized: bool = False
|
|
161
|
+
"""Whether parameter normalization was applied."""
|
|
162
|
+
|
|
163
|
+
original_styles: list[ParameterStyle] = field(default_factory=list)
|
|
164
|
+
"""Original parameter style(s) detected in the SQL."""
|
|
165
|
+
|
|
166
|
+
normalized_style: Optional[ParameterStyle] = None
|
|
167
|
+
"""Target style used for normalization (if normalized)."""
|
|
168
|
+
|
|
169
|
+
placeholder_map: dict[str, Union[str, int]] = field(default_factory=dict)
|
|
170
|
+
"""Mapping from normalized names to original names/positions."""
|
|
171
|
+
|
|
172
|
+
reverse_map: dict[Union[str, int], str] = field(default_factory=dict)
|
|
173
|
+
"""Reverse mapping for quick lookups."""
|
|
174
|
+
|
|
175
|
+
original_param_info: list["ParameterInfo"] = field(default_factory=list)
|
|
176
|
+
"""Original parameter info before normalization."""
|
|
177
|
+
|
|
178
|
+
def __post_init__(self) -> None:
|
|
179
|
+
"""Build reverse map if not provided."""
|
|
180
|
+
if self.placeholder_map and not self.reverse_map:
|
|
181
|
+
self.reverse_map = {v: k for k, v in self.placeholder_map.items()}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class ConvertedParameters:
|
|
186
|
+
"""Result of parameter conversion with clear structure."""
|
|
187
|
+
|
|
188
|
+
transformed_sql: str
|
|
189
|
+
"""SQL after any necessary transformations."""
|
|
190
|
+
|
|
191
|
+
parameter_info: list["ParameterInfo"]
|
|
192
|
+
"""Information about parameters found in the SQL."""
|
|
193
|
+
|
|
194
|
+
merged_parameters: "SQLParameterType"
|
|
195
|
+
"""Parameters after merging from various sources."""
|
|
196
|
+
|
|
197
|
+
normalization_state: ParameterNormalizationState
|
|
198
|
+
"""Complete normalization state for tracking conversions."""
|
|
199
|
+
|
|
200
|
+
|
|
150
201
|
@dataclass
|
|
151
202
|
class ParameterValidator:
|
|
152
203
|
"""Parameter validation."""
|
|
@@ -178,7 +229,7 @@ class ParameterValidator:
|
|
|
178
229
|
elif match.group("pyformat_pos"):
|
|
179
230
|
style = ParameterStyle.POSITIONAL_PYFORMAT
|
|
180
231
|
elif match.group("positional_colon"):
|
|
181
|
-
name = match.group("colon_num")
|
|
232
|
+
name = match.group("colon_num")
|
|
182
233
|
style = ParameterStyle.POSITIONAL_COLON
|
|
183
234
|
elif match.group("named_colon"):
|
|
184
235
|
name = match.group("colon_name")
|
|
@@ -192,6 +243,7 @@ class ParameterValidator:
|
|
|
192
243
|
name = name_candidate
|
|
193
244
|
style = ParameterStyle.NAMED_DOLLAR
|
|
194
245
|
else:
|
|
246
|
+
name = name_candidate # Keep the numeric value as name for NUMERIC style
|
|
195
247
|
style = ParameterStyle.NUMERIC
|
|
196
248
|
elif match.group("qmark"):
|
|
197
249
|
style = ParameterStyle.QMARK
|
|
@@ -244,7 +296,6 @@ class ParameterValidator:
|
|
|
244
296
|
if not parameters_info:
|
|
245
297
|
return ParameterStyle.NONE
|
|
246
298
|
|
|
247
|
-
# Check for dominant styles
|
|
248
299
|
# Note: This logic prioritizes pyformat if present, then named, then positional.
|
|
249
300
|
is_pyformat_named = any(p.style == ParameterStyle.NAMED_PYFORMAT for p in parameters_info)
|
|
250
301
|
is_pyformat_positional = any(p.style == ParameterStyle.POSITIONAL_PYFORMAT for p in parameters_info)
|
|
@@ -276,12 +327,12 @@ class ParameterValidator:
|
|
|
276
327
|
for p_style in (
|
|
277
328
|
ParameterStyle.NAMED_COLON,
|
|
278
329
|
ParameterStyle.POSITIONAL_COLON,
|
|
279
|
-
ParameterStyle.NAMED_AT,
|
|
280
330
|
ParameterStyle.NAMED_DOLLAR,
|
|
331
|
+
ParameterStyle.NAMED_AT,
|
|
281
332
|
):
|
|
282
333
|
if any(p.style == p_style for p in parameters_info):
|
|
283
334
|
return p_style
|
|
284
|
-
return ParameterStyle.NAMED_COLON
|
|
335
|
+
return ParameterStyle.NAMED_COLON
|
|
285
336
|
|
|
286
337
|
if has_positional:
|
|
287
338
|
# Similarly, could choose QMARK or NUMERIC based on presence.
|
|
@@ -308,16 +359,19 @@ class ParameterValidator:
|
|
|
308
359
|
if not parameters_info:
|
|
309
360
|
return None
|
|
310
361
|
|
|
311
|
-
# Oracle numeric parameters (:1, :2) are positional despite having a "name"
|
|
312
362
|
if all(p.style == ParameterStyle.POSITIONAL_COLON for p in parameters_info):
|
|
313
363
|
return list
|
|
314
364
|
|
|
315
365
|
if any(
|
|
316
|
-
p.name is not None and p.style
|
|
366
|
+
p.name is not None and p.style not in {ParameterStyle.POSITIONAL_COLON, ParameterStyle.NUMERIC}
|
|
367
|
+
for p in parameters_info
|
|
317
368
|
): # True for NAMED styles and PYFORMAT_NAMED
|
|
318
369
|
return dict
|
|
319
|
-
# All parameters must have p.name is None or be
|
|
320
|
-
if all(
|
|
370
|
+
# All parameters must have p.name is None or be positional styles (POSITIONAL_COLON, NUMERIC)
|
|
371
|
+
if all(
|
|
372
|
+
p.name is None or p.style in {ParameterStyle.POSITIONAL_COLON, ParameterStyle.NUMERIC}
|
|
373
|
+
for p in parameters_info
|
|
374
|
+
):
|
|
321
375
|
return list
|
|
322
376
|
# This case implies a mix of parameters where some have names and some don't,
|
|
323
377
|
# but not fitting the clear dict/list categories above.
|
|
@@ -409,13 +463,9 @@ class ParameterValidator:
|
|
|
409
463
|
required_names = {p.name for p in parameters_info if p.name is not None}
|
|
410
464
|
provided_names = set(provided_params.keys())
|
|
411
465
|
|
|
412
|
-
# Check for mixed parameter merging pattern: _arg_N for positional parameters
|
|
413
466
|
positional_count = sum(1 for p in parameters_info if p.name is None)
|
|
414
|
-
expected_positional_names = {f"
|
|
415
|
-
|
|
416
|
-
# For mixed parameters, we expect both named and generated positional names
|
|
467
|
+
expected_positional_names = {f"arg_{p.ordinal}" for p in parameters_info if p.name is None}
|
|
417
468
|
if positional_count > 0 and required_names:
|
|
418
|
-
# Mixed parameter style - accept both named params and _arg_N params
|
|
419
469
|
all_expected_names = required_names | expected_positional_names
|
|
420
470
|
|
|
421
471
|
missing = all_expected_names - provided_names
|
|
@@ -428,16 +478,13 @@ class ParameterValidator:
|
|
|
428
478
|
msg = f"Extra parameters provided: {sorted(extra)}"
|
|
429
479
|
raise ExtraParameterError(msg, original_sql)
|
|
430
480
|
else:
|
|
431
|
-
# Pure named parameters - original logic
|
|
432
481
|
missing = required_names - provided_names
|
|
433
482
|
if missing:
|
|
434
|
-
# Sort for consistent error messages
|
|
435
483
|
msg = f"Missing required named parameters: {sorted(missing)}"
|
|
436
484
|
raise MissingParameterError(msg, original_sql)
|
|
437
485
|
|
|
438
486
|
extra = provided_names - required_names
|
|
439
487
|
if extra:
|
|
440
|
-
# Sort for consistent error messages
|
|
441
488
|
msg = f"Extra parameters provided: {sorted(extra)}"
|
|
442
489
|
raise ExtraParameterError(msg, original_sql)
|
|
443
490
|
|
|
@@ -451,10 +498,10 @@ class ParameterValidator:
|
|
|
451
498
|
MissingParameterError: When required parameters are missing.
|
|
452
499
|
ExtraParameterError: When extra parameters are provided.
|
|
453
500
|
"""
|
|
454
|
-
# Filter for parameters that are truly positional (name is None or Oracle numeric)
|
|
455
|
-
# This is important if parameters_info could contain mixed (which determine_parameter_input_type tries to handle)
|
|
456
501
|
expected_positional_params_count = sum(
|
|
457
|
-
1
|
|
502
|
+
1
|
|
503
|
+
for p in parameters_info
|
|
504
|
+
if p.name is None or p.style in {ParameterStyle.POSITIONAL_COLON, ParameterStyle.NUMERIC}
|
|
458
505
|
)
|
|
459
506
|
actual_count = len(provided_params)
|
|
460
507
|
|
|
@@ -494,25 +541,22 @@ class ParameterConverter:
|
|
|
494
541
|
|
|
495
542
|
Returns:
|
|
496
543
|
A tuple containing:
|
|
497
|
-
- transformed_sql: SQL string with unique named placeholders (e.g., :
|
|
544
|
+
- transformed_sql: SQL string with unique named placeholders (e.g., :param_0).
|
|
498
545
|
- placeholder_map: Dictionary mapping new unique names to original names or ordinal index.
|
|
499
546
|
"""
|
|
500
547
|
transformed_sql_parts = []
|
|
501
548
|
placeholder_map: dict[str, Union[str, int]] = {}
|
|
502
549
|
current_pos = 0
|
|
503
|
-
# parameters_info is already sorted by position due to finditer order in extract_parameters.
|
|
504
|
-
# No need for: sorted_params = sorted(parameters_info, key=lambda p: p.position)
|
|
505
|
-
|
|
506
550
|
for i, p_info in enumerate(parameters_info):
|
|
507
551
|
transformed_sql_parts.append(original_sql[current_pos : p_info.position])
|
|
508
552
|
|
|
509
|
-
unique_placeholder_name = f":
|
|
510
|
-
map_key = f"
|
|
553
|
+
unique_placeholder_name = f":param_{i}"
|
|
554
|
+
map_key = f"param_{i}"
|
|
511
555
|
|
|
512
|
-
if p_info.name:
|
|
556
|
+
if p_info.name:
|
|
513
557
|
placeholder_map[map_key] = p_info.name
|
|
514
|
-
else:
|
|
515
|
-
placeholder_map[map_key] = p_info.ordinal
|
|
558
|
+
else:
|
|
559
|
+
placeholder_map[map_key] = p_info.ordinal
|
|
516
560
|
|
|
517
561
|
transformed_sql_parts.append(unique_placeholder_name)
|
|
518
562
|
current_pos = p_info.position + len(p_info.placeholder_text)
|
|
@@ -520,6 +564,93 @@ class ParameterConverter:
|
|
|
520
564
|
transformed_sql_parts.append(original_sql[current_pos:])
|
|
521
565
|
return "".join(transformed_sql_parts), placeholder_map
|
|
522
566
|
|
|
567
|
+
def convert_placeholders(
|
|
568
|
+
self, sql: str, target_style: "ParameterStyle", parameter_info: "Optional[list[ParameterInfo]]" = None
|
|
569
|
+
) -> str:
|
|
570
|
+
"""Convert SQL placeholders to a target style.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
sql: The SQL string with placeholders
|
|
574
|
+
target_style: The target parameter style to convert to
|
|
575
|
+
parameter_info: Optional list of parameter info (will be extracted if not provided)
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
SQL string with converted placeholders
|
|
579
|
+
"""
|
|
580
|
+
if parameter_info is None:
|
|
581
|
+
parameter_info = self.validator.extract_parameters(sql)
|
|
582
|
+
|
|
583
|
+
if not parameter_info:
|
|
584
|
+
return sql
|
|
585
|
+
|
|
586
|
+
result_parts = []
|
|
587
|
+
current_pos = 0
|
|
588
|
+
|
|
589
|
+
for i, param in enumerate(parameter_info):
|
|
590
|
+
result_parts.append(sql[current_pos : param.position])
|
|
591
|
+
|
|
592
|
+
if target_style == ParameterStyle.QMARK:
|
|
593
|
+
placeholder = "?"
|
|
594
|
+
elif target_style == ParameterStyle.NUMERIC:
|
|
595
|
+
placeholder = f"${i + 1}"
|
|
596
|
+
elif target_style == ParameterStyle.POSITIONAL_PYFORMAT:
|
|
597
|
+
placeholder = "%s"
|
|
598
|
+
elif target_style == ParameterStyle.NAMED_COLON:
|
|
599
|
+
if param.style in {
|
|
600
|
+
ParameterStyle.POSITIONAL_COLON,
|
|
601
|
+
ParameterStyle.QMARK,
|
|
602
|
+
ParameterStyle.NUMERIC,
|
|
603
|
+
ParameterStyle.POSITIONAL_PYFORMAT,
|
|
604
|
+
}:
|
|
605
|
+
name = f"param_{i}"
|
|
606
|
+
else:
|
|
607
|
+
name = param.name or f"param_{i}"
|
|
608
|
+
placeholder = f":{name}"
|
|
609
|
+
elif target_style == ParameterStyle.NAMED_PYFORMAT:
|
|
610
|
+
if param.style in {
|
|
611
|
+
ParameterStyle.POSITIONAL_COLON,
|
|
612
|
+
ParameterStyle.QMARK,
|
|
613
|
+
ParameterStyle.NUMERIC,
|
|
614
|
+
ParameterStyle.POSITIONAL_PYFORMAT,
|
|
615
|
+
}:
|
|
616
|
+
name = f"param_{i}"
|
|
617
|
+
else:
|
|
618
|
+
name = param.name or f"param_{i}"
|
|
619
|
+
placeholder = f"%({name})s"
|
|
620
|
+
elif target_style == ParameterStyle.NAMED_AT:
|
|
621
|
+
if param.style in {
|
|
622
|
+
ParameterStyle.POSITIONAL_COLON,
|
|
623
|
+
ParameterStyle.QMARK,
|
|
624
|
+
ParameterStyle.NUMERIC,
|
|
625
|
+
ParameterStyle.POSITIONAL_PYFORMAT,
|
|
626
|
+
}:
|
|
627
|
+
name = f"param_{i}"
|
|
628
|
+
else:
|
|
629
|
+
name = param.name or f"param_{i}"
|
|
630
|
+
placeholder = f"@{name}"
|
|
631
|
+
elif target_style == ParameterStyle.NAMED_DOLLAR:
|
|
632
|
+
if param.style in {
|
|
633
|
+
ParameterStyle.POSITIONAL_COLON,
|
|
634
|
+
ParameterStyle.QMARK,
|
|
635
|
+
ParameterStyle.NUMERIC,
|
|
636
|
+
ParameterStyle.POSITIONAL_PYFORMAT,
|
|
637
|
+
}:
|
|
638
|
+
name = f"param_{i}"
|
|
639
|
+
else:
|
|
640
|
+
name = param.name or f"param_{i}"
|
|
641
|
+
placeholder = f"${name}"
|
|
642
|
+
elif target_style == ParameterStyle.POSITIONAL_COLON:
|
|
643
|
+
placeholder = f":{i + 1}"
|
|
644
|
+
else:
|
|
645
|
+
placeholder = param.placeholder_text
|
|
646
|
+
|
|
647
|
+
result_parts.append(placeholder)
|
|
648
|
+
current_pos = param.position + len(param.placeholder_text)
|
|
649
|
+
|
|
650
|
+
result_parts.append(sql[current_pos:])
|
|
651
|
+
|
|
652
|
+
return "".join(result_parts)
|
|
653
|
+
|
|
523
654
|
def convert_parameters(
|
|
524
655
|
self,
|
|
525
656
|
sql: str,
|
|
@@ -527,7 +658,7 @@ class ParameterConverter:
|
|
|
527
658
|
args: "Optional[Sequence[Any]]" = None,
|
|
528
659
|
kwargs: "Optional[Mapping[str, Any]]" = None,
|
|
529
660
|
validate: bool = True,
|
|
530
|
-
) ->
|
|
661
|
+
) -> ConvertedParameters:
|
|
531
662
|
"""Convert and merge parameters, and transform SQL for parsing.
|
|
532
663
|
|
|
533
664
|
Args:
|
|
@@ -538,15 +669,12 @@ class ParameterConverter:
|
|
|
538
669
|
validate: Whether to validate parameters
|
|
539
670
|
|
|
540
671
|
Returns:
|
|
541
|
-
|
|
542
|
-
where extra_info contains 'was_normalized' flag and other metadata
|
|
672
|
+
ConvertedParameters object with all conversion information
|
|
543
673
|
"""
|
|
544
674
|
parameters_info = self.validator.extract_parameters(sql)
|
|
545
675
|
|
|
546
|
-
# Check if normalization is needed for SQLGlot compatibility
|
|
547
676
|
needs_normalization = any(p.style in SQLGLOT_INCOMPATIBLE_STYLES for p in parameters_info)
|
|
548
677
|
|
|
549
|
-
# Check if we have mixed parameter styles and both args and kwargs
|
|
550
678
|
has_positional = any(p.name is None for p in parameters_info)
|
|
551
679
|
has_named = any(p.name is not None for p in parameters_info)
|
|
552
680
|
has_mixed_styles = has_positional and has_named
|
|
@@ -558,25 +686,29 @@ class ParameterConverter:
|
|
|
558
686
|
|
|
559
687
|
if validate:
|
|
560
688
|
self.validator.validate_parameters(parameters_info, merged_params, sql)
|
|
561
|
-
|
|
562
|
-
# Conditional normalization
|
|
563
689
|
if needs_normalization:
|
|
564
690
|
transformed_sql, placeholder_map = self._transform_sql_for_parsing(sql, parameters_info)
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
691
|
+
normalization_state = ParameterNormalizationState(
|
|
692
|
+
was_normalized=True,
|
|
693
|
+
original_styles=list({p.style for p in parameters_info}),
|
|
694
|
+
normalized_style=ParameterStyle.NAMED_COLON,
|
|
695
|
+
placeholder_map=placeholder_map,
|
|
696
|
+
original_param_info=parameters_info,
|
|
697
|
+
)
|
|
570
698
|
else:
|
|
571
|
-
# No normalization needed, return SQL as-is
|
|
572
699
|
transformed_sql = sql
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
700
|
+
normalization_state = ParameterNormalizationState(
|
|
701
|
+
was_normalized=False,
|
|
702
|
+
original_styles=list({p.style for p in parameters_info}),
|
|
703
|
+
original_param_info=parameters_info,
|
|
704
|
+
)
|
|
578
705
|
|
|
579
|
-
return
|
|
706
|
+
return ConvertedParameters(
|
|
707
|
+
transformed_sql=transformed_sql,
|
|
708
|
+
parameter_info=parameters_info,
|
|
709
|
+
merged_parameters=merged_params,
|
|
710
|
+
normalization_state=normalization_state,
|
|
711
|
+
)
|
|
580
712
|
|
|
581
713
|
@staticmethod
|
|
582
714
|
def _merge_mixed_parameters(
|
|
@@ -594,15 +726,12 @@ class ParameterConverter:
|
|
|
594
726
|
"""
|
|
595
727
|
merged: dict[str, Any] = {}
|
|
596
728
|
|
|
597
|
-
# Add named parameters from kwargs
|
|
598
729
|
merged.update(kwargs)
|
|
599
730
|
|
|
600
|
-
# Add positional parameters with generated names
|
|
601
731
|
positional_count = 0
|
|
602
732
|
for param_info in parameters_info:
|
|
603
|
-
if param_info.name is None and positional_count < len(args):
|
|
604
|
-
|
|
605
|
-
param_name = f"_arg_{param_info.ordinal}"
|
|
733
|
+
if param_info.name is None and positional_count < len(args):
|
|
734
|
+
param_name = f"arg_{param_info.ordinal}"
|
|
606
735
|
merged[param_name] = args[positional_count]
|
|
607
736
|
positional_count += 1
|
|
608
737
|
|
|
@@ -629,11 +758,9 @@ class ParameterConverter:
|
|
|
629
758
|
if kwargs is not None:
|
|
630
759
|
return dict(kwargs) # Make a copy
|
|
631
760
|
|
|
632
|
-
# No kwargs, consider args if parameters is None
|
|
633
761
|
if args is not None:
|
|
634
762
|
return list(args) # Convert tuple of args to list for consistency and mutability if needed later
|
|
635
763
|
|
|
636
|
-
# Return None if nothing provided
|
|
637
764
|
return None
|
|
638
765
|
|
|
639
766
|
@staticmethod
|
|
@@ -654,51 +781,55 @@ class ParameterConverter:
|
|
|
654
781
|
Returns:
|
|
655
782
|
Parameters with TypedParameter wrapping where appropriate
|
|
656
783
|
"""
|
|
657
|
-
if parameters is None
|
|
658
|
-
return None
|
|
784
|
+
return None if parameters is None else parameters
|
|
659
785
|
|
|
660
|
-
|
|
661
|
-
# in the literal parameterizer when it extracts literals and creates
|
|
662
|
-
# TypedParameter objects for them.
|
|
663
|
-
return parameters
|
|
664
|
-
|
|
665
|
-
def _denormalize_sql(
|
|
786
|
+
def _convert_sql_placeholders(
|
|
666
787
|
self, rendered_sql: str, final_parameter_info: "list[ParameterInfo]", target_style: "ParameterStyle"
|
|
667
788
|
) -> str:
|
|
668
789
|
"""Internal method to convert SQL from canonical format to target style.
|
|
669
790
|
|
|
670
791
|
Args:
|
|
671
|
-
rendered_sql: SQL with canonical placeholders (:
|
|
792
|
+
rendered_sql: SQL with canonical placeholders (:param_N)
|
|
672
793
|
final_parameter_info: Complete parameter info list
|
|
673
794
|
target_style: Target parameter style
|
|
674
795
|
|
|
675
796
|
Returns:
|
|
676
797
|
SQL with target style placeholders
|
|
677
798
|
"""
|
|
678
|
-
# Extract canonical placeholders from rendered SQL
|
|
679
799
|
canonical_params = self.validator.extract_parameters(rendered_sql)
|
|
680
800
|
|
|
681
|
-
|
|
801
|
+
# When we have more canonical parameters than final_parameter_info,
|
|
802
|
+
# it's likely because the ParameterizeLiterals transformer added extra parameters.
|
|
803
|
+
# We need to denormalize ALL parameters to ensure proper placeholder conversion.
|
|
804
|
+
# The final_parameter_info only contains the original parameters, but we need
|
|
805
|
+
# to handle all placeholders in the SQL (including those added by transformers).
|
|
806
|
+
if len(canonical_params) > len(final_parameter_info):
|
|
807
|
+
# Extend final_parameter_info to match canonical_params
|
|
808
|
+
# Use the canonical param info for the extra parameters
|
|
809
|
+
final_parameter_info = list(final_parameter_info)
|
|
810
|
+
for i in range(len(final_parameter_info), len(canonical_params)):
|
|
811
|
+
# Create a synthetic ParameterInfo for the extra parameter
|
|
812
|
+
canonical = canonical_params[i]
|
|
813
|
+
# Use the ordinal from the canonical parameter
|
|
814
|
+
final_parameter_info.append(canonical)
|
|
815
|
+
elif len(canonical_params) < len(final_parameter_info):
|
|
682
816
|
from sqlspec.exceptions import SQLTransformationError
|
|
683
817
|
|
|
684
818
|
msg = (
|
|
685
819
|
f"Parameter count mismatch during denormalization. "
|
|
686
|
-
f"Expected {len(final_parameter_info)} parameters, "
|
|
820
|
+
f"Expected at least {len(final_parameter_info)} parameters, "
|
|
687
821
|
f"found {len(canonical_params)} in SQL"
|
|
688
822
|
)
|
|
689
823
|
raise SQLTransformationError(msg)
|
|
690
824
|
|
|
691
825
|
result_sql = rendered_sql
|
|
692
826
|
|
|
693
|
-
# Replace in reverse order to preserve positions
|
|
694
827
|
for i in range(len(canonical_params) - 1, -1, -1):
|
|
695
828
|
canonical = canonical_params[i]
|
|
696
829
|
source_info = final_parameter_info[i]
|
|
697
830
|
|
|
698
831
|
start = canonical.position
|
|
699
832
|
end = start + len(canonical.placeholder_text)
|
|
700
|
-
|
|
701
|
-
# Generate target placeholder
|
|
702
833
|
new_placeholder = self._get_placeholder_for_style(target_style, source_info)
|
|
703
834
|
result_sql = result_sql[:start] + new_placeholder + result_sql[end:]
|
|
704
835
|
|
|
@@ -720,17 +851,17 @@ class ParameterConverter:
|
|
|
720
851
|
if target_style == ParameterStyle.NUMERIC:
|
|
721
852
|
return f"${param_info.ordinal + 1}"
|
|
722
853
|
if target_style == ParameterStyle.NAMED_COLON:
|
|
723
|
-
return f":{param_info.name}" if param_info.name else f":
|
|
854
|
+
return f":{param_info.name}" if param_info.name else f":arg_{param_info.ordinal}"
|
|
724
855
|
if target_style == ParameterStyle.POSITIONAL_COLON:
|
|
725
|
-
|
|
856
|
+
if param_info.style == ParameterStyle.POSITIONAL_COLON and param_info.name and param_info.name.isdigit():
|
|
857
|
+
return f":{param_info.name}"
|
|
726
858
|
return f":{param_info.ordinal + 1}"
|
|
727
859
|
if target_style == ParameterStyle.NAMED_AT:
|
|
728
|
-
return f"@{param_info.name}" if param_info.name else f"@
|
|
860
|
+
return f"@{param_info.name}" if param_info.name else f"@arg_{param_info.ordinal}"
|
|
729
861
|
if target_style == ParameterStyle.NAMED_DOLLAR:
|
|
730
|
-
return f"${param_info.name}" if param_info.name else f"$
|
|
862
|
+
return f"${param_info.name}" if param_info.name else f"$arg_{param_info.ordinal}"
|
|
731
863
|
if target_style == ParameterStyle.NAMED_PYFORMAT:
|
|
732
|
-
return f"%({param_info.name})s" if param_info.name else f"%(
|
|
864
|
+
return f"%({param_info.name})s" if param_info.name else f"%(arg_{param_info.ordinal})s"
|
|
733
865
|
if target_style == ParameterStyle.POSITIONAL_PYFORMAT:
|
|
734
866
|
return "%s"
|
|
735
|
-
# Fallback to original
|
|
736
867
|
return param_info.placeholder_text
|