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
@@ -4,28 +4,1323 @@ Provides a fluent interface for building SQL SELECT queries with
4
4
  parameter binding and validation.
5
5
  """
6
6
 
7
+ # pyright: reportPrivateUsage=false, reportPrivateImportUsage=false
8
+
7
9
  import re
8
- from typing import Any, Callable, Final, Optional, Union
10
+ from typing import TYPE_CHECKING, Any, Final, Union, cast
9
11
 
12
+ from mypy_extensions import trait
10
13
  from sqlglot import exp
11
14
  from typing_extensions import Self
12
15
 
13
16
  from sqlspec.builder._base import QueryBuilder, SafeQuery
14
- from sqlspec.builder.mixins import (
15
- CommonTableExpressionMixin,
16
- HavingClauseMixin,
17
- JoinClauseMixin,
18
- LimitOffsetClauseMixin,
19
- OrderByClauseMixin,
20
- PivotClauseMixin,
21
- SelectClauseMixin,
22
- SetOperationMixin,
23
- UnpivotClauseMixin,
24
- WhereClauseMixin,
17
+ from sqlspec.builder._join import JoinClauseMixin
18
+ from sqlspec.builder._parsing_utils import (
19
+ extract_column_name,
20
+ extract_expression,
21
+ parse_column_expression,
22
+ parse_condition_expression,
23
+ parse_order_expression,
24
+ parse_table_expression,
25
+ to_expression,
25
26
  )
27
+ from sqlspec.core.parameters import ParameterStyle, ParameterValidator
26
28
  from sqlspec.core.result import SQLResult
29
+ from sqlspec.core.statement import SQL
30
+ from sqlspec.exceptions import SQLBuilderError
31
+ from sqlspec.utils.type_guards import (
32
+ has_expression_and_parameters,
33
+ has_expression_and_sql,
34
+ has_query_builder_parameters,
35
+ has_sqlglot_expression,
36
+ is_expression,
37
+ is_iterable_parameters,
38
+ )
39
+
40
+ BETWEEN_BOUND_COUNT = 2
41
+ PAIR_LENGTH = 2
42
+ TRIPLE_LENGTH = 3
43
+
44
+ if TYPE_CHECKING:
45
+ from collections.abc import Callable
46
+
47
+ from sqlspec.builder._column import Column, ColumnExpression, FunctionColumn
48
+ from sqlspec.builder._expression_wrappers import ExpressionWrapper
49
+ from sqlspec.protocols import SQLBuilderProtocol
50
+
51
+ __all__ = (
52
+ "Case",
53
+ "CaseBuilder",
54
+ "CommonTableExpressionMixin",
55
+ "HavingClauseMixin",
56
+ "LimitOffsetClauseMixin",
57
+ "OrderByClauseMixin",
58
+ "PivotClauseMixin",
59
+ "ReturningClauseMixin",
60
+ "Select",
61
+ "SelectClauseMixin",
62
+ "SetOperationMixin",
63
+ "SubqueryBuilder",
64
+ "UnpivotClauseMixin",
65
+ "WhereClauseMixin",
66
+ "WindowFunctionBuilder",
67
+ )
68
+
69
+
70
+ class Case:
71
+ """Represent a SQL CASE expression with structured components."""
72
+
73
+ __slots__ = ("conditions", "default")
74
+
75
+ def __init__(self, *ifs: exp.Expression, default: exp.Expression | None = None) -> None:
76
+ self.conditions = list(ifs)
77
+ self.default = default
78
+
79
+ def when(self, condition: str | exp.Expression, result: Any) -> "Case":
80
+ condition_expr = parse_condition_expression(condition)
81
+ result_expr = to_expression(result)
82
+ self.conditions.append(exp.If(this=condition_expr, true=result_expr))
83
+ return self
84
+
85
+ def else_(self, value: Any) -> "Case":
86
+ self.default = to_expression(value)
87
+ return self
88
+
89
+ def end(self) -> "Case":
90
+ return self
91
+
92
+ def as_(self, alias: str) -> exp.Alias:
93
+ return cast("exp.Alias", exp.alias_(self.expression, alias))
94
+
95
+ @property
96
+ def expression(self) -> exp.Case:
97
+ return exp.Case(ifs=self.conditions, default=self.default)
98
+
99
+
100
+ class CaseBuilder:
101
+ """Fluent builder for CASE expressions used within SELECT clauses."""
102
+
103
+ __slots__ = ()
104
+
105
+ def __call__(self, *args: Any, default: Any | None = None) -> Case:
106
+ conditions = [to_expression(arg) for arg in args]
107
+ default_expr = to_expression(default) if default is not None else None
108
+ return Case(*conditions, default=default_expr)
109
+
110
+
111
+ class SubqueryBuilder:
112
+ """Helper to build subquery expressions for EXISTS/IN/ANY/ALL operations."""
113
+
114
+ __slots__ = ("_operation",)
115
+
116
+ def __init__(self, operation: str) -> None:
117
+ self._operation = operation
118
+
119
+ def __call__(self, subquery: Any) -> exp.Expression:
120
+ if isinstance(subquery, exp.Expression):
121
+ subquery_expr = subquery
122
+ elif hasattr(subquery, "build") and callable(getattr(subquery, "build", None)):
123
+ built_query = subquery.build()
124
+ sql_text = built_query.sql if hasattr(built_query, "sql") else str(built_query)
125
+ parsed_expr: exp.Expression | None = exp.maybe_parse(sql_text)
126
+ if parsed_expr is None:
127
+ msg = f"Could not parse subquery SQL: {sql_text}"
128
+ raise SQLBuilderError(msg)
129
+ subquery_expr = parsed_expr
130
+ else:
131
+ parsed_expr = exp.maybe_parse(str(subquery))
132
+ if parsed_expr is None:
133
+ msg = f"Could not convert subquery to expression: {subquery}"
134
+ raise SQLBuilderError(msg)
135
+ subquery_expr = parsed_expr
136
+
137
+ if self._operation == "exists":
138
+ return exp.Exists(this=subquery_expr)
139
+ if self._operation == "in":
140
+ return exp.In(expressions=[subquery_expr])
141
+ if self._operation == "any":
142
+ return exp.Any(this=subquery_expr)
143
+ if self._operation == "all":
144
+ return exp.All(this=subquery_expr)
145
+ msg = f"Unknown subquery operation: {self._operation}"
146
+ raise SQLBuilderError(msg)
147
+
148
+
149
+ class WindowFunctionBuilder:
150
+ """Helper to fluently construct window function expressions."""
151
+
152
+ __slots__ = ("_function_args", "_function_name", "_order_by", "_partition_by")
153
+
154
+ def __init__(self, function_name: str, *function_args: Any) -> None:
155
+ self._function_name = function_name
156
+ self._function_args: list[exp.Expression] = [to_expression(arg) for arg in function_args]
157
+ self._partition_by: list[exp.Expression] = []
158
+ self._order_by: list[exp.Ordered] = []
159
+
160
+ def __call__(self, *function_args: Any) -> "WindowFunctionBuilder":
161
+ self._function_args = [to_expression(arg) for arg in function_args]
162
+ return self
163
+
164
+ def partition_by(self, *columns: str | exp.Expression) -> "WindowFunctionBuilder":
165
+ self._partition_by = [exp.column(column) if isinstance(column, str) else column for column in columns]
166
+ return self
167
+
168
+ def order_by(self, *columns: str | exp.Expression) -> "WindowFunctionBuilder":
169
+ ordered_columns: list[exp.Ordered] = []
170
+ for column in columns:
171
+ if isinstance(column, str):
172
+ ordered_columns.append(exp.column(column).asc())
173
+ elif isinstance(column, exp.Ordered):
174
+ ordered_columns.append(column)
175
+ else:
176
+ ordered_columns.append(exp.Ordered(this=column, desc=False))
177
+ self._order_by = ordered_columns
178
+ return self
179
+
180
+ def _build_function_expression(self) -> exp.Expression:
181
+ expressions = self._function_args or []
182
+ return exp.Anonymous(this=self._function_name, expressions=expressions)
183
+
184
+ def build(self) -> exp.Window:
185
+ over_args: dict[str, Any] = {}
186
+ if self._partition_by:
187
+ over_args["partition_by"] = self._partition_by
188
+ if self._order_by:
189
+ over_args["order"] = exp.Order(expressions=self._order_by)
190
+ return exp.Window(this=self._build_function_expression(), **over_args)
191
+
192
+ def as_(self, alias: str) -> exp.Alias:
193
+ return cast("exp.Alias", exp.alias_(self.build(), alias))
194
+
195
+
196
+ def _ensure_select_expression(
197
+ mixin: "SQLBuilderProtocol", *, error_message: str, initialize: bool = True
198
+ ) -> exp.Select:
199
+ expression = mixin.get_expression()
200
+ if expression is None and initialize:
201
+ mixin.set_expression(exp.Select())
202
+ expression = mixin.get_expression()
203
+
204
+ if not isinstance(expression, exp.Select):
205
+ raise SQLBuilderError(error_message)
206
+
207
+ return expression
208
+
209
+
210
+ @trait
211
+ class SelectClauseMixin:
212
+ """Mixin providing SELECT clause methods."""
213
+
214
+ __slots__ = ()
215
+
216
+ def get_expression(self) -> exp.Expression | None: ...
217
+ def set_expression(self, expression: exp.Expression) -> None: ...
218
+
219
+ def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn", SQL, Case]) -> Self:
220
+ builder = cast("SQLBuilderProtocol", self)
221
+ select_expr = _ensure_select_expression(builder, error_message="Cannot add columns to non-SELECT expression.")
222
+ for column in columns:
223
+ column_expr = column.expression if isinstance(column, Case) else parse_column_expression(column, builder)
224
+ select_expr = select_expr.select(column_expr, copy=False)
225
+ self.set_expression(select_expr)
226
+ return cast("Self", builder)
227
+
228
+ def distinct(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn", SQL]) -> Self:
229
+ builder = cast("SQLBuilderProtocol", self)
230
+ select_expr = _ensure_select_expression(builder, error_message="Cannot add DISTINCT to non-SELECT expression.")
231
+ if not columns:
232
+ select_expr.set("distinct", exp.Distinct())
233
+ else:
234
+ distinct_columns = [parse_column_expression(column, builder) for column in columns]
235
+ select_expr.set("distinct", exp.Distinct(expressions=distinct_columns))
236
+ builder.set_expression(select_expr)
237
+ return cast("Self", builder)
238
+
239
+ def from_(self, table: str | exp.Expression | Any, alias: str | None = None) -> Self:
240
+ builder = cast("SQLBuilderProtocol", self)
241
+ select_expr = _ensure_select_expression(builder, error_message="FROM clause only valid for SELECT.")
242
+ if isinstance(table, str):
243
+ from_expr = parse_table_expression(table, alias)
244
+ elif is_expression(table):
245
+ from_expr = exp.alias_(table, alias) if alias else table
246
+ elif has_query_builder_parameters(table):
247
+ subquery = table.build()
248
+ sql_text = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
249
+ subquery_exp = exp.paren(exp.maybe_parse(sql_text, dialect=getattr(builder, "dialect", None)))
250
+ from_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
251
+ else:
252
+ from_expr = table
253
+ builder.set_expression(select_expr.from_(from_expr, copy=False))
254
+ return cast("Self", builder)
255
+
256
+ def group_by(self, *columns: str | exp.Expression) -> Self:
257
+ builder = cast("SQLBuilderProtocol", self)
258
+ select_expr = builder.get_expression()
259
+ if select_expr is None or not isinstance(select_expr, exp.Select):
260
+ return cast("Self", builder)
261
+
262
+ for column in columns:
263
+ column_expr = exp.column(column) if isinstance(column, str) else column
264
+ select_expr = select_expr.group_by(column_expr, copy=False)
265
+ builder.set_expression(select_expr)
266
+ return cast("Self", builder)
267
+
268
+ def group_by_rollup(self, *columns: str | exp.Expression) -> Self:
269
+ column_exprs = [exp.column(column) if isinstance(column, str) else column for column in columns]
270
+ rollup_expr = exp.Rollup(expressions=column_exprs)
271
+ return self.group_by(rollup_expr)
272
+
273
+ def group_by_cube(self, *columns: str | exp.Expression) -> Self:
274
+ column_exprs = [exp.column(column) if isinstance(column, str) else column for column in columns]
275
+ cube_expr = exp.Cube(expressions=column_exprs)
276
+ return self.group_by(cube_expr)
277
+
278
+ def group_by_grouping_sets(self, *column_sets: tuple[str, ...] | list[str]) -> Self:
279
+ grouping_sets = [
280
+ exp.Tuple(expressions=[exp.column(col) if isinstance(col, str) else col for col in column_set])
281
+ for column_set in column_sets
282
+ ]
283
+ grouping_expr = exp.GroupingSets(expressions=grouping_sets)
284
+ return self.group_by(grouping_expr)
285
+
286
+
287
+ @trait
288
+ class OrderByClauseMixin:
289
+ __slots__ = ()
290
+
291
+ _expression: exp.Expression | None
292
+
293
+ def order_by(self, *items: Union[str, exp.Ordered, "Column"], desc: bool = False) -> Self:
294
+ builder = cast("SQLBuilderProtocol", self)
295
+ select_expr = _ensure_select_expression(builder, error_message="ORDER BY only valid for SELECT.")
296
+
297
+ current_expr = select_expr
298
+ for item in items:
299
+ if isinstance(item, str):
300
+ order_item = parse_order_expression(item)
301
+ if desc:
302
+ order_item = order_item.desc()
303
+ else:
304
+ extracted_item = extract_expression(item)
305
+ order_item = extracted_item.desc() if desc and not isinstance(item, exp.Ordered) else extracted_item
306
+ current_expr = current_expr.order_by(order_item, copy=False)
307
+ builder.set_expression(current_expr)
308
+ return cast("Self", builder)
309
+
310
+
311
+ @trait
312
+ class LimitOffsetClauseMixin:
313
+ __slots__ = ()
314
+
315
+ _expression: exp.Expression | None
316
+
317
+ def limit(self, value: int) -> Self:
318
+ builder = cast("SQLBuilderProtocol", self)
319
+ select_expr = _ensure_select_expression(builder, error_message="LIMIT only valid for SELECT.")
320
+ builder.set_expression(select_expr.limit(exp.convert(value), copy=False))
321
+ return cast("Self", builder)
322
+
323
+ def offset(self, value: int) -> Self:
324
+ builder = cast("SQLBuilderProtocol", self)
325
+ select_expr = _ensure_select_expression(builder, error_message="OFFSET only valid for SELECT.")
326
+ builder.set_expression(select_expr.offset(exp.convert(value), copy=False))
327
+ return cast("Self", builder)
328
+
329
+
330
+ @trait
331
+ class ReturningClauseMixin:
332
+ __slots__ = ()
333
+
334
+ _expression: exp.Expression | None
335
+
336
+ def returning(self, *columns: Union[str, exp.Expression, "Column", "ExpressionWrapper", Case]) -> Self:
337
+ if self._expression is None:
338
+ msg = "Cannot add RETURNING: expression not initialized."
339
+ raise SQLBuilderError(msg)
340
+ if not isinstance(self._expression, (exp.Insert, exp.Update, exp.Delete)):
341
+ msg = "RETURNING only supported for INSERT, UPDATE, DELETE statements."
342
+ raise SQLBuilderError(msg)
343
+ returning_exprs = [extract_expression(col) for col in columns]
344
+ self._expression.set("returning", exp.Returning(expressions=returning_exprs))
345
+ return self
346
+
347
+
348
+ @trait
349
+ class WhereClauseMixin:
350
+ __slots__ = ()
351
+
352
+ def get_expression(self) -> exp.Expression | None: ...
353
+ def set_expression(self, expression: exp.Expression) -> None: ...
354
+
355
+ def _create_parameterized_condition(
356
+ self,
357
+ column: str | exp.Column,
358
+ value: Any,
359
+ condition_factory: "Callable[[exp.Expression, exp.Placeholder], exp.Expression]",
360
+ ) -> exp.Expression:
361
+ builder = cast("SQLBuilderProtocol", self)
362
+ column_name = extract_column_name(column)
363
+ param_name = builder._generate_unique_parameter_name(column_name)
364
+ _, param_name = builder.add_parameter(value, name=param_name)
365
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
366
+ placeholder = exp.Placeholder(this=param_name)
367
+ return condition_factory(col_expr, placeholder)
368
+
369
+ def _merge_sql_object_parameters(self, sql_obj: Any) -> None:
370
+ if not has_expression_and_parameters(sql_obj):
371
+ return
372
+
373
+ builder = cast("SQLBuilderProtocol", self)
374
+ sql_parameters = getattr(sql_obj, "parameters", {})
375
+ for param_name, param_value in sql_parameters.items():
376
+ unique_name = builder._generate_unique_parameter_name(param_name)
377
+ builder.add_parameter(param_value, name=unique_name)
378
+
379
+ def _get_existing_where_clause(self) -> exp.Where | None:
380
+ builder = cast("SQLBuilderProtocol", self)
381
+ expression = builder.get_expression()
382
+ if isinstance(expression, (exp.Select, exp.Update, exp.Delete)):
383
+ where_clause = expression.args.get("where")
384
+ if isinstance(where_clause, exp.Where):
385
+ return where_clause
386
+ return None
387
+
388
+ def _combine_with_or(self, new_condition: exp.Expression) -> Self:
389
+ builder = cast("SQLBuilderProtocol", self)
390
+ expression = builder.get_expression()
391
+ if expression is None or not isinstance(expression, (exp.Select, exp.Update, exp.Delete)):
392
+ msg = "OR WHERE clause not supported for current expression. Use where() first."
393
+ raise SQLBuilderError(msg)
394
+
395
+ where_clause = self._get_existing_where_clause()
396
+ if where_clause is None or where_clause.this is None:
397
+ msg = "Cannot add OR WHERE clause: no existing WHERE clause found. Use where() before or_where()."
398
+ raise SQLBuilderError(msg)
399
+
400
+ combined_condition = exp.Or(this=where_clause.this, expression=new_condition)
401
+ where_clause.set("this", combined_condition)
402
+ builder.set_expression(expression)
403
+ return cast("Self", builder)
404
+
405
+ def _handle_in_operator(
406
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
407
+ ) -> exp.Expression:
408
+ builder = cast("SQLBuilderProtocol", self)
409
+ if has_query_builder_parameters(value) or isinstance(value, exp.Expression):
410
+ subquery_expr = self._normalize_subquery_expression(value, builder)
411
+ return exp.In(this=column_exp, expressions=[subquery_expr])
412
+ if is_iterable_parameters(value):
413
+ placeholders = []
414
+ for index, element in enumerate(value):
415
+ name_seed = column_name if len(value) == 1 else f"{column_name}_{index + 1}"
416
+ param_name = builder._generate_unique_parameter_name(name_seed)
417
+ _, param_name = builder.add_parameter(element, name=param_name)
418
+ placeholders.append(exp.Placeholder(this=param_name))
419
+ return exp.In(this=column_exp, expressions=placeholders)
420
+
421
+ param_name = builder._generate_unique_parameter_name(column_name)
422
+ _, param_name = builder.add_parameter(value, name=param_name)
423
+ return exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)])
424
+
425
+ def _handle_not_in_operator(
426
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
427
+ ) -> exp.Expression:
428
+ builder = cast("SQLBuilderProtocol", self)
429
+ if has_query_builder_parameters(value) or isinstance(value, exp.Expression):
430
+ subquery_expr = self._normalize_subquery_expression(value, builder)
431
+ return exp.Not(this=exp.In(this=column_exp, expressions=[subquery_expr]))
432
+ if is_iterable_parameters(value):
433
+ placeholders = []
434
+ for index, element in enumerate(value):
435
+ name_seed = column_name if len(value) == 1 else f"{column_name}_{index + 1}"
436
+ param_name = builder._generate_unique_parameter_name(name_seed)
437
+ _, param_name = builder.add_parameter(element, name=param_name)
438
+ placeholders.append(exp.Placeholder(this=param_name))
439
+ return exp.Not(this=exp.In(this=column_exp, expressions=placeholders))
440
+
441
+ param_name = builder._generate_unique_parameter_name(column_name)
442
+ _, param_name = builder.add_parameter(value, name=param_name)
443
+ return exp.Not(this=exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)]))
444
+
445
+ def _handle_is_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
446
+ value_expr = exp.Null() if value is None else exp.convert(value)
447
+ return exp.Is(this=column_exp, expression=value_expr)
448
+
449
+ def _handle_is_not_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
450
+ value_expr = exp.Null() if value is None else exp.convert(value)
451
+ return exp.Not(this=exp.Is(this=column_exp, expression=value_expr))
452
+
453
+ def _handle_between_operator(
454
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
455
+ ) -> exp.Expression:
456
+ if is_iterable_parameters(value) and len(value) == BETWEEN_BOUND_COUNT:
457
+ builder = cast("SQLBuilderProtocol", self)
458
+ low, high = value
459
+ low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
460
+ high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
461
+ _, low_param = builder.add_parameter(low, name=low_param)
462
+ _, high_param = builder.add_parameter(high, name=high_param)
463
+ return exp.Between(
464
+ this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
465
+ )
466
+ msg = f"BETWEEN operator requires a tuple of two values, got {type(value).__name__}"
467
+ raise SQLBuilderError(msg)
468
+
469
+ def _handle_not_between_operator(
470
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
471
+ ) -> exp.Expression:
472
+ if is_iterable_parameters(value) and len(value) == BETWEEN_BOUND_COUNT:
473
+ builder = cast("SQLBuilderProtocol", self)
474
+ low, high = value
475
+ low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
476
+ high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
477
+ _, low_param = builder.add_parameter(low, name=low_param)
478
+ _, high_param = builder.add_parameter(high, name=high_param)
479
+ return exp.Not(
480
+ this=exp.Between(
481
+ this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
482
+ )
483
+ )
484
+ msg = f"NOT BETWEEN operator requires a tuple of two values, got {type(value).__name__}"
485
+ raise SQLBuilderError(msg)
486
+
487
+ def _create_any_condition(self, column_expr: exp.Expression, values: Any, column_name: str) -> exp.Expression:
488
+ builder = cast("SQLBuilderProtocol", self)
489
+ if has_query_builder_parameters(values):
490
+ subquery_expr = self._normalize_subquery_expression(values, builder)
491
+ return exp.EQ(this=column_expr, expression=exp.Any(this=subquery_expr))
492
+ if isinstance(values, exp.Expression):
493
+ return exp.EQ(this=column_expr, expression=exp.Any(this=values))
494
+ if has_sqlglot_expression(values):
495
+ raw_expr = getattr(values, "sqlglot_expression", None)
496
+ if isinstance(raw_expr, exp.Expression):
497
+ return exp.EQ(this=column_expr, expression=exp.Any(this=raw_expr))
498
+ parsed_expr: exp.Expression | None = exp.maybe_parse(str(values), dialect=builder.dialect)
499
+ if parsed_expr is not None:
500
+ return exp.EQ(this=column_expr, expression=exp.Any(this=parsed_expr))
501
+ if has_expression_and_sql(values):
502
+ self._merge_sql_object_parameters(values)
503
+ expression_attr = getattr(values, "expression", None)
504
+ if isinstance(expression_attr, exp.Expression):
505
+ return exp.EQ(this=column_expr, expression=exp.Any(this=expression_attr))
506
+ sql_text = getattr(values, "sql", "")
507
+ parsed_expr = exp.maybe_parse(sql_text, dialect=builder.dialect)
508
+ if parsed_expr is not None:
509
+ return exp.EQ(this=column_expr, expression=exp.Any(this=parsed_expr))
510
+ if isinstance(values, str):
511
+ parsed_expr = exp.maybe_parse(values, dialect=builder.dialect)
512
+ if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
513
+ return exp.EQ(this=column_expr, expression=exp.Any(this=exp.paren(parsed_expr)))
514
+ msg = "Unsupported type for 'values' in WHERE ANY"
515
+ raise SQLBuilderError(msg)
516
+ if not is_iterable_parameters(values) or isinstance(values, (bytes, bytearray)):
517
+ msg = "Unsupported type for 'values' in WHERE ANY"
518
+ raise SQLBuilderError(msg)
519
+ placeholders: list[exp.Expression] = []
520
+ values_list = list(values)
521
+ for index, element in enumerate(values_list):
522
+ if len(values_list) == 1:
523
+ param_name = builder._generate_unique_parameter_name(column_name)
524
+ else:
525
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_any_{index + 1}")
526
+ _, param_name = builder.add_parameter(element, name=param_name)
527
+ placeholders.append(exp.Placeholder(this=param_name))
528
+ tuple_expr = exp.Tuple(expressions=placeholders)
529
+ return exp.EQ(this=column_expr, expression=exp.Any(this=tuple_expr))
530
+
531
+ def _create_not_any_condition(self, column_expr: exp.Expression, values: Any, column_name: str) -> exp.Expression:
532
+ builder = cast("SQLBuilderProtocol", self)
533
+ if has_query_builder_parameters(values):
534
+ subquery_expr = self._normalize_subquery_expression(values, builder)
535
+ return exp.NEQ(this=column_expr, expression=exp.Any(this=subquery_expr))
536
+ if isinstance(values, exp.Expression):
537
+ return exp.NEQ(this=column_expr, expression=exp.Any(this=values))
538
+ if has_sqlglot_expression(values):
539
+ raw_expr = getattr(values, "sqlglot_expression", None)
540
+ if isinstance(raw_expr, exp.Expression):
541
+ return exp.NEQ(this=column_expr, expression=exp.Any(this=raw_expr))
542
+ parsed_expr: exp.Expression | None = exp.maybe_parse(str(values), dialect=builder.dialect)
543
+ if parsed_expr is not None:
544
+ return exp.NEQ(this=column_expr, expression=exp.Any(this=parsed_expr))
545
+ if has_expression_and_sql(values):
546
+ self._merge_sql_object_parameters(values)
547
+ expression_attr = getattr(values, "expression", None)
548
+ if isinstance(expression_attr, exp.Expression):
549
+ return exp.NEQ(this=column_expr, expression=exp.Any(this=expression_attr))
550
+ sql_text = getattr(values, "sql", "")
551
+ parsed_expr = exp.maybe_parse(sql_text, dialect=builder.dialect)
552
+ if parsed_expr is not None:
553
+ return exp.NEQ(this=column_expr, expression=exp.Any(this=parsed_expr))
554
+ if isinstance(values, str):
555
+ parsed_expr = exp.maybe_parse(values, dialect=builder.dialect)
556
+ if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
557
+ return exp.NEQ(this=column_expr, expression=exp.Any(this=exp.paren(parsed_expr)))
558
+ msg = "Unsupported type for 'values' in WHERE NOT ANY"
559
+ raise SQLBuilderError(msg)
560
+ if not is_iterable_parameters(values) or isinstance(values, (bytes, bytearray)):
561
+ msg = "Unsupported type for 'values' in WHERE NOT ANY"
562
+ raise SQLBuilderError(msg)
563
+ placeholders: list[exp.Expression] = []
564
+ values_list = list(values)
565
+ for index, element in enumerate(values_list):
566
+ if len(values_list) == 1:
567
+ param_name = builder._generate_unique_parameter_name(column_name)
568
+ else:
569
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_not_any_{index + 1}")
570
+ _, param_name = builder.add_parameter(element, name=param_name)
571
+ placeholders.append(exp.Placeholder(this=param_name))
572
+ tuple_expr = exp.Tuple(expressions=placeholders)
573
+ return exp.NEQ(this=column_expr, expression=exp.Any(this=tuple_expr))
574
+
575
+ def _normalize_subquery_expression(self, subquery: Any, builder: "SQLBuilderProtocol") -> exp.Expression:
576
+ if has_query_builder_parameters(subquery):
577
+ subquery_builder = cast("QueryBuilder", subquery)
578
+ safe_query: SafeQuery = subquery_builder.build()
579
+ parsed_subquery: exp.Expression | None = exp.maybe_parse(safe_query.sql, dialect=builder.dialect)
580
+ if parsed_subquery is None:
581
+ msg = f"Could not parse subquery SQL: {safe_query.sql}"
582
+ raise SQLBuilderError(msg)
583
+ subquery_expr = exp.paren(parsed_subquery)
584
+ parameters: Any = safe_query.parameters
585
+ if isinstance(parameters, dict):
586
+ param_mapping: dict[str, str] = {}
587
+ query_builder = cast("QueryBuilder", builder)
588
+ for param_name, param_value in parameters.items():
589
+ unique_name = query_builder._generate_unique_parameter_name(param_name)
590
+ param_mapping[param_name] = unique_name
591
+ query_builder.add_parameter(param_value, name=unique_name)
592
+ if param_mapping:
593
+ updated = query_builder._update_placeholders_in_expression(parsed_subquery, param_mapping)
594
+ subquery_expr = exp.paren(updated)
595
+ elif isinstance(parameters, (list, tuple)):
596
+ for param_value in parameters:
597
+ builder.add_parameter(param_value)
598
+ elif parameters is not None:
599
+ builder.add_parameter(parameters)
600
+ return subquery_expr
601
+
602
+ if has_expression_and_sql(subquery):
603
+ self._merge_sql_object_parameters(subquery)
604
+ expression_attr = getattr(subquery, "expression", None)
605
+ if isinstance(expression_attr, exp.Expression):
606
+ return expression_attr
607
+ sql_text = getattr(subquery, "sql", "")
608
+ parsed_from_sql: exp.Expression | None = exp.maybe_parse(sql_text, dialect=builder.dialect)
609
+ if parsed_from_sql is None:
610
+ msg = f"Could not parse subquery SQL: {sql_text}"
611
+ raise SQLBuilderError(msg)
612
+ return parsed_from_sql
613
+
614
+ if isinstance(subquery, exp.Expression):
615
+ return subquery
616
+
617
+ if isinstance(subquery, str):
618
+ parsed_expression_from_str: exp.Expression | None = exp.maybe_parse(subquery, dialect=builder.dialect)
619
+ if parsed_expression_from_str is None:
620
+ msg = f"Could not parse subquery SQL: {subquery}"
621
+ raise SQLBuilderError(msg)
622
+ return parsed_expression_from_str
623
+
624
+ converted_expr: exp.Expression = exp.convert(subquery)
625
+ return converted_expr
626
+
627
+ def _create_or_expression(self, conditions: "list[exp.Expression]") -> exp.Expression:
628
+ if not conditions:
629
+ msg = "OR expression requires at least one condition"
630
+ raise SQLBuilderError(msg)
631
+
632
+ or_condition = conditions[0]
633
+ for condition in conditions[1:]:
634
+ or_condition = exp.Or(this=or_condition, expression=condition)
635
+ return or_condition
636
+
637
+ def _process_tuple_condition(self, condition: "tuple[Any, ...]") -> exp.Expression:
638
+ if len(condition) == PAIR_LENGTH:
639
+ column, value = condition
640
+ return self._create_parameterized_condition(
641
+ column, value, lambda col, placeholder: exp.EQ(this=col, expression=placeholder)
642
+ )
643
+
644
+ if len(condition) != TRIPLE_LENGTH:
645
+ msg = f"Condition tuple must have 2 or 3 elements, got {len(condition)}"
646
+ raise SQLBuilderError(msg)
647
+
648
+ column_raw, operator, value = condition
649
+ operator_upper = str(operator).upper()
650
+ column_expr = parse_column_expression(column_raw)
651
+ column_name = extract_column_name(column_raw)
652
+
653
+ simple_operator_map: dict[str, Callable[[exp.Expression, exp.Placeholder], exp.Expression]] = {
654
+ "=": lambda col, placeholder: exp.EQ(this=col, expression=placeholder),
655
+ "==": lambda col, placeholder: exp.EQ(this=col, expression=placeholder),
656
+ "!=": lambda col, placeholder: exp.NEQ(this=col, expression=placeholder),
657
+ "<>": lambda col, placeholder: exp.NEQ(this=col, expression=placeholder),
658
+ ">": lambda col, placeholder: exp.GT(this=col, expression=placeholder),
659
+ ">=": lambda col, placeholder: exp.GTE(this=col, expression=placeholder),
660
+ "<": lambda col, placeholder: exp.LT(this=col, expression=placeholder),
661
+ "<=": lambda col, placeholder: exp.LTE(this=col, expression=placeholder),
662
+ "LIKE": lambda col, placeholder: exp.Like(this=col, expression=placeholder),
663
+ "NOT LIKE": lambda col, placeholder: exp.Not(this=exp.Like(this=col, expression=placeholder)),
664
+ }
665
+
666
+ if operator_upper in simple_operator_map:
667
+ return self._create_parameterized_condition(column_raw, value, simple_operator_map[operator_upper])
668
+
669
+ if operator_upper == "IN":
670
+ return self._handle_in_operator(column_expr, value, column_name)
671
+ if operator_upper == "NOT IN":
672
+ return self._handle_not_in_operator(column_expr, value, column_name)
673
+ if operator_upper == "IS":
674
+ return self._handle_is_operator(column_expr, value)
675
+ if operator_upper == "IS NOT":
676
+ return self._handle_is_not_operator(column_expr, value)
677
+ if operator_upper == "BETWEEN":
678
+ return self._handle_between_operator(column_expr, value, column_name)
679
+ if operator_upper == "NOT BETWEEN":
680
+ return self._handle_not_between_operator(column_expr, value, column_name)
681
+
682
+ msg = f"Unsupported operator: {operator}"
683
+ raise SQLBuilderError(msg)
684
+
685
+ def _process_where_condition(
686
+ self,
687
+ condition: Union[
688
+ str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression", SQL
689
+ ],
690
+ values: tuple[Any, ...],
691
+ operator: str | None,
692
+ kwargs: dict[str, Any],
693
+ ) -> exp.Expression:
694
+ if values or kwargs:
695
+ if not isinstance(condition, str):
696
+ msg = "When values are provided, condition must be a string"
697
+ raise SQLBuilderError(msg)
698
+
699
+ validator = ParameterValidator()
700
+ param_info = validator.extract_parameters(condition)
701
+
702
+ if param_info:
703
+ param_dict = dict(kwargs)
704
+ positional_params = [
705
+ info
706
+ for info in param_info
707
+ if info.style in {ParameterStyle.NUMERIC, ParameterStyle.POSITIONAL_COLON, ParameterStyle.QMARK}
708
+ ]
709
+
710
+ if len(values) != len(positional_params):
711
+ msg = (
712
+ "Parameter count mismatch: condition has "
713
+ f"{len(positional_params)} positional placeholders, got {len(values)} values"
714
+ )
715
+ raise SQLBuilderError(msg)
716
+
717
+ for index, value in enumerate(values):
718
+ param_dict[f"param_{index}"] = value
719
+
720
+ condition = SQL(condition, param_dict)
721
+ elif len(values) == 1 and not kwargs:
722
+ if operator is not None:
723
+ return self._process_tuple_condition((condition, operator, values[0]))
724
+ return self._process_tuple_condition((condition, values[0]))
725
+ else:
726
+ msg = f"Cannot bind parameters to condition without placeholders: {condition}"
727
+ raise SQLBuilderError(msg)
728
+
729
+ builder = cast("SQLBuilderProtocol", self)
730
+
731
+ if isinstance(condition, str):
732
+ return parse_condition_expression(condition)
733
+ if isinstance(condition, (exp.Expression, exp.Condition)):
734
+ return condition
735
+ if isinstance(condition, tuple):
736
+ return self._process_tuple_condition(condition)
737
+ if has_query_builder_parameters(condition):
738
+ column_expr_obj = cast("ColumnExpression", condition)
739
+ expression_attr = cast("exp.Expression | None", getattr(column_expr_obj, "_expression", None))
740
+ if expression_attr is None:
741
+ msg = "Column expression is missing underlying sqlglot expression."
742
+ raise SQLBuilderError(msg)
743
+ return expression_attr
744
+ if has_sqlglot_expression(condition):
745
+ raw_expr = getattr(condition, "sqlglot_expression", None)
746
+ if isinstance(raw_expr, exp.Expression):
747
+ return builder._parameterize_expression(raw_expr)
748
+ return parse_condition_expression(str(condition))
749
+ if has_expression_and_sql(condition):
750
+ expression_attr = getattr(condition, "expression", None)
751
+ if isinstance(expression_attr, exp.Expression):
752
+ self._merge_sql_object_parameters(condition)
753
+ return expression_attr
754
+ sql_text = getattr(condition, "sql", "")
755
+ self._merge_sql_object_parameters(condition)
756
+ return parse_condition_expression(sql_text)
757
+
758
+ msg = f"Unsupported condition type: {type(condition).__name__}"
759
+ raise SQLBuilderError(msg)
760
+
761
+ def where(
762
+ self,
763
+ condition: Union[
764
+ str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression", SQL
765
+ ],
766
+ *values: Any,
767
+ operator: str | None = None,
768
+ **kwargs: Any,
769
+ ) -> Self:
770
+ builder = cast("SQLBuilderProtocol", self)
771
+ current_expr = builder.get_expression()
772
+ if current_expr is None:
773
+ msg = "Cannot add WHERE clause: expression is not initialized."
774
+ raise SQLBuilderError(msg)
775
+
776
+ if isinstance(current_expr, exp.Delete) and not current_expr.args.get("this"):
777
+ msg = "WHERE clause requires a table to be set. Use from() to set the table first."
778
+ raise SQLBuilderError(msg)
779
+
780
+ where_expr = self._process_where_condition(condition, values, operator, kwargs)
781
+
782
+ if isinstance(current_expr, (exp.Select, exp.Update, exp.Delete)):
783
+ updated_expr = current_expr.where(where_expr, copy=False)
784
+ builder.set_expression(updated_expr)
785
+ return cast("Self", builder)
786
+ msg = f"WHERE clause not supported for {type(current_expr).__name__}"
787
+ raise SQLBuilderError(msg)
788
+
789
+ def where_eq(self, column: str | exp.Column, value: Any) -> Self:
790
+ condition = self._create_parameterized_condition(
791
+ column, value, lambda col, placeholder: exp.EQ(this=col, expression=placeholder)
792
+ )
793
+ return self.where(condition)
794
+
795
+ def where_neq(self, column: str | exp.Column, value: Any) -> Self:
796
+ condition = self._create_parameterized_condition(
797
+ column, value, lambda col, placeholder: exp.NEQ(this=col, expression=placeholder)
798
+ )
799
+ return self.where(condition)
800
+
801
+ def where_lt(self, column: str | exp.Column, value: Any) -> Self:
802
+ condition = self._create_parameterized_condition(
803
+ column, value, lambda col, placeholder: exp.LT(this=col, expression=placeholder)
804
+ )
805
+ return self.where(condition)
806
+
807
+ def where_lte(self, column: str | exp.Column, value: Any) -> Self:
808
+ condition = self._create_parameterized_condition(
809
+ column, value, lambda col, placeholder: exp.LTE(this=col, expression=placeholder)
810
+ )
811
+ return self.where(condition)
812
+
813
+ def where_gt(self, column: str | exp.Column, value: Any) -> Self:
814
+ condition = self._create_parameterized_condition(
815
+ column, value, lambda col, placeholder: exp.GT(this=col, expression=placeholder)
816
+ )
817
+ return self.where(condition)
818
+
819
+ def where_gte(self, column: str | exp.Column, value: Any) -> Self:
820
+ condition = self._create_parameterized_condition(
821
+ column, value, lambda col, placeholder: exp.GTE(this=col, expression=placeholder)
822
+ )
823
+ return self.where(condition)
824
+
825
+ def where_between(self, column: str | exp.Column, low: Any, high: Any) -> Self:
826
+ builder = cast("SQLBuilderProtocol", self)
827
+ column_name = extract_column_name(column)
828
+ low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
829
+ high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
830
+ _, low_param = builder.add_parameter(low, name=low_param)
831
+ _, high_param = builder.add_parameter(high, name=high_param)
832
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
833
+ condition: exp.Expression = col_expr.between(exp.Placeholder(this=low_param), exp.Placeholder(this=high_param))
834
+ return self.where(condition)
835
+
836
+ def where_like(self, column: str | exp.Column, pattern: str, escape: str | None = None) -> Self:
837
+ builder = cast("SQLBuilderProtocol", self)
838
+ column_name = extract_column_name(column)
839
+ param_name = builder._generate_unique_parameter_name(column_name)
840
+ _, param_name = builder.add_parameter(pattern, name=param_name)
841
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
842
+ if escape is not None:
843
+ condition = exp.Like(
844
+ this=col_expr, expression=exp.Placeholder(this=param_name), escape=exp.convert(str(escape))
845
+ )
846
+ else:
847
+ condition = col_expr.like(exp.Placeholder(this=param_name))
848
+ return self.where(condition)
849
+
850
+ def where_not_like(self, column: str | exp.Column, pattern: str) -> Self:
851
+ condition = self._create_parameterized_condition(
852
+ column, pattern, lambda col, placeholder: exp.Not(this=exp.Like(this=col, expression=placeholder))
853
+ )
854
+ return self.where(condition)
855
+
856
+ def where_ilike(self, column: str | exp.Column, pattern: str) -> Self:
857
+ condition = self._create_parameterized_condition(
858
+ column, pattern, lambda col, placeholder: col.ilike(placeholder)
859
+ )
860
+ return self.where(condition)
861
+
862
+ def where_is_null(self, column: str | exp.Column) -> Self:
863
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
864
+ condition: exp.Expression = col_expr.is_(exp.null())
865
+ return self.where(condition)
866
+
867
+ def where_is_not_null(self, column: str | exp.Column) -> Self:
868
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
869
+ condition: exp.Expression = col_expr.is_(exp.null()).not_()
870
+ return self.where(condition)
871
+
872
+ def where_in(self, column: str | exp.Column, values: Any) -> Self:
873
+ builder = cast("SQLBuilderProtocol", self)
874
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
875
+ if has_query_builder_parameters(values) or isinstance(values, (exp.Expression, str)):
876
+ subquery_exp = self._normalize_subquery_expression(values, builder)
877
+ return self.where(exp.In(this=col_expr, expressions=[subquery_exp]))
27
878
 
28
- __all__ = ("Select",)
879
+ condition = self._handle_in_operator(col_expr, values, extract_column_name(column))
880
+ return self.where(condition)
881
+
882
+ def where_not_in(self, column: str | exp.Column, values: Any) -> Self:
883
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
884
+ condition = self._handle_not_in_operator(col_expr, values, extract_column_name(column))
885
+ return self.where(condition)
886
+
887
+ def where_any(self, column: str | exp.Column, subquery: Any) -> Self:
888
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
889
+ column_name = extract_column_name(column)
890
+ condition = self._create_any_condition(col_expr, subquery, column_name)
891
+ return self.where(condition)
892
+
893
+ def where_not_any(self, column: str | exp.Column, subquery: Any) -> Self:
894
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
895
+ column_name = extract_column_name(column)
896
+ condition = self._create_not_any_condition(col_expr, subquery, column_name)
897
+ return self.where(condition)
898
+
899
+ def where_exists(self, subquery: Any) -> Self:
900
+ builder = cast("SQLBuilderProtocol", self)
901
+ subquery_expr = self._normalize_subquery_expression(subquery, builder)
902
+ return self.where(exp.Exists(this=subquery_expr))
903
+
904
+ def where_not_exists(self, subquery: Any) -> Self:
905
+ builder = cast("SQLBuilderProtocol", self)
906
+ subquery_expr = self._normalize_subquery_expression(subquery, builder)
907
+ return self.where(exp.Not(this=exp.Exists(this=subquery_expr)))
908
+
909
+ def where_like_any(self, column: str | exp.Column, patterns: list[str]) -> Self:
910
+ conditions = [
911
+ self._create_parameterized_condition(column, pattern, lambda col, placeholder: col.like(placeholder))
912
+ for pattern in patterns
913
+ ]
914
+ or_condition = self._create_or_expression(conditions)
915
+ return self.where(or_condition)
916
+
917
+ def or_where_eq(self, column: str | exp.Column, value: Any) -> Self:
918
+ condition = self._create_parameterized_condition(
919
+ column, value, lambda col, placeholder: exp.EQ(this=col, expression=placeholder)
920
+ )
921
+ return self._combine_with_or(condition)
922
+
923
+ def or_where_neq(self, column: str | exp.Column, value: Any) -> Self:
924
+ condition = self._create_parameterized_condition(
925
+ column, value, lambda col, placeholder: exp.NEQ(this=col, expression=placeholder)
926
+ )
927
+ return self._combine_with_or(condition)
928
+
929
+ def or_where_lt(self, column: str | exp.Column, value: Any) -> Self:
930
+ condition = self._create_parameterized_condition(
931
+ column, value, lambda col, placeholder: exp.LT(this=col, expression=placeholder)
932
+ )
933
+ return self._combine_with_or(condition)
934
+
935
+ def or_where_lte(self, column: str | exp.Column, value: Any) -> Self:
936
+ condition = self._create_parameterized_condition(
937
+ column, value, lambda col, placeholder: exp.LTE(this=col, expression=placeholder)
938
+ )
939
+ return self._combine_with_or(condition)
940
+
941
+ def or_where_gt(self, column: str | exp.Column, value: Any) -> Self:
942
+ condition = self._create_parameterized_condition(
943
+ column, value, lambda col, placeholder: exp.GT(this=col, expression=placeholder)
944
+ )
945
+ return self._combine_with_or(condition)
946
+
947
+ def or_where_gte(self, column: str | exp.Column, value: Any) -> Self:
948
+ condition = self._create_parameterized_condition(
949
+ column, value, lambda col, placeholder: exp.GTE(this=col, expression=placeholder)
950
+ )
951
+ return self._combine_with_or(condition)
952
+
953
+ def or_where_between(self, column: str | exp.Column, low: Any, high: Any) -> Self:
954
+ column_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
955
+ condition = self._handle_between_operator(column_expr, (low, high), extract_column_name(column))
956
+ return self._combine_with_or(condition)
957
+
958
+ def or_where_like(self, column: str | exp.Column, pattern: str, escape: str | None = None) -> Self:
959
+ column_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
960
+ builder = cast("SQLBuilderProtocol", self)
961
+ column_name = extract_column_name(column)
962
+ param_name = builder._generate_unique_parameter_name(column_name)
963
+ _, param_name = builder.add_parameter(pattern, name=param_name)
964
+ placeholder = exp.Placeholder(this=param_name)
965
+ if escape is not None:
966
+ condition = exp.Like(this=column_expr, expression=placeholder, escape=exp.convert(str(escape)))
967
+ else:
968
+ condition = column_expr.like(placeholder)
969
+ return self._combine_with_or(cast("exp.Expression", condition))
970
+
971
+ def or_where_not_like(self, column: str | exp.Column, pattern: str) -> Self:
972
+ condition = self._create_parameterized_condition(
973
+ column, pattern, lambda col, placeholder: col.like(placeholder).not_()
974
+ )
975
+ return self._combine_with_or(condition)
976
+
977
+ def or_where_ilike(self, column: str | exp.Column, pattern: str) -> Self:
978
+ condition = self._create_parameterized_condition(
979
+ column, pattern, lambda col, placeholder: col.ilike(placeholder)
980
+ )
981
+ return self._combine_with_or(condition)
982
+
983
+ def or_where_is_null(self, column: str | exp.Column) -> Self:
984
+ column_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
985
+ condition: exp.Expression = column_expr.is_(exp.null())
986
+ return self._combine_with_or(condition)
987
+
988
+ def or_where_is_not_null(self, column: str | exp.Column) -> Self:
989
+ column_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
990
+ condition: exp.Expression = column_expr.is_(exp.null()).not_()
991
+ return self._combine_with_or(condition)
992
+
993
+ def or_where_null(self, column: str | exp.Column) -> Self:
994
+ return self.or_where_is_null(column)
995
+
996
+ def or_where_not_null(self, column: str | exp.Column) -> Self:
997
+ return self.or_where_is_not_null(column)
998
+
999
+ def or_where_in(self, column: str | exp.Column, values: Any) -> Self:
1000
+ column_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
1001
+ condition = self._handle_in_operator(column_expr, values, extract_column_name(column))
1002
+ return self._combine_with_or(condition)
1003
+
1004
+ def or_where_not_in(self, column: str | exp.Column, values: Any) -> Self:
1005
+ column_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
1006
+ condition = self._handle_not_in_operator(column_expr, values, extract_column_name(column))
1007
+ return self._combine_with_or(condition)
1008
+
1009
+ def or_where_any(self, column: str | exp.Column, subquery: Any) -> Self:
1010
+ column_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
1011
+ condition = self._create_any_condition(column_expr, subquery, extract_column_name(column))
1012
+ return self._combine_with_or(condition)
1013
+
1014
+ def or_where_not_any(self, column: str | exp.Column, subquery: Any) -> Self:
1015
+ column_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
1016
+ condition = self._create_not_any_condition(column_expr, subquery, extract_column_name(column))
1017
+ return self._combine_with_or(condition)
1018
+
1019
+ def or_where_exists(self, subquery: Any) -> Self:
1020
+ builder = cast("SQLBuilderProtocol", self)
1021
+ subquery_expr = self._normalize_subquery_expression(subquery, builder)
1022
+ condition = exp.Exists(this=subquery_expr)
1023
+ return self._combine_with_or(condition)
1024
+
1025
+ def or_where_not_exists(self, subquery: Any) -> Self:
1026
+ builder = cast("SQLBuilderProtocol", self)
1027
+ subquery_expr = self._normalize_subquery_expression(subquery, builder)
1028
+ condition = exp.Not(this=exp.Exists(this=subquery_expr))
1029
+ return self._combine_with_or(condition)
1030
+
1031
+ def where_or(self, *conditions: str | tuple[str, Any] | tuple[str, str, Any] | exp.Expression) -> Self:
1032
+ if not conditions:
1033
+ msg = "where_or() requires at least one condition"
1034
+ raise SQLBuilderError(msg)
1035
+
1036
+ builder = cast("SQLBuilderProtocol", self)
1037
+ if builder.get_expression() is None:
1038
+ msg = "Cannot add WHERE OR clause: expression is not initialized."
1039
+ raise SQLBuilderError(msg)
1040
+
1041
+ processed_conditions = [self._process_where_condition(condition, (), None, {}) for condition in conditions]
1042
+ or_condition = self._create_or_expression(processed_conditions)
1043
+ return self.where(or_condition)
1044
+
1045
+ def or_where(
1046
+ self,
1047
+ condition: Union[
1048
+ str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression", SQL
1049
+ ],
1050
+ *values: Any,
1051
+ operator: str | None = None,
1052
+ **kwargs: Any,
1053
+ ) -> Self:
1054
+ or_condition = self._process_where_condition(condition, values, operator, kwargs)
1055
+ return self._combine_with_or(or_condition)
1056
+
1057
+
1058
+ @trait
1059
+ class HavingClauseMixin:
1060
+ __slots__ = ()
1061
+
1062
+ def having(self, condition: str | exp.Expression | exp.Condition | tuple[str, Any] | tuple[str, str, Any]) -> Self:
1063
+ builder = cast("SQLBuilderProtocol", self)
1064
+ current_expr = builder.get_expression()
1065
+ if current_expr is None or not isinstance(current_expr, exp.Select):
1066
+ return cast("Self", builder)
1067
+
1068
+ if isinstance(condition, tuple):
1069
+ where_mixin = cast("WhereClauseMixin", self)
1070
+ having_expr = where_mixin._process_tuple_condition(condition)
1071
+ else:
1072
+ having_expr = parse_condition_expression(condition)
1073
+
1074
+ builder.set_expression(current_expr.having(having_expr, copy=False))
1075
+ return cast("Self", builder)
1076
+
1077
+
1078
+ @trait
1079
+ class PivotClauseMixin:
1080
+ __slots__ = ()
1081
+
1082
+ def pivot(
1083
+ self,
1084
+ aggregate_function: str | exp.Expression,
1085
+ aggregate_column: str | exp.Expression,
1086
+ pivot_column: str | exp.Expression,
1087
+ pivot_values: list[str | int | float | exp.Expression],
1088
+ alias: str | None = None,
1089
+ ) -> "Select":
1090
+ builder = cast("SQLBuilderProtocol", self)
1091
+ current_expr = builder.get_expression()
1092
+ if not isinstance(current_expr, exp.Select):
1093
+ msg = "Pivot can only be applied to a Select expression managed by SelectBuilder."
1094
+ raise TypeError(msg)
1095
+
1096
+ agg_name = aggregate_function if isinstance(aggregate_function, str) else aggregate_function.name
1097
+ agg_column = exp.column(aggregate_column) if isinstance(aggregate_column, str) else aggregate_column
1098
+ pivot_col_expr = exp.column(pivot_column) if isinstance(pivot_column, str) else pivot_column
1099
+
1100
+ pivot_agg_expr = exp.func(agg_name, agg_column)
1101
+
1102
+ pivot_value_exprs: list[exp.Expression] = []
1103
+ for raw_value in pivot_values:
1104
+ if isinstance(raw_value, exp.Expression):
1105
+ pivot_value_exprs.append(raw_value)
1106
+ elif isinstance(raw_value, (str, int, float)):
1107
+ pivot_value_exprs.append(exp.convert(raw_value))
1108
+ else:
1109
+ pivot_value_exprs.append(exp.convert(str(raw_value)))
1110
+
1111
+ in_expr = exp.In(this=pivot_col_expr, expressions=pivot_value_exprs)
1112
+ pivot_node = exp.Pivot(expressions=[pivot_agg_expr], fields=[in_expr], unpivot=False)
1113
+
1114
+ if alias:
1115
+ pivot_node.set("alias", exp.TableAlias(this=exp.to_identifier(alias)))
1116
+
1117
+ from_clause = current_expr.args.get("from")
1118
+ if from_clause and isinstance(from_clause, exp.From):
1119
+ table = from_clause.this
1120
+ if isinstance(table, exp.Table):
1121
+ existing = table.args.get("pivots", [])
1122
+ existing.append(pivot_node)
1123
+ table.set("pivots", existing)
1124
+
1125
+ return cast("Select", self)
1126
+
1127
+
1128
+ @trait
1129
+ class UnpivotClauseMixin:
1130
+ __slots__ = ()
1131
+
1132
+ def unpivot(
1133
+ self,
1134
+ value_column_name: str,
1135
+ name_column_name: str,
1136
+ columns_to_unpivot: list[str | exp.Expression],
1137
+ alias: str | None = None,
1138
+ ) -> "Select":
1139
+ builder = cast("SQLBuilderProtocol", self)
1140
+ current_expr = builder.get_expression()
1141
+ if not isinstance(current_expr, exp.Select):
1142
+ msg = "Unpivot can only be applied to a Select expression managed by Select."
1143
+ raise TypeError(msg)
1144
+
1145
+ value_identifier = exp.to_identifier(value_column_name)
1146
+ name_identifier = exp.to_identifier(name_column_name)
1147
+
1148
+ unpivot_columns: list[exp.Expression] = []
1149
+ for column in columns_to_unpivot:
1150
+ if isinstance(column, exp.Expression):
1151
+ unpivot_columns.append(column)
1152
+ elif isinstance(column, str):
1153
+ unpivot_columns.append(exp.column(column))
1154
+ else:
1155
+ unpivot_columns.append(exp.column(str(column)))
1156
+
1157
+ in_expr = exp.In(this=name_identifier, expressions=unpivot_columns)
1158
+ unpivot_node = exp.Pivot(expressions=[value_identifier], fields=[in_expr], unpivot=True)
1159
+
1160
+ if alias:
1161
+ unpivot_node.set("alias", exp.TableAlias(this=exp.to_identifier(alias)))
1162
+
1163
+ from_clause = current_expr.args.get("from")
1164
+ if from_clause and isinstance(from_clause, exp.From):
1165
+ table = from_clause.this
1166
+ if isinstance(table, exp.Table):
1167
+ existing = table.args.get("pivots", [])
1168
+ existing.append(unpivot_node)
1169
+ table.set("pivots", existing)
1170
+
1171
+ return cast("Select", self)
1172
+
1173
+
1174
+ @trait
1175
+ class CommonTableExpressionMixin:
1176
+ __slots__ = ()
1177
+
1178
+ def get_expression(self) -> exp.Expression | None: ...
1179
+ def set_expression(self, expression: exp.Expression) -> None: ...
1180
+
1181
+ _with_ctes: Any
1182
+ dialect: Any
1183
+
1184
+ def add_parameter(self, value: Any, name: str | None = None) -> tuple[Any, str]:
1185
+ msg = "Method must be provided by QueryBuilder subclass"
1186
+ raise NotImplementedError(msg)
1187
+
1188
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
1189
+ msg = "Method must be provided by QueryBuilder subclass"
1190
+ raise NotImplementedError(msg)
1191
+
1192
+ def _update_placeholders_in_expression(
1193
+ self, expression: exp.Expression, param_mapping: dict[str, str]
1194
+ ) -> exp.Expression:
1195
+ msg = "Method must be provided by QueryBuilder subclass"
1196
+ raise NotImplementedError(msg)
1197
+
1198
+ def with_(self, name: str, query: Any | str, recursive: bool = False, columns: list[str] | None = None) -> Self:
1199
+ builder = cast("QueryBuilder", self)
1200
+ expression = builder.get_expression()
1201
+ if expression is None:
1202
+ msg = "Cannot add WITH clause: expression not initialized."
1203
+ raise SQLBuilderError(msg)
1204
+
1205
+ if not isinstance(expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)):
1206
+ msg = f"Cannot add WITH clause to {type(expression).__name__} expression."
1207
+ raise SQLBuilderError(msg)
1208
+
1209
+ cte_select: exp.Expression | None
1210
+ if isinstance(query, str):
1211
+ cte_select = exp.maybe_parse(query, dialect=self.dialect)
1212
+ elif isinstance(query, exp.Expression):
1213
+ cte_select = query
1214
+ else:
1215
+ built_query = query.to_statement()
1216
+ cte_sql = built_query.sql
1217
+ cte_select = exp.maybe_parse(cte_sql, dialect=self.dialect)
1218
+
1219
+ parameters = built_query.parameters
1220
+ if isinstance(parameters, dict):
1221
+ param_mapping: dict[str, str] = {}
1222
+ for param_name, param_value in parameters.items():
1223
+ unique_name = self._generate_unique_parameter_name(f"{name}_{param_name}")
1224
+ param_mapping[param_name] = unique_name
1225
+ self.add_parameter(param_value, name=unique_name)
1226
+ if cte_select is not None:
1227
+ cte_select = self._update_placeholders_in_expression(cte_select, param_mapping)
1228
+ elif isinstance(parameters, (list, tuple)):
1229
+ for param_value in parameters:
1230
+ self.add_parameter(param_value)
1231
+ elif parameters is not None:
1232
+ self.add_parameter(parameters)
1233
+
1234
+ if cte_select is None:
1235
+ msg = f"Could not parse CTE query: {query}"
1236
+ raise SQLBuilderError(msg)
1237
+
1238
+ if columns:
1239
+ cte_alias_expr = exp.alias_(cte_select, name, table=[exp.to_identifier(col) for col in columns])
1240
+ else:
1241
+ cte_alias_expr = exp.alias_(cte_select, name)
1242
+
1243
+ existing_with = expression.args.get("with")
1244
+ if existing_with:
1245
+ existing_with.expressions.append(cte_alias_expr)
1246
+ if recursive:
1247
+ existing_with.set("recursive", recursive)
1248
+ else:
1249
+ if isinstance(expression, (exp.Select, exp.Insert, exp.Update)):
1250
+ updated = expression.with_(cte_alias_expr, as_=name, copy=False)
1251
+ builder.set_expression(updated)
1252
+ if recursive:
1253
+ with_clause = updated.find(exp.With)
1254
+ if with_clause:
1255
+ with_clause.set("recursive", recursive)
1256
+ builder._with_ctes[name] = exp.CTE(this=cte_select, alias=exp.to_table(name))
1257
+
1258
+ return cast("Self", builder)
1259
+
1260
+
1261
+ @trait
1262
+ class SetOperationMixin:
1263
+ __slots__ = ()
1264
+
1265
+ def get_expression(self) -> exp.Expression | None: ...
1266
+ def set_expression(self, expression: exp.Expression) -> None: ...
1267
+ def set_parameters(self, parameters: dict[str, Any]) -> None: ...
1268
+
1269
+ dialect: Any = None
1270
+
1271
+ def union(self, other: Any, all_: bool = False) -> Self:
1272
+ return self._combine_with_other(other, operator="union", distinct=not all_)
1273
+
1274
+ def intersect(self, other: Any) -> Self:
1275
+ return self._combine_with_other(other, operator="intersect", distinct=True)
1276
+
1277
+ def except_(self, other: Any) -> Self:
1278
+ return self._combine_with_other(other, operator="except", distinct=True)
1279
+
1280
+ def _combine_with_other(self, other: Any, *, operator: str, distinct: bool) -> Self:
1281
+ builder = cast("QueryBuilder", self)
1282
+
1283
+ if not hasattr(other, "_build_final_expression") or not hasattr(other, "parameters"):
1284
+ msg = "Set operations require another SQLSpec query builder."
1285
+ raise SQLBuilderError(msg)
1286
+
1287
+ other_builder = cast("QueryBuilder", other)
1288
+ left_expr = builder._build_final_expression(copy=True)
1289
+ right_expr = other_builder._build_final_expression(copy=True)
1290
+
1291
+ merged_parameters: dict[str, Any] = dict(builder.parameters)
1292
+ rename_map: dict[str, str] = {}
1293
+ for param_name, param_value in other_builder.parameters.items():
1294
+ target_name = param_name
1295
+ if target_name in merged_parameters:
1296
+ counter = 1
1297
+ while True:
1298
+ candidate = f"{param_name}_right_{counter}"
1299
+ if candidate not in merged_parameters:
1300
+ target_name = candidate
1301
+ break
1302
+ counter += 1
1303
+ rename_map[param_name] = target_name
1304
+ merged_parameters[target_name] = param_value
1305
+
1306
+ if rename_map:
1307
+ right_expr = builder._update_placeholders_in_expression(right_expr, rename_map)
1308
+
1309
+ combined: exp.Expression
1310
+ if operator == "union":
1311
+ combined = exp.union(left_expr, right_expr, distinct=distinct)
1312
+ elif operator == "intersect":
1313
+ combined = exp.intersect(left_expr, right_expr, distinct=distinct)
1314
+ elif operator == "except":
1315
+ combined = exp.except_(left_expr, right_expr)
1316
+ else: # pragma: no cover - defensive
1317
+ msg = f"Unsupported set operation: {operator}"
1318
+ raise SQLBuilderError(msg)
1319
+
1320
+ new_builder = builder._spawn_like_self()
1321
+ new_builder.set_expression(combined)
1322
+ new_builder.set_parameters(merged_parameters)
1323
+ return cast("Self", new_builder)
29
1324
 
30
1325
 
31
1326
  TABLE_HINT_PATTERN: Final[str] = r"\b{}\b(\s+AS\s+\w+)?"
@@ -57,8 +1352,8 @@ class Select(
57
1352
  >>> result = driver.execute(builder)
58
1353
  """
59
1354
 
60
- __slots__ = ("_hints", "_with_parts")
61
- _expression: Optional[exp.Expression]
1355
+ __slots__ = ("_hints",)
1356
+ _expression: exp.Expression | None
62
1357
 
63
1358
  def __init__(self, *columns: str, **kwargs: Any) -> None:
64
1359
  """Initialize SELECT with optional columns.
@@ -73,8 +1368,6 @@ class Select(
73
1368
  """
74
1369
  super().__init__(**kwargs)
75
1370
 
76
- # Initialize Select-specific attributes
77
- self._with_parts: dict[str, Union[exp.CTE, Select]] = {}
78
1371
  self._hints: list[dict[str, object]] = []
79
1372
 
80
1373
  self._initialize_expression()
@@ -98,12 +1391,7 @@ class Select(
98
1391
  return self._expression
99
1392
 
100
1393
  def with_hint(
101
- self,
102
- hint: "str",
103
- *,
104
- location: "str" = "statement",
105
- table: "Optional[str]" = None,
106
- dialect: "Optional[str]" = None,
1394
+ self, hint: "str", *, location: "str" = "statement", table: "str | None" = None, dialect: "str | None" = None
107
1395
  ) -> "Self":
108
1396
  """Attach an optimizer or dialect-specific hint to the query.
109
1397
 
@@ -139,7 +1427,7 @@ class Select(
139
1427
  def parse_hint_safely(hint: Any) -> exp.Expression:
140
1428
  try:
141
1429
  hint_str = str(hint)
142
- hint_expr: Optional[exp.Expression] = exp.maybe_parse(hint_str, dialect=self.dialect_name)
1430
+ hint_expr: exp.Expression | None = exp.maybe_parse(hint_str, dialect=self.dialect_name)
143
1431
  return hint_expr or exp.Anonymous(this=hint_str)
144
1432
  except Exception:
145
1433
  return exp.Anonymous(this=str(hint))
@@ -169,3 +1457,148 @@ class Select(
169
1457
  )
170
1458
 
171
1459
  return SafeQuery(sql=modified_sql, parameters=safe_query.parameters, dialect=safe_query.dialect)
1460
+
1461
+ def _validate_select_expression(self) -> None:
1462
+ """Validate that current expression is a valid SELECT statement.
1463
+
1464
+ Raises:
1465
+ SQLBuilderError: If expression is None or not a SELECT statement
1466
+ """
1467
+ if self._expression is None or not isinstance(self._expression, exp.Select):
1468
+ msg = "Locking clauses can only be applied to SELECT statements"
1469
+ raise SQLBuilderError(msg)
1470
+
1471
+ def _validate_lock_parameters(self, skip_locked: bool, nowait: bool) -> None:
1472
+ """Validate locking parameters for conflicting options.
1473
+
1474
+ Args:
1475
+ skip_locked: Whether SKIP LOCKED option is enabled
1476
+ nowait: Whether NOWAIT option is enabled
1477
+
1478
+ Raises:
1479
+ SQLBuilderError: If both skip_locked and nowait are True
1480
+ """
1481
+ if skip_locked and nowait:
1482
+ msg = "Cannot use both skip_locked and nowait"
1483
+ raise SQLBuilderError(msg)
1484
+
1485
+ def for_update(
1486
+ self, *, skip_locked: bool = False, nowait: bool = False, of: "str | list[str] | None" = None
1487
+ ) -> "Self":
1488
+ """Add FOR UPDATE clause to SELECT statement for row-level locking.
1489
+
1490
+ Args:
1491
+ skip_locked: Skip rows that are already locked (SKIP LOCKED)
1492
+ nowait: Return immediately if row is locked (NOWAIT)
1493
+ of: Table names/aliases to lock (FOR UPDATE OF table)
1494
+
1495
+ Returns:
1496
+ Self for method chaining
1497
+ """
1498
+ self._validate_select_expression()
1499
+ self._validate_lock_parameters(skip_locked, nowait)
1500
+
1501
+ assert self._expression is not None
1502
+ select_expr = cast("exp.Select", self._expression)
1503
+
1504
+ lock_args = {"update": True}
1505
+
1506
+ if skip_locked:
1507
+ lock_args["wait"] = False
1508
+ elif nowait:
1509
+ lock_args["wait"] = True
1510
+
1511
+ if of:
1512
+ tables = [of] if isinstance(of, str) else of
1513
+ lock_args["expressions"] = [exp.table_(t) for t in tables] # type: ignore[assignment]
1514
+
1515
+ lock = exp.Lock(**lock_args)
1516
+
1517
+ current_locks = select_expr.args.get("locks", [])
1518
+ current_locks.append(lock)
1519
+ select_expr.set("locks", current_locks)
1520
+
1521
+ return self
1522
+
1523
+ def for_share(
1524
+ self, *, skip_locked: bool = False, nowait: bool = False, of: "str | list[str] | None" = None
1525
+ ) -> "Self":
1526
+ """Add FOR SHARE clause for shared row-level locking.
1527
+
1528
+ Args:
1529
+ skip_locked: Skip rows that are already locked (SKIP LOCKED)
1530
+ nowait: Return immediately if row is locked (NOWAIT)
1531
+ of: Table names/aliases to lock (FOR SHARE OF table)
1532
+
1533
+ Returns:
1534
+ Self for method chaining
1535
+ """
1536
+ self._validate_select_expression()
1537
+ self._validate_lock_parameters(skip_locked, nowait)
1538
+
1539
+ assert self._expression is not None
1540
+ select_expr = cast("exp.Select", self._expression)
1541
+
1542
+ lock_args = {"update": False}
1543
+
1544
+ if skip_locked:
1545
+ lock_args["wait"] = False
1546
+ elif nowait:
1547
+ lock_args["wait"] = True
1548
+
1549
+ if of:
1550
+ tables = [of] if isinstance(of, str) else of
1551
+ lock_args["expressions"] = [exp.table_(t) for t in tables] # type: ignore[assignment]
1552
+
1553
+ lock = exp.Lock(**lock_args)
1554
+
1555
+ current_locks = select_expr.args.get("locks", [])
1556
+ current_locks.append(lock)
1557
+ select_expr.set("locks", current_locks)
1558
+
1559
+ return self
1560
+
1561
+ def for_key_share(self) -> "Self":
1562
+ """Add FOR KEY SHARE clause (PostgreSQL-specific).
1563
+
1564
+ FOR KEY SHARE is like FOR SHARE, but the lock is weaker:
1565
+ SELECT FOR UPDATE is blocked, but not SELECT FOR NO KEY UPDATE.
1566
+
1567
+ Returns:
1568
+ Self for method chaining
1569
+ """
1570
+ self._validate_select_expression()
1571
+
1572
+ assert self._expression is not None
1573
+ select_expr = cast("exp.Select", self._expression)
1574
+
1575
+ lock = exp.Lock(update=False, key=True)
1576
+
1577
+ current_locks = select_expr.args.get("locks", [])
1578
+ current_locks.append(lock)
1579
+ select_expr.set("locks", current_locks)
1580
+
1581
+ return self
1582
+
1583
+ def for_no_key_update(self) -> "Self":
1584
+ """Add FOR NO KEY UPDATE clause (PostgreSQL-specific).
1585
+
1586
+ FOR NO KEY UPDATE is like FOR UPDATE, but the lock is weaker:
1587
+ it does not block SELECT FOR KEY SHARE commands that attempt to
1588
+ acquire a share lock on the same rows.
1589
+
1590
+ Returns:
1591
+ Self for method chaining
1592
+ """
1593
+ self._validate_select_expression()
1594
+
1595
+ assert self._expression is not None
1596
+ select_expr = cast("exp.Select", self._expression)
1597
+
1598
+ lock = exp.Lock(update=True, key=False)
1599
+
1600
+ current_locks = select_expr.args.get("locks", [])
1601
+ current_locks.append(lock)
1602
+ select_expr.set("locks", current_locks)
1603
+
1604
+ return self