sqlspec 0.13.1__py3-none-any.whl → 0.16.2__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 (185) hide show
  1. sqlspec/__init__.py +71 -8
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +930 -136
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +116 -285
  10. sqlspec/adapters/adbc/driver.py +462 -340
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +202 -150
  14. sqlspec/adapters/aiosqlite/driver.py +226 -247
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -199
  18. sqlspec/adapters/asyncmy/driver.py +257 -215
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +81 -214
  22. sqlspec/adapters/asyncpg/driver.py +284 -359
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -299
  26. sqlspec/adapters/bigquery/driver.py +474 -634
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +414 -397
  30. sqlspec/adapters/duckdb/driver.py +342 -393
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -458
  34. sqlspec/adapters/oracledb/driver.py +505 -531
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -307
  38. sqlspec/adapters/psqlpy/driver.py +504 -213
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -472
  42. sqlspec/adapters/psycopg/driver.py +704 -825
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +208 -142
  46. sqlspec/adapters/sqlite/driver.py +263 -278
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
  50. sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
  51. sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
  53. sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
  54. sqlspec/builder/_insert.py +421 -0
  55. sqlspec/builder/_merge.py +71 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
  57. sqlspec/builder/_select.py +170 -0
  58. sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
  59. sqlspec/builder/mixins/__init__.py +55 -0
  60. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  61. sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
  62. sqlspec/builder/mixins/_insert_operations.py +244 -0
  63. sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
  64. sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
  65. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  66. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  67. sqlspec/builder/mixins/_select_operations.py +604 -0
  68. sqlspec/builder/mixins/_update_operations.py +202 -0
  69. sqlspec/builder/mixins/_where_clause.py +644 -0
  70. sqlspec/cli.py +247 -0
  71. sqlspec/config.py +183 -138
  72. sqlspec/core/__init__.py +63 -0
  73. sqlspec/core/cache.py +871 -0
  74. sqlspec/core/compiler.py +417 -0
  75. sqlspec/core/filters.py +830 -0
  76. sqlspec/core/hashing.py +310 -0
  77. sqlspec/core/parameters.py +1237 -0
  78. sqlspec/core/result.py +677 -0
  79. sqlspec/{statement → core}/splitter.py +321 -191
  80. sqlspec/core/statement.py +676 -0
  81. sqlspec/driver/__init__.py +7 -10
  82. sqlspec/driver/_async.py +422 -163
  83. sqlspec/driver/_common.py +545 -287
  84. sqlspec/driver/_sync.py +426 -160
  85. sqlspec/driver/mixins/__init__.py +2 -13
  86. sqlspec/driver/mixins/_result_tools.py +193 -0
  87. sqlspec/driver/mixins/_sql_translator.py +65 -14
  88. sqlspec/exceptions.py +5 -252
  89. sqlspec/extensions/aiosql/adapter.py +93 -96
  90. sqlspec/extensions/litestar/__init__.py +2 -1
  91. sqlspec/extensions/litestar/cli.py +48 -0
  92. sqlspec/extensions/litestar/config.py +0 -1
  93. sqlspec/extensions/litestar/handlers.py +15 -26
  94. sqlspec/extensions/litestar/plugin.py +21 -16
  95. sqlspec/extensions/litestar/providers.py +17 -52
  96. sqlspec/loader.py +423 -104
  97. sqlspec/migrations/__init__.py +35 -0
  98. sqlspec/migrations/base.py +414 -0
  99. sqlspec/migrations/commands.py +443 -0
  100. sqlspec/migrations/loaders.py +402 -0
  101. sqlspec/migrations/runner.py +213 -0
  102. sqlspec/migrations/tracker.py +140 -0
  103. sqlspec/migrations/utils.py +129 -0
  104. sqlspec/protocols.py +51 -186
  105. sqlspec/storage/__init__.py +1 -1
  106. sqlspec/storage/backends/base.py +37 -40
  107. sqlspec/storage/backends/fsspec.py +136 -112
  108. sqlspec/storage/backends/obstore.py +138 -160
  109. sqlspec/storage/capabilities.py +5 -4
  110. sqlspec/storage/registry.py +57 -106
  111. sqlspec/typing.py +136 -115
  112. sqlspec/utils/__init__.py +2 -2
  113. sqlspec/utils/correlation.py +0 -3
  114. sqlspec/utils/deprecation.py +6 -6
  115. sqlspec/utils/fixtures.py +6 -6
  116. sqlspec/utils/logging.py +0 -2
  117. sqlspec/utils/module_loader.py +7 -12
  118. sqlspec/utils/singleton.py +0 -1
  119. sqlspec/utils/sync_tools.py +17 -38
  120. sqlspec/utils/text.py +12 -51
  121. sqlspec/utils/type_guards.py +482 -235
  122. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
  123. sqlspec-0.16.2.dist-info/RECORD +134 -0
  124. sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
  125. sqlspec/driver/connection.py +0 -207
  126. sqlspec/driver/mixins/_csv_writer.py +0 -91
  127. sqlspec/driver/mixins/_pipeline.py +0 -512
  128. sqlspec/driver/mixins/_result_utils.py +0 -140
  129. sqlspec/driver/mixins/_storage.py +0 -926
  130. sqlspec/driver/mixins/_type_coercion.py +0 -130
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/service/__init__.py +0 -4
  133. sqlspec/service/_util.py +0 -147
  134. sqlspec/service/base.py +0 -1131
  135. sqlspec/service/pagination.py +0 -26
  136. sqlspec/statement/__init__.py +0 -21
  137. sqlspec/statement/builder/insert.py +0 -288
  138. sqlspec/statement/builder/merge.py +0 -95
  139. sqlspec/statement/builder/mixins/__init__.py +0 -65
  140. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  141. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  142. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  143. sqlspec/statement/builder/mixins/_from.py +0 -63
  144. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  145. sqlspec/statement/builder/mixins/_having.py +0 -35
  146. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  147. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  148. sqlspec/statement/builder/mixins/_insert_values.py +0 -67
  149. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  150. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  151. sqlspec/statement/builder/mixins/_pivot.py +0 -79
  152. sqlspec/statement/builder/mixins/_returning.py +0 -37
  153. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  154. sqlspec/statement/builder/mixins/_set_ops.py +0 -122
  155. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  156. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  157. sqlspec/statement/builder/mixins/_update_set.py +0 -94
  158. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  159. sqlspec/statement/builder/mixins/_where.py +0 -401
  160. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  161. sqlspec/statement/builder/select.py +0 -221
  162. sqlspec/statement/filters.py +0 -596
  163. sqlspec/statement/parameter_manager.py +0 -220
  164. sqlspec/statement/parameters.py +0 -867
  165. sqlspec/statement/pipelines/__init__.py +0 -210
  166. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  167. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  168. sqlspec/statement/pipelines/context.py +0 -115
  169. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  170. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  171. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  172. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  173. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  174. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  175. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  176. sqlspec/statement/pipelines/validators/_performance.py +0 -718
  177. sqlspec/statement/pipelines/validators/_security.py +0 -967
  178. sqlspec/statement/result.py +0 -435
  179. sqlspec/statement/sql.py +0 -1704
  180. sqlspec/statement/sql_compiler.py +0 -140
  181. sqlspec/utils/cached_property.py +0 -25
  182. sqlspec-0.13.1.dist-info/RECORD +0 -150
  183. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
  184. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
  185. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
sqlspec/statement/sql.py DELETED
@@ -1,1704 +0,0 @@
1
- """SQL statement handling with centralized parameter management."""
2
-
3
- import operator
4
- from dataclasses import dataclass, field
5
- from typing import TYPE_CHECKING, Any, Optional, Union
6
-
7
- import sqlglot
8
- import sqlglot.expressions as exp
9
- from sqlglot.errors import ParseError
10
- from typing_extensions import TypeAlias
11
-
12
- from sqlspec.exceptions import RiskLevel, SQLParsingError, SQLValidationError
13
- from sqlspec.statement.filters import StatementFilter
14
- from sqlspec.statement.parameters import (
15
- SQLGLOT_INCOMPATIBLE_STYLES,
16
- ParameterConverter,
17
- ParameterStyle,
18
- ParameterValidator,
19
- )
20
- from sqlspec.statement.pipelines import SQLProcessingContext, StatementPipeline
21
- from sqlspec.statement.pipelines.transformers import CommentAndHintRemover, ParameterizeLiterals
22
- from sqlspec.statement.pipelines.validators import DMLSafetyValidator, ParameterStyleValidator
23
- from sqlspec.utils.logging import get_logger
24
- from sqlspec.utils.type_guards import (
25
- can_append_to_statement,
26
- can_extract_parameters,
27
- has_parameter_value,
28
- has_risk_level,
29
- is_dict,
30
- is_expression,
31
- is_statement_filter,
32
- supports_limit,
33
- supports_offset,
34
- supports_order_by,
35
- supports_where,
36
- )
37
-
38
- if TYPE_CHECKING:
39
- from sqlglot.dialects.dialect import DialectType
40
-
41
- from sqlspec.statement.parameters import ParameterNormalizationState
42
-
43
- __all__ = ("SQL", "SQLConfig", "Statement")
44
-
45
- logger = get_logger("sqlspec.statement")
46
-
47
- Statement: TypeAlias = Union[str, exp.Expression, "SQL"]
48
-
49
- # Parameter naming constants
50
- PARAM_PREFIX = "param_"
51
- POS_PARAM_PREFIX = "pos_param_"
52
- KW_POS_PARAM_PREFIX = "kw_pos_param_"
53
- ARG_PREFIX = "arg_"
54
-
55
- # Cache and limit constants
56
- DEFAULT_CACHE_SIZE = 1000
57
-
58
- # Oracle/Colon style parameter constants
59
- COLON_PARAM_ONE = "1"
60
- COLON_PARAM_MIN_INDEX = 1
61
-
62
-
63
- @dataclass
64
- class _ProcessedState:
65
- """Cached state from pipeline processing."""
66
-
67
- processed_expression: exp.Expression
68
- processed_sql: str
69
- merged_parameters: Any
70
- validation_errors: list[Any] = field(default_factory=list)
71
- analysis_results: dict[str, Any] = field(default_factory=dict)
72
- transformation_results: dict[str, Any] = field(default_factory=dict)
73
-
74
-
75
- @dataclass
76
- class SQLConfig:
77
- """Configuration for SQL statement behavior.
78
-
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.
82
-
83
- Component Lists:
84
- transformers: Optional list of SQL transformers for explicit staging
85
- validators: Optional list of SQL validators for explicit staging
86
- analyzers: Optional list of SQL analyzers for explicit staging
87
-
88
- Configuration Options:
89
- parameter_converter: Handles parameter style conversions
90
- parameter_validator: Validates parameter usage and styles
91
- analysis_cache_size: Cache size for analysis results
92
- input_sql_had_placeholders: Populated by SQL.__init__ to track original SQL state
93
- dialect: SQL dialect to use for parsing and generation
94
-
95
- Parameter Style Configuration:
96
- allowed_parameter_styles: Allowed parameter styles (e.g., ('qmark', 'named_colon'))
97
- target_parameter_style: Target parameter style for SQL generation
98
- allow_mixed_parameter_styles: Whether to allow mixing parameter styles in same query
99
- """
100
-
101
- enable_parsing: bool = True
102
- enable_validation: bool = True
103
- enable_transformations: bool = True
104
- enable_analysis: bool = False
105
- enable_normalization: bool = True
106
- strict_mode: bool = False
107
- cache_parsed_expression: bool = True
108
- parse_errors_as_warnings: bool = True
109
-
110
- transformers: "Optional[list[Any]]" = None
111
- validators: "Optional[list[Any]]" = None
112
- analyzers: "Optional[list[Any]]" = None
113
-
114
- parameter_converter: ParameterConverter = field(default_factory=ParameterConverter)
115
- parameter_validator: ParameterValidator = field(default_factory=ParameterValidator)
116
- analysis_cache_size: int = 1000
117
- input_sql_had_placeholders: bool = False
118
- dialect: "Optional[DialectType]" = None
119
-
120
- allowed_parameter_styles: "Optional[tuple[str, ...]]" = None
121
- target_parameter_style: "Optional[str]" = None
122
- allow_mixed_parameter_styles: bool = False
123
-
124
- def validate_parameter_style(self, style: "Union[ParameterStyle, str]") -> bool:
125
- """Check if a parameter style is allowed.
126
-
127
- Args:
128
- style: Parameter style to validate (can be ParameterStyle enum or string)
129
-
130
- Returns:
131
- True if the style is allowed, False otherwise
132
- """
133
- if self.allowed_parameter_styles is None:
134
- return True
135
- style_str = str(style)
136
- return style_str in self.allowed_parameter_styles
137
-
138
- def get_statement_pipeline(self) -> StatementPipeline:
139
- """Get the configured statement pipeline.
140
-
141
- Returns:
142
- StatementPipeline configured with transformers, validators, and analyzers
143
- """
144
- transformers = []
145
- if self.transformers is not None:
146
- transformers = list(self.transformers)
147
- elif self.enable_transformations:
148
- placeholder_style = self.target_parameter_style or "?"
149
- transformers = [CommentAndHintRemover(), ParameterizeLiterals(placeholder_style=placeholder_style)]
150
-
151
- validators = []
152
- if self.validators is not None:
153
- validators = list(self.validators)
154
- elif self.enable_validation:
155
- validators = [ParameterStyleValidator(fail_on_violation=self.strict_mode), DMLSafetyValidator()]
156
-
157
- analyzers = []
158
- if self.analyzers is not None:
159
- analyzers = list(self.analyzers)
160
- elif self.enable_analysis:
161
- analyzers = []
162
-
163
- return StatementPipeline(transformers=transformers, validators=validators, analyzers=analyzers)
164
-
165
-
166
- class SQL:
167
- """Immutable SQL statement with centralized parameter management.
168
-
169
- The SQL class is the single source of truth for:
170
- - SQL expression/statement
171
- - Positional parameters
172
- - Named parameters
173
- - Applied filters
174
-
175
- All methods that modify state return new SQL instances.
176
- """
177
-
178
- __slots__ = (
179
- "_builder_result_type",
180
- "_config",
181
- "_dialect",
182
- "_filters",
183
- "_is_many",
184
- "_is_script",
185
- "_named_params",
186
- "_original_parameters",
187
- "_original_sql",
188
- "_parameter_normalization_state",
189
- "_placeholder_mapping",
190
- "_positional_params",
191
- "_processed_state",
192
- "_processing_context",
193
- "_raw_sql",
194
- "_statement",
195
- )
196
-
197
- def __init__(
198
- self,
199
- statement: "Union[str, exp.Expression, 'SQL']",
200
- *parameters: "Union[Any, StatementFilter, list[Union[Any, StatementFilter]]]",
201
- _dialect: "DialectType" = None,
202
- _config: "Optional[SQLConfig]" = None,
203
- _builder_result_type: "Optional[type]" = None,
204
- _existing_state: "Optional[dict[str, Any]]" = None,
205
- **kwargs: Any,
206
- ) -> None:
207
- """Initialize SQL with centralized parameter management."""
208
- if "config" in kwargs and _config is None:
209
- _config = kwargs.pop("config")
210
- self._config = _config or SQLConfig()
211
- self._dialect = _dialect or (self._config.dialect if self._config else None)
212
- self._builder_result_type = _builder_result_type
213
- self._processed_state: Optional[_ProcessedState] = None
214
- self._processing_context: Optional[SQLProcessingContext] = None
215
- self._positional_params: list[Any] = []
216
- self._named_params: dict[str, Any] = {}
217
- self._filters: list[StatementFilter] = []
218
- self._statement: exp.Expression
219
- self._raw_sql: str = ""
220
- self._original_parameters: Any = None
221
- self._original_sql: str = ""
222
- self._placeholder_mapping: dict[str, Union[str, int]] = {}
223
- self._parameter_normalization_state: Optional[ParameterNormalizationState] = None
224
- self._is_many: bool = False
225
- self._is_script: bool = False
226
-
227
- if isinstance(statement, SQL):
228
- self._init_from_sql_object(statement, _dialect, _config, _builder_result_type)
229
- else:
230
- self._init_from_str_or_expression(statement)
231
-
232
- if _existing_state:
233
- self._load_from_existing_state(_existing_state)
234
-
235
- if not isinstance(statement, SQL) and not _existing_state:
236
- self._set_original_parameters(*parameters)
237
-
238
- self._process_parameters(*parameters, **kwargs)
239
-
240
- def _init_from_sql_object(
241
- self,
242
- statement: "SQL",
243
- dialect: "DialectType",
244
- config: "Optional[SQLConfig]",
245
- builder_result_type: "Optional[type]",
246
- ) -> None:
247
- """Initialize attributes from an existing SQL object."""
248
- self._statement = statement._statement
249
- self._dialect = dialect or statement._dialect
250
- self._config = config or statement._config
251
- self._builder_result_type = builder_result_type or statement._builder_result_type
252
- self._is_many = statement._is_many
253
- self._is_script = statement._is_script
254
- self._raw_sql = statement._raw_sql
255
- self._original_parameters = statement._original_parameters
256
- self._original_sql = statement._original_sql
257
- self._placeholder_mapping = statement._placeholder_mapping.copy()
258
- self._parameter_normalization_state = statement._parameter_normalization_state
259
- self._positional_params.extend(statement._positional_params)
260
- self._named_params.update(statement._named_params)
261
- self._filters.extend(statement._filters)
262
-
263
- def _init_from_str_or_expression(self, statement: "Union[str, exp.Expression]") -> None:
264
- """Initialize attributes from a SQL string or expression."""
265
- if isinstance(statement, str):
266
- self._raw_sql = statement
267
- self._statement = self._to_expression(statement)
268
- else:
269
- self._raw_sql = statement.sql(dialect=self._dialect) # pyright: ignore
270
- self._statement = statement
271
-
272
- def _load_from_existing_state(self, existing_state: "dict[str, Any]") -> None:
273
- """Load state from a dictionary (used by copy)."""
274
- self._positional_params = list(existing_state.get("positional_params", self._positional_params))
275
- self._named_params = dict(existing_state.get("named_params", self._named_params))
276
- self._filters = list(existing_state.get("filters", self._filters))
277
- self._is_many = existing_state.get("is_many", self._is_many)
278
- self._is_script = existing_state.get("is_script", self._is_script)
279
- self._raw_sql = existing_state.get("raw_sql", self._raw_sql)
280
- self._original_parameters = existing_state.get("original_parameters", self._original_parameters)
281
-
282
- 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])):
285
- self._original_parameters = None
286
- elif len(parameters) == 1 and isinstance(parameters[0], (list, tuple)):
287
- self._original_parameters = parameters[0]
288
- else:
289
- self._original_parameters = parameters
290
-
291
- def _process_parameters(self, *parameters: Any, **kwargs: Any) -> None:
292
- """Process positional and keyword arguments for parameters and filters."""
293
- for param in parameters:
294
- self._process_parameter_item(param)
295
-
296
- if "parameters" in kwargs:
297
- param_value = kwargs.pop("parameters")
298
- if isinstance(param_value, (list, tuple)):
299
- self._positional_params.extend(param_value)
300
- elif is_dict(param_value):
301
- self._named_params.update(param_value)
302
- else:
303
- self._positional_params.append(param_value)
304
-
305
- for key, value in kwargs.items():
306
- if not key.startswith("_"):
307
- self._named_params[key] = value
308
-
309
- def _process_parameter_item(self, item: Any) -> None:
310
- """Process a single item from the parameters list."""
311
- if is_statement_filter(item):
312
- self._filters.append(item)
313
- pos_params, named_params = self._extract_filter_parameters(item)
314
- self._positional_params.extend(pos_params)
315
- self._named_params.update(named_params)
316
- elif isinstance(item, list):
317
- for sub_item in item:
318
- self._process_parameter_item(sub_item)
319
- elif is_dict(item):
320
- self._named_params.update(item)
321
- elif isinstance(item, tuple):
322
- self._positional_params.extend(item)
323
- else:
324
- self._positional_params.append(item)
325
-
326
- def _ensure_processed(self) -> None:
327
- """Ensure the SQL has been processed through the pipeline (lazy initialization).
328
-
329
- This method implements the facade pattern with lazy processing.
330
- It's called by public methods that need processed state.
331
- """
332
- if self._processed_state is not None:
333
- return
334
-
335
- final_expr, final_params = self._build_final_state()
336
- has_placeholders = self._detect_placeholders()
337
- initial_sql_for_context, final_params = self._prepare_context_sql(final_expr, final_params)
338
-
339
- context = self._create_processing_context(initial_sql_for_context, final_expr, final_params, has_placeholders)
340
- result = self._run_pipeline(context)
341
-
342
- processed_sql, merged_params = self._process_pipeline_result(result, final_params, context)
343
-
344
- self._finalize_processed_state(result, processed_sql, merged_params)
345
-
346
- def _detect_placeholders(self) -> bool:
347
- """Detect if the raw SQL has placeholders."""
348
- if self._raw_sql:
349
- validator = self._config.parameter_validator
350
- raw_param_info = validator.extract_parameters(self._raw_sql)
351
- has_placeholders = bool(raw_param_info)
352
- if has_placeholders:
353
- self._config.input_sql_had_placeholders = True
354
- return has_placeholders
355
- return self._config.input_sql_had_placeholders
356
-
357
- def _prepare_context_sql(self, final_expr: exp.Expression, final_params: Any) -> tuple[str, Any]:
358
- """Prepare SQL string and parameters for context."""
359
- initial_sql_for_context = self._raw_sql or final_expr.sql(dialect=self._dialect or self._config.dialect)
360
-
361
- if is_expression(final_expr) and self._placeholder_mapping:
362
- initial_sql_for_context = final_expr.sql(dialect=self._dialect or self._config.dialect)
363
- if self._placeholder_mapping:
364
- final_params = self._normalize_parameters(final_params)
365
-
366
- return initial_sql_for_context, final_params
367
-
368
- def _normalize_parameters(self, final_params: Any) -> Any:
369
- """Normalize parameters based on placeholder mapping."""
370
- if is_dict(final_params):
371
- normalized_params = {}
372
- for placeholder_key, original_name in self._placeholder_mapping.items():
373
- if str(original_name) in final_params:
374
- normalized_params[placeholder_key] = final_params[str(original_name)]
375
- non_oracle_params = {
376
- key: value
377
- for key, value in final_params.items()
378
- if key not in {str(name) for name in self._placeholder_mapping.values()}
379
- }
380
- normalized_params.update(non_oracle_params)
381
- return normalized_params
382
- if isinstance(final_params, (list, tuple)):
383
- validator = self._config.parameter_validator
384
- param_info = validator.extract_parameters(self._raw_sql)
385
-
386
- all_numeric = all(p.name and p.name.isdigit() for p in param_info)
387
-
388
- if all_numeric:
389
- normalized_params = {}
390
-
391
- min_param_num = min(int(p.name) for p in param_info if p.name)
392
-
393
- for i, param in enumerate(final_params):
394
- param_num = str(i + min_param_num)
395
- normalized_params[param_num] = param
396
-
397
- return normalized_params
398
- normalized_params = {}
399
- for i, param in enumerate(final_params):
400
- if i < len(param_info):
401
- placeholder_key = f"{PARAM_PREFIX}{param_info[i].ordinal}"
402
- normalized_params[placeholder_key] = param
403
- return normalized_params
404
- return final_params
405
-
406
- def _create_processing_context(
407
- self, initial_sql_for_context: str, final_expr: exp.Expression, final_params: Any, has_placeholders: bool
408
- ) -> SQLProcessingContext:
409
- """Create SQL processing context."""
410
- context = SQLProcessingContext(
411
- initial_sql_string=initial_sql_for_context,
412
- dialect=self._dialect or self._config.dialect,
413
- config=self._config,
414
- initial_expression=final_expr,
415
- current_expression=final_expr,
416
- merged_parameters=final_params,
417
- input_sql_had_placeholders=has_placeholders or self._config.input_sql_had_placeholders,
418
- )
419
-
420
- if self._placeholder_mapping:
421
- context.extra_info["placeholder_map"] = self._placeholder_mapping
422
-
423
- # Set normalization state if available
424
- if self._parameter_normalization_state:
425
- context.parameter_normalization = self._parameter_normalization_state
426
-
427
- validator = self._config.parameter_validator
428
- context.parameter_info = validator.extract_parameters(context.initial_sql_string)
429
-
430
- return context
431
-
432
- def _run_pipeline(self, context: SQLProcessingContext) -> Any:
433
- """Run the SQL processing pipeline."""
434
- pipeline = self._config.get_statement_pipeline()
435
- result = pipeline.execute_pipeline(context)
436
- self._processing_context = result.context
437
- return result
438
-
439
- def _process_pipeline_result(
440
- self, result: Any, final_params: Any, context: SQLProcessingContext
441
- ) -> tuple[str, Any]:
442
- """Process the result from the pipeline."""
443
- processed_expr = result.expression
444
-
445
- if isinstance(processed_expr, exp.Anonymous):
446
- processed_sql = self._raw_sql or context.initial_sql_string
447
- 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)
450
-
451
- if self._placeholder_mapping and self._original_sql:
452
- processed_sql, result = self._denormalize_sql(processed_sql, result)
453
-
454
- merged_params = self._merge_pipeline_parameters(result, final_params)
455
-
456
- return processed_sql, merged_params
457
-
458
- def _denormalize_sql(self, processed_sql: str, result: Any) -> tuple[str, Any]:
459
- """Denormalize SQL back to original parameter style."""
460
-
461
- original_sql = self._original_sql
462
- param_info = self._config.parameter_validator.extract_parameters(original_sql)
463
- 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
- if ParameterStyle.POSITIONAL_PYFORMAT in target_styles:
470
- processed_sql = self._config.parameter_converter._convert_sql_placeholders(
471
- processed_sql, param_info, ParameterStyle.POSITIONAL_PYFORMAT
472
- )
473
- logger.debug("Denormalized SQL to: '%s'", processed_sql)
474
- elif ParameterStyle.NAMED_PYFORMAT in target_styles:
475
- processed_sql = self._config.parameter_converter._convert_sql_placeholders(
476
- processed_sql, param_info, ParameterStyle.NAMED_PYFORMAT
477
- )
478
- logger.debug("Denormalized SQL to: '%s'", processed_sql)
479
- # Also denormalize the parameters back to their original names
480
- if (
481
- self._placeholder_mapping
482
- and result.context.merged_parameters
483
- and is_dict(result.context.merged_parameters)
484
- ):
485
- result.context.merged_parameters = self._denormalize_pyformat_params(result.context.merged_parameters)
486
- elif ParameterStyle.POSITIONAL_COLON in target_styles:
487
- processed_param_info = self._config.parameter_validator.extract_parameters(processed_sql)
488
- has_param_placeholders = any(p.name and p.name.startswith(PARAM_PREFIX) for p in processed_param_info)
489
-
490
- if has_param_placeholders:
491
- logger.debug("Skipping denormalization for param_N placeholders")
492
- else:
493
- processed_sql = self._config.parameter_converter._convert_sql_placeholders(
494
- processed_sql, param_info, ParameterStyle.POSITIONAL_COLON
495
- )
496
- logger.debug("Denormalized SQL to: '%s'", processed_sql)
497
- if (
498
- self._placeholder_mapping
499
- and result.context.merged_parameters
500
- and is_dict(result.context.merged_parameters)
501
- ):
502
- 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
-
510
- return processed_sql, result
511
-
512
- def _denormalize_colon_params(self, params: "dict[str, Any]") -> "dict[str, Any]":
513
- """Denormalize colon-style parameters back to numeric format."""
514
- # For positional colon style, all params should have numeric keys
515
- # Just return the params as-is if they already have the right format
516
- if all(key.isdigit() for key in params):
517
- return params
518
-
519
- # For positional colon, we need ALL parameters in the final result
520
- # This includes both user parameters and extracted literals
521
- # We should NOT filter out extracted parameters (param_0, param_1, etc)
522
- # because they need to be included in the final parameter conversion
523
- return params
524
-
525
- def _denormalize_pyformat_params(self, params: "dict[str, Any]") -> "dict[str, Any]":
526
- """Denormalize pyformat parameters back to their original names."""
527
- denormalized_params = {}
528
- for placeholder_key, original_name in self._placeholder_mapping.items():
529
- if placeholder_key in params:
530
- # 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
536
-
537
- def _merge_pipeline_parameters(self, result: Any, final_params: Any) -> Any:
538
- """Merge parameters from the pipeline processing."""
539
- merged_params = result.context.merged_parameters
540
-
541
- # If we have extracted parameters from the pipeline, only merge them if:
542
- # 1. We don't already have parameters in merged_params, OR
543
- # 2. The original params were None and we need to use the extracted ones
544
- if result.context.extracted_parameters_from_pipeline:
545
- if merged_params is None:
546
- # No existing parameters - use the extracted ones
547
- merged_params = result.context.extracted_parameters_from_pipeline
548
- elif merged_params == final_params and final_params is None:
549
- # Both are None, use extracted parameters
550
- merged_params = result.context.extracted_parameters_from_pipeline
551
- elif merged_params != result.context.extracted_parameters_from_pipeline:
552
- # Only merge if the extracted parameters are different from what we already have
553
- # This prevents the duplication issue where the same parameters get added twice
554
- if is_dict(merged_params):
555
- for i, param in enumerate(result.context.extracted_parameters_from_pipeline):
556
- param_name = f"{PARAM_PREFIX}{i}"
557
- merged_params[param_name] = param
558
- elif isinstance(merged_params, (list, tuple)):
559
- # Only extend if we don't already have these parameters
560
- # Convert to list and extend with extracted parameters
561
- if isinstance(merged_params, tuple):
562
- merged_params = list(merged_params)
563
- merged_params.extend(result.context.extracted_parameters_from_pipeline)
564
- else:
565
- # Single parameter case - convert to list with original + extracted
566
- merged_params = [merged_params, *list(result.context.extracted_parameters_from_pipeline)]
567
-
568
- return merged_params
569
-
570
- def _finalize_processed_state(self, result: Any, processed_sql: str, merged_params: Any) -> None:
571
- """Finalize the processed state."""
572
- self._processed_state = _ProcessedState(
573
- processed_expression=result.expression,
574
- processed_sql=processed_sql,
575
- merged_parameters=merged_params,
576
- validation_errors=list(result.context.validation_errors),
577
- analysis_results={},
578
- transformation_results={},
579
- )
580
-
581
- if self._config.strict_mode and self._processed_state.validation_errors:
582
- highest_risk_error = max(
583
- self._processed_state.validation_errors, key=lambda e: e.risk_level.value if has_risk_level(e) else 0
584
- )
585
- raise SQLValidationError(
586
- message=highest_risk_error.message,
587
- sql=self._raw_sql or processed_sql,
588
- risk_level=getattr(highest_risk_error, "risk_level", RiskLevel.HIGH),
589
- )
590
-
591
- def _to_expression(self, statement: "Union[str, exp.Expression]") -> exp.Expression:
592
- """Convert string to sqlglot expression."""
593
- if is_expression(statement):
594
- return statement
595
-
596
- if not statement or (isinstance(statement, str) and not statement.strip()):
597
- return exp.Select()
598
-
599
- if not self._config.enable_parsing:
600
- return exp.Anonymous(this=statement)
601
-
602
- if not isinstance(statement, str):
603
- return exp.Anonymous(this="")
604
- validator = self._config.parameter_validator
605
- param_info = validator.extract_parameters(statement)
606
-
607
- # Check if normalization is needed
608
- needs_normalization = any(p.style in SQLGLOT_INCOMPATIBLE_STYLES for p in param_info)
609
-
610
- normalized_sql = statement
611
- placeholder_mapping: dict[str, Any] = {}
612
-
613
- if needs_normalization:
614
- converter = self._config.parameter_converter
615
- normalized_sql, placeholder_mapping = converter._transform_sql_for_parsing(statement, param_info)
616
- self._original_sql = statement
617
- self._placeholder_mapping = placeholder_mapping
618
-
619
- # Create normalization state
620
- from sqlspec.statement.parameters import ParameterNormalizationState
621
-
622
- self._parameter_normalization_state = ParameterNormalizationState(
623
- was_normalized=True,
624
- original_styles=list({p.style for p in param_info}),
625
- normalized_style=ParameterStyle.NAMED_COLON,
626
- placeholder_map=placeholder_mapping,
627
- original_param_info=param_info,
628
- )
629
- else:
630
- self._parameter_normalization_state = None
631
-
632
- try:
633
- expressions = sqlglot.parse(normalized_sql, dialect=self._dialect) # pyright: ignore
634
- if not expressions:
635
- return exp.Anonymous(this=statement)
636
- first_expr = expressions[0]
637
- if first_expr is None:
638
- return exp.Anonymous(this=statement)
639
-
640
- except ParseError as e:
641
- if getattr(self._config, "parse_errors_as_warnings", False):
642
- logger.warning(
643
- "Failed to parse SQL, returning Anonymous expression.", extra={"sql": statement, "error": str(e)}
644
- )
645
- return exp.Anonymous(this=statement)
646
-
647
- msg = f"Failed to parse SQL: {statement}"
648
- raise SQLParsingError(msg) from e
649
- return first_expr
650
-
651
- @staticmethod
652
- def _extract_filter_parameters(filter_obj: StatementFilter) -> tuple[list[Any], dict[str, Any]]:
653
- """Extract parameters from a filter object."""
654
- if can_extract_parameters(filter_obj):
655
- return filter_obj.extract_parameters()
656
- return [], {}
657
-
658
- def copy(
659
- self,
660
- statement: "Optional[Union[str, exp.Expression]]" = None,
661
- parameters: "Optional[Any]" = None,
662
- dialect: "DialectType" = None,
663
- config: "Optional[SQLConfig]" = None,
664
- **kwargs: Any,
665
- ) -> "SQL":
666
- """Create a copy with optional modifications.
667
-
668
- This is the primary method for creating modified SQL objects.
669
- """
670
- existing_state = {
671
- "positional_params": list(self._positional_params),
672
- "named_params": dict(self._named_params),
673
- "filters": list(self._filters),
674
- "is_many": self._is_many,
675
- "is_script": self._is_script,
676
- "raw_sql": self._raw_sql,
677
- }
678
- existing_state["original_parameters"] = self._original_parameters
679
-
680
- new_statement = statement if statement is not None else self._statement
681
- new_dialect = dialect if dialect is not None else self._dialect
682
- new_config = config if config is not None else self._config
683
-
684
- if parameters is not None:
685
- existing_state["positional_params"] = []
686
- existing_state["named_params"] = {}
687
- return SQL(
688
- new_statement,
689
- parameters,
690
- _dialect=new_dialect,
691
- _config=new_config,
692
- _builder_result_type=self._builder_result_type,
693
- _existing_state=None,
694
- **kwargs,
695
- )
696
-
697
- return SQL(
698
- new_statement,
699
- _dialect=new_dialect,
700
- _config=new_config,
701
- _builder_result_type=self._builder_result_type,
702
- _existing_state=existing_state,
703
- **kwargs,
704
- )
705
-
706
- def add_named_parameter(self, name: "str", value: Any) -> "SQL":
707
- """Add a named parameter and return a new SQL instance."""
708
- new_obj = self.copy()
709
- new_obj._named_params[name] = value
710
- return new_obj
711
-
712
- def get_unique_parameter_name(
713
- self, base_name: "str", namespace: "Optional[str]" = None, preserve_original: bool = False
714
- ) -> str:
715
- """Generate a unique parameter name.
716
-
717
- Args:
718
- base_name: The base parameter name
719
- namespace: Optional namespace prefix (e.g., 'cte', 'subquery')
720
- preserve_original: If True, try to preserve the original name
721
-
722
- Returns:
723
- A unique parameter name
724
- """
725
- all_param_names = set(self._named_params.keys())
726
-
727
- candidate = f"{namespace}_{base_name}" if namespace else base_name
728
-
729
- if preserve_original and candidate not in all_param_names:
730
- return candidate
731
-
732
- if candidate not in all_param_names:
733
- return candidate
734
-
735
- counter = 1
736
- while True:
737
- new_candidate = f"{candidate}_{counter}"
738
- if new_candidate not in all_param_names:
739
- return new_candidate
740
- counter += 1
741
-
742
- def where(self, condition: "Union[str, exp.Expression, exp.Condition]") -> "SQL":
743
- """Apply WHERE clause and return new SQL instance."""
744
- condition_expr = self._to_expression(condition) if isinstance(condition, str) else condition
745
-
746
- if supports_where(self._statement):
747
- new_statement = self._statement.where(condition_expr) # pyright: ignore
748
- else:
749
- new_statement = exp.Select().from_(self._statement).where(condition_expr) # pyright: ignore
750
-
751
- return self.copy(statement=new_statement)
752
-
753
- def filter(self, filter_obj: StatementFilter) -> "SQL":
754
- """Apply a filter and return a new SQL instance."""
755
- new_obj = self.copy()
756
- new_obj._filters.append(filter_obj)
757
- pos_params, named_params = self._extract_filter_parameters(filter_obj)
758
- new_obj._positional_params.extend(pos_params)
759
- new_obj._named_params.update(named_params)
760
- return new_obj
761
-
762
- def as_many(self, parameters: "Optional[list[Any]]" = None) -> "SQL":
763
- """Mark for executemany with optional parameters."""
764
- new_obj = self.copy()
765
- new_obj._is_many = True
766
- if parameters is not None:
767
- new_obj._positional_params = []
768
- new_obj._named_params = {}
769
- new_obj._original_parameters = parameters
770
- return new_obj
771
-
772
- def as_script(self) -> "SQL":
773
- """Mark as script for execution."""
774
- new_obj = self.copy()
775
- new_obj._is_script = True
776
- return new_obj
777
-
778
- def _build_final_state(self) -> tuple[exp.Expression, Any]:
779
- """Build final expression and parameters after applying filters."""
780
- final_expr = self._statement
781
-
782
- for filter_obj in self._filters:
783
- if can_append_to_statement(filter_obj):
784
- 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)
787
- result = filter_obj.append_to_statement(temp_sql)
788
- final_expr = result._statement if isinstance(result, SQL) else result
789
-
790
- 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):
798
- param_name = f"arg_{i}"
799
- while param_name in final_params:
800
- param_name = f"arg_{i}_{id(param)}"
801
- final_params[param_name] = param
802
- else:
803
- final_params = None
804
-
805
- return final_expr, final_params
806
-
807
- @property
808
- def sql(self) -> str:
809
- """Get SQL string."""
810
- if not self._raw_sql or (self._raw_sql and not self._raw_sql.strip()):
811
- return ""
812
-
813
- if self._is_script and self._raw_sql:
814
- return self._raw_sql
815
- if not self._config.enable_parsing and self._raw_sql:
816
- return self._raw_sql
817
-
818
- self._ensure_processed()
819
- if self._processed_state is None:
820
- msg = "Failed to process SQL statement"
821
- raise RuntimeError(msg)
822
- return self._processed_state.processed_sql
823
-
824
- @property
825
- def expression(self) -> "Optional[exp.Expression]":
826
- """Get the final expression."""
827
- if not self._config.enable_parsing:
828
- return None
829
- self._ensure_processed()
830
- if self._processed_state is None:
831
- msg = "Failed to process SQL statement"
832
- raise RuntimeError(msg)
833
- return self._processed_state.processed_expression
834
-
835
- @property
836
- def parameters(self) -> Any:
837
- """Get merged parameters."""
838
- if self._is_many and self._original_parameters is not None:
839
- return self._original_parameters
840
-
841
- if (
842
- self._original_parameters is not None
843
- and isinstance(self._original_parameters, tuple)
844
- and not self._named_params
845
- ):
846
- return self._original_parameters
847
-
848
- self._ensure_processed()
849
- if self._processed_state is None:
850
- msg = "Failed to process SQL statement"
851
- raise RuntimeError(msg)
852
- params = self._processed_state.merged_parameters
853
- if params is None:
854
- return {}
855
- return params
856
-
857
- @property
858
- def is_many(self) -> bool:
859
- """Check if this is for executemany."""
860
- return self._is_many
861
-
862
- @property
863
- def is_script(self) -> bool:
864
- """Check if this is a script."""
865
- return self._is_script
866
-
867
- @property
868
- def dialect(self) -> "Optional[DialectType]":
869
- """Get the SQL dialect."""
870
- return self._dialect
871
-
872
- def to_sql(self, placeholder_style: "Optional[str]" = None) -> "str":
873
- """Convert to SQL string with given placeholder style."""
874
- if self._is_script:
875
- return self.sql
876
- sql, _ = self.compile(placeholder_style=placeholder_style)
877
- return sql
878
-
879
- def get_parameters(self, style: "Optional[str]" = None) -> Any:
880
- """Get parameters in the requested style."""
881
- _, params = self.compile(placeholder_style=style)
882
- return params
883
-
884
- def _compile_execute_many(self, placeholder_style: "Optional[str]") -> "tuple[str, Any]":
885
- """Handle compilation for execute_many operations."""
886
- sql = self.sql
887
-
888
- self._ensure_processed()
889
-
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)
896
-
897
- if placeholder_style:
898
- sql, params = self._convert_placeholder_style(sql, params, placeholder_style)
899
-
900
- return sql, params
901
-
902
- def _get_extracted_parameters(self) -> "list[Any]":
903
- """Get extracted parameters from pipeline processing."""
904
- extracted_params = []
905
- if self._processed_state and self._processed_state.merged_parameters:
906
- merged = self._processed_state.merged_parameters
907
- if isinstance(merged, list):
908
- if merged and not isinstance(merged[0], (tuple, list)):
909
- extracted_params = merged
910
- elif self._processing_context and self._processing_context.extracted_parameters_from_pipeline:
911
- extracted_params = self._processing_context.extracted_parameters_from_pipeline
912
- return extracted_params
913
-
914
- def _merge_extracted_params_with_sets(self, params: Any, extracted_params: "list[Any]") -> "list[tuple[Any, ...]]":
915
- """Merge extracted parameters with each parameter set."""
916
- enhanced_params = []
917
- for param_set in params:
918
- if isinstance(param_set, (list, tuple)):
919
- extracted_values = []
920
- for extracted in extracted_params:
921
- if has_parameter_value(extracted):
922
- extracted_values.append(extracted.value)
923
- else:
924
- extracted_values.append(extracted)
925
- enhanced_set = list(param_set) + extracted_values
926
- enhanced_params.append(tuple(enhanced_set))
927
- else:
928
- extracted_values = []
929
- for extracted in extracted_params:
930
- if has_parameter_value(extracted):
931
- extracted_values.append(extracted.value)
932
- else:
933
- extracted_values.append(extracted)
934
- enhanced_params.append((param_set, *extracted_values))
935
- return enhanced_params
936
-
937
- def compile(self, placeholder_style: "Optional[str]" = None) -> "tuple[str, Any]":
938
- """Compile to SQL and parameters."""
939
- if self._is_script:
940
- return self.sql, None
941
-
942
- if self._is_many and self._original_parameters is not None:
943
- return self._compile_execute_many(placeholder_style)
944
-
945
- if not self._config.enable_parsing and self._raw_sql:
946
- return self._raw_sql, self._raw_parameters
947
-
948
- self._ensure_processed()
949
-
950
- if self._processed_state is None:
951
- msg = "Failed to process SQL statement"
952
- raise RuntimeError(msg)
953
- sql = self._processed_state.processed_sql
954
- params = self._processed_state.merged_parameters
955
-
956
- if params is not None and self._processing_context:
957
- parameter_mapping = self._processing_context.metadata.get("parameter_position_mapping")
958
- if parameter_mapping:
959
- params = self._reorder_parameters(params, parameter_mapping)
960
-
961
- # Handle denormalization if needed
962
- if self._processing_context and self._processing_context.parameter_normalization:
963
- norm_state = self._processing_context.parameter_normalization
964
-
965
- # If original SQL had incompatible styles, denormalize back to the original style
966
- # when no specific style requested OR when the requested style matches the original
967
- if norm_state.was_normalized and norm_state.original_styles:
968
- original_style = norm_state.original_styles[0]
969
- should_denormalize = placeholder_style is None or (
970
- placeholder_style and ParameterStyle(placeholder_style) == original_style
971
- )
972
-
973
- if should_denormalize and original_style in SQLGLOT_INCOMPATIBLE_STYLES:
974
- # Denormalize SQL back to original style
975
- sql = self._config.parameter_converter._convert_sql_placeholders(
976
- sql, norm_state.original_param_info, original_style
977
- )
978
- # Also denormalize parameters if needed
979
- if original_style == ParameterStyle.POSITIONAL_COLON and is_dict(params):
980
- params = self._denormalize_colon_params(params)
981
-
982
- params = self._unwrap_typed_parameters(params)
983
-
984
- if placeholder_style is None:
985
- return sql, params
986
-
987
- if placeholder_style:
988
- sql, params = self._apply_placeholder_style(sql, params, placeholder_style)
989
-
990
- return sql, params
991
-
992
- def _apply_placeholder_style(self, sql: "str", params: Any, placeholder_style: "str") -> "tuple[str, Any]":
993
- """Apply placeholder style conversion to SQL and parameters."""
994
- # Just use the params passed in - they've already been processed
995
- sql, params = self._convert_placeholder_style(sql, params, placeholder_style)
996
- return sql, params
997
-
998
- @staticmethod
999
- def _unwrap_typed_parameters(params: Any) -> Any:
1000
- """Unwrap TypedParameter objects to their actual values.
1001
-
1002
- Args:
1003
- params: Parameters that may contain TypedParameter objects
1004
-
1005
- Returns:
1006
- Parameters with TypedParameter objects unwrapped to their values
1007
- """
1008
- if params is None:
1009
- return None
1010
-
1011
- if is_dict(params):
1012
- unwrapped_dict = {}
1013
- for key, value in params.items():
1014
- if has_parameter_value(value):
1015
- unwrapped_dict[key] = value.value
1016
- else:
1017
- unwrapped_dict[key] = value
1018
- return unwrapped_dict
1019
-
1020
- if isinstance(params, (list, tuple)):
1021
- unwrapped_list = []
1022
- for value in params:
1023
- if has_parameter_value(value):
1024
- unwrapped_list.append(value.value)
1025
- else:
1026
- unwrapped_list.append(value)
1027
- return type(params)(unwrapped_list)
1028
-
1029
- if has_parameter_value(params):
1030
- return params.value
1031
-
1032
- return params
1033
-
1034
- @staticmethod
1035
- def _reorder_parameters(params: Any, mapping: dict[int, int]) -> Any:
1036
- """Reorder parameters based on the position mapping.
1037
-
1038
- Args:
1039
- params: Original parameters (list, tuple, or dict)
1040
- mapping: Dict mapping new positions to original positions
1041
-
1042
- Returns:
1043
- Reordered parameters in the same format as input
1044
- """
1045
- if isinstance(params, (list, tuple)):
1046
- reordered_list = [None] * len(params) # pyright: ignore
1047
- for new_pos, old_pos in mapping.items():
1048
- if old_pos < len(params):
1049
- reordered_list[new_pos] = params[old_pos] # pyright: ignore
1050
-
1051
- for i, val in enumerate(reordered_list):
1052
- if val is None and i < len(params) and i not in mapping:
1053
- reordered_list[i] = params[i] # pyright: ignore
1054
-
1055
- return tuple(reordered_list) if isinstance(params, tuple) else reordered_list
1056
-
1057
- if is_dict(params):
1058
- if all(key.startswith(PARAM_PREFIX) and key[len(PARAM_PREFIX) :].isdigit() for key in params):
1059
- reordered_dict: dict[str, Any] = {}
1060
- for new_pos, old_pos in mapping.items():
1061
- old_key = f"{PARAM_PREFIX}{old_pos}"
1062
- new_key = f"{PARAM_PREFIX}{new_pos}"
1063
- if old_key in params:
1064
- reordered_dict[new_key] = params[old_key]
1065
-
1066
- for key, value in params.items():
1067
- if key not in reordered_dict and key.startswith(PARAM_PREFIX):
1068
- idx = int(key[6:])
1069
- if idx not in mapping:
1070
- reordered_dict[key] = value
1071
-
1072
- return reordered_dict
1073
- return params
1074
- return params
1075
-
1076
- def _convert_placeholder_style(self, sql: str, params: Any, placeholder_style: str) -> tuple[str, Any]:
1077
- """Convert SQL and parameters to the requested placeholder style.
1078
-
1079
- Args:
1080
- sql: The SQL string to convert
1081
- params: The parameters to convert
1082
- placeholder_style: Target placeholder style
1083
-
1084
- Returns:
1085
- Tuple of (converted_sql, converted_params)
1086
- """
1087
- if self._is_many and isinstance(params, list) and params and isinstance(params[0], (list, tuple)):
1088
- converter = self._config.parameter_converter
1089
- param_info = converter.validator.extract_parameters(sql)
1090
-
1091
- if param_info:
1092
- target_style = (
1093
- ParameterStyle(placeholder_style) if isinstance(placeholder_style, str) else placeholder_style
1094
- )
1095
- sql = self._replace_placeholders_in_sql(sql, param_info, target_style)
1096
-
1097
- return sql, params
1098
-
1099
- converter = self._config.parameter_converter
1100
-
1101
- # For POSITIONAL_COLON style, use original parameter info if available to preserve numeric identifiers
1102
- target_style = ParameterStyle(placeholder_style) if isinstance(placeholder_style, str) else placeholder_style
1103
- if (
1104
- target_style == ParameterStyle.POSITIONAL_COLON
1105
- and self._processing_context
1106
- and self._processing_context.parameter_normalization
1107
- and self._processing_context.parameter_normalization.original_param_info
1108
- ):
1109
- param_info = self._processing_context.parameter_normalization.original_param_info
1110
- else:
1111
- param_info = converter.validator.extract_parameters(sql)
1112
-
1113
- # CRITICAL FIX: For POSITIONAL_COLON, we need to ensure param_info reflects
1114
- # all placeholders in the current SQL, not just the original ones.
1115
- # This handles cases where transformers (like ParameterizeLiterals) add new placeholders.
1116
- if target_style == ParameterStyle.POSITIONAL_COLON and param_info:
1117
- # Re-extract from current SQL to get all placeholders
1118
- current_param_info = converter.validator.extract_parameters(sql)
1119
- if len(current_param_info) > len(param_info):
1120
- # More placeholders in current SQL means transformers added some
1121
- # Use the current info to ensure all placeholders get parameters
1122
- param_info = current_param_info
1123
-
1124
- if not param_info:
1125
- return sql, params
1126
-
1127
- if target_style == ParameterStyle.STATIC:
1128
- return self._embed_static_parameters(sql, params, param_info)
1129
-
1130
- if param_info and all(p.style == target_style for p in param_info):
1131
- converted_params = self._convert_parameters_format(params, param_info, target_style)
1132
- return sql, converted_params
1133
-
1134
- sql = self._replace_placeholders_in_sql(sql, param_info, target_style)
1135
-
1136
- params = self._convert_parameters_format(params, param_info, target_style)
1137
-
1138
- return sql, params
1139
-
1140
- def _embed_static_parameters(self, sql: str, params: Any, param_info: list[Any]) -> tuple[str, Any]:
1141
- """Embed parameter values directly into SQL for STATIC style.
1142
-
1143
- This is used for scripts and other cases where parameters need to be
1144
- embedded directly in the SQL string rather than passed separately.
1145
-
1146
- Args:
1147
- sql: The SQL string with placeholders
1148
- params: The parameter values
1149
- param_info: List of parameter information from extraction
1150
-
1151
- Returns:
1152
- Tuple of (sql_with_embedded_values, None)
1153
- """
1154
- param_list: list[Any] = []
1155
- if is_dict(params):
1156
- for p in param_info:
1157
- if p.name and p.name in params:
1158
- param_list.append(params[p.name])
1159
- elif f"{PARAM_PREFIX}{p.ordinal}" in params:
1160
- param_list.append(params[f"{PARAM_PREFIX}{p.ordinal}"])
1161
- elif f"arg_{p.ordinal}" in params:
1162
- param_list.append(params[f"arg_{p.ordinal}"])
1163
- else:
1164
- param_list.append(params.get(str(p.ordinal), None))
1165
- elif isinstance(params, (list, tuple)):
1166
- param_list = list(params)
1167
- elif params is not None:
1168
- param_list = [params]
1169
-
1170
- sorted_params = sorted(param_info, key=lambda p: p.position, reverse=True)
1171
-
1172
- for p in sorted_params:
1173
- if p.ordinal < len(param_list):
1174
- value = param_list[p.ordinal]
1175
-
1176
- if has_parameter_value(value):
1177
- value = value.value
1178
-
1179
- if value is None:
1180
- literal_str = "NULL"
1181
- elif isinstance(value, bool):
1182
- literal_str = "TRUE" if value else "FALSE"
1183
- elif isinstance(value, str):
1184
- literal_expr = sqlglot.exp.Literal.string(value)
1185
- literal_str = literal_expr.sql(dialect=self._dialect)
1186
- elif isinstance(value, (int, float)):
1187
- literal_expr = sqlglot.exp.Literal.number(value)
1188
- literal_str = literal_expr.sql(dialect=self._dialect)
1189
- else:
1190
- literal_expr = sqlglot.exp.Literal.string(str(value))
1191
- literal_str = literal_expr.sql(dialect=self._dialect)
1192
-
1193
- start = p.position
1194
- end = start + len(p.placeholder_text)
1195
- sql = sql[:start] + literal_str + sql[end:]
1196
-
1197
- return sql, None
1198
-
1199
- def _replace_placeholders_in_sql(self, sql: str, param_info: list[Any], target_style: ParameterStyle) -> str:
1200
- """Replace placeholders in SQL string with target style placeholders.
1201
-
1202
- Args:
1203
- sql: The SQL string
1204
- param_info: List of parameter information
1205
- target_style: Target parameter style
1206
-
1207
- Returns:
1208
- SQL string with replaced placeholders
1209
- """
1210
- sorted_params = sorted(param_info, key=lambda p: p.position, reverse=True)
1211
-
1212
- for p in sorted_params:
1213
- new_placeholder = self._generate_placeholder(p, target_style)
1214
- start = p.position
1215
- end = start + len(p.placeholder_text)
1216
- sql = sql[:start] + new_placeholder + sql[end:]
1217
-
1218
- return sql
1219
-
1220
- @staticmethod
1221
- def _generate_placeholder(param: Any, target_style: ParameterStyle) -> str:
1222
- """Generate a placeholder string for the given parameter style.
1223
-
1224
- Args:
1225
- param: Parameter information object
1226
- target_style: Target parameter style
1227
-
1228
- Returns:
1229
- Placeholder string
1230
- """
1231
- if target_style in {ParameterStyle.STATIC, ParameterStyle.QMARK}:
1232
- return "?"
1233
- if target_style == ParameterStyle.NUMERIC:
1234
- return f"${param.ordinal + 1}"
1235
- if target_style == ParameterStyle.NAMED_COLON:
1236
- if param.name and not param.name.isdigit():
1237
- return f":{param.name}"
1238
- return f":arg_{param.ordinal}"
1239
- if target_style == ParameterStyle.NAMED_AT:
1240
- return f"@{param.name or f'param_{param.ordinal}'}"
1241
- if target_style == ParameterStyle.POSITIONAL_COLON:
1242
- # For Oracle positional colon, preserve the original numeric identifier if it was already :N style
1243
- if (
1244
- hasattr(param, "style")
1245
- and param.style == ParameterStyle.POSITIONAL_COLON
1246
- and hasattr(param, "name")
1247
- and param.name
1248
- and param.name.isdigit()
1249
- ):
1250
- return f":{param.name}"
1251
- return f":{param.ordinal + 1}"
1252
- if target_style == ParameterStyle.POSITIONAL_PYFORMAT:
1253
- return "%s"
1254
- if target_style == ParameterStyle.NAMED_PYFORMAT:
1255
- return f"%({param.name or f'arg_{param.ordinal}'})s"
1256
- return str(param.placeholder_text)
1257
-
1258
- def _convert_parameters_format(self, params: Any, param_info: list[Any], target_style: ParameterStyle) -> Any:
1259
- """Convert parameters to the appropriate format for the target style.
1260
-
1261
- Args:
1262
- params: Original parameters
1263
- param_info: List of parameter information
1264
- target_style: Target parameter style
1265
-
1266
- Returns:
1267
- Converted parameters
1268
- """
1269
- if target_style == ParameterStyle.POSITIONAL_COLON:
1270
- return self._convert_to_positional_colon_format(params, param_info)
1271
- if target_style in {ParameterStyle.QMARK, ParameterStyle.NUMERIC, ParameterStyle.POSITIONAL_PYFORMAT}:
1272
- return self._convert_to_positional_format(params, param_info)
1273
- if target_style == ParameterStyle.NAMED_COLON:
1274
- return self._convert_to_named_colon_format(params, param_info)
1275
- if target_style == ParameterStyle.NAMED_PYFORMAT:
1276
- return self._convert_to_named_pyformat_format(params, param_info)
1277
- return params
1278
-
1279
- def _convert_list_to_colon_dict(
1280
- self, params: "Union[list[Any], tuple[Any, ...]]", param_info: "list[Any]"
1281
- ) -> "dict[str, Any]":
1282
- """Convert list/tuple parameters to colon-style dict format."""
1283
- result_dict: dict[str, Any] = {}
1284
-
1285
- if param_info:
1286
- all_numeric = all(p.name and p.name.isdigit() for p in param_info)
1287
- if all_numeric:
1288
- for i, value in enumerate(params):
1289
- result_dict[str(i + 1)] = value
1290
- else:
1291
- for i, value in enumerate(params):
1292
- if i < len(param_info):
1293
- param_name = param_info[i].name or str(i + 1)
1294
- result_dict[param_name] = value
1295
- else:
1296
- result_dict[str(i + 1)] = value
1297
- else:
1298
- for i, value in enumerate(params):
1299
- result_dict[str(i + 1)] = value
1300
-
1301
- return result_dict
1302
-
1303
- def _convert_single_value_to_colon_dict(self, params: Any, param_info: "list[Any]") -> "dict[str, Any]":
1304
- """Convert single value parameter to colon-style dict format."""
1305
- result_dict: dict[str, Any] = {}
1306
- if param_info and param_info[0].name and param_info[0].name.isdigit():
1307
- result_dict[param_info[0].name] = params
1308
- else:
1309
- result_dict["1"] = params
1310
- return result_dict
1311
-
1312
- 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."""
1314
- result_dict: dict[str, Any] = {}
1315
-
1316
- # When we have mixed parameters (extracted literals + user oracle params),
1317
- # we need to be careful about the ordering. The extracted literals should
1318
- # fill positions based on where they appear in the SQL, not based on
1319
- # matching parameter names.
1320
-
1321
- # Separate extracted parameters and user oracle parameters
1322
- extracted_params = []
1323
- user_oracle_params = {}
1324
- extracted_keys_sorted = []
1325
-
1326
- for key, value in params.items():
1327
- if has_parameter_value(value):
1328
- extracted_params.append((key, value))
1329
- elif key.isdigit():
1330
- user_oracle_params[key] = value
1331
- elif key.startswith("param_") and key[6:].isdigit():
1332
- param_idx = int(key[6:])
1333
- oracle_key = str(param_idx + 1)
1334
- if oracle_key not in user_oracle_params:
1335
- extracted_keys_sorted.append((param_idx, key, value))
1336
- else:
1337
- extracted_params.append((key, value))
1338
-
1339
- extracted_keys_sorted.sort(key=operator.itemgetter(0))
1340
- for _, key, value in extracted_keys_sorted:
1341
- extracted_params.append((key, value))
1342
-
1343
- # Build lists of parameter values in order
1344
- extracted_values = []
1345
- for _, value in extracted_params:
1346
- if has_parameter_value(value):
1347
- extracted_values.append(value.value)
1348
- else:
1349
- extracted_values.append(value)
1350
-
1351
- user_values = [user_oracle_params[key] for key in sorted(user_oracle_params.keys(), key=int)]
1352
-
1353
- # Now assign parameters based on position
1354
- # Extracted parameters go first (they were literals in original positions)
1355
- # User parameters follow
1356
- all_values = extracted_values + user_values
1357
-
1358
- for i, p in enumerate(sorted(param_info, key=lambda x: x.ordinal)):
1359
- oracle_key = str(p.ordinal + 1)
1360
- if i < len(all_values):
1361
- result_dict[oracle_key] = all_values[i]
1362
-
1363
- return result_dict
1364
-
1365
- def _convert_to_positional_colon_format(self, params: Any, param_info: list[Any]) -> Any:
1366
- """Convert to dict format for positional colon style.
1367
-
1368
- Positional colon style uses :1, :2, etc. placeholders and expects
1369
- parameters as a dict with string keys "1", "2", etc.
1370
-
1371
- For execute_many operations, returns a list of parameter sets.
1372
-
1373
- Args:
1374
- params: Original parameters
1375
- param_info: List of parameter information
1376
-
1377
- Returns:
1378
- Dict of parameters with string keys "1", "2", etc., or list for execute_many
1379
- """
1380
- if self._is_many and isinstance(params, list) and params and isinstance(params[0], (list, tuple)):
1381
- return params
1382
-
1383
- if isinstance(params, (list, tuple)):
1384
- return self._convert_list_to_colon_dict(params, param_info)
1385
-
1386
- if not is_dict(params) and param_info:
1387
- return self._convert_single_value_to_colon_dict(params, param_info)
1388
-
1389
- if is_dict(params):
1390
- if all(key.isdigit() for key in params):
1391
- return params
1392
-
1393
- if all(key.startswith("param_") for key in params):
1394
- param_result_dict: dict[str, Any] = {}
1395
- 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:
1399
- if p.name and p.name.isdigit():
1400
- # For Oracle numeric parameters, preserve the original number
1401
- param_result_dict[p.name] = params[normalized_key]
1402
- else:
1403
- # For other cases, use sequential numbering
1404
- param_result_dict[str(p.ordinal + 1)] = params[normalized_key]
1405
- return param_result_dict
1406
-
1407
- has_oracle_numeric = any(key.isdigit() for key in params)
1408
- has_param_normalized = any(key.startswith("param_") for key in params)
1409
- has_typed_params = any(has_parameter_value(v) for v in params.values())
1410
-
1411
- if (has_oracle_numeric and has_param_normalized) or has_typed_params:
1412
- return self._process_mixed_colon_params(params, param_info)
1413
-
1414
- result_dict: dict[str, Any] = {}
1415
-
1416
- if param_info:
1417
- # Process all parameters in order of their ordinals
1418
- for p in sorted(param_info, key=lambda x: x.ordinal):
1419
- oracle_key = str(p.ordinal + 1)
1420
- value = None
1421
-
1422
- # Try different ways to find the parameter value
1423
- if p.name and (
1424
- p.name in params
1425
- or (p.name.isdigit() and p.name in params)
1426
- or (p.name.startswith("param_") and p.name in params)
1427
- ):
1428
- value = params[p.name]
1429
-
1430
- # If not found by name, try by ordinal-based keys
1431
- if value is None:
1432
- # Try param_N format (common for pipeline parameters)
1433
- param_key = f"param_{p.ordinal}"
1434
- if param_key in params:
1435
- value = params[param_key]
1436
- # Try arg_N format
1437
- elif f"arg_{p.ordinal}" in params:
1438
- value = params[f"arg_{p.ordinal}"]
1439
- # For positional colon, also check if there's a numeric key
1440
- # that matches the ordinal position
1441
- elif str(p.ordinal + 1) in params:
1442
- value = params[str(p.ordinal + 1)]
1443
-
1444
- # Unwrap TypedParameter if needed
1445
- if value is not None:
1446
- if has_parameter_value(value):
1447
- value = value.value
1448
- result_dict[oracle_key] = value
1449
-
1450
- return result_dict
1451
-
1452
- return params
1453
-
1454
- @staticmethod
1455
- def _convert_to_positional_format(params: Any, param_info: list[Any]) -> Any:
1456
- """Convert to list format for positional parameter styles.
1457
-
1458
- Args:
1459
- params: Original parameters
1460
- param_info: List of parameter information
1461
-
1462
- Returns:
1463
- List of parameters
1464
- """
1465
- result_list: list[Any] = []
1466
- if is_dict(params):
1467
- param_values_by_ordinal: dict[int, Any] = {}
1468
-
1469
- for p in param_info:
1470
- if p.name and p.name in params:
1471
- param_values_by_ordinal[p.ordinal] = params[p.name]
1472
-
1473
- for p in param_info:
1474
- if p.name is None and p.ordinal not in param_values_by_ordinal:
1475
- arg_key = f"arg_{p.ordinal}"
1476
- param_key = f"param_{p.ordinal}"
1477
- if arg_key in params:
1478
- param_values_by_ordinal[p.ordinal] = params[arg_key]
1479
- elif param_key in params:
1480
- param_values_by_ordinal[p.ordinal] = params[param_key]
1481
-
1482
- remaining_params = {
1483
- k: v
1484
- for k, v in params.items()
1485
- if k not in {p.name for p in param_info if p.name} and not k.startswith(("arg_", "param_"))
1486
- }
1487
-
1488
- unmatched_ordinals = [p.ordinal for p in param_info if p.ordinal not in param_values_by_ordinal]
1489
-
1490
- for ordinal, (_, value) in zip(unmatched_ordinals, remaining_params.items()):
1491
- param_values_by_ordinal[ordinal] = value
1492
-
1493
- for p in param_info:
1494
- val = param_values_by_ordinal.get(p.ordinal)
1495
- if val is not None:
1496
- if has_parameter_value(val):
1497
- result_list.append(val.value)
1498
- else:
1499
- result_list.append(val)
1500
- else:
1501
- result_list.append(None)
1502
-
1503
- return result_list
1504
- if isinstance(params, (list, tuple)):
1505
- # Special case: if params is empty, preserve it (don't create None values)
1506
- # This is important for execute_many with empty parameter lists
1507
- if not params:
1508
- return params
1509
-
1510
- # Handle mixed parameter styles correctly
1511
- # For mixed styles, assign parameters in order of appearance, not by numeric reference
1512
- if param_info and any(p.style == ParameterStyle.NUMERIC for p in param_info):
1513
- # Create mapping from ordinal to parameter value
1514
- param_mapping: dict[int, Any] = {}
1515
-
1516
- # Sort parameter info by position to get order of appearance
1517
- sorted_params = sorted(param_info, key=lambda p: p.position)
1518
-
1519
- # Assign parameters sequentially in order of appearance
1520
- for i, param_info_item in enumerate(sorted_params):
1521
- if i < len(params):
1522
- param_mapping[param_info_item.ordinal] = params[i]
1523
-
1524
- # Build result list ordered by original ordinal values
1525
- for i in range(len(param_info)):
1526
- val = param_mapping.get(i)
1527
- if val is not None:
1528
- if has_parameter_value(val):
1529
- result_list.append(val.value)
1530
- else:
1531
- result_list.append(val)
1532
- else:
1533
- result_list.append(None)
1534
-
1535
- return result_list
1536
-
1537
- # Standard conversion for non-mixed styles
1538
- for param in params:
1539
- if has_parameter_value(param):
1540
- result_list.append(param.value)
1541
- else:
1542
- result_list.append(param)
1543
- return result_list
1544
- return params
1545
-
1546
- @staticmethod
1547
- def _convert_to_named_colon_format(params: Any, param_info: list[Any]) -> Any:
1548
- """Convert to dict format for named colon style.
1549
-
1550
- Args:
1551
- params: Original parameters
1552
- param_info: List of parameter information
1553
-
1554
- Returns:
1555
- Dict of parameters with generated names
1556
- """
1557
- result_dict: dict[str, Any] = {}
1558
- if is_dict(params):
1559
- if all(p.name in params for p in param_info if p.name):
1560
- return params
1561
- for p in param_info:
1562
- if p.name and p.name in params:
1563
- result_dict[p.name] = params[p.name]
1564
- elif f"param_{p.ordinal}" in params:
1565
- result_dict[p.name or f"arg_{p.ordinal}"] = params[f"param_{p.ordinal}"]
1566
- return result_dict
1567
- if isinstance(params, (list, tuple)):
1568
- for i, value in enumerate(params):
1569
- if has_parameter_value(value):
1570
- value = value.value
1571
-
1572
- if i < len(param_info):
1573
- p = param_info[i]
1574
- param_name = p.name or f"arg_{i}"
1575
- result_dict[param_name] = value
1576
- else:
1577
- param_name = f"arg_{i}"
1578
- result_dict[param_name] = value
1579
- return result_dict
1580
- return params
1581
-
1582
- @staticmethod
1583
- def _convert_to_named_pyformat_format(params: Any, param_info: list[Any]) -> Any:
1584
- """Convert to dict format for named pyformat style.
1585
-
1586
- Args:
1587
- params: Original parameters
1588
- param_info: List of parameter information
1589
-
1590
- Returns:
1591
- Dict of parameters with names
1592
- """
1593
- if isinstance(params, (list, tuple)):
1594
- result_dict: dict[str, Any] = {}
1595
- for i, p in enumerate(param_info):
1596
- if i < len(params):
1597
- param_name = p.name or f"param_{i}"
1598
- result_dict[param_name] = params[i]
1599
- return result_dict
1600
- return params
1601
-
1602
- @property
1603
- def validation_errors(self) -> list[Any]:
1604
- """Get validation errors."""
1605
- if not self._config.enable_validation:
1606
- return []
1607
- self._ensure_processed()
1608
- if not self._processed_state:
1609
- msg = "Failed to process SQL statement"
1610
- raise RuntimeError(msg)
1611
- return self._processed_state.validation_errors
1612
-
1613
- @property
1614
- def has_errors(self) -> bool:
1615
- """Check if there are validation errors."""
1616
- return bool(self.validation_errors)
1617
-
1618
- @property
1619
- def is_safe(self) -> bool:
1620
- """Check if statement is safe."""
1621
- return not self.has_errors
1622
-
1623
- def validate(self) -> list[Any]:
1624
- """Validate the SQL statement and return validation errors."""
1625
- return self.validation_errors
1626
-
1627
- @property
1628
- def parameter_info(self) -> list[Any]:
1629
- """Get parameter information from the SQL statement.
1630
-
1631
- Returns the original parameter info before any normalization.
1632
- """
1633
- validator = self._config.parameter_validator
1634
- if self._raw_sql:
1635
- return validator.extract_parameters(self._raw_sql)
1636
-
1637
- self._ensure_processed()
1638
-
1639
- if self._processing_context:
1640
- return self._processing_context.parameter_info
1641
-
1642
- return []
1643
-
1644
- @property
1645
- def _raw_parameters(self) -> Any:
1646
- """Get raw parameters for compatibility."""
1647
- return self._original_parameters
1648
-
1649
- @property
1650
- def _sql(self) -> str:
1651
- """Get SQL string for compatibility."""
1652
- return self.sql
1653
-
1654
- @property
1655
- def _expression(self) -> "Optional[exp.Expression]":
1656
- """Get expression for compatibility."""
1657
- return self.expression
1658
-
1659
- @property
1660
- def statement(self) -> exp.Expression:
1661
- """Get statement for compatibility."""
1662
- 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)