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/driver/_common.py CHANGED
@@ -1,373 +1,631 @@
1
- """Common driver attributes and utilities."""
1
+ """Common driver attributes and utilities.
2
2
 
3
- import re
4
- from abc import ABC
5
- from collections.abc import Mapping, Sequence
6
- from typing import TYPE_CHECKING, Any, ClassVar, Generic, Optional
3
+ This module provides core driver infrastructure including execution result handling,
4
+ common driver attributes, parameter processing, and SQL compilation utilities.
5
+ """
7
6
 
8
- import sqlglot
7
+ from typing import TYPE_CHECKING, Any, Final, NamedTuple, Optional, Union, cast
8
+
9
+ from mypy_extensions import trait
9
10
  from sqlglot import exp
10
- from sqlglot.tokens import TokenType
11
-
12
- from sqlspec.driver.parameters import normalize_parameter_sequence
13
- from sqlspec.exceptions import NotFoundError
14
- from sqlspec.statement import SQLConfig
15
- from sqlspec.statement.parameters import ParameterStyle, ParameterValidator, TypedParameter
16
- from sqlspec.statement.splitter import split_sql_script
17
- from sqlspec.typing import ConnectionT, DictRow, RowT, T
11
+
12
+ from sqlspec.builder import QueryBuilder
13
+ from sqlspec.core import SQL, OperationType, ParameterStyle, SQLResult, Statement, StatementConfig, TypedParameter
14
+ from sqlspec.core.cache import get_cache_config, sql_cache
15
+ from sqlspec.core.splitter import split_sql_script
16
+ from sqlspec.exceptions import ImproperConfigurationError
18
17
  from sqlspec.utils.logging import get_logger
19
18
 
20
19
  if TYPE_CHECKING:
21
- from sqlglot.dialects.dialect import DialectType
22
-
20
+ from collections.abc import Sequence
23
21
 
24
- __all__ = ("CommonDriverAttributesMixin",)
22
+ from sqlspec.core.filters import FilterTypeT, StatementFilter
23
+ from sqlspec.typing import StatementParameters
25
24
 
26
25
 
27
- logger = get_logger("driver")
26
+ __all__ = (
27
+ "DEFAULT_EXECUTION_RESULT",
28
+ "EXEC_CURSOR_RESULT",
29
+ "EXEC_ROWCOUNT_OVERRIDE",
30
+ "EXEC_SPECIAL_DATA",
31
+ "CommonDriverAttributesMixin",
32
+ "ExecutionResult",
33
+ "ScriptExecutionResult",
34
+ )
28
35
 
29
36
 
30
- class CommonDriverAttributesMixin(ABC, Generic[ConnectionT, RowT]):
31
- """Common attributes and methods for driver adapters."""
37
+ logger = get_logger("driver")
32
38
 
33
- __slots__ = ("config", "connection", "default_row_type")
34
39
 
35
- dialect: "DialectType"
36
- """The SQL dialect supported by the underlying database driver."""
37
- supported_parameter_styles: "tuple[ParameterStyle, ...]"
38
- """The parameter styles supported by this driver."""
39
- default_parameter_style: "ParameterStyle"
40
- """The default parameter style to convert to when unsupported style is detected."""
41
- supports_native_parquet_export: "ClassVar[bool]" = False
42
- """Indicates if the driver supports native Parquet export operations."""
43
- supports_native_parquet_import: "ClassVar[bool]" = False
44
- """Indicates if the driver supports native Parquet import operations."""
45
- supports_native_arrow_export: "ClassVar[bool]" = False
46
- """Indicates if the driver supports native Arrow export operations."""
47
- supports_native_arrow_import: "ClassVar[bool]" = False
48
- """Indicates if the driver supports native Arrow import operations."""
40
+ class ScriptExecutionResult(NamedTuple):
41
+ """Result from script execution with statement count information.
42
+
43
+ This named tuple eliminates the need for redundant script splitting
44
+ by providing statement count information during execution rather than
45
+ requiring re-parsing after execution.
46
+
47
+ Attributes:
48
+ cursor_result: The result returned by the database cursor/driver
49
+ rowcount_override: Optional override for the number of affected rows
50
+ special_data: Any special metadata or additional information
51
+ statement_count: Total number of statements in the script
52
+ successful_statements: Number of statements that executed successfully
53
+ """
54
+
55
+ cursor_result: Any
56
+ rowcount_override: Optional[int]
57
+ special_data: Any
58
+ statement_count: int
59
+ successful_statements: int
60
+
61
+
62
+ class ExecutionResult(NamedTuple):
63
+ """Comprehensive execution result containing all data needed for SQLResult building.
64
+
65
+ This named tuple consolidates all execution result data to eliminate the need
66
+ for additional data extraction calls and script re-parsing in build_statement_result.
67
+
68
+ Attributes:
69
+ cursor_result: The raw result returned by the database cursor/driver
70
+ rowcount_override: Optional override for the number of affected rows
71
+ special_data: Any special metadata or additional information from execution
72
+ selected_data: For SELECT operations, the extracted row data
73
+ column_names: For SELECT operations, the column names
74
+ data_row_count: For SELECT operations, the number of rows returned
75
+ statement_count: For script operations, total number of statements
76
+ successful_statements: For script operations, number of successful statements
77
+ is_script_result: Whether this result is from script execution
78
+ is_select_result: Whether this result is from a SELECT operation
79
+ is_many_result: Whether this result is from an execute_many operation
80
+ """
81
+
82
+ cursor_result: Any
83
+ rowcount_override: Optional[int]
84
+ special_data: Any
85
+ selected_data: Optional["list[dict[str, Any]]"]
86
+ column_names: Optional["list[str]"]
87
+ data_row_count: Optional[int]
88
+ statement_count: Optional[int]
89
+ successful_statements: Optional[int]
90
+ is_script_result: bool
91
+ is_select_result: bool
92
+ is_many_result: bool
93
+ last_inserted_id: Optional[Union[int, str]] = None
94
+
95
+
96
+ EXEC_CURSOR_RESULT = 0
97
+ EXEC_ROWCOUNT_OVERRIDE = 1
98
+ EXEC_SPECIAL_DATA = 2
99
+ DEFAULT_EXECUTION_RESULT: Final[tuple[Any, Optional[int], Any]] = (None, None, None)
100
+
101
+
102
+ @trait
103
+ class CommonDriverAttributesMixin:
104
+ """Common attributes and methods for driver adapters.
105
+
106
+ This mixin provides the foundation for all SQLSpec drivers, including
107
+ connection and configuration management, parameter processing, caching,
108
+ and SQL compilation.
109
+ """
110
+
111
+ __slots__ = ("connection", "driver_features", "statement_config")
112
+ connection: "Any"
113
+ statement_config: "StatementConfig"
114
+ driver_features: "dict[str, Any]"
49
115
 
50
116
  def __init__(
51
- self,
52
- connection: "ConnectionT",
53
- config: "Optional[SQLConfig]" = None,
54
- default_row_type: "type[DictRow]" = dict[str, Any],
117
+ self, connection: "Any", statement_config: "StatementConfig", driver_features: "Optional[dict[str, Any]]" = None
55
118
  ) -> None:
56
- """Initialize with connection, config, and default_row_type.
119
+ """Initialize driver adapter with connection and configuration.
57
120
 
58
121
  Args:
59
- connection: The database connection
60
- config: SQL statement configuration
61
- default_row_type: Default row type for results (DictRow, TupleRow, etc.)
122
+ connection: Database connection instance
123
+ statement_config: Statement configuration for the driver
124
+ driver_features: Driver-specific features like extensions, secrets, and connection callbacks
62
125
  """
63
126
  self.connection = connection
64
- self.config = config or SQLConfig()
65
- self.default_row_type = default_row_type or dict[str, Any]
127
+ self.statement_config = statement_config
128
+ self.driver_features = driver_features or {}
66
129
 
67
- def _connection(self, connection: "Optional[ConnectionT]" = None) -> "ConnectionT":
68
- return connection or self.connection
69
-
70
- def returns_rows(self, expression: "Optional[exp.Expression]") -> bool:
71
- """Check if the SQL expression is expected to return rows.
130
+ def create_execution_result(
131
+ self,
132
+ cursor_result: Any,
133
+ *,
134
+ rowcount_override: Optional[int] = None,
135
+ special_data: Any = None,
136
+ selected_data: Optional["list[dict[str, Any]]"] = None,
137
+ column_names: Optional["list[str]"] = None,
138
+ data_row_count: Optional[int] = None,
139
+ statement_count: Optional[int] = None,
140
+ successful_statements: Optional[int] = None,
141
+ is_script_result: bool = False,
142
+ is_select_result: bool = False,
143
+ is_many_result: bool = False,
144
+ last_inserted_id: Optional[Union[int, str]] = None,
145
+ ) -> ExecutionResult:
146
+ """Create ExecutionResult with all necessary data for any operation type.
72
147
 
73
148
  Args:
74
- expression: The SQL expression.
149
+ cursor_result: The raw result returned by the database cursor/driver
150
+ rowcount_override: Optional override for the number of affected rows
151
+ special_data: Any special metadata or additional information
152
+ selected_data: For SELECT operations, the extracted row data
153
+ column_names: For SELECT operations, the column names
154
+ data_row_count: For SELECT operations, the number of rows returned
155
+ statement_count: For script operations, total number of statements
156
+ successful_statements: For script operations, number of successful statements
157
+ is_script_result: Whether this result is from script execution
158
+ is_select_result: Whether this result is from a SELECT operation
159
+ is_many_result: Whether this result is from an execute_many operation
160
+ last_inserted_id: The ID of the last inserted row (if applicable)
75
161
 
76
162
  Returns:
77
- True if the expression is a SELECT, VALUES, or WITH statement
78
- that is not a CTE definition.
163
+ ExecutionResult configured for the specified operation type
79
164
  """
80
- if expression is None:
81
- return False
82
- if isinstance(expression, (exp.Select, exp.Values, exp.Table, exp.Show, exp.Describe, exp.Pragma, exp.Command)):
83
- return True
84
- if isinstance(expression, exp.With) and expression.expressions:
85
- return self.returns_rows(expression.expressions[-1])
86
- if isinstance(expression, (exp.Insert, exp.Update, exp.Delete)):
87
- return bool(expression.find(exp.Returning))
88
- if isinstance(expression, exp.Anonymous):
89
- return self._check_anonymous_returns_rows(expression)
90
- return False
91
-
92
- def _check_anonymous_returns_rows(self, expression: "exp.Anonymous") -> bool:
93
- """Check if an Anonymous expression returns rows using robust methods.
94
-
95
- This method handles SQL that failed to parse (often due to database-specific
96
- placeholders) by:
97
- 1. Attempting to re-parse with placeholders sanitized
98
- 2. Using the tokenizer as a fallback for keyword detection
165
+ return ExecutionResult(
166
+ cursor_result=cursor_result,
167
+ rowcount_override=rowcount_override,
168
+ special_data=special_data,
169
+ selected_data=selected_data,
170
+ column_names=column_names,
171
+ data_row_count=data_row_count,
172
+ statement_count=statement_count,
173
+ successful_statements=successful_statements,
174
+ is_script_result=is_script_result,
175
+ is_select_result=is_select_result,
176
+ is_many_result=is_many_result,
177
+ last_inserted_id=last_inserted_id,
178
+ )
179
+
180
+ def build_statement_result(self, statement: "SQL", execution_result: ExecutionResult) -> "SQLResult":
181
+ """Build and return the SQLResult from ExecutionResult data.
182
+
183
+ Creates SQLResult objects from ExecutionResult data without requiring
184
+ additional data extraction calls or script re-parsing.
99
185
 
100
186
  Args:
101
- expression: The Anonymous expression to check
187
+ statement: SQL statement that was executed
188
+ execution_result: ExecutionResult containing all necessary data
102
189
 
103
190
  Returns:
104
- True if the expression likely returns rows
191
+ SQLResult with complete execution data
105
192
  """
193
+ if execution_result.is_script_result:
194
+ return SQLResult(
195
+ statement=statement,
196
+ data=[],
197
+ rows_affected=execution_result.rowcount_override or 0,
198
+ operation_type="SCRIPT",
199
+ total_statements=execution_result.statement_count or 0,
200
+ successful_statements=execution_result.successful_statements or 0,
201
+ metadata=execution_result.special_data or {"status_message": "OK"},
202
+ )
203
+
204
+ if execution_result.is_select_result:
205
+ return SQLResult(
206
+ statement=statement,
207
+ data=execution_result.selected_data or [],
208
+ column_names=execution_result.column_names or [],
209
+ rows_affected=execution_result.data_row_count or 0,
210
+ operation_type="SELECT",
211
+ metadata=execution_result.special_data or {},
212
+ )
213
+
214
+ return SQLResult(
215
+ statement=statement,
216
+ data=[],
217
+ rows_affected=execution_result.rowcount_override or 0,
218
+ operation_type=self._determine_operation_type(statement),
219
+ last_inserted_id=execution_result.last_inserted_id,
220
+ metadata=execution_result.special_data or {"status_message": "OK"},
221
+ )
106
222
 
107
- sql_text = str(expression.this) if expression.this else ""
108
- if not sql_text.strip():
109
- return False
223
+ def _determine_operation_type(self, statement: "Any") -> OperationType:
224
+ """Determine operation type from SQL statement expression.
110
225
 
111
- # Regex to find common SQL placeholders: ?, %s, $1, $2, :name, etc.
112
- placeholder_regex = re.compile(r"(\?|%s|\$\d+|:\w+|%\(\w+\)s)")
226
+ Examines the statement's expression type to determine if it's
227
+ INSERT, UPDATE, DELETE, SELECT, SCRIPT, or generic EXECUTE.
228
+
229
+ Args:
230
+ statement: SQL statement object with expression attribute
231
+
232
+ Returns:
233
+ OperationType literal value
234
+ """
235
+ if statement.is_script:
236
+ return "SCRIPT"
113
237
 
114
- # Approach 1: Try to re-parse with placeholders replaced
115
- try:
116
- sanitized_sql = placeholder_regex.sub("1", sql_text)
117
-
118
- # If we replaced any placeholders, try parsing again
119
- if sanitized_sql != sql_text:
120
- parsed = sqlglot.parse_one(sanitized_sql, read=None)
121
- if isinstance(
122
- parsed, (exp.Select, exp.Values, exp.Table, exp.Show, exp.Describe, exp.Pragma, exp.Command)
123
- ):
124
- return True
125
- if isinstance(parsed, exp.With) and parsed.expressions:
126
- return self.returns_rows(parsed.expressions[-1])
127
- if isinstance(parsed, (exp.Insert, exp.Update, exp.Delete)):
128
- return bool(parsed.find(exp.Returning))
129
- if not isinstance(parsed, exp.Anonymous):
130
- return False
131
- except Exception:
132
- logger.debug("Could not parse using placeholders. Using tokenizer. %s", sql_text)
133
-
134
- # Approach 2: Use tokenizer for robust keyword detection
135
238
  try:
136
- tokens = list(sqlglot.tokenize(sql_text, read=None))
137
- row_returning_tokens = {
138
- TokenType.SELECT,
139
- TokenType.WITH,
140
- TokenType.VALUES,
141
- TokenType.TABLE,
142
- TokenType.SHOW,
143
- TokenType.DESCRIBE,
144
- TokenType.PRAGMA,
145
- }
146
- for token in tokens:
147
- if token.token_type in {TokenType.COMMENT, TokenType.SEMICOLON}:
148
- continue
149
- return token.token_type in row_returning_tokens
150
-
151
- except Exception:
152
- return False
153
-
154
- return False
239
+ expression = statement.expression
240
+ except AttributeError:
241
+ return "EXECUTE"
242
+
243
+ if not expression:
244
+ return "EXECUTE"
245
+
246
+ expr_type = type(expression).__name__.upper()
247
+
248
+ if "ANONYMOUS" in expr_type and statement.is_script:
249
+ return "SCRIPT"
250
+
251
+ if "INSERT" in expr_type:
252
+ return "INSERT"
253
+ if "UPDATE" in expr_type:
254
+ return "UPDATE"
255
+ if "DELETE" in expr_type:
256
+ return "DELETE"
257
+ if "SELECT" in expr_type:
258
+ return "SELECT"
259
+ if "COPY" in expr_type:
260
+ return "COPY"
261
+ return "EXECUTE"
262
+
263
+ def prepare_statement(
264
+ self,
265
+ statement: "Union[Statement, QueryBuilder]",
266
+ parameters: "tuple[Union[StatementParameters, StatementFilter], ...]" = (),
267
+ *,
268
+ statement_config: "StatementConfig",
269
+ kwargs: "Optional[dict[str, Any]]" = None,
270
+ ) -> "SQL":
271
+ """Build SQL statement from various input types.
272
+
273
+ Ensures dialect is set and preserves existing state when rebuilding SQL objects.
274
+ """
275
+ kwargs = kwargs or {}
276
+
277
+ if isinstance(statement, QueryBuilder):
278
+ return statement.to_statement(statement_config)
279
+ if isinstance(statement, SQL):
280
+ if parameters or kwargs:
281
+ merged_parameters = (
282
+ (*statement._positional_parameters, *parameters) if parameters else statement._positional_parameters
283
+ )
284
+ return SQL(statement.sql, *merged_parameters, statement_config=statement_config, **kwargs)
285
+ needs_rebuild = False
286
+
287
+ if statement_config.dialect and (
288
+ not statement.statement_config.dialect or statement.statement_config.dialect != statement_config.dialect
289
+ ):
290
+ needs_rebuild = True
155
291
 
156
- @staticmethod
157
- def check_not_found(item_or_none: "Optional[T]" = None) -> "T":
158
- """Raise :exc:`sqlspec.exceptions.NotFoundError` if ``item_or_none`` is ``None``.
292
+ if (
293
+ statement.statement_config.parameter_config.default_execution_parameter_style
294
+ != statement_config.parameter_config.default_execution_parameter_style
295
+ ):
296
+ needs_rebuild = True
159
297
 
160
- Args:
161
- item_or_none: Item to be tested for existence.
298
+ if needs_rebuild:
299
+ sql_text = statement._raw_sql or statement.sql
300
+
301
+ if statement.is_many and statement.parameters:
302
+ new_sql = SQL(sql_text, statement.parameters, statement_config=statement_config, is_many=True)
303
+ elif statement._named_parameters:
304
+ new_sql = SQL(sql_text, statement_config=statement_config, **statement._named_parameters)
305
+ else:
306
+ new_sql = SQL(sql_text, *statement._positional_parameters, statement_config=statement_config)
307
+
308
+ return new_sql
309
+ return statement
310
+ return SQL(statement, *parameters, statement_config=statement_config, **kwargs)
311
+
312
+ def split_script_statements(
313
+ self, script: str, statement_config: "StatementConfig", strip_trailing_semicolon: bool = False
314
+ ) -> list[str]:
315
+ """Split a SQL script into individual statements.
162
316
 
163
- Raises:
164
- NotFoundError: If ``item_or_none`` is ``None``
317
+ Uses a lexer-driven state machine to handle multi-statement scripts,
318
+ including complex constructs like PL/SQL blocks, T-SQL batches, and nested blocks.
319
+
320
+ Args:
321
+ script: The SQL script to split
322
+ statement_config: Statement configuration containing dialect information
323
+ strip_trailing_semicolon: If True, remove trailing semicolons from statements
165
324
 
166
325
  Returns:
167
- The item, if it exists.
326
+ A list of individual SQL statements
168
327
  """
169
- if item_or_none is None:
170
- msg = "No result found when one was expected"
171
- raise NotFoundError(msg)
172
- return item_or_none
173
-
174
- def _convert_parameters_to_driver_format( # noqa: C901
175
- self, sql: str, parameters: Any, target_style: "Optional[ParameterStyle]" = None
328
+ return [
329
+ sql_script.strip()
330
+ for sql_script in split_sql_script(
331
+ script, dialect=str(statement_config.dialect), strip_trailing_terminator=strip_trailing_semicolon
332
+ )
333
+ if sql_script.strip()
334
+ ]
335
+
336
+ def prepare_driver_parameters(
337
+ self, parameters: Any, statement_config: "StatementConfig", is_many: bool = False
176
338
  ) -> Any:
177
- """Convert parameters to the format expected by the driver, but only when necessary.
339
+ """Prepare parameters for database driver consumption.
178
340
 
179
- This method analyzes the SQL to understand what parameter style is used
180
- and only converts when there's a mismatch between provided parameters
181
- and what the driver expects.
341
+ Normalizes parameter structure and unwraps TypedParameter objects
342
+ to their underlying values, which database drivers expect.
182
343
 
183
344
  Args:
184
- sql: The SQL string with placeholders
185
- parameters: The parameters in any format (dict, list, tuple, scalar)
186
- target_style: Optional override for the target parameter style
345
+ parameters: Parameters in any format (dict, list, tuple, scalar, TypedParameter)
346
+ statement_config: Statement configuration for parameter style detection
347
+ is_many: If True, handle as executemany parameter sequence
187
348
 
188
349
  Returns:
189
- Parameters in the format expected by the database driver
350
+ Parameters with TypedParameter objects unwrapped to primitive values
190
351
  """
191
- if parameters is None:
352
+ if parameters is None and statement_config.parameter_config.needs_static_script_compilation:
192
353
  return None
193
354
 
194
- validator = ParameterValidator()
195
- param_info_list = validator.extract_parameters(sql)
355
+ if not parameters:
356
+ return []
196
357
 
197
- if not param_info_list:
198
- return None
358
+ if is_many:
359
+ if isinstance(parameters, list):
360
+ return [self._format_parameter_set_for_many(param_set, statement_config) for param_set in parameters]
361
+ return [self._format_parameter_set_for_many(parameters, statement_config)]
362
+ return self._format_parameter_set(parameters, statement_config)
199
363
 
200
- if target_style is None:
201
- target_style = self.default_parameter_style
202
-
203
- actual_styles = {p.style for p in param_info_list if p.style}
204
- if len(actual_styles) == 1:
205
- detected_style = actual_styles.pop()
206
- if detected_style != target_style:
207
- target_style = detected_style
208
-
209
- # Analyze what format the driver expects based on the placeholder style
210
- driver_expects_dict = target_style in {
211
- ParameterStyle.NAMED_COLON,
212
- ParameterStyle.POSITIONAL_COLON,
213
- ParameterStyle.NAMED_AT,
214
- ParameterStyle.NAMED_DOLLAR,
215
- ParameterStyle.NAMED_PYFORMAT,
216
- }
364
+ def _format_parameter_set_for_many(self, parameters: Any, statement_config: "StatementConfig") -> Any:
365
+ """Prepare a single parameter set for execute_many operations.
217
366
 
218
- params_are_dict = isinstance(parameters, (dict, Mapping))
219
- params_are_sequence = isinstance(parameters, (list, tuple, Sequence)) and not isinstance(
220
- parameters, (str, bytes)
221
- )
367
+ Unlike _format_parameter_set, this method handles parameter sets without
368
+ converting the structure itself to array format.
222
369
 
223
- # Single scalar parameter
224
- if len(param_info_list) == 1 and not params_are_dict and not params_are_sequence:
225
- if driver_expects_dict:
226
- param_info = param_info_list[0]
227
- if param_info.name:
228
- return {param_info.name: parameters}
229
- return {f"param_{param_info.ordinal}": parameters}
230
- return [parameters]
231
-
232
- if driver_expects_dict and params_are_dict:
233
- if target_style == ParameterStyle.POSITIONAL_COLON and all(
234
- p.name and p.name.isdigit() for p in param_info_list
235
- ):
236
- # If all parameters are numeric but named, convert to dict
237
- # SQL has numeric placeholders but params might have named keys
238
- numeric_keys_expected = {p.name for p in param_info_list if p.name}
239
- if not numeric_keys_expected.issubset(parameters.keys()):
240
- # Need to convert named keys to numeric positions
241
- numeric_result: dict[str, Any] = {}
242
- param_values = list(parameters.values())
243
- for param_info in param_info_list:
244
- if param_info.name and param_info.ordinal < len(param_values):
245
- numeric_result[param_info.name] = param_values[param_info.ordinal]
246
- return numeric_result
247
-
248
- # Special case: Auto-generated param_N style when SQL expects specific names
249
- if all(key.startswith("param_") and key[6:].isdigit() for key in parameters):
250
- sql_param_names = {p.name for p in param_info_list if p.name}
251
- if sql_param_names and not any(name.startswith("param_") for name in sql_param_names):
252
- # SQL has specific names, not param_N style - don't use these params as-is
253
- # This likely indicates a mismatch in parameter generation
254
- # For now, pass through and let validation catch it
255
- pass
256
-
257
- return parameters
258
-
259
- if not driver_expects_dict and params_are_sequence:
260
- # Formats match - return as-is
261
- return parameters
262
-
263
- # Formats don't match - need conversion
264
- if driver_expects_dict and params_are_sequence:
265
- dict_result: dict[str, Any] = {}
266
- for i, (param_info, value) in enumerate(zip(param_info_list, parameters)):
267
- if param_info.name:
268
- if param_info.style == ParameterStyle.POSITIONAL_COLON and param_info.name.isdigit():
269
- # Oracle uses string keys even for numeric placeholders
270
- dict_result[param_info.name] = value
271
- else:
272
- dict_result[param_info.name] = value
273
- else:
274
- # Use param_N format for unnamed placeholders
275
- dict_result[f"param_{i}"] = value
276
- return dict_result
277
-
278
- if not driver_expects_dict and params_are_dict:
279
- # First check if it's already in param_N format
280
- if all(key.startswith("param_") and key[6:].isdigit() for key in parameters):
281
- positional_result: list[Any] = []
282
- for i in range(len(param_info_list)):
283
- key = f"param_{i}"
284
- if key in parameters:
285
- positional_result.append(parameters[key])
286
- return positional_result
287
-
288
- positional_params: list[Any] = []
289
- for param_info in param_info_list:
290
- if param_info.name and param_info.name in parameters:
291
- positional_params.append(parameters[param_info.name])
292
- elif f"param_{param_info.ordinal}" in parameters:
293
- positional_params.append(parameters[f"param_{param_info.ordinal}"])
294
- else:
295
- # Try to match by position if we have a simple dict
296
- param_values = list(parameters.values())
297
- if param_info.ordinal < len(param_values):
298
- positional_params.append(param_values[param_info.ordinal])
299
- return positional_params or list(parameters.values())
370
+ Args:
371
+ parameters: Single parameter set (tuple, list, or dict)
372
+ statement_config: Statement configuration for parameter style detection
373
+
374
+ Returns:
375
+ Processed parameter set with individual values coerced but structure preserved
376
+ """
377
+ if not parameters:
378
+ return []
300
379
 
301
- # This shouldn't happen, but return as-is
302
- return parameters
380
+ def apply_type_coercion(value: Any) -> Any:
381
+ """Apply type coercion to a single value."""
382
+ unwrapped_value = value.value if isinstance(value, TypedParameter) else value
303
383
 
304
- def _split_script_statements(self, script: str, strip_trailing_semicolon: bool = False) -> list[str]:
305
- """Split a SQL script into individual statements.
384
+ if statement_config.parameter_config.type_coercion_map:
385
+ for type_check, converter in statement_config.parameter_config.type_coercion_map.items():
386
+ if type_check in {list, tuple} and isinstance(unwrapped_value, (list, tuple)):
387
+ continue
388
+ if isinstance(unwrapped_value, type_check):
389
+ return converter(unwrapped_value)
390
+
391
+ return unwrapped_value
392
+
393
+ if isinstance(parameters, dict):
394
+ return {k: apply_type_coercion(v) for k, v in parameters.items()}
395
+
396
+ if isinstance(parameters, (list, tuple)):
397
+ coerced_params = [apply_type_coercion(p) for p in parameters]
398
+ return tuple(coerced_params) if isinstance(parameters, tuple) else coerced_params
399
+
400
+ return apply_type_coercion(parameters)
306
401
 
307
- This method uses a robust lexer-driven state machine to handle
308
- multi-statement scripts, including complex constructs like
309
- PL/SQL blocks, T-SQL batches, and nested blocks.
402
+ def _format_parameter_set(self, parameters: Any, statement_config: "StatementConfig") -> Any:
403
+ """Prepare a single parameter set for database driver consumption.
310
404
 
311
405
  Args:
312
- script: The SQL script to split
313
- strip_trailing_semicolon: If True, remove trailing semicolons from statements
406
+ parameters: Single parameter set in any format
407
+ statement_config: Statement configuration for parameter style detection
314
408
 
315
409
  Returns:
316
- A list of individual SQL statements
317
-
318
- Note:
319
- This is particularly useful for databases that don't natively
320
- support multi-statement execution (e.g., Oracle, some async drivers).
410
+ Processed parameter set with TypedParameter objects unwrapped and type coercion applied
321
411
  """
322
- # The split_sql_script function already handles dialect mapping and fallback
323
- return split_sql_script(script, dialect=str(self.dialect), strip_trailing_semicolon=strip_trailing_semicolon)
412
+ if not parameters:
413
+ return []
324
414
 
325
- def _prepare_driver_parameters(self, parameters: Any) -> Any:
326
- """Prepare parameters for database driver consumption by unwrapping TypedParameter objects.
415
+ def apply_type_coercion(value: Any) -> Any:
416
+ """Apply type coercion to a single value."""
417
+ unwrapped_value = value.value if isinstance(value, TypedParameter) else value
327
418
 
328
- This method normalizes parameter structure and unwraps TypedParameter objects
329
- to their underlying values, which database drivers expect.
419
+ if statement_config.parameter_config.type_coercion_map:
420
+ for type_check, converter in statement_config.parameter_config.type_coercion_map.items():
421
+ if isinstance(unwrapped_value, type_check):
422
+ return converter(unwrapped_value)
423
+
424
+ return unwrapped_value
425
+
426
+ if isinstance(parameters, dict):
427
+ if not parameters:
428
+ return []
429
+ if statement_config.parameter_config.supported_execution_parameter_styles and (
430
+ ParameterStyle.NAMED_PYFORMAT in statement_config.parameter_config.supported_execution_parameter_styles
431
+ or ParameterStyle.NAMED_COLON in statement_config.parameter_config.supported_execution_parameter_styles
432
+ ):
433
+ return {k: apply_type_coercion(v) for k, v in parameters.items()}
434
+ if statement_config.parameter_config.default_parameter_style in {
435
+ ParameterStyle.NUMERIC,
436
+ ParameterStyle.QMARK,
437
+ ParameterStyle.POSITIONAL_PYFORMAT,
438
+ }:
439
+ ordered_parameters = []
440
+ sorted_items = sorted(
441
+ parameters.items(),
442
+ key=lambda item: int(item[0])
443
+ if item[0].isdigit()
444
+ else (int(item[0][6:]) if item[0].startswith("param_") and item[0][6:].isdigit() else float("inf")),
445
+ )
446
+ for _, value in sorted_items:
447
+ ordered_parameters.append(apply_type_coercion(value))
448
+ return ordered_parameters
449
+
450
+ return {k: apply_type_coercion(v) for k, v in parameters.items()}
451
+
452
+ if isinstance(parameters, (list, tuple)):
453
+ coerced_params = [apply_type_coercion(p) for p in parameters]
454
+ if statement_config.parameter_config.preserve_parameter_format and isinstance(parameters, tuple):
455
+ return tuple(coerced_params)
456
+ return coerced_params
457
+
458
+ return [apply_type_coercion(parameters)]
459
+
460
+ def _get_compiled_sql(
461
+ self, statement: "SQL", statement_config: "StatementConfig", flatten_single_parameters: bool = False
462
+ ) -> tuple[str, Any]:
463
+ """Get compiled SQL with parameter style conversion and caching.
464
+
465
+ Compiles the SQL statement and applies parameter style conversion.
466
+ Results are cached when caching is enabled.
330
467
 
331
468
  Args:
332
- parameters: Parameters in any format (dict, list, tuple, scalar, TypedParameter)
469
+ statement: SQL statement to compile
470
+ statement_config: Complete statement configuration including parameter config, dialect, etc.
471
+ flatten_single_parameters: If True, flatten single-element lists for scalar parameters
333
472
 
334
473
  Returns:
335
- Parameters with TypedParameter objects unwrapped to primitive values
474
+ Tuple of (compiled_sql, parameters)
336
475
  """
476
+ cache_config = get_cache_config()
477
+ cache_key = None
478
+ if cache_config.compiled_cache_enabled and statement_config.enable_caching:
479
+ cache_key = self._generate_compilation_cache_key(statement, statement_config, flatten_single_parameters)
480
+ cached_result = sql_cache.get(cache_key)
481
+ if cached_result is not None:
482
+ return cached_result
483
+
484
+ compiled_sql, execution_parameters = statement.compile()
485
+
486
+ prepared_parameters = self.prepare_driver_parameters(
487
+ execution_parameters, statement_config, is_many=statement.is_many
488
+ )
337
489
 
338
- normalized = normalize_parameter_sequence(parameters)
339
- if not normalized:
340
- return []
490
+ if statement_config.parameter_config.output_transformer:
491
+ compiled_sql, prepared_parameters = statement_config.parameter_config.output_transformer(
492
+ compiled_sql, prepared_parameters
493
+ )
494
+
495
+ if cache_key is not None:
496
+ sql_cache.set(cache_key, (compiled_sql, prepared_parameters))
497
+
498
+ return compiled_sql, prepared_parameters
499
+
500
+ def _generate_compilation_cache_key(
501
+ self, statement: "SQL", config: "StatementConfig", flatten_single_parameters: bool
502
+ ) -> str:
503
+ """Generate cache key that includes all compilation context.
504
+
505
+ Creates a deterministic cache key that includes all factors that affect SQL compilation,
506
+ preventing cache contamination between different compilation contexts.
507
+ """
508
+ context_hash = hash(
509
+ (
510
+ config.parameter_config.hash(),
511
+ config.dialect,
512
+ statement.is_script,
513
+ statement.is_many,
514
+ flatten_single_parameters,
515
+ bool(config.parameter_config.output_transformer),
516
+ bool(config.parameter_config.ast_transformer),
517
+ bool(config.parameter_config.needs_static_script_compilation),
518
+ )
519
+ )
520
+
521
+ params = statement.parameters
522
+ params_key: Any
523
+
524
+ def make_hashable(obj: Any) -> Any:
525
+ """Recursively convert unhashable types to hashable ones."""
526
+ if isinstance(obj, (list, tuple)):
527
+ return tuple(make_hashable(item) for item in obj)
528
+ if isinstance(obj, dict):
529
+ return tuple(sorted((k, make_hashable(v)) for k, v in obj.items()))
530
+ if isinstance(obj, set):
531
+ return frozenset(make_hashable(item) for item in obj)
532
+ return obj
341
533
 
342
- return [self._coerce_parameter(p) if isinstance(p, TypedParameter) else p for p in normalized]
534
+ try:
535
+ if isinstance(params, dict):
536
+ params_key = make_hashable(params)
537
+ elif isinstance(params, (list, tuple)) and params:
538
+ if isinstance(params[0], dict):
539
+ params_key = tuple(make_hashable(d) for d in params)
540
+ else:
541
+ params_key = make_hashable(params)
542
+ elif isinstance(params, (list, tuple)):
543
+ params_key = ()
544
+ else:
545
+ params_key = params
546
+ except (TypeError, AttributeError):
547
+ params_key = str(params)
343
548
 
344
- def _prepare_driver_parameters_many(self, parameters: Any) -> "list[Any]":
345
- """Prepare parameter sequences for executemany operations.
549
+ base_hash = hash((statement.sql, params_key, statement.is_many, statement.is_script))
550
+ return f"compiled:{base_hash}:{context_hash}"
346
551
 
347
- This method handles sequences of parameter sets, unwrapping TypedParameter
348
- objects in each set for database driver consumption.
552
+ def _get_dominant_parameter_style(self, parameters: "list[Any]") -> "Optional[ParameterStyle]":
553
+ """Determine the dominant parameter style from parameter info list.
349
554
 
350
555
  Args:
351
- parameters: Sequence of parameter sets for executemany
556
+ parameters: List of ParameterInfo objects from validator.extract_parameters()
352
557
 
353
558
  Returns:
354
- List of parameter sets with TypedParameter objects unwrapped
559
+ The dominant parameter style, or None if no parameters
355
560
  """
356
561
  if not parameters:
357
- return []
358
- return [self._prepare_driver_parameters(param_set) for param_set in parameters]
562
+ return None
359
563
 
360
- def _coerce_parameter(self, param: "TypedParameter") -> Any:
361
- """Coerce TypedParameter to driver-safe value.
564
+ style_counts: dict[ParameterStyle, int] = {}
565
+ for param in parameters:
566
+ style_counts[param.style] = style_counts.get(param.style, 0) + 1
567
+
568
+ precedence = {
569
+ ParameterStyle.QMARK: 1,
570
+ ParameterStyle.NUMERIC: 2,
571
+ ParameterStyle.POSITIONAL_COLON: 3,
572
+ ParameterStyle.POSITIONAL_PYFORMAT: 4,
573
+ ParameterStyle.NAMED_AT: 5,
574
+ ParameterStyle.NAMED_DOLLAR: 6,
575
+ ParameterStyle.NAMED_COLON: 7,
576
+ ParameterStyle.NAMED_PYFORMAT: 8,
577
+ }
362
578
 
363
- This method extracts the underlying value from a TypedParameter object.
364
- Individual drivers can override this method to perform driver-specific
365
- type coercion using the rich type information available in TypedParameter.
579
+ return max(style_counts.keys(), key=lambda style: (style_counts[style], -precedence.get(style, 99)))
580
+
581
+ @staticmethod
582
+ def find_filter(
583
+ filter_type: "type[FilterTypeT]",
584
+ filters: "Sequence[StatementFilter | StatementParameters] | Sequence[StatementFilter]",
585
+ ) -> "FilterTypeT | None":
586
+ """Get the filter specified by filter type from the filters.
366
587
 
367
588
  Args:
368
- param: TypedParameter object with value and type information
589
+ filter_type: The type of filter to find.
590
+ filters: filter types to apply to the query
369
591
 
370
592
  Returns:
371
- The underlying parameter value suitable for the database driver
593
+ The match filter instance or None
594
+ """
595
+ return next(
596
+ (cast("FilterTypeT | None", filter_) for filter_ in filters if isinstance(filter_, filter_type)), None
597
+ )
598
+
599
+ def _create_count_query(self, original_sql: "SQL") -> "SQL":
600
+ """Create a COUNT query from the original SQL statement.
601
+
602
+ Transforms the original SELECT statement to count total rows while preserving
603
+ WHERE, HAVING, and GROUP BY clauses but removing ORDER BY, LIMIT, and OFFSET.
372
604
  """
373
- return param.value
605
+ if not original_sql.expression:
606
+ msg = "Cannot create COUNT query from empty SQL expression"
607
+ raise ImproperConfigurationError(msg)
608
+ expr = original_sql.expression
609
+
610
+ if isinstance(expr, exp.Select):
611
+ if expr.args.get("group"):
612
+ subquery = expr.subquery(alias="grouped_data")
613
+ count_expr = exp.select(exp.Count(this=exp.Star())).from_(subquery)
614
+ else:
615
+ count_expr = exp.select(exp.Count(this=exp.Star())).from_(
616
+ cast("exp.Expression", expr.args.get("from")), copy=False
617
+ )
618
+ if expr.args.get("where"):
619
+ count_expr = count_expr.where(cast("exp.Expression", expr.args.get("where")), copy=False)
620
+ if expr.args.get("having"):
621
+ count_expr = count_expr.having(cast("exp.Expression", expr.args.get("having")), copy=False)
622
+
623
+ count_expr.set("order", None)
624
+ count_expr.set("limit", None)
625
+ count_expr.set("offset", None)
626
+
627
+ return SQL(count_expr, *original_sql._positional_parameters, statement_config=original_sql.statement_config)
628
+
629
+ subquery = cast("exp.Select", expr).subquery(alias="total_query")
630
+ count_expr = exp.select(exp.Count(this=exp.Star())).from_(subquery)
631
+ return SQL(count_expr, *original_sql._positional_parameters, statement_config=original_sql.statement_config)