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.

Files changed (75) hide show
  1. sqlspec/__init__.py +1 -1
  2. sqlspec/_sql.py +54 -159
  3. sqlspec/adapters/adbc/config.py +24 -30
  4. sqlspec/adapters/adbc/driver.py +42 -61
  5. sqlspec/adapters/aiosqlite/config.py +5 -10
  6. sqlspec/adapters/aiosqlite/driver.py +9 -25
  7. sqlspec/adapters/aiosqlite/pool.py +43 -35
  8. sqlspec/adapters/asyncmy/config.py +10 -7
  9. sqlspec/adapters/asyncmy/driver.py +18 -39
  10. sqlspec/adapters/asyncpg/config.py +4 -0
  11. sqlspec/adapters/asyncpg/driver.py +32 -79
  12. sqlspec/adapters/bigquery/config.py +12 -65
  13. sqlspec/adapters/bigquery/driver.py +39 -133
  14. sqlspec/adapters/duckdb/config.py +11 -15
  15. sqlspec/adapters/duckdb/driver.py +61 -85
  16. sqlspec/adapters/duckdb/pool.py +2 -5
  17. sqlspec/adapters/oracledb/_types.py +8 -1
  18. sqlspec/adapters/oracledb/config.py +55 -38
  19. sqlspec/adapters/oracledb/driver.py +35 -92
  20. sqlspec/adapters/oracledb/migrations.py +257 -0
  21. sqlspec/adapters/psqlpy/config.py +13 -9
  22. sqlspec/adapters/psqlpy/driver.py +28 -103
  23. sqlspec/adapters/psycopg/config.py +9 -5
  24. sqlspec/adapters/psycopg/driver.py +107 -175
  25. sqlspec/adapters/sqlite/config.py +7 -5
  26. sqlspec/adapters/sqlite/driver.py +37 -73
  27. sqlspec/adapters/sqlite/pool.py +3 -12
  28. sqlspec/base.py +1 -8
  29. sqlspec/builder/__init__.py +1 -1
  30. sqlspec/builder/_base.py +34 -20
  31. sqlspec/builder/_ddl.py +407 -183
  32. sqlspec/builder/_insert.py +1 -1
  33. sqlspec/builder/mixins/_insert_operations.py +26 -6
  34. sqlspec/builder/mixins/_merge_operations.py +1 -1
  35. sqlspec/builder/mixins/_select_operations.py +1 -5
  36. sqlspec/config.py +32 -13
  37. sqlspec/core/__init__.py +89 -14
  38. sqlspec/core/cache.py +57 -104
  39. sqlspec/core/compiler.py +57 -112
  40. sqlspec/core/filters.py +1 -21
  41. sqlspec/core/hashing.py +13 -47
  42. sqlspec/core/parameters.py +272 -261
  43. sqlspec/core/result.py +12 -27
  44. sqlspec/core/splitter.py +17 -21
  45. sqlspec/core/statement.py +150 -159
  46. sqlspec/driver/_async.py +2 -15
  47. sqlspec/driver/_common.py +16 -95
  48. sqlspec/driver/_sync.py +2 -15
  49. sqlspec/driver/mixins/_result_tools.py +8 -29
  50. sqlspec/driver/mixins/_sql_translator.py +6 -8
  51. sqlspec/exceptions.py +1 -2
  52. sqlspec/loader.py +43 -115
  53. sqlspec/migrations/__init__.py +1 -1
  54. sqlspec/migrations/base.py +34 -45
  55. sqlspec/migrations/commands.py +34 -15
  56. sqlspec/migrations/loaders.py +1 -1
  57. sqlspec/migrations/runner.py +104 -19
  58. sqlspec/migrations/tracker.py +49 -2
  59. sqlspec/protocols.py +3 -6
  60. sqlspec/storage/__init__.py +4 -4
  61. sqlspec/storage/backends/fsspec.py +5 -6
  62. sqlspec/storage/backends/obstore.py +7 -8
  63. sqlspec/storage/registry.py +3 -3
  64. sqlspec/utils/__init__.py +2 -2
  65. sqlspec/utils/logging.py +6 -10
  66. sqlspec/utils/sync_tools.py +27 -4
  67. sqlspec/utils/text.py +6 -1
  68. {sqlspec-0.17.1.dist-info → sqlspec-0.18.0.dist-info}/METADATA +1 -1
  69. sqlspec-0.18.0.dist-info/RECORD +138 -0
  70. sqlspec/builder/_ddl_utils.py +0 -103
  71. sqlspec-0.17.1.dist-info/RECORD +0 -138
  72. {sqlspec-0.17.1.dist-info → sqlspec-0.18.0.dist-info}/WHEEL +0 -0
  73. {sqlspec-0.17.1.dist-info → sqlspec-0.18.0.dist-info}/entry_points.txt +0 -0
  74. {sqlspec-0.17.1.dist-info → sqlspec-0.18.0.dist-info}/licenses/LICENSE +0 -0
  75. {sqlspec-0.17.1.dist-info → sqlspec-0.18.0.dist-info}/licenses/NOTICE +0 -0
@@ -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: High-level coordinator with caching
12
+ - ParameterProcessor: Parameter processing coordinator
13
13
  - ParameterStyleConfig: Configuration for parameter processing
14
14
 
15
15
  Features:
16
- - Two-phase processing: SQLGlot compatibility and execution format
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=True)
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 SQLGlot parsing and execution
99
+ Maintains type information through parsing and execution
101
100
  format conversion.
102
101
 
103
102
  Use Cases:
104
- - Preserve boolean values through SQLGlot parsing
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
- SQLGlot compatibility.
341
+ compatibility.
344
342
 
345
343
  Features:
346
- - Cached parameter extraction results
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
- # 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
- ):
418
+ if any(match.group(g) for g in skip_groups):
384
419
  continue
385
420
 
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
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=position, ordinal=ordinal, placeholder_text=placeholder_text
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, # %s, %d - modulo operator conflict
450
- ParameterStyle.NAMED_PYFORMAT, # %(name)s - complex format string
451
- ParameterStyle.POSITIONAL_COLON, # :1, :2 - numbered colon parameters
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: SQLGlot compatibility normalization
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 SQLGlot parsing of problematic parameter styles
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, # Same logic as colon
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 SQLGlot-parsable format.
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 that SQLGlot can parse.
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: # Named styles
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 _convert_parameter_format( # noqa: C901
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 # Already in correct format
754
+ return parameters
673
755
  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
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 # Already in correct format
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
- 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)])
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
- 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
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
- 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
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
- # 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"
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 # No parameters needed for static SQL
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: # 1-based ordinal
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
- # Build the parameter key using the same logic as in _embed_static_parameters
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
- # 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"
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: # 1-based ordinal
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 # Already dict format
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
- """HIGH-LEVEL parameter processing engine with complete pipeline.
912
+ """Parameter processing engine.
905
913
 
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).
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 SQLGlot compatibility (TypedParameter)
918
+ 1. Type wrapping for compatibility (TypedParameter)
911
919
  2. Driver-specific type coercions (type_coercion_map)
912
- 3. Phase 1: SQLGlot normalization if needed
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 caching and component coordination."""
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
- # Cache size is a class-level constant
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 pre-processing workflow:
946
- 1. Type wrapping for SQLGlot compatibility
947
- 2. Phase 1: SQLGlot normalization if needed
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
- # 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
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
- # 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
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
- # 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
- )
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 SQLGlot parsing only (Phase 1 only).
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 SQLGlot parsing, without converting to execution format.
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 (sqlglot_compatible_sql, parameters)
1090
+ Tuple of (compatible_sql, parameters)
1064
1091
  """
1065
- # 1. Determine if Phase 1 normalization is needed
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 # No supported styles defined, need conversion
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
- # If we have a single original style that's supported for execution, preserve it
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
- # Recursively apply coercion to elements in the coerced result if it's a sequence
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
- # Recursively apply coercion to elements in the coerced result if it's a sequence
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