sqlspec 0.14.0__py3-none-any.whl → 0.15.0__py3-none-any.whl

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

Potentially problematic release.


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

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