sqlspec 0.17.1__py3-none-any.whl → 0.18.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/__init__.py +1 -1
- sqlspec/_sql.py +54 -159
- sqlspec/adapters/adbc/config.py +24 -30
- sqlspec/adapters/adbc/driver.py +42 -61
- sqlspec/adapters/aiosqlite/config.py +5 -10
- sqlspec/adapters/aiosqlite/driver.py +9 -25
- sqlspec/adapters/aiosqlite/pool.py +43 -35
- sqlspec/adapters/asyncmy/config.py +10 -7
- sqlspec/adapters/asyncmy/driver.py +18 -39
- sqlspec/adapters/asyncpg/config.py +4 -0
- sqlspec/adapters/asyncpg/driver.py +32 -79
- sqlspec/adapters/bigquery/config.py +12 -65
- sqlspec/adapters/bigquery/driver.py +39 -133
- sqlspec/adapters/duckdb/config.py +11 -15
- sqlspec/adapters/duckdb/driver.py +61 -85
- sqlspec/adapters/duckdb/pool.py +2 -5
- sqlspec/adapters/oracledb/_types.py +8 -1
- sqlspec/adapters/oracledb/config.py +55 -38
- sqlspec/adapters/oracledb/driver.py +35 -92
- sqlspec/adapters/oracledb/migrations.py +257 -0
- sqlspec/adapters/psqlpy/config.py +13 -9
- sqlspec/adapters/psqlpy/driver.py +28 -103
- sqlspec/adapters/psycopg/config.py +9 -5
- sqlspec/adapters/psycopg/driver.py +107 -175
- sqlspec/adapters/sqlite/config.py +7 -5
- sqlspec/adapters/sqlite/driver.py +37 -73
- sqlspec/adapters/sqlite/pool.py +3 -12
- sqlspec/base.py +1 -8
- sqlspec/builder/__init__.py +1 -1
- sqlspec/builder/_base.py +34 -20
- sqlspec/builder/_ddl.py +407 -183
- sqlspec/builder/_insert.py +1 -1
- sqlspec/builder/mixins/_insert_operations.py +26 -6
- sqlspec/builder/mixins/_merge_operations.py +1 -1
- sqlspec/builder/mixins/_select_operations.py +1 -5
- sqlspec/config.py +32 -13
- sqlspec/core/__init__.py +89 -14
- sqlspec/core/cache.py +57 -104
- sqlspec/core/compiler.py +57 -112
- sqlspec/core/filters.py +1 -21
- sqlspec/core/hashing.py +13 -47
- sqlspec/core/parameters.py +272 -261
- sqlspec/core/result.py +12 -27
- sqlspec/core/splitter.py +17 -21
- sqlspec/core/statement.py +150 -159
- sqlspec/driver/_async.py +2 -15
- sqlspec/driver/_common.py +16 -95
- sqlspec/driver/_sync.py +2 -15
- sqlspec/driver/mixins/_result_tools.py +8 -29
- sqlspec/driver/mixins/_sql_translator.py +6 -8
- sqlspec/exceptions.py +1 -2
- sqlspec/loader.py +43 -115
- sqlspec/migrations/__init__.py +1 -1
- sqlspec/migrations/base.py +34 -45
- sqlspec/migrations/commands.py +34 -15
- sqlspec/migrations/loaders.py +1 -1
- sqlspec/migrations/runner.py +104 -19
- sqlspec/migrations/tracker.py +49 -2
- sqlspec/protocols.py +3 -6
- sqlspec/storage/__init__.py +4 -4
- sqlspec/storage/backends/fsspec.py +5 -6
- sqlspec/storage/backends/obstore.py +7 -8
- sqlspec/storage/registry.py +3 -3
- sqlspec/utils/__init__.py +2 -2
- sqlspec/utils/logging.py +6 -10
- sqlspec/utils/sync_tools.py +27 -4
- sqlspec/utils/text.py +6 -1
- {sqlspec-0.17.1.dist-info → sqlspec-0.18.0.dist-info}/METADATA +1 -1
- sqlspec-0.18.0.dist-info/RECORD +138 -0
- sqlspec/builder/_ddl_utils.py +0 -103
- sqlspec-0.17.1.dist-info/RECORD +0 -138
- {sqlspec-0.17.1.dist-info → sqlspec-0.18.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.17.1.dist-info → sqlspec-0.18.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.17.1.dist-info → sqlspec-0.18.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.17.1.dist-info → sqlspec-0.18.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/core/parameters.py
CHANGED
|
@@ -9,14 +9,13 @@ Components:
|
|
|
9
9
|
- ParameterInfo: Tracks parameter metadata
|
|
10
10
|
- ParameterValidator: Extracts and validates parameters
|
|
11
11
|
- ParameterConverter: Handles parameter style conversions
|
|
12
|
-
- ParameterProcessor:
|
|
12
|
+
- ParameterProcessor: Parameter processing coordinator
|
|
13
13
|
- ParameterStyleConfig: Configuration for parameter processing
|
|
14
14
|
|
|
15
15
|
Features:
|
|
16
|
-
- Two-phase processing:
|
|
16
|
+
- Two-phase processing: compatibility and execution format
|
|
17
17
|
- Type-specific parameter wrapping
|
|
18
18
|
- Parameter style conversions
|
|
19
|
-
- Caching system for parameter extraction and conversion
|
|
20
19
|
- Support for multiple parameter styles and database adapters
|
|
21
20
|
"""
|
|
22
21
|
|
|
@@ -93,15 +92,15 @@ class ParameterStyle(str, Enum):
|
|
|
93
92
|
POSITIONAL_PYFORMAT = "pyformat_positional"
|
|
94
93
|
|
|
95
94
|
|
|
96
|
-
@mypyc_attr(allow_interpreted_subclasses=
|
|
95
|
+
@mypyc_attr(allow_interpreted_subclasses=False)
|
|
97
96
|
class TypedParameter:
|
|
98
97
|
"""Parameter wrapper that preserves type information.
|
|
99
98
|
|
|
100
|
-
Maintains type information through
|
|
99
|
+
Maintains type information through parsing and execution
|
|
101
100
|
format conversion.
|
|
102
101
|
|
|
103
102
|
Use Cases:
|
|
104
|
-
- Preserve boolean values through
|
|
103
|
+
- Preserve boolean values through parsing
|
|
105
104
|
- Maintain Decimal precision
|
|
106
105
|
- Handle date/datetime formatting
|
|
107
106
|
- Preserve array/list structures
|
|
@@ -126,7 +125,6 @@ class TypedParameter:
|
|
|
126
125
|
def __hash__(self) -> int:
|
|
127
126
|
"""Cached hash value with optimization."""
|
|
128
127
|
if self._hash is None:
|
|
129
|
-
# Optimize by avoiding tuple creation for common case
|
|
130
128
|
value_id = id(self.value)
|
|
131
129
|
self._hash = hash((value_id, self.original_type, self.semantic_name))
|
|
132
130
|
return self._hash
|
|
@@ -340,10 +338,10 @@ class ParameterValidator:
|
|
|
340
338
|
"""Parameter validation and extraction.
|
|
341
339
|
|
|
342
340
|
Extracts parameter information from SQL strings and determines
|
|
343
|
-
|
|
341
|
+
compatibility.
|
|
344
342
|
|
|
345
343
|
Features:
|
|
346
|
-
-
|
|
344
|
+
- Parameter extraction results
|
|
347
345
|
- Regex-based parameter detection
|
|
348
346
|
- Dialect-specific compatibility checking
|
|
349
347
|
"""
|
|
@@ -354,6 +352,42 @@ class ParameterValidator:
|
|
|
354
352
|
"""Initialize validator with parameter cache."""
|
|
355
353
|
self._parameter_cache: dict[str, list[ParameterInfo]] = {}
|
|
356
354
|
|
|
355
|
+
def _extract_parameter_style(self, match: "re.Match[str]") -> "tuple[Optional[ParameterStyle], Optional[str]]":
|
|
356
|
+
"""Extract parameter style and name from regex match.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
match: Regex match object
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Tuple of (style, name) or (None, None) if not a parameter
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
if match.group("qmark"):
|
|
366
|
+
return ParameterStyle.QMARK, None
|
|
367
|
+
|
|
368
|
+
if match.group("named_colon"):
|
|
369
|
+
return ParameterStyle.NAMED_COLON, match.group("colon_name")
|
|
370
|
+
|
|
371
|
+
if match.group("numeric"):
|
|
372
|
+
return ParameterStyle.NUMERIC, match.group("numeric_num")
|
|
373
|
+
|
|
374
|
+
if match.group("named_at"):
|
|
375
|
+
return ParameterStyle.NAMED_AT, match.group("at_name")
|
|
376
|
+
|
|
377
|
+
if match.group("pyformat_named"):
|
|
378
|
+
return ParameterStyle.NAMED_PYFORMAT, match.group("pyformat_name")
|
|
379
|
+
|
|
380
|
+
if match.group("pyformat_pos"):
|
|
381
|
+
return ParameterStyle.POSITIONAL_PYFORMAT, None
|
|
382
|
+
|
|
383
|
+
if match.group("positional_colon"):
|
|
384
|
+
return ParameterStyle.POSITIONAL_COLON, match.group("colon_num")
|
|
385
|
+
|
|
386
|
+
if match.group("named_dollar_param"):
|
|
387
|
+
return ParameterStyle.NAMED_DOLLAR, match.group("dollar_param_name")
|
|
388
|
+
|
|
389
|
+
return None, None
|
|
390
|
+
|
|
357
391
|
def extract_parameters(self, sql: str) -> "list[ParameterInfo]":
|
|
358
392
|
"""Extract all parameters from SQL.
|
|
359
393
|
|
|
@@ -370,65 +404,26 @@ class ParameterValidator:
|
|
|
370
404
|
parameters: list[ParameterInfo] = []
|
|
371
405
|
ordinal = 0
|
|
372
406
|
|
|
407
|
+
skip_groups = (
|
|
408
|
+
"dquote",
|
|
409
|
+
"squote",
|
|
410
|
+
"dollar_quoted_string",
|
|
411
|
+
"line_comment",
|
|
412
|
+
"block_comment",
|
|
413
|
+
"pg_q_operator",
|
|
414
|
+
"pg_cast",
|
|
415
|
+
)
|
|
416
|
+
|
|
373
417
|
for match in _PARAMETER_REGEX.finditer(sql):
|
|
374
|
-
|
|
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
|
-
):
|
|
418
|
+
if any(match.group(g) for g in skip_groups):
|
|
384
419
|
continue
|
|
385
420
|
|
|
386
|
-
|
|
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
|
|
421
|
+
style, name = self._extract_parameter_style(match)
|
|
427
422
|
|
|
428
423
|
if style is not None:
|
|
429
424
|
parameters.append(
|
|
430
425
|
ParameterInfo(
|
|
431
|
-
name=name, style=style, position=
|
|
426
|
+
name=name, style=style, position=match.start(), ordinal=ordinal, placeholder_text=match.group(0)
|
|
432
427
|
)
|
|
433
428
|
)
|
|
434
429
|
ordinal += 1
|
|
@@ -446,9 +441,9 @@ class ParameterValidator:
|
|
|
446
441
|
Set of parameter styles incompatible with SQLGlot
|
|
447
442
|
"""
|
|
448
443
|
base_incompatible = {
|
|
449
|
-
ParameterStyle.POSITIONAL_PYFORMAT,
|
|
450
|
-
ParameterStyle.NAMED_PYFORMAT,
|
|
451
|
-
ParameterStyle.POSITIONAL_COLON,
|
|
444
|
+
ParameterStyle.POSITIONAL_PYFORMAT,
|
|
445
|
+
ParameterStyle.NAMED_PYFORMAT,
|
|
446
|
+
ParameterStyle.POSITIONAL_COLON,
|
|
452
447
|
}
|
|
453
448
|
|
|
454
449
|
if dialect and dialect.lower() in {"mysql", "mariadb"}:
|
|
@@ -467,12 +462,12 @@ class ParameterConverter:
|
|
|
467
462
|
"""Parameter style conversion.
|
|
468
463
|
|
|
469
464
|
Handles two-phase parameter processing:
|
|
470
|
-
- Phase 1:
|
|
465
|
+
- Phase 1: Compatibility normalization
|
|
471
466
|
- Phase 2: Execution format conversion
|
|
472
467
|
|
|
473
468
|
Features:
|
|
474
469
|
- Converts incompatible styles to canonical format
|
|
475
|
-
- Enables
|
|
470
|
+
- Enables parsing of problematic parameter styles
|
|
476
471
|
- Handles parameter format changes (list ↔ dict, positional ↔ named)
|
|
477
472
|
"""
|
|
478
473
|
|
|
@@ -489,7 +484,7 @@ class ParameterConverter:
|
|
|
489
484
|
ParameterStyle.QMARK: self._convert_to_positional_format,
|
|
490
485
|
ParameterStyle.NUMERIC: self._convert_to_positional_format,
|
|
491
486
|
ParameterStyle.POSITIONAL_PYFORMAT: self._convert_to_positional_format,
|
|
492
|
-
ParameterStyle.NAMED_AT: self._convert_to_named_colon_format,
|
|
487
|
+
ParameterStyle.NAMED_AT: self._convert_to_named_colon_format,
|
|
493
488
|
ParameterStyle.NAMED_DOLLAR: self._convert_to_named_colon_format,
|
|
494
489
|
}
|
|
495
490
|
|
|
@@ -505,10 +500,10 @@ class ParameterConverter:
|
|
|
505
500
|
}
|
|
506
501
|
|
|
507
502
|
def normalize_sql_for_parsing(self, sql: str, dialect: Optional[str] = None) -> "tuple[str, list[ParameterInfo]]":
|
|
508
|
-
"""Convert SQL to
|
|
503
|
+
"""Convert SQL to parsable format.
|
|
509
504
|
|
|
510
505
|
Takes raw SQL with potentially incompatible parameter styles and converts
|
|
511
|
-
them to a canonical format
|
|
506
|
+
them to a canonical format for parsing.
|
|
512
507
|
|
|
513
508
|
Args:
|
|
514
509
|
sql: Raw SQL string with any parameter style
|
|
@@ -586,13 +581,11 @@ class ParameterConverter:
|
|
|
586
581
|
msg = f"Unsupported target parameter style: {target_style}"
|
|
587
582
|
raise ValueError(msg)
|
|
588
583
|
|
|
589
|
-
# Optimize parameter style detection
|
|
590
584
|
param_styles = {p.style for p in param_info}
|
|
591
585
|
use_sequential_for_qmark = (
|
|
592
586
|
len(param_styles) == 1 and ParameterStyle.QMARK in param_styles and target_style == ParameterStyle.NUMERIC
|
|
593
587
|
)
|
|
594
588
|
|
|
595
|
-
# Build unique parameters mapping efficiently
|
|
596
589
|
unique_params: dict[str, int] = {}
|
|
597
590
|
for param in param_info:
|
|
598
591
|
param_key = (
|
|
@@ -604,17 +597,14 @@ class ParameterConverter:
|
|
|
604
597
|
if param_key not in unique_params:
|
|
605
598
|
unique_params[param_key] = len(unique_params)
|
|
606
599
|
|
|
607
|
-
# Convert SQL with optimized string operations
|
|
608
600
|
converted_sql = sql
|
|
609
601
|
placeholder_text_len_cache: dict[str, int] = {}
|
|
610
602
|
|
|
611
603
|
for param in reversed(param_info):
|
|
612
|
-
# Cache placeholder text length to avoid recalculation
|
|
613
604
|
if param.placeholder_text not in placeholder_text_len_cache:
|
|
614
605
|
placeholder_text_len_cache[param.placeholder_text] = len(param.placeholder_text)
|
|
615
606
|
text_len = placeholder_text_len_cache[param.placeholder_text]
|
|
616
607
|
|
|
617
|
-
# Generate new placeholder based on target style
|
|
618
608
|
if target_style in {
|
|
619
609
|
ParameterStyle.QMARK,
|
|
620
610
|
ParameterStyle.NUMERIC,
|
|
@@ -627,18 +617,112 @@ class ParameterConverter:
|
|
|
627
617
|
else param.placeholder_text
|
|
628
618
|
)
|
|
629
619
|
new_placeholder = generator(unique_params[param_key])
|
|
630
|
-
else:
|
|
620
|
+
else:
|
|
631
621
|
param_name = param.name or f"param_{param.ordinal}"
|
|
632
622
|
new_placeholder = generator(param_name)
|
|
633
623
|
|
|
634
|
-
# Optimized string replacement
|
|
635
624
|
converted_sql = (
|
|
636
625
|
converted_sql[: param.position] + new_placeholder + converted_sql[param.position + text_len :]
|
|
637
626
|
)
|
|
638
627
|
|
|
639
628
|
return converted_sql
|
|
640
629
|
|
|
641
|
-
def
|
|
630
|
+
def _convert_sequence_to_dict(self, parameters: Sequence, param_info: "list[ParameterInfo]") -> "dict[str, Any]":
|
|
631
|
+
"""Convert sequence parameters to dictionary for named styles.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
parameters: Sequence of parameter values
|
|
635
|
+
param_info: Parameter information from SQL
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
Dictionary mapping parameter names to values
|
|
639
|
+
"""
|
|
640
|
+
param_dict = {}
|
|
641
|
+
for i, param in enumerate(param_info):
|
|
642
|
+
if i < len(parameters):
|
|
643
|
+
name = param.name or f"param_{param.ordinal}"
|
|
644
|
+
param_dict[name] = parameters[i]
|
|
645
|
+
return param_dict
|
|
646
|
+
|
|
647
|
+
def _extract_param_value_mixed_styles(
|
|
648
|
+
self, param: ParameterInfo, parameters: Mapping, param_keys: "list[str]"
|
|
649
|
+
) -> "tuple[Any, bool]":
|
|
650
|
+
"""Extract parameter value for mixed style parameters.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
param: Parameter information
|
|
654
|
+
parameters: Parameter mapping
|
|
655
|
+
param_keys: List of parameter keys
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
Tuple of (value, found_flag)
|
|
659
|
+
"""
|
|
660
|
+
if param.name and param.name in parameters:
|
|
661
|
+
return parameters[param.name], True
|
|
662
|
+
|
|
663
|
+
if (
|
|
664
|
+
param.style == ParameterStyle.NUMERIC
|
|
665
|
+
and param.name
|
|
666
|
+
and param.name.isdigit()
|
|
667
|
+
and param.ordinal < len(param_keys)
|
|
668
|
+
):
|
|
669
|
+
key_to_use = param_keys[param.ordinal]
|
|
670
|
+
return parameters[key_to_use], True
|
|
671
|
+
|
|
672
|
+
if f"param_{param.ordinal}" in parameters:
|
|
673
|
+
return parameters[f"param_{param.ordinal}"], True
|
|
674
|
+
|
|
675
|
+
ordinal_key = str(param.ordinal + 1)
|
|
676
|
+
if ordinal_key in parameters:
|
|
677
|
+
return parameters[ordinal_key], True
|
|
678
|
+
|
|
679
|
+
return None, False
|
|
680
|
+
|
|
681
|
+
def _extract_param_value_single_style(self, param: ParameterInfo, parameters: Mapping) -> Any:
|
|
682
|
+
"""Extract parameter value for single style parameters.
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
param: Parameter information
|
|
686
|
+
parameters: Parameter mapping
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
Parameter value or None if not found
|
|
690
|
+
"""
|
|
691
|
+
if param.name and param.name in parameters:
|
|
692
|
+
return parameters[param.name]
|
|
693
|
+
if f"param_{param.ordinal}" in parameters:
|
|
694
|
+
return parameters[f"param_{param.ordinal}"]
|
|
695
|
+
|
|
696
|
+
ordinal_key = str(param.ordinal + 1)
|
|
697
|
+
if ordinal_key in parameters:
|
|
698
|
+
return parameters[ordinal_key]
|
|
699
|
+
|
|
700
|
+
return None
|
|
701
|
+
|
|
702
|
+
def _preserve_original_format(self, param_values: "list[Any]", original_parameters: Any) -> Any:
|
|
703
|
+
"""Preserve the original parameter container format.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
param_values: List of parameter values
|
|
707
|
+
original_parameters: Original parameter container
|
|
708
|
+
|
|
709
|
+
Returns:
|
|
710
|
+
Parameters in original format
|
|
711
|
+
"""
|
|
712
|
+
if isinstance(original_parameters, tuple):
|
|
713
|
+
return tuple(param_values)
|
|
714
|
+
if isinstance(original_parameters, list):
|
|
715
|
+
return param_values
|
|
716
|
+
|
|
717
|
+
if hasattr(original_parameters, "__class__") and callable(original_parameters.__class__):
|
|
718
|
+
try:
|
|
719
|
+
return original_parameters.__class__(param_values)
|
|
720
|
+
except (TypeError, ValueError):
|
|
721
|
+
return tuple(param_values)
|
|
722
|
+
|
|
723
|
+
return param_values
|
|
724
|
+
|
|
725
|
+
def _convert_parameter_format(
|
|
642
726
|
self,
|
|
643
727
|
parameters: Any,
|
|
644
728
|
param_info: "list[ParameterInfo]",
|
|
@@ -658,7 +742,6 @@ class ParameterConverter:
|
|
|
658
742
|
if not parameters or not param_info:
|
|
659
743
|
return parameters
|
|
660
744
|
|
|
661
|
-
# Determine if target style expects named or positional parameters
|
|
662
745
|
is_named_style = target_style in {
|
|
663
746
|
ParameterStyle.NAMED_COLON,
|
|
664
747
|
ParameterStyle.NAMED_AT,
|
|
@@ -667,82 +750,34 @@ class ParameterConverter:
|
|
|
667
750
|
}
|
|
668
751
|
|
|
669
752
|
if is_named_style:
|
|
670
|
-
# Convert to dict format if needed
|
|
671
753
|
if isinstance(parameters, Mapping):
|
|
672
|
-
return parameters
|
|
754
|
+
return parameters
|
|
673
755
|
if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
|
|
674
|
-
|
|
675
|
-
|
|
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
|
|
756
|
+
return self._convert_sequence_to_dict(parameters, param_info)
|
|
757
|
+
|
|
682
758
|
elif isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
|
|
683
|
-
return parameters
|
|
759
|
+
return parameters
|
|
760
|
+
|
|
684
761
|
elif isinstance(parameters, Mapping):
|
|
685
|
-
# Convert named to positional
|
|
686
762
|
param_values = []
|
|
687
|
-
|
|
688
|
-
# Handle mixed parameter styles by creating a comprehensive parameter mapping
|
|
689
763
|
parameter_styles = {p.style for p in param_info}
|
|
690
764
|
has_mixed_styles = len(parameter_styles) > 1
|
|
691
765
|
|
|
692
766
|
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
767
|
param_keys = list(parameters.keys())
|
|
696
|
-
|
|
697
768
|
for param in param_info:
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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)])
|
|
769
|
+
value, found = self._extract_param_value_mixed_styles(param, parameters, param_keys)
|
|
770
|
+
if found:
|
|
771
|
+
param_values.append(value)
|
|
718
772
|
else:
|
|
719
|
-
# Original logic for single parameter style
|
|
720
773
|
for param in param_info:
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
|
774
|
+
value = self._extract_param_value_single_style(param, parameters)
|
|
775
|
+
if value is not None:
|
|
776
|
+
param_values.append(value)
|
|
777
|
+
|
|
732
778
|
if preserve_parameter_format and original_parameters is not None:
|
|
733
|
-
|
|
734
|
-
|
|
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
|
|
779
|
+
return self._preserve_original_format(param_values, original_parameters)
|
|
780
|
+
|
|
746
781
|
return param_values
|
|
747
782
|
|
|
748
783
|
return parameters
|
|
@@ -754,23 +789,13 @@ class ParameterConverter:
|
|
|
754
789
|
if not param_info:
|
|
755
790
|
return sql, None
|
|
756
791
|
|
|
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
792
|
unique_params: dict[str, int] = {}
|
|
761
793
|
for param in param_info:
|
|
762
|
-
# Create a unique key for each parameter based on what makes it distinct
|
|
763
794
|
if param.style in {ParameterStyle.QMARK, ParameterStyle.POSITIONAL_PYFORMAT}:
|
|
764
|
-
# For sequential positional parameters, each occurrence gets its own value
|
|
765
795
|
param_key = f"{param.placeholder_text}_{param.ordinal}"
|
|
766
|
-
elif param.style == ParameterStyle.NUMERIC and param.name:
|
|
767
|
-
|
|
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"
|
|
796
|
+
elif (param.style == ParameterStyle.NUMERIC and param.name) or param.name:
|
|
797
|
+
param_key = param.placeholder_text
|
|
772
798
|
else:
|
|
773
|
-
# Fallback: treat each occurrence as unique
|
|
774
799
|
param_key = f"{param.placeholder_text}_{param.ordinal}"
|
|
775
800
|
|
|
776
801
|
if param_key not in unique_params:
|
|
@@ -778,14 +803,11 @@ class ParameterConverter:
|
|
|
778
803
|
|
|
779
804
|
static_sql = sql
|
|
780
805
|
for param in reversed(param_info):
|
|
781
|
-
# Get parameter value using unique parameter mapping
|
|
782
806
|
param_value = self._get_parameter_value_with_reuse(parameters, param, unique_params)
|
|
783
807
|
|
|
784
|
-
# Convert to SQL literal
|
|
785
808
|
if param_value is None:
|
|
786
809
|
literal = "NULL"
|
|
787
810
|
elif isinstance(param_value, str):
|
|
788
|
-
# Escape single quotes
|
|
789
811
|
escaped = param_value.replace("'", "''")
|
|
790
812
|
literal = f"'{escaped}'"
|
|
791
813
|
elif isinstance(param_value, bool):
|
|
@@ -793,25 +815,22 @@ class ParameterConverter:
|
|
|
793
815
|
elif isinstance(param_value, (int, float)):
|
|
794
816
|
literal = str(param_value)
|
|
795
817
|
else:
|
|
796
|
-
# Convert to string and quote
|
|
797
818
|
literal = f"'{param_value!s}'"
|
|
798
819
|
|
|
799
|
-
# Replace placeholder with literal value
|
|
800
820
|
static_sql = (
|
|
801
821
|
static_sql[: param.position] + literal + static_sql[param.position + len(param.placeholder_text) :]
|
|
802
822
|
)
|
|
803
823
|
|
|
804
|
-
return static_sql, None
|
|
824
|
+
return static_sql, None
|
|
805
825
|
|
|
806
826
|
def _get_parameter_value(self, parameters: Any, param: ParameterInfo) -> Any:
|
|
807
827
|
"""Extract parameter value based on parameter info and format."""
|
|
808
828
|
if isinstance(parameters, Mapping):
|
|
809
|
-
# Try by name first, then by ordinal key
|
|
810
829
|
if param.name and param.name in parameters:
|
|
811
830
|
return parameters[param.name]
|
|
812
831
|
if f"param_{param.ordinal}" in parameters:
|
|
813
832
|
return parameters[f"param_{param.ordinal}"]
|
|
814
|
-
if str(param.ordinal + 1) in parameters:
|
|
833
|
+
if str(param.ordinal + 1) in parameters:
|
|
815
834
|
return parameters[str(param.ordinal + 1)]
|
|
816
835
|
elif isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
|
|
817
836
|
if param.ordinal < len(parameters):
|
|
@@ -832,41 +851,31 @@ class ParameterConverter:
|
|
|
832
851
|
Returns:
|
|
833
852
|
Parameter value, correctly handling reused parameters
|
|
834
853
|
"""
|
|
835
|
-
|
|
854
|
+
|
|
836
855
|
if param.style in {ParameterStyle.QMARK, ParameterStyle.POSITIONAL_PYFORMAT}:
|
|
837
|
-
# For sequential positional parameters, each occurrence gets its own value
|
|
838
856
|
param_key = f"{param.placeholder_text}_{param.ordinal}"
|
|
839
|
-
elif param.style == ParameterStyle.NUMERIC and param.name:
|
|
840
|
-
|
|
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"
|
|
857
|
+
elif (param.style == ParameterStyle.NUMERIC and param.name) or param.name:
|
|
858
|
+
param_key = param.placeholder_text
|
|
845
859
|
else:
|
|
846
|
-
# Fallback: treat each occurrence as unique
|
|
847
860
|
param_key = f"{param.placeholder_text}_{param.ordinal}"
|
|
848
861
|
|
|
849
|
-
# Get the unique ordinal for this parameter key
|
|
850
862
|
unique_ordinal = unique_params.get(param_key)
|
|
851
863
|
if unique_ordinal is None:
|
|
852
864
|
return None
|
|
853
865
|
|
|
854
866
|
if isinstance(parameters, Mapping):
|
|
855
|
-
# For named parameters, try different key formats
|
|
856
867
|
if param.name and param.name in parameters:
|
|
857
868
|
return parameters[param.name]
|
|
858
869
|
if f"param_{unique_ordinal}" in parameters:
|
|
859
870
|
return parameters[f"param_{unique_ordinal}"]
|
|
860
|
-
if str(unique_ordinal + 1) in parameters:
|
|
871
|
+
if str(unique_ordinal + 1) in parameters:
|
|
861
872
|
return parameters[str(unique_ordinal + 1)]
|
|
862
873
|
elif isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
|
|
863
|
-
# Use the unique ordinal to get the correct parameter value
|
|
864
874
|
if unique_ordinal < len(parameters):
|
|
865
875
|
return parameters[unique_ordinal]
|
|
866
876
|
|
|
867
877
|
return None
|
|
868
878
|
|
|
869
|
-
# Format converter methods for different parameter styles
|
|
870
879
|
def _convert_to_positional_format(self, parameters: Any, param_info: "list[ParameterInfo]") -> Any:
|
|
871
880
|
"""Convert parameters to positional format (list/tuple)."""
|
|
872
881
|
return self._convert_parameter_format(
|
|
@@ -882,9 +891,8 @@ class ParameterConverter:
|
|
|
882
891
|
def _convert_to_positional_colon_format(self, parameters: Any, param_info: "list[ParameterInfo]") -> Any:
|
|
883
892
|
"""Convert parameters to positional colon format with 1-based keys."""
|
|
884
893
|
if isinstance(parameters, Mapping):
|
|
885
|
-
return parameters
|
|
894
|
+
return parameters
|
|
886
895
|
|
|
887
|
-
# Convert to 1-based ordinal keys for Oracle
|
|
888
896
|
param_dict = {}
|
|
889
897
|
if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
|
|
890
898
|
for i, value in enumerate(parameters):
|
|
@@ -901,36 +909,89 @@ class ParameterConverter:
|
|
|
901
909
|
|
|
902
910
|
@mypyc_attr(allow_interpreted_subclasses=False)
|
|
903
911
|
class ParameterProcessor:
|
|
904
|
-
"""
|
|
912
|
+
"""Parameter processing engine.
|
|
905
913
|
|
|
906
|
-
This is the main entry point for the
|
|
907
|
-
that coordinates Phase 1 (
|
|
914
|
+
This is the main entry point for the parameter processing system
|
|
915
|
+
that coordinates Phase 1 (compatibility) and Phase 2 (execution format).
|
|
908
916
|
|
|
909
917
|
Processing Pipeline:
|
|
910
|
-
1. Type wrapping for
|
|
918
|
+
1. Type wrapping for compatibility (TypedParameter)
|
|
911
919
|
2. Driver-specific type coercions (type_coercion_map)
|
|
912
|
-
3. Phase 1:
|
|
920
|
+
3. Phase 1: Normalization if needed
|
|
913
921
|
4. Phase 2: Execution format conversion if needed
|
|
914
922
|
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
923
|
"""
|
|
921
924
|
|
|
922
925
|
__slots__ = ("_cache", "_cache_size", "_converter", "_validator")
|
|
923
926
|
|
|
924
|
-
# Class-level constants
|
|
925
927
|
DEFAULT_CACHE_SIZE = 1000
|
|
926
928
|
|
|
927
929
|
def __init__(self) -> None:
|
|
928
|
-
"""Initialize processor with
|
|
930
|
+
"""Initialize processor with component coordination."""
|
|
929
931
|
self._cache: dict[str, tuple[str, Any]] = {}
|
|
930
932
|
self._cache_size = 0
|
|
931
933
|
self._validator = ParameterValidator()
|
|
932
934
|
self._converter = ParameterConverter()
|
|
933
|
-
|
|
935
|
+
|
|
936
|
+
def _handle_static_embedding(
|
|
937
|
+
self, sql: str, parameters: Any, config: ParameterStyleConfig, is_many: bool, cache_key: str
|
|
938
|
+
) -> "tuple[str, Any]":
|
|
939
|
+
"""Handle static parameter embedding for script compilation.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
sql: SQL string
|
|
943
|
+
parameters: Parameter values
|
|
944
|
+
config: Parameter configuration
|
|
945
|
+
is_many: Whether this is for execute_many
|
|
946
|
+
cache_key: Cache key for result
|
|
947
|
+
|
|
948
|
+
Returns:
|
|
949
|
+
Tuple of (static_sql, static_params)
|
|
950
|
+
"""
|
|
951
|
+
coerced_params = parameters
|
|
952
|
+
if config.type_coercion_map and parameters:
|
|
953
|
+
coerced_params = self._apply_type_coercions(parameters, config.type_coercion_map, is_many)
|
|
954
|
+
|
|
955
|
+
static_sql, static_params = self._converter.convert_placeholder_style(
|
|
956
|
+
sql, coerced_params, ParameterStyle.STATIC, is_many
|
|
957
|
+
)
|
|
958
|
+
self._cache[cache_key] = (static_sql, static_params)
|
|
959
|
+
return static_sql, static_params
|
|
960
|
+
|
|
961
|
+
def _process_parameters_conversion(
|
|
962
|
+
self,
|
|
963
|
+
sql: str,
|
|
964
|
+
parameters: Any,
|
|
965
|
+
config: ParameterStyleConfig,
|
|
966
|
+
original_styles: "set[ParameterStyle]",
|
|
967
|
+
needs_execution_conversion: bool,
|
|
968
|
+
needs_sqlglot_normalization: bool,
|
|
969
|
+
is_many: bool,
|
|
970
|
+
) -> "tuple[str, Any]":
|
|
971
|
+
"""Process parameter conversion phase.
|
|
972
|
+
|
|
973
|
+
Args:
|
|
974
|
+
sql: Processed SQL string
|
|
975
|
+
parameters: Processed parameters
|
|
976
|
+
config: Parameter configuration
|
|
977
|
+
original_styles: Original parameter styles detected
|
|
978
|
+
needs_execution_conversion: Whether execution conversion is needed
|
|
979
|
+
needs_sqlglot_normalization: Whether SQLGlot normalization is needed
|
|
980
|
+
is_many: Whether this is for execute_many
|
|
981
|
+
|
|
982
|
+
Returns:
|
|
983
|
+
Tuple of (processed_sql, processed_parameters)
|
|
984
|
+
"""
|
|
985
|
+
if not (needs_execution_conversion or needs_sqlglot_normalization):
|
|
986
|
+
return sql, parameters
|
|
987
|
+
|
|
988
|
+
if is_many and config.preserve_original_params_for_many and isinstance(parameters, (list, tuple)):
|
|
989
|
+
target_style = self._determine_target_execution_style(original_styles, config)
|
|
990
|
+
processed_sql, _ = self._converter.convert_placeholder_style(sql, parameters, target_style, is_many)
|
|
991
|
+
return processed_sql, parameters
|
|
992
|
+
|
|
993
|
+
target_style = self._determine_target_execution_style(original_styles, config)
|
|
994
|
+
return self._converter.convert_placeholder_style(sql, parameters, target_style, is_many)
|
|
934
995
|
|
|
935
996
|
def process(
|
|
936
997
|
self,
|
|
@@ -942,9 +1003,9 @@ class ParameterProcessor:
|
|
|
942
1003
|
) -> "tuple[str, Any]":
|
|
943
1004
|
"""Complete parameter processing pipeline.
|
|
944
1005
|
|
|
945
|
-
This method coordinates the entire parameter
|
|
946
|
-
1. Type wrapping for
|
|
947
|
-
2. Phase 1:
|
|
1006
|
+
This method coordinates the entire parameter processing workflow:
|
|
1007
|
+
1. Type wrapping for compatibility
|
|
1008
|
+
2. Phase 1: Normalization if needed
|
|
948
1009
|
3. Phase 2: Execution format conversion
|
|
949
1010
|
4. Driver-specific type coercions
|
|
950
1011
|
5. Final output transformation
|
|
@@ -959,37 +1020,20 @@ class ParameterProcessor:
|
|
|
959
1020
|
Returns:
|
|
960
1021
|
Tuple of (final_sql, execution_parameters)
|
|
961
1022
|
"""
|
|
962
|
-
# 1. Cache lookup for processed results
|
|
963
1023
|
cache_key = f"{sql}:{hash(repr(parameters))}:{config.default_parameter_style}:{is_many}:{dialect}"
|
|
964
1024
|
if cache_key in self._cache:
|
|
965
1025
|
return self._cache[cache_key]
|
|
966
1026
|
|
|
967
|
-
# 2. Determine what transformations are needed
|
|
968
1027
|
param_info = self._validator.extract_parameters(sql)
|
|
969
1028
|
original_styles = {p.style for p in param_info} if param_info else set()
|
|
970
1029
|
needs_sqlglot_normalization = self._needs_sqlglot_normalization(param_info, dialect)
|
|
971
1030
|
needs_execution_conversion = self._needs_execution_conversion(param_info, config)
|
|
972
1031
|
|
|
973
|
-
|
|
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
|
|
1032
|
+
needs_static_embedding = config.needs_static_script_compilation and param_info and parameters and not is_many
|
|
978
1033
|
|
|
979
1034
|
if needs_static_embedding:
|
|
980
|
-
|
|
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
|
|
1035
|
+
return self._handle_static_embedding(sql, parameters, config, is_many, cache_key)
|
|
991
1036
|
|
|
992
|
-
# 3. Fast path: Skip processing if no transformation needed
|
|
993
1037
|
if (
|
|
994
1038
|
not needs_sqlglot_normalization
|
|
995
1039
|
and not needs_execution_conversion
|
|
@@ -998,47 +1042,30 @@ class ParameterProcessor:
|
|
|
998
1042
|
):
|
|
999
1043
|
return sql, parameters
|
|
1000
1044
|
|
|
1001
|
-
# 4. Progressive transformation pipeline
|
|
1002
1045
|
processed_sql, processed_parameters = sql, parameters
|
|
1003
1046
|
|
|
1004
|
-
# Phase A: Type wrapping for SQLGlot compatibility
|
|
1005
1047
|
if processed_parameters:
|
|
1006
1048
|
processed_parameters = self._apply_type_wrapping(processed_parameters)
|
|
1007
1049
|
|
|
1008
|
-
# Phase B: Phase 1 - SQLGlot normalization if needed
|
|
1009
1050
|
if needs_sqlglot_normalization:
|
|
1010
1051
|
processed_sql, _ = self._converter.normalize_sql_for_parsing(processed_sql, dialect)
|
|
1011
1052
|
|
|
1012
|
-
# Phase C: NULL parameter removal moved to compiler where AST is available
|
|
1013
|
-
|
|
1014
|
-
# Phase D: Type coercion (database-specific)
|
|
1015
1053
|
if config.type_coercion_map and processed_parameters:
|
|
1016
1054
|
processed_parameters = self._apply_type_coercions(processed_parameters, config.type_coercion_map, is_many)
|
|
1017
1055
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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
|
-
)
|
|
1056
|
+
processed_sql, processed_parameters = self._process_parameters_conversion(
|
|
1057
|
+
processed_sql,
|
|
1058
|
+
processed_parameters,
|
|
1059
|
+
config,
|
|
1060
|
+
original_styles,
|
|
1061
|
+
needs_execution_conversion,
|
|
1062
|
+
needs_sqlglot_normalization,
|
|
1063
|
+
is_many,
|
|
1064
|
+
)
|
|
1036
1065
|
|
|
1037
|
-
# Phase F: Output transformation (custom hooks)
|
|
1038
1066
|
if config.output_transformer:
|
|
1039
1067
|
processed_sql, processed_parameters = config.output_transformer(processed_sql, processed_parameters)
|
|
1040
1068
|
|
|
1041
|
-
# 5. Cache result and return
|
|
1042
1069
|
if self._cache_size < self.DEFAULT_CACHE_SIZE:
|
|
1043
1070
|
self._cache[cache_key] = (processed_sql, processed_parameters)
|
|
1044
1071
|
self._cache_size += 1
|
|
@@ -1048,10 +1075,10 @@ class ParameterProcessor:
|
|
|
1048
1075
|
def _get_sqlglot_compatible_sql(
|
|
1049
1076
|
self, sql: str, parameters: Any, config: ParameterStyleConfig, dialect: Optional[str] = None
|
|
1050
1077
|
) -> "tuple[str, Any]":
|
|
1051
|
-
"""Get SQL normalized for
|
|
1078
|
+
"""Get SQL normalized for parsing only (Phase 1 only).
|
|
1052
1079
|
|
|
1053
1080
|
This method performs only Phase 1 normalization to make SQL compatible
|
|
1054
|
-
with
|
|
1081
|
+
with parsing, without converting to execution format.
|
|
1055
1082
|
|
|
1056
1083
|
Args:
|
|
1057
1084
|
sql: Raw SQL string
|
|
@@ -1060,17 +1087,15 @@ class ParameterProcessor:
|
|
|
1060
1087
|
dialect: SQL dialect for compatibility
|
|
1061
1088
|
|
|
1062
1089
|
Returns:
|
|
1063
|
-
Tuple of (
|
|
1090
|
+
Tuple of (compatible_sql, parameters)
|
|
1064
1091
|
"""
|
|
1065
|
-
|
|
1092
|
+
|
|
1066
1093
|
param_info = self._validator.extract_parameters(sql)
|
|
1067
1094
|
|
|
1068
|
-
# 2. Apply only Phase 1 normalization if needed
|
|
1069
1095
|
if self._needs_sqlglot_normalization(param_info, dialect):
|
|
1070
1096
|
normalized_sql, _ = self._converter.normalize_sql_for_parsing(sql, dialect)
|
|
1071
1097
|
return normalized_sql, parameters
|
|
1072
1098
|
|
|
1073
|
-
# 3. No normalization needed - return original SQL
|
|
1074
1099
|
return sql, parameters
|
|
1075
1100
|
|
|
1076
1101
|
def _needs_execution_conversion(self, param_info: "list[ParameterInfo]", config: ParameterStyleConfig) -> bool:
|
|
@@ -1084,7 +1109,6 @@ class ParameterProcessor:
|
|
|
1084
1109
|
|
|
1085
1110
|
current_styles = {p.style for p in param_info}
|
|
1086
1111
|
|
|
1087
|
-
# Check if mixed styles are explicitly allowed AND the execution environment supports multiple styles
|
|
1088
1112
|
if (
|
|
1089
1113
|
config.allow_mixed_parameter_styles
|
|
1090
1114
|
and len(current_styles) > 1
|
|
@@ -1094,19 +1118,16 @@ class ParameterProcessor:
|
|
|
1094
1118
|
):
|
|
1095
1119
|
return False
|
|
1096
1120
|
|
|
1097
|
-
# Check for mixed styles - if not allowed, force conversion to single style
|
|
1098
1121
|
if len(current_styles) > 1:
|
|
1099
1122
|
return True
|
|
1100
1123
|
|
|
1101
|
-
# If we have a single current style and it's supported by the execution environment, preserve it
|
|
1102
1124
|
if len(current_styles) == 1:
|
|
1103
1125
|
current_style = next(iter(current_styles))
|
|
1104
1126
|
supported_styles = config.supported_execution_parameter_styles
|
|
1105
1127
|
if supported_styles is None:
|
|
1106
|
-
return True
|
|
1128
|
+
return True
|
|
1107
1129
|
return current_style not in supported_styles
|
|
1108
1130
|
|
|
1109
|
-
# Multiple styles detected - transformation needed
|
|
1110
1131
|
return True
|
|
1111
1132
|
|
|
1112
1133
|
def _needs_sqlglot_normalization(self, param_info: "list[ParameterInfo]", dialect: Optional[str] = None) -> bool:
|
|
@@ -1127,22 +1148,19 @@ class ParameterProcessor:
|
|
|
1127
1148
|
This preserves the original parameter style when possible, only converting
|
|
1128
1149
|
when necessary for execution compatibility.
|
|
1129
1150
|
"""
|
|
1130
|
-
|
|
1151
|
+
|
|
1131
1152
|
if len(original_styles) == 1 and config.supported_execution_parameter_styles is not None:
|
|
1132
1153
|
original_style = next(iter(original_styles))
|
|
1133
1154
|
if original_style in config.supported_execution_parameter_styles:
|
|
1134
1155
|
return original_style
|
|
1135
1156
|
|
|
1136
|
-
# Otherwise use the configured execution style or fallback to default parameter style
|
|
1137
1157
|
return config.default_execution_parameter_style or config.default_parameter_style
|
|
1138
1158
|
|
|
1139
1159
|
def _apply_type_wrapping(self, parameters: Any) -> Any:
|
|
1140
1160
|
"""Apply type wrapping using singledispatch for performance."""
|
|
1141
1161
|
if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
|
|
1142
|
-
# Optimize with direct iteration instead of list comprehension for better memory usage
|
|
1143
1162
|
return [_wrap_parameter_by_type(p) for p in parameters]
|
|
1144
1163
|
if isinstance(parameters, Mapping):
|
|
1145
|
-
# Optimize dict comprehension with items() iteration
|
|
1146
1164
|
wrapped_dict = {}
|
|
1147
1165
|
for k, v in parameters.items():
|
|
1148
1166
|
wrapped_dict[k] = _wrap_parameter_by_type(v)
|
|
@@ -1161,13 +1179,12 @@ class ParameterProcessor:
|
|
|
1161
1179
|
"""
|
|
1162
1180
|
|
|
1163
1181
|
def coerce_value(value: Any) -> Any:
|
|
1164
|
-
# Handle TypedParameter objects - use the wrapped value and original type
|
|
1165
1182
|
if isinstance(value, TypedParameter):
|
|
1166
1183
|
wrapped_value = value.value
|
|
1167
1184
|
original_type = value.original_type
|
|
1168
1185
|
if original_type in type_coercion_map:
|
|
1169
1186
|
coerced = type_coercion_map[original_type](wrapped_value)
|
|
1170
|
-
|
|
1187
|
+
|
|
1171
1188
|
if isinstance(coerced, (list, tuple)) and not isinstance(coerced, (str, bytes)):
|
|
1172
1189
|
coerced = [coerce_value(item) for item in coerced]
|
|
1173
1190
|
elif isinstance(coerced, dict):
|
|
@@ -1175,11 +1192,10 @@ class ParameterProcessor:
|
|
|
1175
1192
|
return coerced
|
|
1176
1193
|
return wrapped_value
|
|
1177
1194
|
|
|
1178
|
-
# Handle regular values
|
|
1179
1195
|
value_type = type(value)
|
|
1180
1196
|
if value_type in type_coercion_map:
|
|
1181
1197
|
coerced = type_coercion_map[value_type](value)
|
|
1182
|
-
|
|
1198
|
+
|
|
1183
1199
|
if isinstance(coerced, (list, tuple)) and not isinstance(coerced, (str, bytes)):
|
|
1184
1200
|
coerced = [coerce_value(item) for item in coerced]
|
|
1185
1201
|
elif isinstance(coerced, dict):
|
|
@@ -1195,12 +1211,9 @@ class ParameterProcessor:
|
|
|
1195
1211
|
return {k: coerce_value(v) for k, v in param_set.items()}
|
|
1196
1212
|
return coerce_value(param_set)
|
|
1197
1213
|
|
|
1198
|
-
# Handle execute_many case specially - apply coercions to individual parameter values,
|
|
1199
|
-
# not to the parameter set tuples/lists themselves
|
|
1200
1214
|
if is_many and isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
|
|
1201
1215
|
return [coerce_parameter_set(param_set) for param_set in parameters]
|
|
1202
1216
|
|
|
1203
|
-
# Regular single execution - apply coercions to all parameters
|
|
1204
1217
|
if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
|
|
1205
1218
|
return [coerce_value(p) for p in parameters]
|
|
1206
1219
|
if isinstance(parameters, Mapping):
|
|
@@ -1208,7 +1221,6 @@ class ParameterProcessor:
|
|
|
1208
1221
|
return coerce_value(parameters)
|
|
1209
1222
|
|
|
1210
1223
|
|
|
1211
|
-
# Helper functions for parameter processing
|
|
1212
1224
|
def is_iterable_parameters(obj: Any) -> bool:
|
|
1213
1225
|
"""Check if object is iterable parameters (not string/bytes).
|
|
1214
1226
|
|
|
@@ -1223,7 +1235,6 @@ def is_iterable_parameters(obj: Any) -> bool:
|
|
|
1223
1235
|
)
|
|
1224
1236
|
|
|
1225
1237
|
|
|
1226
|
-
# Public API functions that preserve exact current interfaces
|
|
1227
1238
|
def wrap_with_type(value: Any, semantic_name: Optional[str] = None) -> Any:
|
|
1228
1239
|
"""Public API for type wrapping - preserves current interface.
|
|
1229
1240
|
|