sqlspec 0.25.0__py3-none-any.whl → 0.27.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 (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +153 -0
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +102 -0
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -24,21 +24,64 @@ from sqlspec.builder._ddl import (
24
24
  Truncate,
25
25
  )
26
26
  from sqlspec.builder._delete import Delete
27
+ from sqlspec.builder._dml import (
28
+ DeleteFromClauseMixin,
29
+ InsertFromSelectMixin,
30
+ InsertIntoClauseMixin,
31
+ InsertValuesMixin,
32
+ UpdateFromClauseMixin,
33
+ UpdateSetClauseMixin,
34
+ UpdateTableClauseMixin,
35
+ )
36
+ from sqlspec.builder._expression_wrappers import (
37
+ AggregateExpression,
38
+ ConversionExpression,
39
+ FunctionExpression,
40
+ MathExpression,
41
+ StringExpression,
42
+ )
43
+ from sqlspec.builder._factory import SQLFactory, sql
27
44
  from sqlspec.builder._insert import Insert
45
+ from sqlspec.builder._join import JoinBuilder
28
46
  from sqlspec.builder._merge import Merge
29
- from sqlspec.builder._select import Select
47
+ from sqlspec.builder._parsing_utils import (
48
+ extract_expression,
49
+ parse_column_expression,
50
+ parse_condition_expression,
51
+ parse_order_expression,
52
+ parse_table_expression,
53
+ to_expression,
54
+ )
55
+ from sqlspec.builder._select import (
56
+ Case,
57
+ CaseBuilder,
58
+ CommonTableExpressionMixin,
59
+ HavingClauseMixin,
60
+ LimitOffsetClauseMixin,
61
+ OrderByClauseMixin,
62
+ PivotClauseMixin,
63
+ ReturningClauseMixin,
64
+ Select,
65
+ SelectClauseMixin,
66
+ SetOperationMixin,
67
+ SubqueryBuilder,
68
+ UnpivotClauseMixin,
69
+ WhereClauseMixin,
70
+ WindowFunctionBuilder,
71
+ )
30
72
  from sqlspec.builder._update import Update
31
- from sqlspec.builder.mixins import WhereClauseMixin
32
- from sqlspec.builder.mixins._join_operations import JoinBuilder
33
- from sqlspec.builder.mixins._select_operations import Case, SubqueryBuilder, WindowFunctionBuilder
34
73
  from sqlspec.exceptions import SQLBuilderError
35
74
 
36
75
  __all__ = (
76
+ "AggregateExpression",
37
77
  "AlterTable",
38
78
  "Case",
79
+ "CaseBuilder",
39
80
  "Column",
40
81
  "ColumnExpression",
41
82
  "CommentOn",
83
+ "CommonTableExpressionMixin",
84
+ "ConversionExpression",
42
85
  "CreateIndex",
43
86
  "CreateMaterializedView",
44
87
  "CreateSchema",
@@ -47,22 +90,48 @@ __all__ = (
47
90
  "CreateView",
48
91
  "DDLBuilder",
49
92
  "Delete",
93
+ "DeleteFromClauseMixin",
50
94
  "DropIndex",
51
95
  "DropSchema",
52
96
  "DropTable",
53
97
  "DropView",
54
98
  "FunctionColumn",
99
+ "FunctionExpression",
100
+ "HavingClauseMixin",
55
101
  "Insert",
102
+ "InsertFromSelectMixin",
103
+ "InsertIntoClauseMixin",
104
+ "InsertValuesMixin",
56
105
  "JoinBuilder",
106
+ "LimitOffsetClauseMixin",
107
+ "MathExpression",
57
108
  "Merge",
109
+ "OrderByClauseMixin",
110
+ "PivotClauseMixin",
58
111
  "QueryBuilder",
59
112
  "RenameTable",
113
+ "ReturningClauseMixin",
60
114
  "SQLBuilderError",
115
+ "SQLFactory",
61
116
  "SafeQuery",
62
117
  "Select",
118
+ "SelectClauseMixin",
119
+ "SetOperationMixin",
120
+ "StringExpression",
63
121
  "SubqueryBuilder",
64
122
  "Truncate",
123
+ "UnpivotClauseMixin",
65
124
  "Update",
125
+ "UpdateFromClauseMixin",
126
+ "UpdateSetClauseMixin",
127
+ "UpdateTableClauseMixin",
66
128
  "WhereClauseMixin",
67
129
  "WindowFunctionBuilder",
130
+ "extract_expression",
131
+ "parse_column_expression",
132
+ "parse_condition_expression",
133
+ "parse_order_expression",
134
+ "parse_table_expression",
135
+ "sql",
136
+ "to_expression",
68
137
  )
sqlspec/builder/_base.py CHANGED
@@ -3,8 +3,10 @@
3
3
  Provides abstract base classes and core functionality for SQL query builders.
4
4
  """
5
5
 
6
+ import hashlib
7
+ import uuid
6
8
  from abc import ABC, abstractmethod
7
- from typing import TYPE_CHECKING, Any, NoReturn, Optional, Union, cast
9
+ from typing import TYPE_CHECKING, Any, NoReturn, cast
8
10
 
9
11
  import sqlglot
10
12
  from sqlglot import Dialect, exp
@@ -19,13 +21,15 @@ from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
19
21
  from sqlspec.core.statement import SQL, StatementConfig
20
22
  from sqlspec.exceptions import SQLBuilderError
21
23
  from sqlspec.utils.logging import get_logger
22
- from sqlspec.utils.type_guards import has_expression_and_parameters, has_sql_method, has_with_method
24
+ from sqlspec.utils.type_guards import has_expression_and_parameters, has_sql_method, has_with_method, is_expression
23
25
 
24
26
  if TYPE_CHECKING:
25
27
  from sqlspec.core.result import SQLResult
26
28
 
27
29
  __all__ = ("QueryBuilder", "SafeQuery")
28
30
 
31
+ MAX_PARAMETER_COLLISION_ATTEMPTS = 1000
32
+
29
33
  logger = get_logger(__name__)
30
34
 
31
35
 
@@ -34,9 +38,7 @@ class SafeQuery:
34
38
 
35
39
  __slots__ = ("dialect", "parameters", "sql")
36
40
 
37
- def __init__(
38
- self, sql: str, parameters: Optional[dict[str, Any]] = None, dialect: Optional[DialectType] = None
39
- ) -> None:
41
+ def __init__(self, sql: str, parameters: dict[str, Any] | None = None, dialect: DialectType | None = None) -> None:
40
42
  self.sql = sql
41
43
  self.parameters = parameters if parameters is not None else {}
42
44
  self.dialect = dialect
@@ -64,8 +66,8 @@ class QueryBuilder(ABC):
64
66
 
65
67
  def __init__(
66
68
  self,
67
- dialect: Optional[DialectType] = None,
68
- schema: Optional[dict[str, dict[str, str]]] = None,
69
+ dialect: DialectType | None = None,
70
+ schema: dict[str, dict[str, str]] | None = None,
69
71
  enable_optimization: bool = True,
70
72
  optimize_joins: bool = True,
71
73
  optimize_predicates: bool = True,
@@ -78,7 +80,7 @@ class QueryBuilder(ABC):
78
80
  self.optimize_predicates = optimize_predicates
79
81
  self.simplify_expressions = simplify_expressions
80
82
 
81
- self._expression: Optional[exp.Expression] = None
83
+ self._expression: exp.Expression | None = None
82
84
  self._parameters: dict[str, Any] = {}
83
85
  self._parameter_counter: int = 0
84
86
  self._with_ctes: dict[str, exp.CTE] = {}
@@ -91,7 +93,7 @@ class QueryBuilder(ABC):
91
93
  "QueryBuilder._create_base_expression must return a valid sqlglot expression."
92
94
  )
93
95
 
94
- def get_expression(self) -> Optional[exp.Expression]:
96
+ def get_expression(self) -> exp.Expression | None:
95
97
  """Get expression reference (no copy).
96
98
 
97
99
  Returns:
@@ -104,13 +106,9 @@ class QueryBuilder(ABC):
104
106
 
105
107
  Args:
106
108
  expression: SQLGlot expression to set
107
-
108
- Raises:
109
- TypeError: If expression is not a SQLGlot Expression
110
109
  """
111
- if not isinstance(expression, exp.Expression):
112
- msg = f"Expected Expression, got {type(expression)}"
113
- raise TypeError(msg)
110
+ if not is_expression(expression):
111
+ self._raise_invalid_expression_type(expression)
114
112
  self._expression = expression
115
113
 
116
114
  def has_expression(self) -> bool:
@@ -139,7 +137,7 @@ class QueryBuilder(ABC):
139
137
  """
140
138
 
141
139
  @staticmethod
142
- def _raise_sql_builder_error(message: str, cause: Optional[BaseException] = None) -> NoReturn:
140
+ def _raise_sql_builder_error(message: str, cause: BaseException | None = None) -> NoReturn:
143
141
  """Helper to raise SQLBuilderError, potentially with a cause.
144
142
 
145
143
  Args:
@@ -151,7 +149,121 @@ class QueryBuilder(ABC):
151
149
  """
152
150
  raise SQLBuilderError(message) from cause
153
151
 
154
- def _add_parameter(self, value: Any, context: Optional[str] = None) -> str:
152
+ @staticmethod
153
+ def _raise_invalid_expression_type(expression: Any) -> NoReturn:
154
+ """Raise error for invalid expression type.
155
+
156
+ Args:
157
+ expression: The invalid expression object
158
+
159
+ Raises:
160
+ TypeError: Always raised for type mismatch
161
+ """
162
+ msg = f"Expected Expression, got {type(expression)}"
163
+ raise TypeError(msg)
164
+
165
+ @staticmethod
166
+ def _raise_cte_query_error(alias: str, message: str) -> NoReturn:
167
+ """Raise error for CTE query issues.
168
+
169
+ Args:
170
+ alias: CTE alias name
171
+ message: Specific error message
172
+
173
+ Raises:
174
+ SQLBuilderError: Always raised for CTE errors
175
+ """
176
+ msg = f"CTE '{alias}': {message}"
177
+ raise SQLBuilderError(msg)
178
+
179
+ @staticmethod
180
+ def _raise_cte_parse_error(cause: BaseException) -> NoReturn:
181
+ """Raise error for CTE parsing failures.
182
+
183
+ Args:
184
+ cause: The original parsing exception
185
+
186
+ Raises:
187
+ SQLBuilderError: Always raised with chained cause
188
+ """
189
+ msg = f"Failed to parse CTE query: {cause!s}"
190
+ raise SQLBuilderError(msg) from cause
191
+
192
+ def _build_final_expression(self, *, copy: bool = False) -> exp.Expression:
193
+ """Construct the current expression with attached CTEs.
194
+
195
+ Args:
196
+ copy: Whether to copy the underlying expression tree before
197
+ applying transformations.
198
+
199
+ Returns:
200
+ Expression representing the current builder state with CTEs applied.
201
+ """
202
+ if self._expression is None:
203
+ self._raise_sql_builder_error("QueryBuilder expression not initialized.")
204
+
205
+ base_expression = self._expression.copy() if copy else self._expression
206
+
207
+ if not self._with_ctes:
208
+ return base_expression
209
+
210
+ final_expression: exp.Expression = base_expression
211
+ if has_with_method(final_expression):
212
+ for alias, cte_node in self._with_ctes.items():
213
+ final_expression = cast("Any", final_expression).with_(cte_node.args["this"], as_=alias, copy=False)
214
+ return cast("exp.Expression", final_expression)
215
+
216
+ if isinstance(final_expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Union)):
217
+ return exp.With(expressions=list(self._with_ctes.values()), this=final_expression)
218
+
219
+ return final_expression
220
+
221
+ def _spawn_like_self(self: Self) -> Self:
222
+ """Create a new builder instance with matching configuration."""
223
+ return type(self)(
224
+ dialect=self.dialect,
225
+ schema=self.schema,
226
+ enable_optimization=self.enable_optimization,
227
+ optimize_joins=self.optimize_joins,
228
+ optimize_predicates=self.optimize_predicates,
229
+ simplify_expressions=self.simplify_expressions,
230
+ )
231
+
232
+ def _resolve_cte_query(self, alias: str, query: "QueryBuilder | exp.Select | str") -> exp.Select:
233
+ """Resolve a CTE query into a Select expression with merged parameters."""
234
+ if isinstance(query, QueryBuilder):
235
+ query_expr = query.get_expression()
236
+ if query_expr is None:
237
+ self._raise_cte_query_error(alias, "query builder has no expression")
238
+ if not isinstance(query_expr, exp.Select):
239
+ self._raise_cte_query_error(alias, f"expression must be a Select, got {type(query_expr).__name__}")
240
+ cte_select_expression = query_expr.copy()
241
+ param_mapping = self._merge_cte_parameters(alias, query.parameters)
242
+ updated_expression = self._update_placeholders_in_expression(cte_select_expression, param_mapping)
243
+ if not isinstance(updated_expression, exp.Select): # pragma: no cover - defensive
244
+ msg = "CTE placeholder update produced non-select expression"
245
+ raise SQLBuilderError(msg)
246
+ return updated_expression
247
+
248
+ if isinstance(query, str):
249
+ try:
250
+ parsed_expression = sqlglot.parse_one(query, read=self.dialect_name)
251
+ except SQLGlotParseError as e: # pragma: no cover - defensive
252
+ self._raise_cte_parse_error(e)
253
+ if not isinstance(parsed_expression, exp.Select):
254
+ self._raise_cte_query_error(
255
+ alias, f"query string must parse to SELECT, got {type(parsed_expression).__name__}"
256
+ )
257
+ return parsed_expression
258
+
259
+ if isinstance(query, exp.Select):
260
+ return query
261
+
262
+ self._raise_cte_query_error(alias, f"invalid query type: {type(query).__name__}")
263
+ msg = "Unreachable"
264
+ raise AssertionError(msg)
265
+
266
+ def _add_parameter(self, value: Any, context: str | None = None) -> str:
155
267
  """Adds a parameter to the query and returns its placeholder name.
156
268
 
157
269
  Args:
@@ -192,7 +304,7 @@ class QueryBuilder(ABC):
192
304
 
193
305
  return expression.transform(replacer, copy=False)
194
306
 
195
- def add_parameter(self: Self, value: Any, name: Optional[str] = None) -> tuple[Self, str]:
307
+ def add_parameter(self: Self, value: Any, name: str | None = None) -> tuple[Self, str]:
196
308
  """Explicitly adds a parameter to the query.
197
309
 
198
310
  This is useful for parameters that are not directly tied to a
@@ -229,13 +341,11 @@ class QueryBuilder(ABC):
229
341
  if base_name not in self._parameters:
230
342
  return base_name
231
343
 
232
- for i in range(1, 1000):
344
+ for i in range(1, MAX_PARAMETER_COLLISION_ATTEMPTS):
233
345
  name = f"{base_name}_{i}"
234
346
  if name not in self._parameters:
235
347
  return name
236
348
 
237
- import uuid
238
-
239
349
  return f"{base_name}_{uuid.uuid4().hex[:8]}"
240
350
 
241
351
  def _merge_cte_parameters(self, cte_name: str, parameters: dict[str, Any]) -> dict[str, str]:
@@ -275,7 +385,7 @@ class QueryBuilder(ABC):
275
385
 
276
386
  return expression.transform(placeholder_replacer, copy=False)
277
387
 
278
- def _generate_builder_cache_key(self, config: "Optional[StatementConfig]" = None) -> str:
388
+ def _generate_builder_cache_key(self, config: "StatementConfig | None" = None) -> str:
279
389
  """Generate cache key based on builder state and configuration.
280
390
 
281
391
  Args:
@@ -284,8 +394,6 @@ class QueryBuilder(ABC):
284
394
  Returns:
285
395
  A unique cache key representing the builder state and configuration
286
396
  """
287
- import hashlib
288
-
289
397
  dialect_name: str = self.dialect_name or "default"
290
398
 
291
399
  if self._expression is None:
@@ -320,7 +428,7 @@ class QueryBuilder(ABC):
320
428
  state_string = "|".join(state_parts)
321
429
  return f"builder:{hashlib.sha256(state_string.encode()).hexdigest()[:16]}"
322
430
 
323
- def with_cte(self: Self, alias: str, query: "Union[QueryBuilder, exp.Select, str]") -> Self:
431
+ def with_cte(self: Self, alias: str, query: "QueryBuilder | exp.Select | str") -> Self:
324
432
  """Adds a Common Table Expression (CTE) to the query.
325
433
 
326
434
  Args:
@@ -334,41 +442,7 @@ class QueryBuilder(ABC):
334
442
  if alias in self._with_ctes:
335
443
  self._raise_sql_builder_error(f"CTE with alias '{alias}' already exists.")
336
444
 
337
- cte_select_expression: exp.Select
338
-
339
- if isinstance(query, QueryBuilder):
340
- query_expr = query.get_expression()
341
- if query_expr is None:
342
- self._raise_sql_builder_error("CTE query builder has no expression.")
343
- if not isinstance(query_expr, exp.Select):
344
- msg = f"CTE query builder expression must be a Select, got {type(query_expr).__name__}."
345
- self._raise_sql_builder_error(msg)
346
- cte_select_expression = query_expr
347
- param_mapping = self._merge_cte_parameters(alias, query.parameters)
348
- updated_expression = self._update_placeholders_in_expression(cte_select_expression, param_mapping)
349
- if not isinstance(updated_expression, exp.Select):
350
- msg = f"Updated CTE expression must be a Select, got {type(updated_expression).__name__}."
351
- self._raise_sql_builder_error(msg)
352
- cte_select_expression = updated_expression
353
-
354
- elif isinstance(query, str):
355
- try:
356
- parsed_expression = sqlglot.parse_one(query, read=self.dialect_name)
357
- if not isinstance(parsed_expression, exp.Select):
358
- msg = f"CTE query string must parse to a SELECT statement, got {type(parsed_expression).__name__}."
359
- self._raise_sql_builder_error(msg)
360
- cte_select_expression = parsed_expression
361
- except SQLGlotParseError as e:
362
- self._raise_sql_builder_error(f"Failed to parse CTE query string: {e!s}", e)
363
- except Exception as e:
364
- msg = f"An unexpected error occurred while parsing CTE query string: {e!s}"
365
- self._raise_sql_builder_error(msg, e)
366
- elif isinstance(query, exp.Select):
367
- cte_select_expression = query
368
- else:
369
- msg = f"Invalid query type for CTE: {type(query).__name__}"
370
- self._raise_sql_builder_error(msg)
371
-
445
+ cte_select_expression = self._resolve_cte_query(alias, query)
372
446
  self._with_ctes[alias] = exp.CTE(this=cte_select_expression, alias=exp.to_table(alias))
373
447
  return self
374
448
 
@@ -378,18 +452,7 @@ class QueryBuilder(ABC):
378
452
  Returns:
379
453
  SafeQuery: A dataclass containing the SQL string and parameters.
380
454
  """
381
- if self._expression is None:
382
- self._raise_sql_builder_error("QueryBuilder expression not initialized.")
383
-
384
- if self._with_ctes:
385
- final_expression = self._expression
386
- if has_with_method(final_expression):
387
- for alias, cte_node in self._with_ctes.items():
388
- final_expression = cast("Any", final_expression).with_(cte_node.args["this"], as_=alias, copy=False)
389
- elif isinstance(final_expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Union)):
390
- final_expression = exp.With(expressions=list(self._with_ctes.values()), this=final_expression)
391
- else:
392
- final_expression = self._expression
455
+ final_expression = self._build_final_expression()
393
456
 
394
457
  if self.enable_optimization and isinstance(final_expression, exp.Expression):
395
458
  final_expression = self._optimize_expression(final_expression)
@@ -438,15 +501,14 @@ class QueryBuilder(ABC):
438
501
  optimized = optimize(
439
502
  expression, schema=self.schema, dialect=self.dialect_name, optimizer_settings=optimizer_settings
440
503
  )
441
-
442
504
  cache.put("optimized", cache_key, optimized)
443
-
444
505
  except Exception:
506
+ logger.debug("Expression optimization failed, using original expression")
445
507
  return expression
446
508
  else:
447
509
  return optimized
448
510
 
449
- def to_statement(self, config: "Optional[StatementConfig]" = None) -> "SQL":
511
+ def to_statement(self, config: "StatementConfig | None" = None) -> "SQL":
450
512
  """Converts the built query into a SQL statement object.
451
513
 
452
514
  Args:
@@ -471,7 +533,7 @@ class QueryBuilder(ABC):
471
533
 
472
534
  return sql_statement
473
535
 
474
- def _to_statement(self, config: "Optional[StatementConfig]" = None) -> "SQL":
536
+ def _to_statement(self, config: "StatementConfig | None" = None) -> "SQL":
475
537
  """Internal method to create SQL statement.
476
538
 
477
539
  Args:
@@ -482,18 +544,7 @@ class QueryBuilder(ABC):
482
544
  """
483
545
  safe_query = self.build()
484
546
 
485
- if isinstance(safe_query.parameters, dict):
486
- kwargs = safe_query.parameters
487
- parameters: Optional[tuple[Any, ...]] = None
488
- else:
489
- kwargs = None
490
- parameters = (
491
- safe_query.parameters
492
- if isinstance(safe_query.parameters, tuple)
493
- else tuple(safe_query.parameters)
494
- if safe_query.parameters
495
- else None
496
- )
547
+ kwargs, parameters = self._extract_statement_parameters(safe_query.parameters)
497
548
 
498
549
  if config is None:
499
550
  config = StatementConfig(
@@ -521,6 +572,28 @@ class QueryBuilder(ABC):
521
572
  return SQL(sql_string, *parameters, statement_config=config)
522
573
  return SQL(sql_string, statement_config=config)
523
574
 
575
+ def _extract_statement_parameters(
576
+ self, raw_parameters: Any
577
+ ) -> "tuple[dict[str, Any] | None, tuple[Any, ...] | None]":
578
+ """Extract parameters for SQL statement creation.
579
+
580
+ Args:
581
+ raw_parameters: Raw parameter data from SafeQuery
582
+
583
+ Returns:
584
+ Tuple of (kwargs, parameters) for SQL statement construction
585
+ """
586
+ if isinstance(raw_parameters, dict):
587
+ return raw_parameters, None
588
+
589
+ if isinstance(raw_parameters, tuple):
590
+ return None, raw_parameters
591
+
592
+ if raw_parameters:
593
+ return None, tuple(raw_parameters)
594
+
595
+ return None, None
596
+
524
597
  def __str__(self) -> str:
525
598
  """Return the SQL string representation of the query.
526
599
 
@@ -530,7 +603,7 @@ class QueryBuilder(ABC):
530
603
  return self.build().sql
531
604
 
532
605
  @property
533
- def dialect_name(self) -> "Optional[str]":
606
+ def dialect_name(self) -> "str | None":
534
607
  """Returns the name of the dialect, if set."""
535
608
  if isinstance(self.dialect, str):
536
609
  return self.dialect