sqlspec 0.12.1__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

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