sqlspec 0.13.0__py3-none-any.whl → 0.14.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 (110) hide show
  1. sqlspec/__init__.py +39 -1
  2. sqlspec/adapters/adbc/config.py +4 -40
  3. sqlspec/adapters/adbc/driver.py +29 -16
  4. sqlspec/adapters/aiosqlite/config.py +15 -20
  5. sqlspec/adapters/aiosqlite/driver.py +36 -18
  6. sqlspec/adapters/asyncmy/config.py +16 -33
  7. sqlspec/adapters/asyncmy/driver.py +23 -16
  8. sqlspec/adapters/asyncpg/config.py +19 -61
  9. sqlspec/adapters/asyncpg/driver.py +41 -18
  10. sqlspec/adapters/bigquery/config.py +2 -43
  11. sqlspec/adapters/bigquery/driver.py +26 -14
  12. sqlspec/adapters/duckdb/config.py +2 -49
  13. sqlspec/adapters/duckdb/driver.py +35 -16
  14. sqlspec/adapters/oracledb/config.py +30 -83
  15. sqlspec/adapters/oracledb/driver.py +54 -27
  16. sqlspec/adapters/psqlpy/config.py +17 -57
  17. sqlspec/adapters/psqlpy/driver.py +28 -8
  18. sqlspec/adapters/psycopg/config.py +30 -73
  19. sqlspec/adapters/psycopg/driver.py +69 -24
  20. sqlspec/adapters/sqlite/config.py +3 -21
  21. sqlspec/adapters/sqlite/driver.py +50 -26
  22. sqlspec/cli.py +248 -0
  23. sqlspec/config.py +18 -20
  24. sqlspec/driver/_async.py +28 -10
  25. sqlspec/driver/_common.py +5 -4
  26. sqlspec/driver/_sync.py +28 -10
  27. sqlspec/driver/mixins/__init__.py +6 -0
  28. sqlspec/driver/mixins/_cache.py +114 -0
  29. sqlspec/driver/mixins/_pipeline.py +0 -4
  30. sqlspec/{service/base.py → driver/mixins/_query_tools.py} +86 -421
  31. sqlspec/driver/mixins/_result_utils.py +0 -2
  32. sqlspec/driver/mixins/_sql_translator.py +0 -2
  33. sqlspec/driver/mixins/_storage.py +4 -18
  34. sqlspec/driver/mixins/_type_coercion.py +0 -2
  35. sqlspec/driver/parameters.py +4 -4
  36. sqlspec/extensions/aiosql/adapter.py +4 -4
  37. sqlspec/extensions/litestar/__init__.py +2 -1
  38. sqlspec/extensions/litestar/cli.py +48 -0
  39. sqlspec/extensions/litestar/plugin.py +3 -0
  40. sqlspec/loader.py +1 -1
  41. sqlspec/migrations/__init__.py +23 -0
  42. sqlspec/migrations/base.py +390 -0
  43. sqlspec/migrations/commands.py +525 -0
  44. sqlspec/migrations/runner.py +215 -0
  45. sqlspec/migrations/tracker.py +153 -0
  46. sqlspec/migrations/utils.py +89 -0
  47. sqlspec/protocols.py +37 -3
  48. sqlspec/statement/builder/__init__.py +8 -8
  49. sqlspec/statement/builder/{column.py → _column.py} +82 -52
  50. sqlspec/statement/builder/{ddl.py → _ddl.py} +5 -5
  51. sqlspec/statement/builder/_ddl_utils.py +1 -1
  52. sqlspec/statement/builder/{delete.py → _delete.py} +1 -1
  53. sqlspec/statement/builder/{insert.py → _insert.py} +1 -1
  54. sqlspec/statement/builder/{merge.py → _merge.py} +1 -1
  55. sqlspec/statement/builder/_parsing_utils.py +5 -3
  56. sqlspec/statement/builder/{select.py → _select.py} +59 -61
  57. sqlspec/statement/builder/{update.py → _update.py} +2 -2
  58. sqlspec/statement/builder/mixins/__init__.py +24 -30
  59. sqlspec/statement/builder/mixins/{_set_ops.py → _cte_and_set_ops.py} +86 -2
  60. sqlspec/statement/builder/mixins/{_delete_from.py → _delete_operations.py} +2 -0
  61. sqlspec/statement/builder/mixins/{_insert_values.py → _insert_operations.py} +70 -1
  62. sqlspec/statement/builder/mixins/{_merge_clauses.py → _merge_operations.py} +2 -0
  63. sqlspec/statement/builder/mixins/_order_limit_operations.py +123 -0
  64. sqlspec/statement/builder/mixins/{_pivot.py → _pivot_operations.py} +71 -2
  65. sqlspec/statement/builder/mixins/_select_operations.py +612 -0
  66. sqlspec/statement/builder/mixins/{_update_set.py → _update_operations.py} +73 -2
  67. sqlspec/statement/builder/mixins/_where_clause.py +536 -0
  68. sqlspec/statement/cache.py +50 -0
  69. sqlspec/statement/filters.py +37 -8
  70. sqlspec/statement/parameters.py +154 -25
  71. sqlspec/statement/pipelines/__init__.py +1 -1
  72. sqlspec/statement/pipelines/context.py +4 -4
  73. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +3 -3
  74. sqlspec/statement/pipelines/validators/_parameter_style.py +22 -22
  75. sqlspec/statement/pipelines/validators/_performance.py +1 -5
  76. sqlspec/statement/sql.py +246 -176
  77. sqlspec/utils/__init__.py +2 -1
  78. sqlspec/utils/statement_hashing.py +203 -0
  79. sqlspec/utils/type_guards.py +32 -0
  80. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/METADATA +1 -1
  81. sqlspec-0.14.0.dist-info/RECORD +143 -0
  82. sqlspec-0.14.0.dist-info/entry_points.txt +2 -0
  83. sqlspec/service/__init__.py +0 -4
  84. sqlspec/service/_util.py +0 -147
  85. sqlspec/service/pagination.py +0 -26
  86. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  87. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  88. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  89. sqlspec/statement/builder/mixins/_from.py +0 -63
  90. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  91. sqlspec/statement/builder/mixins/_having.py +0 -35
  92. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  93. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  94. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  95. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  96. sqlspec/statement/builder/mixins/_returning.py +0 -37
  97. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  98. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  99. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  100. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  101. sqlspec/statement/builder/mixins/_where.py +0 -401
  102. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  103. sqlspec/statement/parameter_manager.py +0 -220
  104. sqlspec/statement/sql_compiler.py +0 -140
  105. sqlspec-0.13.0.dist-info/RECORD +0 -150
  106. /sqlspec/statement/builder/{base.py → _base.py} +0 -0
  107. /sqlspec/statement/builder/mixins/{_join.py → _join_operations.py} +0 -0
  108. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/WHEEL +0 -0
  109. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/licenses/LICENSE +0 -0
  110. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/statement/sql.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import operator
4
4
  from dataclasses import dataclass, field
5
- from typing import TYPE_CHECKING, Any, Optional, Union
5
+ from typing import TYPE_CHECKING, Any, Callable, Optional, Union
6
6
 
7
7
  import sqlglot
8
8
  import sqlglot.expressions as exp
@@ -10,6 +10,7 @@ from sqlglot.errors import ParseError
10
10
  from typing_extensions import TypeAlias
11
11
 
12
12
  from sqlspec.exceptions import RiskLevel, SQLParsingError, SQLValidationError
13
+ from sqlspec.statement.cache import sql_cache
13
14
  from sqlspec.statement.filters import StatementFilter
14
15
  from sqlspec.statement.parameters import (
15
16
  SQLGLOT_INCOMPATIBLE_STYLES,
@@ -19,8 +20,9 @@ from sqlspec.statement.parameters import (
19
20
  )
20
21
  from sqlspec.statement.pipelines import SQLProcessingContext, StatementPipeline
21
22
  from sqlspec.statement.pipelines.transformers import CommentAndHintRemover, ParameterizeLiterals
22
- from sqlspec.statement.pipelines.validators import DMLSafetyValidator, ParameterStyleValidator
23
+ from sqlspec.statement.pipelines.validators import DMLSafetyValidator, ParameterStyleValidator, SecurityValidator
23
24
  from sqlspec.utils.logging import get_logger
25
+ from sqlspec.utils.statement_hashing import hash_sql_statement
24
26
  from sqlspec.utils.type_guards import (
25
27
  can_append_to_statement,
26
28
  can_extract_parameters,
@@ -29,16 +31,13 @@ from sqlspec.utils.type_guards import (
29
31
  is_dict,
30
32
  is_expression,
31
33
  is_statement_filter,
32
- supports_limit,
33
- supports_offset,
34
- supports_order_by,
35
34
  supports_where,
36
35
  )
37
36
 
38
37
  if TYPE_CHECKING:
39
38
  from sqlglot.dialects.dialect import DialectType
40
39
 
41
- from sqlspec.statement.parameters import ParameterNormalizationState
40
+ from sqlspec.statement.parameters import ParameterStyleTransformationState
42
41
 
43
42
  __all__ = ("SQL", "SQLConfig", "Statement")
44
43
 
@@ -76,25 +75,33 @@ class _ProcessedState:
76
75
  class SQLConfig:
77
76
  """Configuration for SQL statement behavior.
78
77
 
79
- Uses conservative defaults that prioritize compatibility and robustness
80
- over strict enforcement, making it easier to work with diverse SQL dialects
81
- and complex queries.
78
+ Uses conservative defaults that prioritize compatibility and robustness,
79
+ making it easier to work with diverse SQL dialects and complex queries.
82
80
 
83
- Component Lists:
81
+ Pipeline Configuration:
82
+ enable_parsing: Parse SQL strings using sqlglot (default: True)
83
+ enable_validation: Run SQL validators to check for safety issues (default: True)
84
+ enable_transformations: Apply SQL transformers like literal parameterization (default: True)
85
+ enable_analysis: Run SQL analyzers for metadata extraction (default: False)
86
+ enable_expression_simplification: Apply expression simplification transformer (default: False)
87
+ enable_parameter_type_wrapping: Wrap parameters with type information (default: True)
88
+ parse_errors_as_warnings: Treat parse errors as warnings instead of failures (default: True)
89
+ enable_caching: Cache processed SQL statements (default: True)
90
+
91
+ Component Lists (Advanced):
84
92
  transformers: Optional list of SQL transformers for explicit staging
85
93
  validators: Optional list of SQL validators for explicit staging
86
94
  analyzers: Optional list of SQL analyzers for explicit staging
87
95
 
88
- Configuration Options:
96
+ Internal Configuration:
89
97
  parameter_converter: Handles parameter style conversions
90
98
  parameter_validator: Validates parameter usage and styles
91
- analysis_cache_size: Cache size for analysis results
92
99
  input_sql_had_placeholders: Populated by SQL.__init__ to track original SQL state
93
100
  dialect: SQL dialect to use for parsing and generation
94
101
 
95
102
  Parameter Style Configuration:
96
103
  allowed_parameter_styles: Allowed parameter styles (e.g., ('qmark', 'named_colon'))
97
- target_parameter_style: Target parameter style for SQL generation
104
+ default_parameter_style: Target parameter style for SQL generation
98
105
  allow_mixed_parameter_styles: Whether to allow mixing parameter styles in same query
99
106
  """
100
107
 
@@ -102,10 +109,10 @@ class SQLConfig:
102
109
  enable_validation: bool = True
103
110
  enable_transformations: bool = True
104
111
  enable_analysis: bool = False
105
- enable_normalization: bool = True
106
- strict_mode: bool = False
107
- cache_parsed_expression: bool = True
112
+ enable_expression_simplification: bool = False
113
+ enable_parameter_type_wrapping: bool = True
108
114
  parse_errors_as_warnings: bool = True
115
+ enable_caching: bool = True
109
116
 
110
117
  transformers: "Optional[list[Any]]" = None
111
118
  validators: "Optional[list[Any]]" = None
@@ -113,13 +120,13 @@ class SQLConfig:
113
120
 
114
121
  parameter_converter: ParameterConverter = field(default_factory=ParameterConverter)
115
122
  parameter_validator: ParameterValidator = field(default_factory=ParameterValidator)
116
- analysis_cache_size: int = 1000
117
123
  input_sql_had_placeholders: bool = False
118
124
  dialect: "Optional[DialectType]" = None
119
125
 
120
126
  allowed_parameter_styles: "Optional[tuple[str, ...]]" = None
121
- target_parameter_style: "Optional[str]" = None
127
+ default_parameter_style: "Optional[str]" = None
122
128
  allow_mixed_parameter_styles: bool = False
129
+ analyzer_output_handler: "Optional[Callable[[Any], None]]" = None
123
130
 
124
131
  def validate_parameter_style(self, style: "Union[ParameterStyle, str]") -> bool:
125
132
  """Check if a parameter style is allowed.
@@ -145,22 +152,37 @@ class SQLConfig:
145
152
  if self.transformers is not None:
146
153
  transformers = list(self.transformers)
147
154
  elif self.enable_transformations:
148
- placeholder_style = self.target_parameter_style or "?"
155
+ placeholder_style = self.default_parameter_style or "?"
149
156
  transformers = [CommentAndHintRemover(), ParameterizeLiterals(placeholder_style=placeholder_style)]
157
+ if self.enable_expression_simplification:
158
+ from sqlspec.statement.pipelines.transformers import ExpressionSimplifier
159
+
160
+ transformers.append(ExpressionSimplifier())
150
161
 
151
162
  validators = []
152
163
  if self.validators is not None:
153
164
  validators = list(self.validators)
154
165
  elif self.enable_validation:
155
- validators = [ParameterStyleValidator(fail_on_violation=self.strict_mode), DMLSafetyValidator()]
166
+ validators = [
167
+ ParameterStyleValidator(fail_on_violation=not self.parse_errors_as_warnings),
168
+ DMLSafetyValidator(),
169
+ SecurityValidator(),
170
+ ]
156
171
 
157
172
  analyzers = []
158
173
  if self.analyzers is not None:
159
174
  analyzers = list(self.analyzers)
160
175
  elif self.enable_analysis:
161
- analyzers = []
176
+ from sqlspec.statement.pipelines.analyzers import StatementAnalyzer
177
+
178
+ analyzers = [StatementAnalyzer()]
179
+
180
+ return StatementPipeline(transformers=transformers, validators=validators, analyzers=analyzers) # pyright: ignore
162
181
 
163
- return StatementPipeline(transformers=transformers, validators=validators, analyzers=analyzers)
182
+
183
+ def default_analysis_handler(analysis: Any) -> None:
184
+ """Default handler that logs analysis to debug."""
185
+ logger.debug("SQL Analysis: %s", analysis)
164
186
 
165
187
 
166
188
  class SQL:
@@ -185,7 +207,7 @@ class SQL:
185
207
  "_named_params",
186
208
  "_original_parameters",
187
209
  "_original_sql",
188
- "_parameter_normalization_state",
210
+ "_parameter_conversion_state",
189
211
  "_placeholder_mapping",
190
212
  "_positional_params",
191
213
  "_processed_state",
@@ -208,7 +230,7 @@ class SQL:
208
230
  if "config" in kwargs and _config is None:
209
231
  _config = kwargs.pop("config")
210
232
  self._config = _config or SQLConfig()
211
- self._dialect = _dialect or (self._config.dialect if self._config else None)
233
+ self._dialect = _dialect or self._config.dialect
212
234
  self._builder_result_type = _builder_result_type
213
235
  self._processed_state: Optional[_ProcessedState] = None
214
236
  self._processing_context: Optional[SQLProcessingContext] = None
@@ -220,12 +242,12 @@ class SQL:
220
242
  self._original_parameters: Any = None
221
243
  self._original_sql: str = ""
222
244
  self._placeholder_mapping: dict[str, Union[str, int]] = {}
223
- self._parameter_normalization_state: Optional[ParameterNormalizationState] = None
245
+ self._parameter_conversion_state: Optional[ParameterStyleTransformationState] = None
224
246
  self._is_many: bool = False
225
247
  self._is_script: bool = False
226
248
 
227
249
  if isinstance(statement, SQL):
228
- self._init_from_sql_object(statement, _dialect, _config, _builder_result_type)
250
+ self._init_from_sql_object(statement, _dialect, _config or SQLConfig(), _builder_result_type)
229
251
  else:
230
252
  self._init_from_str_or_expression(statement)
231
253
 
@@ -238,13 +260,9 @@ class SQL:
238
260
  self._process_parameters(*parameters, **kwargs)
239
261
 
240
262
  def _init_from_sql_object(
241
- self,
242
- statement: "SQL",
243
- dialect: "DialectType",
244
- config: "Optional[SQLConfig]",
245
- builder_result_type: "Optional[type]",
263
+ self, statement: "SQL", dialect: "DialectType", config: "SQLConfig", builder_result_type: "Optional[type]"
246
264
  ) -> None:
247
- """Initialize attributes from an existing SQL object."""
265
+ """Initialize from an existing SQL object."""
248
266
  self._statement = statement._statement
249
267
  self._dialect = dialect or statement._dialect
250
268
  self._config = config or statement._config
@@ -255,18 +273,18 @@ class SQL:
255
273
  self._original_parameters = statement._original_parameters
256
274
  self._original_sql = statement._original_sql
257
275
  self._placeholder_mapping = statement._placeholder_mapping.copy()
258
- self._parameter_normalization_state = statement._parameter_normalization_state
276
+ self._parameter_conversion_state = statement._parameter_conversion_state
259
277
  self._positional_params.extend(statement._positional_params)
260
278
  self._named_params.update(statement._named_params)
261
279
  self._filters.extend(statement._filters)
262
280
 
263
281
  def _init_from_str_or_expression(self, statement: "Union[str, exp.Expression]") -> None:
264
- """Initialize attributes from a SQL string or expression."""
282
+ """Initialize from a string or expression."""
265
283
  if isinstance(statement, str):
266
284
  self._raw_sql = statement
267
285
  self._statement = self._to_expression(statement)
268
286
  else:
269
- self._raw_sql = statement.sql(dialect=self._dialect) # pyright: ignore
287
+ self._raw_sql = statement.sql(dialect=self._dialect)
270
288
  self._statement = statement
271
289
 
272
290
  def _load_from_existing_state(self, existing_state: "dict[str, Any]") -> None:
@@ -280,8 +298,8 @@ class SQL:
280
298
  self._original_parameters = existing_state.get("original_parameters", self._original_parameters)
281
299
 
282
300
  def _set_original_parameters(self, *parameters: Any) -> None:
283
- """Store the original parameters for compatibility."""
284
- if len(parameters) == 0 or (len(parameters) == 1 and is_statement_filter(parameters[0])):
301
+ """Set the original parameters."""
302
+ if not parameters or (len(parameters) == 1 and is_statement_filter(parameters[0])):
285
303
  self._original_parameters = None
286
304
  elif len(parameters) == 1 and isinstance(parameters[0], (list, tuple)):
287
305
  self._original_parameters = parameters[0]
@@ -289,7 +307,7 @@ class SQL:
289
307
  self._original_parameters = parameters
290
308
 
291
309
  def _process_parameters(self, *parameters: Any, **kwargs: Any) -> None:
292
- """Process positional and keyword arguments for parameters and filters."""
310
+ """Process and categorize parameters."""
293
311
  for param in parameters:
294
312
  self._process_parameter_item(param)
295
313
 
@@ -302,9 +320,11 @@ class SQL:
302
320
  else:
303
321
  self._positional_params.append(param_value)
304
322
 
305
- for key, value in kwargs.items():
306
- if not key.startswith("_"):
307
- self._named_params[key] = value
323
+ self._named_params.update({k: v for k, v in kwargs.items() if not k.startswith("_")})
324
+
325
+ def _cache_key(self) -> str:
326
+ """Generate a cache key for the current SQL state."""
327
+ return hash_sql_statement(self)
308
328
 
309
329
  def _process_parameter_item(self, item: Any) -> None:
310
330
  """Process a single item from the parameters list."""
@@ -332,6 +352,16 @@ class SQL:
332
352
  if self._processed_state is not None:
333
353
  return
334
354
 
355
+ # Check cache first if caching is enabled
356
+ cache_key = None
357
+ if self._config.enable_caching:
358
+ cache_key = self._cache_key()
359
+ cached_state = sql_cache.get(cache_key)
360
+
361
+ if cached_state is not None:
362
+ self._processed_state = cached_state
363
+ return
364
+
335
365
  final_expr, final_params = self._build_final_state()
336
366
  has_placeholders = self._detect_placeholders()
337
367
  initial_sql_for_context, final_params = self._prepare_context_sql(final_expr, final_params)
@@ -343,6 +373,10 @@ class SQL:
343
373
 
344
374
  self._finalize_processed_state(result, processed_sql, merged_params)
345
375
 
376
+ # Store in cache if caching is enabled
377
+ if self._config.enable_caching and cache_key is not None and self._processed_state is not None:
378
+ sql_cache.set(cache_key, self._processed_state)
379
+
346
380
  def _detect_placeholders(self) -> bool:
347
381
  """Detect if the raw SQL has placeholders."""
348
382
  if self._raw_sql:
@@ -361,24 +395,24 @@ class SQL:
361
395
  if is_expression(final_expr) and self._placeholder_mapping:
362
396
  initial_sql_for_context = final_expr.sql(dialect=self._dialect or self._config.dialect)
363
397
  if self._placeholder_mapping:
364
- final_params = self._normalize_parameters(final_params)
398
+ final_params = self._convert_parameters(final_params)
365
399
 
366
400
  return initial_sql_for_context, final_params
367
401
 
368
- def _normalize_parameters(self, final_params: Any) -> Any:
369
- """Normalize parameters based on placeholder mapping."""
402
+ def _convert_parameters(self, final_params: Any) -> Any:
403
+ """Convert parameters based on placeholder mapping."""
370
404
  if is_dict(final_params):
371
- normalized_params = {}
405
+ converted_params = {}
372
406
  for placeholder_key, original_name in self._placeholder_mapping.items():
373
407
  if str(original_name) in final_params:
374
- normalized_params[placeholder_key] = final_params[str(original_name)]
408
+ converted_params[placeholder_key] = final_params[str(original_name)]
375
409
  non_oracle_params = {
376
410
  key: value
377
411
  for key, value in final_params.items()
378
412
  if key not in {str(name) for name in self._placeholder_mapping.values()}
379
413
  }
380
- normalized_params.update(non_oracle_params)
381
- return normalized_params
414
+ converted_params.update(non_oracle_params)
415
+ return converted_params
382
416
  if isinstance(final_params, (list, tuple)):
383
417
  validator = self._config.parameter_validator
384
418
  param_info = validator.extract_parameters(self._raw_sql)
@@ -386,21 +420,21 @@ class SQL:
386
420
  all_numeric = all(p.name and p.name.isdigit() for p in param_info)
387
421
 
388
422
  if all_numeric:
389
- normalized_params = {}
423
+ converted_params = {}
390
424
 
391
425
  min_param_num = min(int(p.name) for p in param_info if p.name)
392
426
 
393
427
  for i, param in enumerate(final_params):
394
428
  param_num = str(i + min_param_num)
395
- normalized_params[param_num] = param
429
+ converted_params[param_num] = param
396
430
 
397
- return normalized_params
398
- normalized_params = {}
431
+ return converted_params
432
+ converted_params = {}
399
433
  for i, param in enumerate(final_params):
400
434
  if i < len(param_info):
401
435
  placeholder_key = f"{PARAM_PREFIX}{param_info[i].ordinal}"
402
- normalized_params[placeholder_key] = param
403
- return normalized_params
436
+ converted_params[placeholder_key] = param
437
+ return converted_params
404
438
  return final_params
405
439
 
406
440
  def _create_processing_context(
@@ -420,9 +454,9 @@ class SQL:
420
454
  if self._placeholder_mapping:
421
455
  context.extra_info["placeholder_map"] = self._placeholder_mapping
422
456
 
423
- # Set normalization state if available
424
- if self._parameter_normalization_state:
425
- context.parameter_normalization = self._parameter_normalization_state
457
+ # Set conversion state if available
458
+ if self._parameter_conversion_state:
459
+ context.parameter_conversion = self._parameter_conversion_state
426
460
 
427
461
  validator = self._config.parameter_validator
428
462
  context.parameter_info = validator.extract_parameters(context.initial_sql_string)
@@ -445,9 +479,26 @@ class SQL:
445
479
  if isinstance(processed_expr, exp.Anonymous):
446
480
  processed_sql = self._raw_sql or context.initial_sql_string
447
481
  else:
448
- processed_sql = processed_expr.sql(dialect=self._dialect or self._config.dialect, comments=False)
449
- logger.debug("Processed expression SQL: '%s'", processed_sql)
482
+ # Use the initial expression that includes filters, not the processed one
483
+ # The processed expression may have lost LIMIT/OFFSET during pipeline processing
484
+ if hasattr(context, "initial_expression") and context.initial_expression != processed_expr:
485
+ # Check if LIMIT/OFFSET was stripped during processing
486
+ has_limit_in_initial = (
487
+ context.initial_expression is not None
488
+ and hasattr(context.initial_expression, "args")
489
+ and "limit" in context.initial_expression.args
490
+ )
491
+ has_limit_in_processed = hasattr(processed_expr, "args") and "limit" in processed_expr.args
492
+
493
+ if has_limit_in_initial and not has_limit_in_processed:
494
+ # Restore LIMIT/OFFSET from initial expression
495
+ processed_expr = context.initial_expression
450
496
 
497
+ processed_sql = (
498
+ processed_expr.sql(dialect=self._dialect or self._config.dialect, comments=False)
499
+ if processed_expr
500
+ else ""
501
+ )
451
502
  if self._placeholder_mapping and self._original_sql:
452
503
  processed_sql, result = self._denormalize_sql(processed_sql, result)
453
504
 
@@ -461,22 +512,14 @@ class SQL:
461
512
  original_sql = self._original_sql
462
513
  param_info = self._config.parameter_validator.extract_parameters(original_sql)
463
514
  target_styles = {p.style for p in param_info}
464
-
465
- logger.debug(
466
- "Denormalizing SQL: before='%s', original='%s', styles=%s", processed_sql, original_sql, target_styles
467
- )
468
-
469
515
  if ParameterStyle.POSITIONAL_PYFORMAT in target_styles:
470
516
  processed_sql = self._config.parameter_converter._convert_sql_placeholders(
471
517
  processed_sql, param_info, ParameterStyle.POSITIONAL_PYFORMAT
472
518
  )
473
- logger.debug("Denormalized SQL to: '%s'", processed_sql)
474
519
  elif ParameterStyle.NAMED_PYFORMAT in target_styles:
475
520
  processed_sql = self._config.parameter_converter._convert_sql_placeholders(
476
521
  processed_sql, param_info, ParameterStyle.NAMED_PYFORMAT
477
522
  )
478
- logger.debug("Denormalized SQL to: '%s'", processed_sql)
479
- # Also denormalize the parameters back to their original names
480
523
  if (
481
524
  self._placeholder_mapping
482
525
  and result.context.merged_parameters
@@ -487,25 +530,16 @@ class SQL:
487
530
  processed_param_info = self._config.parameter_validator.extract_parameters(processed_sql)
488
531
  has_param_placeholders = any(p.name and p.name.startswith(PARAM_PREFIX) for p in processed_param_info)
489
532
 
490
- if has_param_placeholders:
491
- logger.debug("Skipping denormalization for param_N placeholders")
492
- else:
533
+ if not has_param_placeholders:
493
534
  processed_sql = self._config.parameter_converter._convert_sql_placeholders(
494
535
  processed_sql, param_info, ParameterStyle.POSITIONAL_COLON
495
536
  )
496
- logger.debug("Denormalized SQL to: '%s'", processed_sql)
497
537
  if (
498
538
  self._placeholder_mapping
499
539
  and result.context.merged_parameters
500
540
  and is_dict(result.context.merged_parameters)
501
541
  ):
502
542
  result.context.merged_parameters = self._denormalize_colon_params(result.context.merged_parameters)
503
- else:
504
- logger.debug(
505
- "No denormalization needed: mapping=%s, original=%s",
506
- bool(self._placeholder_mapping),
507
- bool(self._original_sql),
508
- )
509
543
 
510
544
  return processed_sql, result
511
545
 
@@ -524,15 +558,15 @@ class SQL:
524
558
 
525
559
  def _denormalize_pyformat_params(self, params: "dict[str, Any]") -> "dict[str, Any]":
526
560
  """Denormalize pyformat parameters back to their original names."""
527
- denormalized_params = {}
561
+ deconverted_params = {}
528
562
  for placeholder_key, original_name in self._placeholder_mapping.items():
529
563
  if placeholder_key in params:
530
564
  # For pyformat, the original_name is the actual parameter name (e.g., 'max_value')
531
- denormalized_params[str(original_name)] = params[placeholder_key]
532
- # Include any parameters that weren't normalized
533
- non_normalized_params = {key: value for key, value in params.items() if not key.startswith(PARAM_PREFIX)}
534
- denormalized_params.update(non_normalized_params)
535
- return denormalized_params
565
+ deconverted_params[str(original_name)] = params[placeholder_key]
566
+ # Include any parameters that weren't converted
567
+ non_converted_params = {key: value for key, value in params.items() if not key.startswith(PARAM_PREFIX)}
568
+ deconverted_params.update(non_converted_params)
569
+ return deconverted_params
536
570
 
537
571
  def _merge_pipeline_parameters(self, result: Any, final_params: Any) -> Any:
538
572
  """Merge parameters from the pipeline processing."""
@@ -569,16 +603,51 @@ class SQL:
569
603
 
570
604
  def _finalize_processed_state(self, result: Any, processed_sql: str, merged_params: Any) -> None:
571
605
  """Finalize the processed state."""
606
+ # Wrap parameters with type information if enabled
607
+ if self._config.enable_parameter_type_wrapping and merged_params is not None:
608
+ # Get parameter info from the processed SQL
609
+ validator = self._config.parameter_validator
610
+ param_info = validator.extract_parameters(processed_sql)
611
+
612
+ # Wrap parameters with type information
613
+ converter = self._config.parameter_converter
614
+ merged_params = converter.wrap_parameters_with_types(merged_params, param_info)
615
+
616
+ # Extract analyzer results from context metadata
617
+ analysis_results = (
618
+ {key: value for key, value in result.context.metadata.items() if key.endswith("Analyzer")}
619
+ if result.context.metadata
620
+ else {}
621
+ )
622
+
623
+ # If analyzer output handler is configured, call it with the analysis
624
+ if self._config.analyzer_output_handler and analysis_results:
625
+ # Create a structured analysis object from the metadata
626
+
627
+ # Extract the main analyzer results
628
+ analyzer_metadata = analysis_results.get("StatementAnalyzer", {})
629
+ if analyzer_metadata:
630
+ # Create a simplified analysis object for the handler
631
+ analysis = {
632
+ "statement_type": analyzer_metadata.get("statement_type"),
633
+ "complexity_score": analyzer_metadata.get("complexity_score"),
634
+ "table_count": analyzer_metadata.get("table_count"),
635
+ "has_subqueries": analyzer_metadata.get("has_subqueries"),
636
+ "join_count": analyzer_metadata.get("join_count"),
637
+ "duration_ms": analyzer_metadata.get("duration_ms"),
638
+ }
639
+ self._config.analyzer_output_handler(analysis)
640
+
572
641
  self._processed_state = _ProcessedState(
573
642
  processed_expression=result.expression,
574
643
  processed_sql=processed_sql,
575
644
  merged_parameters=merged_params,
576
645
  validation_errors=list(result.context.validation_errors),
577
- analysis_results={},
646
+ analysis_results=analysis_results,
578
647
  transformation_results={},
579
648
  )
580
649
 
581
- if self._config.strict_mode and self._processed_state.validation_errors:
650
+ if not self._config.parse_errors_as_warnings and self._processed_state.validation_errors:
582
651
  highest_risk_error = max(
583
652
  self._processed_state.validation_errors, key=lambda e: e.risk_level.value if has_risk_level(e) else 0
584
653
  )
@@ -604,33 +673,33 @@ class SQL:
604
673
  validator = self._config.parameter_validator
605
674
  param_info = validator.extract_parameters(statement)
606
675
 
607
- # Check if normalization is needed
608
- needs_normalization = any(p.style in SQLGLOT_INCOMPATIBLE_STYLES for p in param_info)
676
+ # Check if conversion is needed
677
+ needs_conversion = any(p.style in SQLGLOT_INCOMPATIBLE_STYLES for p in param_info)
609
678
 
610
- normalized_sql = statement
679
+ converted_sql = statement
611
680
  placeholder_mapping: dict[str, Any] = {}
612
681
 
613
- if needs_normalization:
682
+ if needs_conversion:
614
683
  converter = self._config.parameter_converter
615
- normalized_sql, placeholder_mapping = converter._transform_sql_for_parsing(statement, param_info)
684
+ converted_sql, placeholder_mapping = converter._transform_sql_for_parsing(statement, param_info)
616
685
  self._original_sql = statement
617
686
  self._placeholder_mapping = placeholder_mapping
618
687
 
619
- # Create normalization state
620
- from sqlspec.statement.parameters import ParameterNormalizationState
688
+ # Create conversion state
689
+ from sqlspec.statement.parameters import ParameterStyleTransformationState
621
690
 
622
- self._parameter_normalization_state = ParameterNormalizationState(
623
- was_normalized=True,
691
+ self._parameter_conversion_state = ParameterStyleTransformationState(
692
+ was_transformed=True,
624
693
  original_styles=list({p.style for p in param_info}),
625
- normalized_style=ParameterStyle.NAMED_COLON,
694
+ transformation_style=ParameterStyle.NAMED_COLON,
626
695
  placeholder_map=placeholder_mapping,
627
696
  original_param_info=param_info,
628
697
  )
629
698
  else:
630
- self._parameter_normalization_state = None
699
+ self._parameter_conversion_state = None
631
700
 
632
701
  try:
633
- expressions = sqlglot.parse(normalized_sql, dialect=self._dialect) # pyright: ignore
702
+ expressions = sqlglot.parse(converted_sql, dialect=self._dialect) # pyright: ignore
634
703
  if not expressions:
635
704
  return exp.Anonymous(this=statement)
636
705
  first_expr = expressions[0]
@@ -779,22 +848,34 @@ class SQL:
779
848
  """Build final expression and parameters after applying filters."""
780
849
  final_expr = self._statement
781
850
 
851
+ # Accumulate parameters from both the original SQL and filters
852
+ accumulated_positional = list(self._positional_params)
853
+ accumulated_named = dict(self._named_params)
854
+
782
855
  for filter_obj in self._filters:
783
856
  if can_append_to_statement(filter_obj):
784
857
  temp_sql = SQL(final_expr, config=self._config, dialect=self._dialect)
785
- temp_sql._positional_params = list(self._positional_params)
786
- temp_sql._named_params = dict(self._named_params)
858
+ temp_sql._positional_params = list(accumulated_positional)
859
+ temp_sql._named_params = dict(accumulated_named)
787
860
  result = filter_obj.append_to_statement(temp_sql)
788
- final_expr = result._statement if isinstance(result, SQL) else result
861
+
862
+ if isinstance(result, SQL):
863
+ # Extract the modified expression
864
+ final_expr = result._statement
865
+ # Also preserve any parameters added by the filter
866
+ accumulated_positional = list(result._positional_params)
867
+ accumulated_named = dict(result._named_params)
868
+ else:
869
+ final_expr = result
789
870
 
790
871
  final_params: Any
791
- if self._named_params and not self._positional_params:
792
- final_params = dict(self._named_params)
793
- elif self._positional_params and not self._named_params:
794
- final_params = list(self._positional_params)
795
- elif self._positional_params and self._named_params:
796
- final_params = dict(self._named_params)
797
- for i, param in enumerate(self._positional_params):
872
+ if accumulated_named and not accumulated_positional:
873
+ final_params = dict(accumulated_named)
874
+ elif accumulated_positional and not accumulated_named:
875
+ final_params = list(accumulated_positional)
876
+ elif accumulated_positional and accumulated_named:
877
+ final_params = dict(accumulated_named)
878
+ for i, param in enumerate(accumulated_positional):
798
879
  param_name = f"arg_{i}"
799
880
  while param_name in final_params:
800
881
  param_name = f"arg_{i}_{id(param)}"
@@ -821,6 +902,11 @@ class SQL:
821
902
  raise RuntimeError(msg)
822
903
  return self._processed_state.processed_sql
823
904
 
905
+ @property
906
+ def config(self) -> "SQLConfig":
907
+ """Get the SQL configuration."""
908
+ return self._config
909
+
824
910
  @property
825
911
  def expression(self) -> "Optional[exp.Expression]":
826
912
  """Get the final expression."""
@@ -882,22 +968,48 @@ class SQL:
882
968
  return params
883
969
 
884
970
  def _compile_execute_many(self, placeholder_style: "Optional[str]") -> "tuple[str, Any]":
885
- """Handle compilation for execute_many operations."""
886
- sql = self.sql
971
+ """Compile for execute_many operations.
887
972
 
973
+ The pipeline processed the first parameter set to extract literals.
974
+ Now we need to apply those extracted literals to all parameter sets.
975
+ """
976
+ sql = self.sql
888
977
  self._ensure_processed()
889
978
 
890
- params = self._original_parameters
891
-
892
- extracted_params = self._get_extracted_parameters()
893
-
894
- if extracted_params:
895
- params = self._merge_extracted_params_with_sets(params, extracted_params)
979
+ # Get the original parameter sets
980
+ param_sets = self._original_parameters or []
981
+
982
+ # Get any literals extracted during pipeline processing
983
+ if self._processed_state and self._processing_context:
984
+ extracted_literals = self._processing_context.extracted_parameters_from_pipeline
985
+
986
+ if extracted_literals:
987
+ # Apply extracted literals to each parameter set
988
+ enhanced_params: list[Any] = []
989
+ for param_set in param_sets:
990
+ if isinstance(param_set, (list, tuple)):
991
+ # Add extracted literals to the parameter tuple
992
+ enhanced_set = list(param_set) + [
993
+ p.value if hasattr(p, "value") else p for p in extracted_literals
994
+ ]
995
+ enhanced_params.append(tuple(enhanced_set))
996
+ elif isinstance(param_set, dict):
997
+ # For dict params, add extracted literals with generated names
998
+ enhanced_dict = dict(param_set)
999
+ for i, literal in enumerate(extracted_literals):
1000
+ param_name = f"_literal_{i}"
1001
+ enhanced_dict[param_name] = literal.value if hasattr(literal, "value") else literal
1002
+ enhanced_params.append(enhanced_dict)
1003
+ else:
1004
+ # Single parameter - convert to tuple with literals
1005
+ literals = [p.value if hasattr(p, "value") else p for p in extracted_literals]
1006
+ enhanced_params.append((param_set, *literals))
1007
+ param_sets = enhanced_params
896
1008
 
897
1009
  if placeholder_style:
898
- sql, params = self._convert_placeholder_style(sql, params, placeholder_style)
1010
+ sql, param_sets = self._convert_placeholder_style(sql, param_sets, placeholder_style)
899
1011
 
900
- return sql, params
1012
+ return sql, param_sets
901
1013
 
902
1014
  def _get_extracted_parameters(self) -> "list[Any]":
903
1015
  """Get extracted parameters from pipeline processing."""
@@ -958,13 +1070,13 @@ class SQL:
958
1070
  if parameter_mapping:
959
1071
  params = self._reorder_parameters(params, parameter_mapping)
960
1072
 
961
- # Handle denormalization if needed
962
- if self._processing_context and self._processing_context.parameter_normalization:
963
- norm_state = self._processing_context.parameter_normalization
1073
+ # Handle deconversion if needed
1074
+ if self._processing_context and self._processing_context.parameter_conversion:
1075
+ norm_state = self._processing_context.parameter_conversion
964
1076
 
965
1077
  # If original SQL had incompatible styles, denormalize back to the original style
966
1078
  # when no specific style requested OR when the requested style matches the original
967
- if norm_state.was_normalized and norm_state.original_styles:
1079
+ if norm_state.was_transformed and norm_state.original_styles:
968
1080
  original_style = norm_state.original_styles[0]
969
1081
  should_denormalize = placeholder_style is None or (
970
1082
  placeholder_style and ParameterStyle(placeholder_style) == original_style
@@ -975,7 +1087,7 @@ class SQL:
975
1087
  sql = self._config.parameter_converter._convert_sql_placeholders(
976
1088
  sql, norm_state.original_param_info, original_style
977
1089
  )
978
- # Also denormalize parameters if needed
1090
+ # Also deConvert parameters if needed
979
1091
  if original_style == ParameterStyle.POSITIONAL_COLON and is_dict(params):
980
1092
  params = self._denormalize_colon_params(params)
981
1093
 
@@ -1103,10 +1215,10 @@ class SQL:
1103
1215
  if (
1104
1216
  target_style == ParameterStyle.POSITIONAL_COLON
1105
1217
  and self._processing_context
1106
- and self._processing_context.parameter_normalization
1107
- and self._processing_context.parameter_normalization.original_param_info
1218
+ and self._processing_context.parameter_conversion
1219
+ and self._processing_context.parameter_conversion.original_param_info
1108
1220
  ):
1109
- param_info = self._processing_context.parameter_normalization.original_param_info
1221
+ param_info = self._processing_context.parameter_conversion.original_param_info
1110
1222
  else:
1111
1223
  param_info = converter.validator.extract_parameters(sql)
1112
1224
 
@@ -1310,7 +1422,7 @@ class SQL:
1310
1422
  return result_dict
1311
1423
 
1312
1424
  def _process_mixed_colon_params(self, params: "dict[str, Any]", param_info: "list[Any]") -> "dict[str, Any]":
1313
- """Process mixed colon-style numeric and normalized parameters."""
1425
+ """Process mixed colon-style numeric and converted parameters."""
1314
1426
  result_dict: dict[str, Any] = {}
1315
1427
 
1316
1428
  # When we have mixed parameters (extracted literals + user oracle params),
@@ -1393,22 +1505,22 @@ class SQL:
1393
1505
  if all(key.startswith("param_") for key in params):
1394
1506
  param_result_dict: dict[str, Any] = {}
1395
1507
  for p in sorted(param_info, key=lambda x: x.ordinal):
1396
- # Use the parameter's ordinal to find the normalized key
1397
- normalized_key = f"param_{p.ordinal}"
1398
- if normalized_key in params:
1508
+ # Use the parameter's ordinal to find the converted key
1509
+ converted_key = f"param_{p.ordinal}"
1510
+ if converted_key in params:
1399
1511
  if p.name and p.name.isdigit():
1400
1512
  # For Oracle numeric parameters, preserve the original number
1401
- param_result_dict[p.name] = params[normalized_key]
1513
+ param_result_dict[p.name] = params[converted_key]
1402
1514
  else:
1403
1515
  # For other cases, use sequential numbering
1404
- param_result_dict[str(p.ordinal + 1)] = params[normalized_key]
1516
+ param_result_dict[str(p.ordinal + 1)] = params[converted_key]
1405
1517
  return param_result_dict
1406
1518
 
1407
1519
  has_oracle_numeric = any(key.isdigit() for key in params)
1408
- has_param_normalized = any(key.startswith("param_") for key in params)
1520
+ has_param_converted = any(key.startswith("param_") for key in params)
1409
1521
  has_typed_params = any(has_parameter_value(v) for v in params.values())
1410
1522
 
1411
- if (has_oracle_numeric and has_param_normalized) or has_typed_params:
1523
+ if (has_oracle_numeric and has_param_converted) or has_typed_params:
1412
1524
  return self._process_mixed_colon_params(params, param_info)
1413
1525
 
1414
1526
  result_dict: dict[str, Any] = {}
@@ -1628,7 +1740,7 @@ class SQL:
1628
1740
  def parameter_info(self) -> list[Any]:
1629
1741
  """Get parameter information from the SQL statement.
1630
1742
 
1631
- Returns the original parameter info before any normalization.
1743
+ Returns the original parameter info before any conversion.
1632
1744
  """
1633
1745
  validator = self._config.parameter_validator
1634
1746
  if self._raw_sql:
@@ -1660,45 +1772,3 @@ class SQL:
1660
1772
  def statement(self) -> exp.Expression:
1661
1773
  """Get statement for compatibility."""
1662
1774
  return self._statement
1663
-
1664
- def limit(self, count: int, use_parameter: bool = False) -> "SQL":
1665
- """Add LIMIT clause."""
1666
- if use_parameter:
1667
- param_name = self.get_unique_parameter_name("limit")
1668
- result = self
1669
- result = result.add_named_parameter(param_name, count)
1670
- if supports_limit(result._statement):
1671
- new_statement = result._statement.limit(exp.Placeholder(this=param_name)) # pyright: ignore
1672
- else:
1673
- new_statement = exp.Select().from_(result._statement).limit(exp.Placeholder(this=param_name)) # pyright: ignore
1674
- return result.copy(statement=new_statement)
1675
- if supports_limit(self._statement):
1676
- new_statement = self._statement.limit(count) # pyright: ignore
1677
- else:
1678
- new_statement = exp.Select().from_(self._statement).limit(count) # pyright: ignore
1679
- return self.copy(statement=new_statement)
1680
-
1681
- def offset(self, count: int, use_parameter: bool = False) -> "SQL":
1682
- """Add OFFSET clause."""
1683
- if use_parameter:
1684
- param_name = self.get_unique_parameter_name("offset")
1685
- result = self
1686
- result = result.add_named_parameter(param_name, count)
1687
- if supports_offset(result._statement):
1688
- new_statement = result._statement.offset(exp.Placeholder(this=param_name)) # pyright: ignore
1689
- else:
1690
- new_statement = exp.Select().from_(result._statement).offset(exp.Placeholder(this=param_name)) # pyright: ignore
1691
- return result.copy(statement=new_statement)
1692
- if supports_offset(self._statement):
1693
- new_statement = self._statement.offset(count) # pyright: ignore
1694
- else:
1695
- new_statement = exp.Select().from_(self._statement).offset(count) # pyright: ignore
1696
- return self.copy(statement=new_statement)
1697
-
1698
- def order_by(self, expression: exp.Expression) -> "SQL":
1699
- """Add ORDER BY clause."""
1700
- if supports_order_by(self._statement):
1701
- new_statement = self._statement.order_by(expression) # pyright: ignore
1702
- else:
1703
- new_statement = exp.Select().from_(self._statement).order_by(expression) # pyright: ignore
1704
- return self.copy(statement=new_statement)