sqlspec 0.11.0__py3-none-any.whl → 0.12.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 (155) hide show
  1. sqlspec/__init__.py +16 -3
  2. sqlspec/_serialization.py +3 -10
  3. sqlspec/_sql.py +1147 -0
  4. sqlspec/_typing.py +343 -41
  5. sqlspec/adapters/adbc/__init__.py +2 -6
  6. sqlspec/adapters/adbc/config.py +474 -149
  7. sqlspec/adapters/adbc/driver.py +330 -644
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -462
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +217 -451
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +418 -498
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +592 -634
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +393 -436
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +549 -942
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -550
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +741 -0
  31. sqlspec/adapters/psycopg/driver.py +732 -733
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +243 -426
  35. sqlspec/base.py +220 -825
  36. sqlspec/config.py +354 -0
  37. sqlspec/driver/__init__.py +22 -0
  38. sqlspec/driver/_async.py +252 -0
  39. sqlspec/driver/_common.py +338 -0
  40. sqlspec/driver/_sync.py +261 -0
  41. sqlspec/driver/mixins/__init__.py +17 -0
  42. sqlspec/driver/mixins/_pipeline.py +523 -0
  43. sqlspec/driver/mixins/_result_utils.py +122 -0
  44. sqlspec/driver/mixins/_sql_translator.py +35 -0
  45. sqlspec/driver/mixins/_storage.py +993 -0
  46. sqlspec/driver/mixins/_type_coercion.py +131 -0
  47. sqlspec/exceptions.py +299 -7
  48. sqlspec/extensions/aiosql/__init__.py +10 -0
  49. sqlspec/extensions/aiosql/adapter.py +474 -0
  50. sqlspec/extensions/litestar/__init__.py +1 -6
  51. sqlspec/extensions/litestar/_utils.py +1 -5
  52. sqlspec/extensions/litestar/config.py +5 -6
  53. sqlspec/extensions/litestar/handlers.py +13 -12
  54. sqlspec/extensions/litestar/plugin.py +22 -24
  55. sqlspec/extensions/litestar/providers.py +37 -55
  56. sqlspec/loader.py +528 -0
  57. sqlspec/service/__init__.py +3 -0
  58. sqlspec/service/base.py +24 -0
  59. sqlspec/service/pagination.py +26 -0
  60. sqlspec/statement/__init__.py +21 -0
  61. sqlspec/statement/builder/__init__.py +54 -0
  62. sqlspec/statement/builder/_ddl_utils.py +119 -0
  63. sqlspec/statement/builder/_parsing_utils.py +135 -0
  64. sqlspec/statement/builder/base.py +328 -0
  65. sqlspec/statement/builder/ddl.py +1379 -0
  66. sqlspec/statement/builder/delete.py +80 -0
  67. sqlspec/statement/builder/insert.py +274 -0
  68. sqlspec/statement/builder/merge.py +95 -0
  69. sqlspec/statement/builder/mixins/__init__.py +65 -0
  70. sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
  71. sqlspec/statement/builder/mixins/_case_builder.py +91 -0
  72. sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
  73. sqlspec/statement/builder/mixins/_delete_from.py +34 -0
  74. sqlspec/statement/builder/mixins/_from.py +61 -0
  75. sqlspec/statement/builder/mixins/_group_by.py +119 -0
  76. sqlspec/statement/builder/mixins/_having.py +35 -0
  77. sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
  78. sqlspec/statement/builder/mixins/_insert_into.py +36 -0
  79. sqlspec/statement/builder/mixins/_insert_values.py +69 -0
  80. sqlspec/statement/builder/mixins/_join.py +110 -0
  81. sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
  82. sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
  83. sqlspec/statement/builder/mixins/_order_by.py +46 -0
  84. sqlspec/statement/builder/mixins/_pivot.py +82 -0
  85. sqlspec/statement/builder/mixins/_returning.py +37 -0
  86. sqlspec/statement/builder/mixins/_select_columns.py +60 -0
  87. sqlspec/statement/builder/mixins/_set_ops.py +122 -0
  88. sqlspec/statement/builder/mixins/_unpivot.py +80 -0
  89. sqlspec/statement/builder/mixins/_update_from.py +54 -0
  90. sqlspec/statement/builder/mixins/_update_set.py +91 -0
  91. sqlspec/statement/builder/mixins/_update_table.py +29 -0
  92. sqlspec/statement/builder/mixins/_where.py +374 -0
  93. sqlspec/statement/builder/mixins/_window_functions.py +86 -0
  94. sqlspec/statement/builder/protocols.py +20 -0
  95. sqlspec/statement/builder/select.py +206 -0
  96. sqlspec/statement/builder/update.py +178 -0
  97. sqlspec/statement/filters.py +571 -0
  98. sqlspec/statement/parameters.py +736 -0
  99. sqlspec/statement/pipelines/__init__.py +67 -0
  100. sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
  101. sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
  102. sqlspec/statement/pipelines/base.py +315 -0
  103. sqlspec/statement/pipelines/context.py +119 -0
  104. sqlspec/statement/pipelines/result_types.py +41 -0
  105. sqlspec/statement/pipelines/transformers/__init__.py +8 -0
  106. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
  107. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
  108. sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
  109. sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
  110. sqlspec/statement/pipelines/validators/__init__.py +23 -0
  111. sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
  112. sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
  113. sqlspec/statement/pipelines/validators/_performance.py +703 -0
  114. sqlspec/statement/pipelines/validators/_security.py +990 -0
  115. sqlspec/statement/pipelines/validators/base.py +67 -0
  116. sqlspec/statement/result.py +527 -0
  117. sqlspec/statement/splitter.py +701 -0
  118. sqlspec/statement/sql.py +1198 -0
  119. sqlspec/storage/__init__.py +15 -0
  120. sqlspec/storage/backends/__init__.py +0 -0
  121. sqlspec/storage/backends/base.py +166 -0
  122. sqlspec/storage/backends/fsspec.py +315 -0
  123. sqlspec/storage/backends/obstore.py +464 -0
  124. sqlspec/storage/protocol.py +170 -0
  125. sqlspec/storage/registry.py +315 -0
  126. sqlspec/typing.py +157 -36
  127. sqlspec/utils/correlation.py +155 -0
  128. sqlspec/utils/deprecation.py +3 -6
  129. sqlspec/utils/fixtures.py +6 -11
  130. sqlspec/utils/logging.py +135 -0
  131. sqlspec/utils/module_loader.py +45 -43
  132. sqlspec/utils/serializers.py +4 -0
  133. sqlspec/utils/singleton.py +6 -8
  134. sqlspec/utils/sync_tools.py +15 -27
  135. sqlspec/utils/text.py +58 -26
  136. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/METADATA +100 -26
  137. sqlspec-0.12.0.dist-info/RECORD +145 -0
  138. sqlspec/adapters/bigquery/config/__init__.py +0 -3
  139. sqlspec/adapters/bigquery/config/_common.py +0 -40
  140. sqlspec/adapters/bigquery/config/_sync.py +0 -87
  141. sqlspec/adapters/oracledb/config/__init__.py +0 -9
  142. sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
  143. sqlspec/adapters/oracledb/config/_common.py +0 -131
  144. sqlspec/adapters/oracledb/config/_sync.py +0 -186
  145. sqlspec/adapters/psycopg/config/__init__.py +0 -19
  146. sqlspec/adapters/psycopg/config/_async.py +0 -169
  147. sqlspec/adapters/psycopg/config/_common.py +0 -56
  148. sqlspec/adapters/psycopg/config/_sync.py +0 -168
  149. sqlspec/filters.py +0 -330
  150. sqlspec/mixins.py +0 -306
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.0.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,374 @@
1
+ # ruff: noqa: PLR2004
2
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
3
+
4
+ from sqlglot import exp, parse_one
5
+ from typing_extensions import Self
6
+
7
+ from sqlspec.exceptions import SQLBuilderError
8
+ from sqlspec.statement.builder._parsing_utils import parse_column_expression, parse_condition_expression
9
+
10
+ if TYPE_CHECKING:
11
+ from sqlspec.statement.builder.protocols import BuilderProtocol
12
+
13
+ __all__ = ("WhereClauseMixin",)
14
+
15
+
16
+ class WhereClauseMixin:
17
+ """Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
18
+
19
+ def where(
20
+ self, condition: Union[str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any]]
21
+ ) -> Self:
22
+ """Add a WHERE clause to the statement.
23
+
24
+ Args:
25
+ condition: The condition for the WHERE clause. Can be:
26
+ - A string condition
27
+ - A sqlglot Expression or Condition
28
+ - A 2-tuple (column, value) for equality comparison
29
+ - A 3-tuple (column, operator, value) for custom comparison
30
+
31
+ Raises:
32
+ SQLBuilderError: If the current expression is not a supported statement type.
33
+
34
+ Returns:
35
+ The current builder instance for method chaining.
36
+ """
37
+ # Special case: if this is an UpdateBuilder and _expression is not exp.Update, raise the expected error for test coverage
38
+
39
+ if self.__class__.__name__ == "UpdateBuilder" and not (
40
+ hasattr(self, "_expression") and isinstance(getattr(self, "_expression", None), exp.Update)
41
+ ):
42
+ msg = "Cannot add WHERE clause to non-UPDATE expression"
43
+ raise SQLBuilderError(msg)
44
+ builder = cast("BuilderProtocol", self)
45
+ if builder._expression is None:
46
+ msg = "Cannot add WHERE clause: expression is not initialized."
47
+ raise SQLBuilderError(msg)
48
+ valid_types = (exp.Select, exp.Update, exp.Delete)
49
+ if not isinstance(builder._expression, valid_types):
50
+ msg = f"Cannot add WHERE clause to unsupported expression type: {type(builder._expression).__name__}."
51
+ raise SQLBuilderError(msg)
52
+
53
+ # Check if table is set for DELETE queries
54
+ if isinstance(builder._expression, exp.Delete) and not builder._expression.args.get("this"):
55
+ msg = "WHERE clause requires a table to be set. Use from() to set the table first."
56
+ raise SQLBuilderError(msg)
57
+
58
+ # Normalize the condition using enhanced parsing
59
+ condition_expr: exp.Expression
60
+ if isinstance(condition, tuple):
61
+ # Handle tuple format with proper parameter binding
62
+ if len(condition) == 2:
63
+ # 2-tuple: (column, value) -> column = value
64
+ param_name = builder.add_parameter(condition[1])[1]
65
+ condition_expr = exp.EQ(
66
+ this=parse_column_expression(condition[0]), expression=exp.Placeholder(this=param_name)
67
+ )
68
+ elif len(condition) == 3:
69
+ # 3-tuple: (column, operator, value) -> column operator value
70
+ column, operator, value = condition
71
+ param_name = builder.add_parameter(value)[1]
72
+ col_expr = parse_column_expression(column)
73
+ placeholder_expr = exp.Placeholder(this=param_name)
74
+
75
+ # Map operator strings to sqlglot expression types
76
+ operator_map = {
77
+ "=": exp.EQ,
78
+ "==": exp.EQ,
79
+ "!=": exp.NEQ,
80
+ "<>": exp.NEQ,
81
+ "<": exp.LT,
82
+ "<=": exp.LTE,
83
+ ">": exp.GT,
84
+ ">=": exp.GTE,
85
+ "like": exp.Like,
86
+ "in": exp.In,
87
+ "any": exp.Any,
88
+ }
89
+ operator = operator.lower()
90
+ # Handle special cases for NOT operators
91
+ if operator == "not like":
92
+ condition_expr = exp.Not(this=exp.Like(this=col_expr, expression=placeholder_expr))
93
+ elif operator == "not in":
94
+ condition_expr = exp.Not(this=exp.In(this=col_expr, expression=placeholder_expr))
95
+ elif operator == "not any":
96
+ condition_expr = exp.Not(this=exp.Any(this=col_expr, expression=placeholder_expr))
97
+ else:
98
+ expr_class = operator_map.get(operator)
99
+ if expr_class is None:
100
+ msg = f"Unsupported operator in WHERE condition: {operator}"
101
+ raise SQLBuilderError(msg)
102
+
103
+ condition_expr = expr_class(this=col_expr, expression=placeholder_expr)
104
+ else:
105
+ msg = f"WHERE tuple must have 2 or 3 elements, got {len(condition)}"
106
+ raise SQLBuilderError(msg)
107
+ else:
108
+ condition_expr = parse_condition_expression(condition)
109
+
110
+ # Use dialect if available for Delete
111
+ if isinstance(builder._expression, exp.Delete):
112
+ builder._expression = builder._expression.where(
113
+ condition_expr, dialect=getattr(builder, "dialect_name", None)
114
+ )
115
+ else:
116
+ builder._expression = builder._expression.where(condition_expr, copy=False)
117
+ return cast("Self", builder)
118
+
119
+ # The following methods are moved from the old WhereClauseMixin in _base.py
120
+ def where_eq(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
121
+ _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
122
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
123
+ condition: exp.Expression = col_expr.eq(exp.var(param_name))
124
+ return self.where(condition)
125
+
126
+ def where_neq(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
127
+ _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
128
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
129
+ condition: exp.Expression = col_expr.neq(exp.var(param_name))
130
+ return self.where(condition)
131
+
132
+ def where_lt(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
133
+ _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
134
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
135
+ condition: exp.Expression = exp.LT(this=col_expr, expression=exp.var(param_name))
136
+ return self.where(condition)
137
+
138
+ def where_lte(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
139
+ _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
140
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
141
+ condition: exp.Expression = exp.LTE(this=col_expr, expression=exp.var(param_name))
142
+ return self.where(condition)
143
+
144
+ def where_gt(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
145
+ _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
146
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
147
+ condition: exp.Expression = exp.GT(this=col_expr, expression=exp.var(param_name))
148
+ return self.where(condition)
149
+
150
+ def where_gte(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
151
+ _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
152
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
153
+ condition: exp.Expression = exp.GTE(this=col_expr, expression=exp.var(param_name))
154
+ return self.where(condition)
155
+
156
+ def where_between(self, column: "Union[str, exp.Column]", low: Any, high: Any) -> "Self":
157
+ _, low_param = self.add_parameter(low) # type: ignore[attr-defined]
158
+ _, high_param = self.add_parameter(high) # type: ignore[attr-defined]
159
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
160
+ condition: exp.Expression = col_expr.between(exp.var(low_param), exp.var(high_param))
161
+ return self.where(condition)
162
+
163
+ def where_like(self, column: "Union[str, exp.Column]", pattern: str, escape: Optional[str] = None) -> "Self":
164
+ _, param_name = self.add_parameter(pattern) # type: ignore[attr-defined]
165
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
166
+ if escape is not None:
167
+ cond = exp.Like(this=col_expr, expression=exp.var(param_name), escape=exp.Literal.string(str(escape)))
168
+ else:
169
+ cond = col_expr.like(exp.var(param_name))
170
+ condition: exp.Expression = cond
171
+ return self.where(condition)
172
+
173
+ def where_not_like(self, column: "Union[str, exp.Column]", pattern: str) -> "Self":
174
+ _, param_name = self.add_parameter(pattern) # type: ignore[attr-defined]
175
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
176
+ condition: exp.Expression = col_expr.like(exp.var(param_name)).not_()
177
+ return self.where(condition)
178
+
179
+ def where_ilike(self, column: "Union[str, exp.Column]", pattern: str) -> "Self":
180
+ _, param_name = self.add_parameter(pattern) # type: ignore[attr-defined]
181
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
182
+ condition: exp.Expression = col_expr.ilike(exp.var(param_name))
183
+ return self.where(condition)
184
+
185
+ def where_is_null(self, column: "Union[str, exp.Column]") -> "Self":
186
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
187
+ condition: exp.Expression = col_expr.is_(exp.null())
188
+ return self.where(condition)
189
+
190
+ def where_is_not_null(self, column: "Union[str, exp.Column]") -> "Self":
191
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
192
+ condition: exp.Expression = col_expr.is_(exp.null()).not_()
193
+ return self.where(condition)
194
+
195
+ def where_exists(self, subquery: "Union[str, Any]") -> "Self":
196
+ sub_expr: exp.Expression
197
+ if hasattr(subquery, "_parameters") and hasattr(subquery, "build"):
198
+ subquery_builder_params: dict[str, Any] = subquery._parameters # pyright: ignore
199
+ if subquery_builder_params:
200
+ for p_name, p_value in subquery_builder_params.items():
201
+ self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
202
+ sub_sql_obj = subquery.build() # pyright: ignore
203
+ sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=getattr(self, "dialect_name", None))
204
+ else:
205
+ sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(self, "dialect_name", None))
206
+
207
+ if sub_expr is None:
208
+ msg = "Could not parse subquery for EXISTS"
209
+ raise SQLBuilderError(msg)
210
+
211
+ exists_expr = exp.Exists(this=sub_expr)
212
+ return self.where(exists_expr)
213
+
214
+ def where_not_exists(self, subquery: "Union[str, Any]") -> "Self":
215
+ sub_expr: exp.Expression
216
+ if hasattr(subquery, "_parameters") and hasattr(subquery, "build"):
217
+ subquery_builder_params: dict[str, Any] = subquery._parameters # pyright: ignore
218
+ if subquery_builder_params:
219
+ for p_name, p_value in subquery_builder_params.items():
220
+ self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
221
+ sub_sql_obj = subquery.build() # pyright: ignore
222
+ sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=getattr(self, "dialect_name", None))
223
+ else:
224
+ sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(self, "dialect_name", None))
225
+
226
+ if sub_expr is None:
227
+ msg = "Could not parse subquery for NOT EXISTS"
228
+ raise SQLBuilderError(msg)
229
+
230
+ not_exists_expr = exp.Not(this=exp.Exists(this=sub_expr))
231
+ return self.where(not_exists_expr)
232
+
233
+ def where_not_null(self, column: "Union[str, exp.Column]") -> "Self":
234
+ """Alias for where_is_not_null for compatibility with test expectations."""
235
+ return self.where_is_not_null(column)
236
+
237
+ def where_in(self, column: "Union[str, exp.Column]", values: Any) -> "Self":
238
+ """Add a WHERE ... IN (...) clause. Supports subqueries and iterables."""
239
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
240
+ # Subquery support
241
+ if hasattr(values, "build") or isinstance(values, exp.Expression):
242
+ if hasattr(values, "build"):
243
+ subquery = values.build() # pyright: ignore
244
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect_name", None)))
245
+ else:
246
+ subquery_exp = values
247
+ condition = col_expr.isin(subquery_exp)
248
+ return self.where(condition)
249
+ # Iterable of values
250
+ if not hasattr(values, "__iter__") or isinstance(values, (str, bytes)):
251
+ msg = "Unsupported type for 'values' in WHERE IN"
252
+ raise SQLBuilderError(msg)
253
+ params = []
254
+ for v in values:
255
+ _, param_name = self.add_parameter(v) # type: ignore[attr-defined]
256
+ params.append(exp.var(param_name))
257
+ condition = col_expr.isin(*params)
258
+ return self.where(condition)
259
+
260
+ def where_not_in(self, column: "Union[str, exp.Column]", values: Any) -> "Self":
261
+ """Add a WHERE ... NOT IN (...) clause. Supports subqueries and iterables."""
262
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
263
+ if hasattr(values, "build") or isinstance(values, exp.Expression):
264
+ if hasattr(values, "build"):
265
+ subquery = values.build() # pyright: ignore
266
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect_name", None)))
267
+ else:
268
+ subquery_exp = values
269
+ condition = exp.Not(this=col_expr.isin(subquery_exp))
270
+ return self.where(condition)
271
+ if not hasattr(values, "__iter__") or isinstance(values, (str, bytes)):
272
+ msg = "Values for where_not_in must be a non-string iterable or subquery."
273
+ raise SQLBuilderError(msg)
274
+ params = []
275
+ for v in values:
276
+ _, param_name = self.add_parameter(v) # type: ignore[attr-defined]
277
+ params.append(exp.var(param_name))
278
+ condition = exp.Not(this=col_expr.isin(*params))
279
+ return self.where(condition)
280
+
281
+ def where_null(self, column: "Union[str, exp.Column]") -> "Self":
282
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
283
+ condition: exp.Expression = col_expr.is_(exp.null())
284
+ return self.where(condition)
285
+
286
+ def where_any(self, column: "Union[str, exp.Column]", values: Any) -> "Self":
287
+ """Add a WHERE ... = ANY (...) clause. Supports subqueries and iterables.
288
+
289
+ Args:
290
+ column: The column to compare.
291
+ values: A subquery or iterable of values.
292
+
293
+ Returns:
294
+ The current builder instance for method chaining.
295
+ """
296
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
297
+ if hasattr(values, "build") or isinstance(values, exp.Expression):
298
+ if hasattr(values, "build"):
299
+ subquery = values.build() # pyright: ignore
300
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect_name", None)))
301
+ else:
302
+ subquery_exp = values
303
+ condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
304
+ return self.where(condition)
305
+ if isinstance(values, str):
306
+ # Try to parse as subquery expression with enhanced parsing
307
+ try:
308
+ # Parse as a subquery expression
309
+ parsed_expr = parse_one(values)
310
+ if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
311
+ subquery_exp = exp.paren(parsed_expr)
312
+ condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
313
+ return self.where(condition)
314
+ except Exception: # noqa: S110
315
+ # Subquery parsing failed for WHERE ANY
316
+ pass
317
+ # If parsing fails, fall through to error
318
+ msg = "Unsupported type for 'values' in WHERE ANY"
319
+ raise SQLBuilderError(msg)
320
+ if not hasattr(values, "__iter__") or isinstance(values, bytes):
321
+ msg = "Unsupported type for 'values' in WHERE ANY"
322
+ raise SQLBuilderError(msg)
323
+ params = []
324
+ for v in values:
325
+ _, param_name = self.add_parameter(v) # type: ignore[attr-defined]
326
+ params.append(exp.var(param_name))
327
+ tuple_expr = exp.Tuple(expressions=params)
328
+ condition = exp.EQ(this=col_expr, expression=exp.Any(this=tuple_expr))
329
+ return self.where(condition)
330
+
331
+ def where_not_any(self, column: "Union[str, exp.Column]", values: Any) -> "Self":
332
+ """Add a WHERE ... <> ANY (...) (or NOT = ANY) clause. Supports subqueries and iterables.
333
+
334
+ Args:
335
+ column: The column to compare.
336
+ values: A subquery or iterable of values.
337
+
338
+ Returns:
339
+ The current builder instance for method chaining.
340
+ """
341
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
342
+ if hasattr(values, "build") or isinstance(values, exp.Expression):
343
+ if hasattr(values, "build"):
344
+ subquery = values.build() # pyright: ignore
345
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect_name", None)))
346
+ else:
347
+ subquery_exp = values
348
+ condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
349
+ return self.where(condition)
350
+ if isinstance(values, str):
351
+ # Try to parse as subquery expression with enhanced parsing
352
+ try:
353
+ # Parse as a subquery expression
354
+ parsed_expr = parse_one(values)
355
+ if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
356
+ subquery_exp = exp.paren(parsed_expr)
357
+ condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
358
+ return self.where(condition)
359
+ except Exception: # noqa: S110
360
+ # Subquery parsing failed for WHERE NOT ANY
361
+ pass
362
+ # If parsing fails, fall through to error
363
+ msg = "Unsupported type for 'values' in WHERE NOT ANY"
364
+ raise SQLBuilderError(msg)
365
+ if not hasattr(values, "__iter__") or isinstance(values, bytes):
366
+ msg = "Unsupported type for 'values' in WHERE NOT ANY"
367
+ raise SQLBuilderError(msg)
368
+ params = []
369
+ for v in values:
370
+ _, param_name = self.add_parameter(v) # type: ignore[attr-defined]
371
+ params.append(exp.var(param_name))
372
+ tuple_expr = exp.Tuple(expressions=params)
373
+ condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
374
+ return self.where(condition)
@@ -0,0 +1,86 @@
1
+ from typing import Any, Optional, Union
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ from sqlspec.exceptions import SQLBuilderError
7
+
8
+ __all__ = ("WindowFunctionsMixin",)
9
+
10
+
11
+ class WindowFunctionsMixin:
12
+ """Mixin providing window function methods for SQL builders."""
13
+
14
+ _expression: Optional[exp.Expression] = None
15
+
16
+ def window(
17
+ self,
18
+ function_expr: Union[str, exp.Expression],
19
+ partition_by: Optional[Union[str, list[str], exp.Expression, list[exp.Expression]]] = None,
20
+ order_by: Optional[Union[str, list[str], exp.Expression, list[exp.Expression]]] = None,
21
+ frame: Optional[str] = None,
22
+ alias: Optional[str] = None,
23
+ ) -> Self:
24
+ """Add a window function to the SELECT clause.
25
+
26
+ Args:
27
+ function_expr: The window function expression (e.g., "COUNT(*)", "ROW_NUMBER()").
28
+ partition_by: Column(s) to partition by.
29
+ order_by: Column(s) to order by within the window.
30
+ frame: Window frame specification (e.g., "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW").
31
+ alias: Optional alias for the window function.
32
+
33
+ Raises:
34
+ SQLBuilderError: If the current expression is not a SELECT statement or function parsing fails.
35
+
36
+ Returns:
37
+ The current builder instance for method chaining.
38
+ """
39
+ if self._expression is None:
40
+ self._expression = exp.Select()
41
+ if not isinstance(self._expression, exp.Select):
42
+ msg = "Cannot add window function to a non-SELECT expression."
43
+ raise SQLBuilderError(msg)
44
+
45
+ func_expr_parsed: exp.Expression
46
+ if isinstance(function_expr, str):
47
+ parsed: Optional[exp.Expression] = exp.maybe_parse(function_expr, dialect=getattr(self, "dialect", None))
48
+ if not parsed:
49
+ msg = f"Could not parse function expression: {function_expr}"
50
+ raise SQLBuilderError(msg)
51
+ func_expr_parsed = parsed
52
+ else:
53
+ func_expr_parsed = function_expr
54
+
55
+ over_args: dict[str, Any] = {} # Stringified dict
56
+ if partition_by:
57
+ if isinstance(partition_by, str):
58
+ over_args["partition_by"] = [exp.column(partition_by)]
59
+ elif isinstance(partition_by, list): # Check for list
60
+ over_args["partition_by"] = [exp.column(col) if isinstance(col, str) else col for col in partition_by]
61
+ elif isinstance(partition_by, exp.Expression): # Check for exp.Expression
62
+ over_args["partition_by"] = [partition_by]
63
+
64
+ if order_by:
65
+ if isinstance(order_by, str):
66
+ over_args["order"] = exp.column(order_by).asc()
67
+ elif isinstance(order_by, list):
68
+ # Properly handle multiple ORDER BY columns using Order expression
69
+ order_expressions: list[Union[exp.Expression, exp.Column]] = []
70
+ for col in order_by:
71
+ if isinstance(col, str):
72
+ order_expressions.append(exp.column(col).asc())
73
+ else:
74
+ order_expressions.append(col)
75
+ over_args["order"] = exp.Order(expressions=order_expressions)
76
+ elif isinstance(order_by, exp.Expression):
77
+ over_args["order"] = order_by
78
+
79
+ if frame:
80
+ frame_expr: Optional[exp.Expression] = exp.maybe_parse(frame, dialect=getattr(self, "dialect", None))
81
+ if frame_expr:
82
+ over_args["frame"] = frame_expr
83
+
84
+ window_expr = exp.Window(this=func_expr_parsed, **over_args)
85
+ self._expression.select(exp.alias_(window_expr, alias) if alias else window_expr, copy=False)
86
+ return self
@@ -0,0 +1,20 @@
1
+ from typing import Any, Optional, Protocol, Union
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ __all__ = ("BuilderProtocol", "SelectBuilderProtocol")
7
+
8
+
9
+ class BuilderProtocol(Protocol):
10
+ _expression: Optional[exp.Expression]
11
+ _parameters: dict[str, Any]
12
+ _parameter_counter: int
13
+ dialect: Any
14
+ dialect_name: Optional[str]
15
+
16
+ def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]: ...
17
+
18
+
19
+ class SelectBuilderProtocol(BuilderProtocol, Protocol):
20
+ def select(self, *columns: Union[str, exp.Expression]) -> Self: ...