sqlspec 0.13.1__py3-none-any.whl → 0.14.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 (110) hide show
  1. sqlspec/__init__.py +39 -1
  2. sqlspec/adapters/adbc/config.py +4 -40
  3. sqlspec/adapters/adbc/driver.py +29 -16
  4. sqlspec/adapters/aiosqlite/config.py +2 -20
  5. sqlspec/adapters/aiosqlite/driver.py +36 -18
  6. sqlspec/adapters/asyncmy/config.py +2 -33
  7. sqlspec/adapters/asyncmy/driver.py +23 -16
  8. sqlspec/adapters/asyncpg/config.py +5 -39
  9. sqlspec/adapters/asyncpg/driver.py +41 -18
  10. sqlspec/adapters/bigquery/config.py +2 -43
  11. sqlspec/adapters/bigquery/driver.py +26 -14
  12. sqlspec/adapters/duckdb/config.py +2 -49
  13. sqlspec/adapters/duckdb/driver.py +35 -16
  14. sqlspec/adapters/oracledb/config.py +4 -83
  15. sqlspec/adapters/oracledb/driver.py +54 -27
  16. sqlspec/adapters/psqlpy/config.py +2 -55
  17. sqlspec/adapters/psqlpy/driver.py +28 -8
  18. sqlspec/adapters/psycopg/config.py +4 -73
  19. sqlspec/adapters/psycopg/driver.py +69 -24
  20. sqlspec/adapters/sqlite/config.py +3 -21
  21. sqlspec/adapters/sqlite/driver.py +50 -26
  22. sqlspec/cli.py +248 -0
  23. sqlspec/config.py +18 -20
  24. sqlspec/driver/_async.py +28 -10
  25. sqlspec/driver/_common.py +5 -4
  26. sqlspec/driver/_sync.py +28 -10
  27. sqlspec/driver/mixins/__init__.py +6 -0
  28. sqlspec/driver/mixins/_cache.py +114 -0
  29. sqlspec/driver/mixins/_pipeline.py +0 -4
  30. sqlspec/{service/base.py → driver/mixins/_query_tools.py} +86 -421
  31. sqlspec/driver/mixins/_result_utils.py +0 -2
  32. sqlspec/driver/mixins/_sql_translator.py +0 -2
  33. sqlspec/driver/mixins/_storage.py +4 -18
  34. sqlspec/driver/mixins/_type_coercion.py +0 -2
  35. sqlspec/driver/parameters.py +4 -4
  36. sqlspec/extensions/aiosql/adapter.py +4 -4
  37. sqlspec/extensions/litestar/__init__.py +2 -1
  38. sqlspec/extensions/litestar/cli.py +48 -0
  39. sqlspec/extensions/litestar/plugin.py +3 -0
  40. sqlspec/loader.py +1 -1
  41. sqlspec/migrations/__init__.py +23 -0
  42. sqlspec/migrations/base.py +390 -0
  43. sqlspec/migrations/commands.py +525 -0
  44. sqlspec/migrations/runner.py +215 -0
  45. sqlspec/migrations/tracker.py +153 -0
  46. sqlspec/migrations/utils.py +89 -0
  47. sqlspec/protocols.py +37 -3
  48. sqlspec/statement/builder/__init__.py +8 -8
  49. sqlspec/statement/builder/{column.py → _column.py} +82 -52
  50. sqlspec/statement/builder/{ddl.py → _ddl.py} +5 -5
  51. sqlspec/statement/builder/_ddl_utils.py +1 -1
  52. sqlspec/statement/builder/{delete.py → _delete.py} +1 -1
  53. sqlspec/statement/builder/{insert.py → _insert.py} +1 -1
  54. sqlspec/statement/builder/{merge.py → _merge.py} +1 -1
  55. sqlspec/statement/builder/_parsing_utils.py +5 -3
  56. sqlspec/statement/builder/{select.py → _select.py} +59 -61
  57. sqlspec/statement/builder/{update.py → _update.py} +2 -2
  58. sqlspec/statement/builder/mixins/__init__.py +24 -30
  59. sqlspec/statement/builder/mixins/{_set_ops.py → _cte_and_set_ops.py} +86 -2
  60. sqlspec/statement/builder/mixins/{_delete_from.py → _delete_operations.py} +2 -0
  61. sqlspec/statement/builder/mixins/{_insert_values.py → _insert_operations.py} +70 -1
  62. sqlspec/statement/builder/mixins/{_merge_clauses.py → _merge_operations.py} +2 -0
  63. sqlspec/statement/builder/mixins/_order_limit_operations.py +123 -0
  64. sqlspec/statement/builder/mixins/{_pivot.py → _pivot_operations.py} +71 -2
  65. sqlspec/statement/builder/mixins/_select_operations.py +612 -0
  66. sqlspec/statement/builder/mixins/{_update_set.py → _update_operations.py} +73 -2
  67. sqlspec/statement/builder/mixins/_where_clause.py +536 -0
  68. sqlspec/statement/cache.py +50 -0
  69. sqlspec/statement/filters.py +37 -8
  70. sqlspec/statement/parameters.py +154 -25
  71. sqlspec/statement/pipelines/__init__.py +1 -1
  72. sqlspec/statement/pipelines/context.py +4 -4
  73. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +3 -3
  74. sqlspec/statement/pipelines/validators/_parameter_style.py +22 -22
  75. sqlspec/statement/pipelines/validators/_performance.py +1 -5
  76. sqlspec/statement/sql.py +246 -176
  77. sqlspec/utils/__init__.py +2 -1
  78. sqlspec/utils/statement_hashing.py +203 -0
  79. sqlspec/utils/type_guards.py +32 -0
  80. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/METADATA +1 -1
  81. sqlspec-0.14.0.dist-info/RECORD +143 -0
  82. sqlspec-0.14.0.dist-info/entry_points.txt +2 -0
  83. sqlspec/service/__init__.py +0 -4
  84. sqlspec/service/_util.py +0 -147
  85. sqlspec/service/pagination.py +0 -26
  86. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  87. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  88. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  89. sqlspec/statement/builder/mixins/_from.py +0 -63
  90. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  91. sqlspec/statement/builder/mixins/_having.py +0 -35
  92. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  93. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  94. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  95. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  96. sqlspec/statement/builder/mixins/_returning.py +0 -37
  97. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  98. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  99. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  100. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  101. sqlspec/statement/builder/mixins/_where.py +0 -401
  102. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  103. sqlspec/statement/parameter_manager.py +0 -220
  104. sqlspec/statement/sql_compiler.py +0 -140
  105. sqlspec-0.13.1.dist-info/RECORD +0 -150
  106. /sqlspec/statement/builder/{base.py → _base.py} +0 -0
  107. /sqlspec/statement/builder/mixins/{_join.py → _join_operations.py} +0 -0
  108. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/WHEEL +0 -0
  109. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/LICENSE +0 -0
  110. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,401 +0,0 @@
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
- from sqlspec.utils.type_guards import has_query_builder_parameters, is_iterable_parameters
10
-
11
- if TYPE_CHECKING:
12
- from sqlspec.protocols import SQLBuilderProtocol
13
- from sqlspec.statement.builder.column import ColumnExpression
14
-
15
- __all__ = ("WhereClauseMixin",)
16
-
17
-
18
- class WhereClauseMixin:
19
- """Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
20
-
21
- def where(
22
- self,
23
- condition: Union[str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression"],
24
- ) -> Self:
25
- """Add a WHERE clause to the statement.
26
-
27
- Args:
28
- condition: The condition for the WHERE clause. Can be:
29
- - A string condition
30
- - A sqlglot Expression or Condition
31
- - A 2-tuple (column, value) for equality comparison
32
- - A 3-tuple (column, operator, value) for custom comparison
33
-
34
- Raises:
35
- SQLBuilderError: If the current expression is not a supported statement type.
36
-
37
- Returns:
38
- The current builder instance for method chaining.
39
- """
40
- # Special case: if this is an Update and _expression is not exp.Update, raise the expected error for test coverage
41
-
42
- if self.__class__.__name__ == "Update" and not (
43
- hasattr(self, "_expression") and isinstance(getattr(self, "_expression", None), exp.Update)
44
- ):
45
- msg = "Cannot add WHERE clause to non-UPDATE expression"
46
- raise SQLBuilderError(msg)
47
- builder = cast("SQLBuilderProtocol", self)
48
- if builder._expression is None:
49
- msg = "Cannot add WHERE clause: expression is not initialized."
50
- raise SQLBuilderError(msg)
51
- valid_types = (exp.Select, exp.Update, exp.Delete)
52
- if not isinstance(builder._expression, valid_types):
53
- msg = f"Cannot add WHERE clause to unsupported expression type: {type(builder._expression).__name__}."
54
- raise SQLBuilderError(msg)
55
-
56
- if isinstance(builder._expression, exp.Delete) and not builder._expression.args.get("this"):
57
- msg = "WHERE clause requires a table to be set. Use from() to set the table first."
58
- raise SQLBuilderError(msg)
59
-
60
- # Normalize the condition using enhanced parsing
61
- condition_expr: exp.Expression
62
- if isinstance(condition, tuple):
63
- if len(condition) == 2:
64
- # 2-tuple: (column, value) -> column = value
65
- param_name = builder.add_parameter(condition[1])[1]
66
- condition_expr = exp.EQ(
67
- this=parse_column_expression(condition[0]), expression=exp.Placeholder(this=param_name)
68
- )
69
- elif len(condition) == 3:
70
- # 3-tuple: (column, operator, value) -> column operator value
71
- column, operator, value = condition
72
- param_name = builder.add_parameter(value)[1]
73
- col_expr = parse_column_expression(column)
74
- placeholder_expr = exp.Placeholder(this=param_name)
75
-
76
- # Map operator strings to sqlglot expression types
77
- operator_map = {
78
- "=": exp.EQ,
79
- "==": exp.EQ,
80
- "!=": exp.NEQ,
81
- "<>": exp.NEQ,
82
- "<": exp.LT,
83
- "<=": exp.LTE,
84
- ">": exp.GT,
85
- ">=": exp.GTE,
86
- "like": exp.Like,
87
- "in": exp.In,
88
- "any": exp.Any,
89
- }
90
- operator = operator.lower()
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
- # Handle ColumnExpression objects
108
- elif hasattr(condition, "sqlglot_expression"):
109
- # This is a ColumnExpression from our new Column syntax
110
- raw_expr = getattr(condition, "sqlglot_expression", None)
111
- if raw_expr is not None:
112
- condition_expr = builder._parameterize_expression(raw_expr)
113
- else:
114
- # Fallback if attribute exists but is None
115
- condition_expr = parse_condition_expression(str(condition))
116
- else:
117
- # Existing logic for strings and raw SQLGlot expressions
118
- # Convert to string if it's not a recognized type
119
- if not isinstance(condition, (str, exp.Expression, tuple)):
120
- condition = str(condition)
121
- condition_expr = parse_condition_expression(condition)
122
-
123
- # Use dialect if available for Delete
124
- if isinstance(builder._expression, exp.Delete):
125
- builder._expression = builder._expression.where(
126
- condition_expr, dialect=getattr(builder, "dialect_name", None)
127
- )
128
- else:
129
- builder._expression = builder._expression.where(condition_expr, copy=False)
130
- return cast("Self", builder)
131
-
132
- # The following methods are moved from the old WhereClauseMixin in _base.py
133
- def where_eq(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
134
- _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
135
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
136
- condition: exp.Expression = col_expr.eq(exp.var(param_name))
137
- return self.where(condition)
138
-
139
- def where_neq(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
140
- _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
141
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
142
- condition: exp.Expression = col_expr.neq(exp.var(param_name))
143
- return self.where(condition)
144
-
145
- def where_lt(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
146
- _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
147
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
148
- condition: exp.Expression = exp.LT(this=col_expr, expression=exp.var(param_name))
149
- return self.where(condition)
150
-
151
- def where_lte(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
152
- _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
153
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
154
- condition: exp.Expression = exp.LTE(this=col_expr, expression=exp.var(param_name))
155
- return self.where(condition)
156
-
157
- def where_gt(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
158
- _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
159
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
160
- condition: exp.Expression = exp.GT(this=col_expr, expression=exp.var(param_name))
161
- return self.where(condition)
162
-
163
- def where_gte(self, column: "Union[str, exp.Column]", value: Any) -> "Self":
164
- _, param_name = self.add_parameter(value) # type: ignore[attr-defined]
165
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
166
- condition: exp.Expression = exp.GTE(this=col_expr, expression=exp.var(param_name))
167
- return self.where(condition)
168
-
169
- def where_between(self, column: "Union[str, exp.Column]", low: Any, high: Any) -> "Self":
170
- _, low_param = self.add_parameter(low) # type: ignore[attr-defined]
171
- _, high_param = self.add_parameter(high) # type: ignore[attr-defined]
172
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
173
- condition: exp.Expression = col_expr.between(exp.var(low_param), exp.var(high_param))
174
- return self.where(condition)
175
-
176
- def where_like(self, column: "Union[str, exp.Column]", pattern: str, escape: Optional[str] = None) -> "Self":
177
- _, param_name = self.add_parameter(pattern) # type: ignore[attr-defined]
178
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
179
- if escape is not None:
180
- cond = exp.Like(this=col_expr, expression=exp.var(param_name), escape=exp.Literal.string(str(escape)))
181
- else:
182
- cond = col_expr.like(exp.var(param_name))
183
- condition: exp.Expression = cond
184
- return self.where(condition)
185
-
186
- def where_not_like(self, column: "Union[str, exp.Column]", pattern: str) -> "Self":
187
- _, param_name = self.add_parameter(pattern) # type: ignore[attr-defined]
188
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
189
- condition: exp.Expression = col_expr.like(exp.var(param_name)).not_()
190
- return self.where(condition)
191
-
192
- def where_ilike(self, column: "Union[str, exp.Column]", pattern: str) -> "Self":
193
- _, param_name = self.add_parameter(pattern) # type: ignore[attr-defined]
194
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
195
- condition: exp.Expression = col_expr.ilike(exp.var(param_name))
196
- return self.where(condition)
197
-
198
- def where_is_null(self, column: "Union[str, exp.Column]") -> "Self":
199
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
200
- condition: exp.Expression = col_expr.is_(exp.null())
201
- return self.where(condition)
202
-
203
- def where_is_not_null(self, column: "Union[str, exp.Column]") -> "Self":
204
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
205
- condition: exp.Expression = col_expr.is_(exp.null()).not_()
206
- return self.where(condition)
207
-
208
- def where_exists(self, subquery: "Union[str, Any]") -> "Self":
209
- sub_expr: exp.Expression
210
- if has_query_builder_parameters(subquery):
211
- subquery_builder_params: dict[str, Any] = subquery.parameters
212
- if subquery_builder_params:
213
- for p_name, p_value in subquery_builder_params.items():
214
- self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
215
- sub_sql_obj = subquery.build() # pyright: ignore
216
- sql_str = (
217
- sub_sql_obj.sql if hasattr(sub_sql_obj, "sql") and not callable(sub_sql_obj.sql) else str(sub_sql_obj)
218
- )
219
- sub_expr = exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None))
220
- else:
221
- sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(self, "dialect_name", None))
222
-
223
- if sub_expr is None:
224
- msg = "Could not parse subquery for EXISTS"
225
- raise SQLBuilderError(msg)
226
-
227
- exists_expr = exp.Exists(this=sub_expr)
228
- return self.where(exists_expr)
229
-
230
- def where_not_exists(self, subquery: "Union[str, Any]") -> "Self":
231
- sub_expr: exp.Expression
232
- if has_query_builder_parameters(subquery):
233
- subquery_builder_params: dict[str, Any] = subquery.parameters
234
- if subquery_builder_params:
235
- for p_name, p_value in subquery_builder_params.items():
236
- self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
237
- sub_sql_obj = subquery.build() # pyright: ignore
238
- sql_str = (
239
- sub_sql_obj.sql if hasattr(sub_sql_obj, "sql") and not callable(sub_sql_obj.sql) else str(sub_sql_obj)
240
- )
241
- sub_expr = exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None))
242
- else:
243
- sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(self, "dialect_name", None))
244
-
245
- if sub_expr is None:
246
- msg = "Could not parse subquery for NOT EXISTS"
247
- raise SQLBuilderError(msg)
248
-
249
- not_exists_expr = exp.Not(this=exp.Exists(this=sub_expr))
250
- return self.where(not_exists_expr)
251
-
252
- def where_not_null(self, column: "Union[str, exp.Column]") -> "Self":
253
- """Alias for where_is_not_null for compatibility with test expectations."""
254
- return self.where_is_not_null(column)
255
-
256
- def where_in(self, column: "Union[str, exp.Column]", values: Any) -> "Self":
257
- """Add a WHERE ... IN (...) clause. Supports subqueries and iterables."""
258
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
259
- # Subquery support
260
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
261
- subquery_exp: exp.Expression
262
- if has_query_builder_parameters(values):
263
- subquery = values.build() # pyright: ignore
264
- sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
265
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
266
- else:
267
- subquery_exp = values # type: ignore[assignment]
268
- condition = col_expr.isin(subquery_exp)
269
- return self.where(condition)
270
- # Iterable of values
271
- if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
272
- msg = "Unsupported type for 'values' in WHERE IN"
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 = col_expr.isin(*params)
279
- return self.where(condition)
280
-
281
- def where_not_in(self, column: "Union[str, exp.Column]", values: Any) -> "Self":
282
- """Add a WHERE ... NOT IN (...) clause. Supports subqueries and iterables."""
283
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
284
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
285
- subquery_exp: exp.Expression
286
- if has_query_builder_parameters(values):
287
- subquery = values.build() # pyright: ignore
288
- sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
289
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
290
- else:
291
- subquery_exp = values # type: ignore[assignment]
292
- condition = exp.Not(this=col_expr.isin(subquery_exp))
293
- return self.where(condition)
294
- if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
295
- msg = "Values for where_not_in must be a non-string iterable or subquery."
296
- raise SQLBuilderError(msg)
297
- params = []
298
- for v in values:
299
- _, param_name = self.add_parameter(v) # type: ignore[attr-defined]
300
- params.append(exp.var(param_name))
301
- condition = exp.Not(this=col_expr.isin(*params))
302
- return self.where(condition)
303
-
304
- def where_null(self, column: "Union[str, exp.Column]") -> "Self":
305
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
306
- condition: exp.Expression = col_expr.is_(exp.null())
307
- return self.where(condition)
308
-
309
- def where_any(self, column: "Union[str, exp.Column]", values: Any) -> "Self":
310
- """Add a WHERE ... = ANY (...) clause. Supports subqueries and iterables.
311
-
312
- Args:
313
- column: The column to compare.
314
- values: A subquery or iterable of values.
315
-
316
- Returns:
317
- The current builder instance for method chaining.
318
- """
319
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
320
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
321
- subquery_exp: exp.Expression
322
- if has_query_builder_parameters(values):
323
- subquery = values.build() # pyright: ignore
324
- sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
325
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
326
- else:
327
- subquery_exp = values # type: ignore[assignment]
328
- condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
329
- return self.where(condition)
330
- if isinstance(values, str):
331
- # Try to parse as subquery expression with enhanced parsing
332
- try:
333
- # Parse as a subquery expression
334
- parsed_expr = parse_one(values)
335
- if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
336
- subquery_exp = exp.paren(parsed_expr)
337
- condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
338
- return self.where(condition)
339
- except Exception: # noqa: S110
340
- # Subquery parsing failed for WHERE ANY
341
- pass
342
- # If parsing fails, fall through to error
343
- msg = "Unsupported type for 'values' in WHERE ANY"
344
- raise SQLBuilderError(msg)
345
- if not is_iterable_parameters(values) or isinstance(values, bytes):
346
- msg = "Unsupported type for 'values' in WHERE ANY"
347
- raise SQLBuilderError(msg)
348
- params = []
349
- for v in values:
350
- _, param_name = self.add_parameter(v) # type: ignore[attr-defined]
351
- params.append(exp.var(param_name))
352
- tuple_expr = exp.Tuple(expressions=params)
353
- condition = exp.EQ(this=col_expr, expression=exp.Any(this=tuple_expr))
354
- return self.where(condition)
355
-
356
- def where_not_any(self, column: "Union[str, exp.Column]", values: Any) -> "Self":
357
- """Add a WHERE ... <> ANY (...) (or NOT = ANY) clause. Supports subqueries and iterables.
358
-
359
- Args:
360
- column: The column to compare.
361
- values: A subquery or iterable of values.
362
-
363
- Returns:
364
- The current builder instance for method chaining.
365
- """
366
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
367
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
368
- subquery_exp: exp.Expression
369
- if has_query_builder_parameters(values):
370
- subquery = values.build() # pyright: ignore
371
- sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
372
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
373
- else:
374
- subquery_exp = values # type: ignore[assignment]
375
- condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
376
- return self.where(condition)
377
- if isinstance(values, str):
378
- # Try to parse as subquery expression with enhanced parsing
379
- try:
380
- # Parse as a subquery expression
381
- parsed_expr = parse_one(values)
382
- if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
383
- subquery_exp = exp.paren(parsed_expr)
384
- condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
385
- return self.where(condition)
386
- except Exception: # noqa: S110
387
- # Subquery parsing failed for WHERE NOT ANY
388
- pass
389
- # If parsing fails, fall through to error
390
- msg = "Unsupported type for 'values' in WHERE NOT ANY"
391
- raise SQLBuilderError(msg)
392
- if not is_iterable_parameters(values) or isinstance(values, bytes):
393
- msg = "Unsupported type for 'values' in WHERE NOT ANY"
394
- raise SQLBuilderError(msg)
395
- params = []
396
- for v in values:
397
- _, param_name = self.add_parameter(v) # type: ignore[attr-defined]
398
- params.append(exp.var(param_name))
399
- tuple_expr = exp.Tuple(expressions=params)
400
- condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
401
- return self.where(condition)
@@ -1,86 +0,0 @@
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