sqlspec 0.22.0__py3-none-any.whl → 0.24.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.
- sqlspec/_sql.py +36 -0
- sqlspec/builder/_base.py +68 -21
- sqlspec/builder/_ddl.py +14 -13
- sqlspec/builder/_insert.py +8 -16
- sqlspec/builder/_parsing_utils.py +16 -18
- sqlspec/builder/_update.py +1 -1
- sqlspec/builder/mixins/_cte_and_set_ops.py +20 -1
- sqlspec/builder/mixins/_join_operations.py +205 -85
- sqlspec/builder/mixins/_where_clause.py +743 -124
- sqlspec/utils/type_guards.py +31 -0
- {sqlspec-0.22.0.dist-info → sqlspec-0.24.0.dist-info}/METADATA +1 -1
- {sqlspec-0.22.0.dist-info → sqlspec-0.24.0.dist-info}/RECORD +16 -16
- {sqlspec-0.22.0.dist-info → sqlspec-0.24.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.22.0.dist-info → sqlspec-0.24.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.22.0.dist-info → sqlspec-0.24.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.22.0.dist-info → sqlspec-0.24.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -5,7 +5,7 @@ Provides mixins for WHERE and HAVING clause functionality with
|
|
|
5
5
|
parameter binding and various condition operators.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from sqlspec.core.statement import SQL
|
|
@@ -16,7 +16,13 @@ from typing_extensions import Self
|
|
|
16
16
|
|
|
17
17
|
from sqlspec.builder._parsing_utils import parse_column_expression, parse_condition_expression
|
|
18
18
|
from sqlspec.exceptions import SQLBuilderError
|
|
19
|
-
from sqlspec.utils.type_guards import
|
|
19
|
+
from sqlspec.utils.type_guards import (
|
|
20
|
+
has_expression_and_parameters,
|
|
21
|
+
has_expression_and_sql,
|
|
22
|
+
has_query_builder_parameters,
|
|
23
|
+
has_sqlglot_expression,
|
|
24
|
+
is_iterable_parameters,
|
|
25
|
+
)
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
def _extract_column_name(column: Union[str, exp.Column]) -> str:
|
|
@@ -39,6 +45,7 @@ def _extract_column_name(column: Union[str, exp.Column]) -> str:
|
|
|
39
45
|
return str(column.this.this)
|
|
40
46
|
except AttributeError:
|
|
41
47
|
return str(column.this) if column.this else "column"
|
|
48
|
+
# Fallback for any unexpected types (defensive programming)
|
|
42
49
|
return "column"
|
|
43
50
|
|
|
44
51
|
|
|
@@ -58,6 +65,76 @@ class WhereClauseMixin:
|
|
|
58
65
|
# Type annotation for PyRight - this will be provided by the base class
|
|
59
66
|
_expression: Optional[exp.Expression]
|
|
60
67
|
|
|
68
|
+
def _create_parameterized_condition(
|
|
69
|
+
self,
|
|
70
|
+
column: Union[str, exp.Column],
|
|
71
|
+
value: Any,
|
|
72
|
+
condition_factory: "Callable[[exp.Expression, exp.Placeholder], exp.Expression]",
|
|
73
|
+
) -> exp.Expression:
|
|
74
|
+
"""Create a parameterized condition using the provided factory function.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
column: Column expression
|
|
78
|
+
value: Parameter value
|
|
79
|
+
condition_factory: Function that creates the condition expression
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The created condition expression
|
|
83
|
+
"""
|
|
84
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
85
|
+
column_name = _extract_column_name(column)
|
|
86
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
87
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
88
|
+
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
89
|
+
placeholder = exp.Placeholder(this=param_name)
|
|
90
|
+
return condition_factory(col_expr, placeholder)
|
|
91
|
+
|
|
92
|
+
def _merge_sql_object_parameters(self, sql_obj: Any) -> None:
|
|
93
|
+
"""Merge parameters from a SQL object into the builder.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
sql_obj: Object with parameters attribute containing parameter mappings
|
|
97
|
+
"""
|
|
98
|
+
if not has_expression_and_parameters(sql_obj):
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
102
|
+
sql_parameters = getattr(sql_obj, "parameters", {})
|
|
103
|
+
for param_name, param_value in sql_parameters.items():
|
|
104
|
+
unique_name = builder._generate_unique_parameter_name(param_name)
|
|
105
|
+
builder.add_parameter(param_value, name=unique_name)
|
|
106
|
+
|
|
107
|
+
def _apply_or_where(self, where_method: "Callable[..., Self]", *args: Any, **kwargs: Any) -> Self:
|
|
108
|
+
"""Apply a where method but use OR logic instead of AND.
|
|
109
|
+
|
|
110
|
+
This allows reusing all where_* methods for or_where_* functionality.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
where_method: The where method to apply (e.g., self.where_eq)
|
|
114
|
+
*args: Arguments to pass to the where method
|
|
115
|
+
**kwargs: Keyword arguments to pass to the where method
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Self with OR condition applied
|
|
119
|
+
"""
|
|
120
|
+
# Create a temporary clone to capture the condition
|
|
121
|
+
original_expr = self._expression
|
|
122
|
+
|
|
123
|
+
# Apply the where method to get the condition
|
|
124
|
+
where_method(*args, **kwargs)
|
|
125
|
+
|
|
126
|
+
# Get the last condition added by extracting it from the modified expression
|
|
127
|
+
if isinstance(self._expression, (exp.Select, exp.Update, exp.Delete)) and original_expr != self._expression:
|
|
128
|
+
last_where = self._expression.find(exp.Where)
|
|
129
|
+
if last_where and last_where.this:
|
|
130
|
+
condition = last_where.this
|
|
131
|
+
# Restore original expression
|
|
132
|
+
self._expression = original_expr
|
|
133
|
+
# Apply as OR
|
|
134
|
+
return self.or_where(condition)
|
|
135
|
+
|
|
136
|
+
return self
|
|
137
|
+
|
|
61
138
|
def _handle_in_operator(
|
|
62
139
|
self, column_exp: exp.Expression, value: Any, column_name: str = "column"
|
|
63
140
|
) -> exp.Expression:
|
|
@@ -142,75 +219,56 @@ class WhereClauseMixin:
|
|
|
142
219
|
msg = f"NOT BETWEEN operator requires a tuple of two values, got {type(value).__name__}"
|
|
143
220
|
raise SQLBuilderError(msg)
|
|
144
221
|
|
|
145
|
-
def _process_tuple_condition(self, condition: tuple) -> exp.Expression:
|
|
222
|
+
def _process_tuple_condition(self, condition: "tuple[Any, ...]") -> exp.Expression:
|
|
146
223
|
"""Process tuple-based WHERE conditions."""
|
|
147
|
-
builder = cast("SQLBuilderProtocol", self)
|
|
148
|
-
column_name_raw = str(condition[0])
|
|
149
|
-
column_exp = parse_column_expression(column_name_raw)
|
|
150
|
-
column_name = _extract_column_name(column_name_raw)
|
|
151
|
-
|
|
152
224
|
if len(condition) == 2:
|
|
153
225
|
# (column, value) tuple for equality
|
|
154
|
-
value = condition
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if len(condition) == 3:
|
|
160
|
-
# (column, operator, value) tuple
|
|
161
|
-
operator = str(condition[1]).upper()
|
|
162
|
-
value = condition[2]
|
|
226
|
+
column, value = condition
|
|
227
|
+
return self._create_parameterized_condition(
|
|
228
|
+
column, value, lambda col, placeholder: exp.EQ(this=col, expression=placeholder)
|
|
229
|
+
)
|
|
163
230
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
167
|
-
return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
168
|
-
if operator in {"!=", "<>"}:
|
|
169
|
-
param_name = builder._generate_unique_parameter_name(column_name)
|
|
170
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
171
|
-
return exp.NEQ(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
172
|
-
if operator == ">":
|
|
173
|
-
param_name = builder._generate_unique_parameter_name(column_name)
|
|
174
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
175
|
-
return exp.GT(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
176
|
-
if operator == ">=":
|
|
177
|
-
param_name = builder._generate_unique_parameter_name(column_name)
|
|
178
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
179
|
-
return exp.GTE(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
180
|
-
if operator == "<":
|
|
181
|
-
param_name = builder._generate_unique_parameter_name(column_name)
|
|
182
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
183
|
-
return exp.LT(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
184
|
-
if operator == "<=":
|
|
185
|
-
param_name = builder._generate_unique_parameter_name(column_name)
|
|
186
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
187
|
-
return exp.LTE(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
188
|
-
if operator == "LIKE":
|
|
189
|
-
param_name = builder._generate_unique_parameter_name(column_name)
|
|
190
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
191
|
-
return exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
192
|
-
if operator == "NOT LIKE":
|
|
193
|
-
param_name = builder._generate_unique_parameter_name(column_name)
|
|
194
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
195
|
-
return exp.Not(this=exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name)))
|
|
196
|
-
|
|
197
|
-
if operator == "IN":
|
|
198
|
-
return self._handle_in_operator(column_exp, value, column_name)
|
|
199
|
-
if operator == "NOT IN":
|
|
200
|
-
return self._handle_not_in_operator(column_exp, value, column_name)
|
|
201
|
-
if operator == "IS":
|
|
202
|
-
return self._handle_is_operator(column_exp, value)
|
|
203
|
-
if operator == "IS NOT":
|
|
204
|
-
return self._handle_is_not_operator(column_exp, value)
|
|
205
|
-
if operator == "BETWEEN":
|
|
206
|
-
return self._handle_between_operator(column_exp, value, column_name)
|
|
207
|
-
if operator == "NOT BETWEEN":
|
|
208
|
-
return self._handle_not_between_operator(column_exp, value, column_name)
|
|
209
|
-
|
|
210
|
-
msg = f"Unsupported operator: {operator}"
|
|
231
|
+
if len(condition) != 3:
|
|
232
|
+
msg = f"Condition tuple must have 2 or 3 elements, got {len(condition)}"
|
|
211
233
|
raise SQLBuilderError(msg)
|
|
212
234
|
|
|
213
|
-
|
|
235
|
+
# (column, operator, value) tuple
|
|
236
|
+
column_name_raw, operator, value = condition
|
|
237
|
+
operator = str(operator).upper()
|
|
238
|
+
column_exp = parse_column_expression(column_name_raw)
|
|
239
|
+
column_name = _extract_column_name(column_name_raw)
|
|
240
|
+
|
|
241
|
+
# Simple operators that use direct parameterization
|
|
242
|
+
simple_operators = {
|
|
243
|
+
"=": lambda col, placeholder: exp.EQ(this=col, expression=placeholder),
|
|
244
|
+
"!=": lambda col, placeholder: exp.NEQ(this=col, expression=placeholder),
|
|
245
|
+
"<>": lambda col, placeholder: exp.NEQ(this=col, expression=placeholder),
|
|
246
|
+
">": lambda col, placeholder: exp.GT(this=col, expression=placeholder),
|
|
247
|
+
">=": lambda col, placeholder: exp.GTE(this=col, expression=placeholder),
|
|
248
|
+
"<": lambda col, placeholder: exp.LT(this=col, expression=placeholder),
|
|
249
|
+
"<=": lambda col, placeholder: exp.LTE(this=col, expression=placeholder),
|
|
250
|
+
"LIKE": lambda col, placeholder: exp.Like(this=col, expression=placeholder),
|
|
251
|
+
"NOT LIKE": lambda col, placeholder: exp.Not(this=exp.Like(this=col, expression=placeholder)),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if operator in simple_operators:
|
|
255
|
+
return self._create_parameterized_condition(column_name_raw, value, simple_operators[operator])
|
|
256
|
+
|
|
257
|
+
# Complex operators that need special handling
|
|
258
|
+
if operator == "IN":
|
|
259
|
+
return self._handle_in_operator(column_exp, value, column_name)
|
|
260
|
+
if operator == "NOT IN":
|
|
261
|
+
return self._handle_not_in_operator(column_exp, value, column_name)
|
|
262
|
+
if operator == "IS":
|
|
263
|
+
return self._handle_is_operator(column_exp, value)
|
|
264
|
+
if operator == "IS NOT":
|
|
265
|
+
return self._handle_is_not_operator(column_exp, value)
|
|
266
|
+
if operator == "BETWEEN":
|
|
267
|
+
return self._handle_between_operator(column_exp, value, column_name)
|
|
268
|
+
if operator == "NOT BETWEEN":
|
|
269
|
+
return self._handle_not_between_operator(column_exp, value, column_name)
|
|
270
|
+
|
|
271
|
+
msg = f"Unsupported operator: {operator}"
|
|
214
272
|
raise SQLBuilderError(msg)
|
|
215
273
|
|
|
216
274
|
def where(
|
|
@@ -325,24 +383,18 @@ class WhereClauseMixin:
|
|
|
325
383
|
where_expr = builder._parameterize_expression(raw_expr)
|
|
326
384
|
else:
|
|
327
385
|
where_expr = parse_condition_expression(str(condition))
|
|
328
|
-
elif
|
|
386
|
+
elif has_expression_and_sql(condition):
|
|
329
387
|
# Handle SQL objects (from sql.raw with parameters)
|
|
330
388
|
expression = getattr(condition, "expression", None)
|
|
331
389
|
if expression is not None and isinstance(expression, exp.Expression):
|
|
332
390
|
# Merge parameters from SQL object into builder
|
|
333
|
-
|
|
334
|
-
sql_parameters = getattr(condition, "parameters", {})
|
|
335
|
-
for param_name, param_value in sql_parameters.items():
|
|
336
|
-
builder.add_parameter(param_value, name=param_name)
|
|
391
|
+
self._merge_sql_object_parameters(condition)
|
|
337
392
|
where_expr = expression
|
|
338
393
|
else:
|
|
339
394
|
# If expression is None, fall back to parsing the raw SQL
|
|
340
395
|
sql_text = getattr(condition, "sql", "")
|
|
341
396
|
# Merge parameters even when parsing raw SQL
|
|
342
|
-
|
|
343
|
-
sql_parameters = getattr(condition, "parameters", {})
|
|
344
|
-
for param_name, param_value in sql_parameters.items():
|
|
345
|
-
builder.add_parameter(param_value, name=param_name)
|
|
397
|
+
self._merge_sql_object_parameters(condition)
|
|
346
398
|
where_expr = parse_condition_expression(sql_text)
|
|
347
399
|
else:
|
|
348
400
|
msg = f"Unsupported condition type: {type(condition).__name__}"
|
|
@@ -357,62 +409,40 @@ class WhereClauseMixin:
|
|
|
357
409
|
|
|
358
410
|
def where_eq(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
359
411
|
"""Add WHERE column = value clause."""
|
|
360
|
-
|
|
361
|
-
column_name = _extract_column_name(column)
|
|
362
|
-
param_name = builder._generate_unique_parameter_name(column_name)
|
|
363
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
364
|
-
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
365
|
-
condition: exp.Expression = col_expr.eq(exp.Placeholder(this=param_name))
|
|
412
|
+
condition = self._create_parameterized_condition(column, value, lambda col, placeholder: col.eq(placeholder))
|
|
366
413
|
return self.where(condition)
|
|
367
414
|
|
|
368
415
|
def where_neq(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
369
416
|
"""Add WHERE column != value clause."""
|
|
370
|
-
|
|
371
|
-
column_name = _extract_column_name(column)
|
|
372
|
-
param_name = builder._generate_unique_parameter_name(column_name)
|
|
373
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
374
|
-
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
375
|
-
condition: exp.Expression = col_expr.neq(exp.Placeholder(this=param_name))
|
|
417
|
+
condition = self._create_parameterized_condition(column, value, lambda col, placeholder: col.neq(placeholder))
|
|
376
418
|
return self.where(condition)
|
|
377
419
|
|
|
378
420
|
def where_lt(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
379
421
|
"""Add WHERE column < value clause."""
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
384
|
-
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
385
|
-
condition: exp.Expression = exp.LT(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
422
|
+
condition = self._create_parameterized_condition(
|
|
423
|
+
column, value, lambda col, placeholder: exp.LT(this=col, expression=placeholder)
|
|
424
|
+
)
|
|
386
425
|
return self.where(condition)
|
|
387
426
|
|
|
388
427
|
def where_lte(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
389
428
|
"""Add WHERE column <= value clause."""
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
394
|
-
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
395
|
-
condition: exp.Expression = exp.LTE(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
429
|
+
condition = self._create_parameterized_condition(
|
|
430
|
+
column, value, lambda col, placeholder: exp.LTE(this=col, expression=placeholder)
|
|
431
|
+
)
|
|
396
432
|
return self.where(condition)
|
|
397
433
|
|
|
398
434
|
def where_gt(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
399
435
|
"""Add WHERE column > value clause."""
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
404
|
-
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
405
|
-
condition: exp.Expression = exp.GT(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
436
|
+
condition = self._create_parameterized_condition(
|
|
437
|
+
column, value, lambda col, placeholder: exp.GT(this=col, expression=placeholder)
|
|
438
|
+
)
|
|
406
439
|
return self.where(condition)
|
|
407
440
|
|
|
408
441
|
def where_gte(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
409
442
|
"""Add WHERE column >= value clause."""
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
_, param_name = builder.add_parameter(value, name=param_name)
|
|
414
|
-
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
415
|
-
condition: exp.Expression = exp.GTE(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
443
|
+
condition = self._create_parameterized_condition(
|
|
444
|
+
column, value, lambda col, placeholder: exp.GTE(this=col, expression=placeholder)
|
|
445
|
+
)
|
|
416
446
|
return self.where(condition)
|
|
417
447
|
|
|
418
448
|
def where_between(self, column: Union[str, exp.Column], low: Any, high: Any) -> Self:
|
|
@@ -443,22 +473,16 @@ class WhereClauseMixin:
|
|
|
443
473
|
|
|
444
474
|
def where_not_like(self, column: Union[str, exp.Column], pattern: str) -> Self:
|
|
445
475
|
"""Add WHERE column NOT LIKE pattern clause."""
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
_, param_name = builder.add_parameter(pattern, name=param_name)
|
|
450
|
-
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
451
|
-
condition: exp.Expression = col_expr.like(exp.Placeholder(this=param_name)).not_()
|
|
476
|
+
condition = self._create_parameterized_condition(
|
|
477
|
+
column, pattern, lambda col, placeholder: col.like(placeholder).not_()
|
|
478
|
+
)
|
|
452
479
|
return self.where(condition)
|
|
453
480
|
|
|
454
481
|
def where_ilike(self, column: Union[str, exp.Column], pattern: str) -> Self:
|
|
455
482
|
"""Add WHERE column ILIKE pattern clause."""
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
_, param_name = builder.add_parameter(pattern, name=param_name)
|
|
460
|
-
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
461
|
-
condition: exp.Expression = col_expr.ilike(exp.Placeholder(this=param_name))
|
|
483
|
+
condition = self._create_parameterized_condition(
|
|
484
|
+
column, pattern, lambda col, placeholder: col.ilike(placeholder)
|
|
485
|
+
)
|
|
462
486
|
return self.where(condition)
|
|
463
487
|
|
|
464
488
|
def where_is_null(self, column: Union[str, exp.Column]) -> Self:
|
|
@@ -483,10 +507,11 @@ class WhereClauseMixin:
|
|
|
483
507
|
subquery = values.build() # pyright: ignore
|
|
484
508
|
sql_str = subquery.sql
|
|
485
509
|
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect_name)) # pyright: ignore
|
|
486
|
-
# Merge subquery parameters into parent builder
|
|
510
|
+
# Merge subquery parameters into parent builder with unique naming
|
|
487
511
|
if hasattr(subquery, "parameters") and isinstance(subquery.parameters, dict): # pyright: ignore[reportAttributeAccessIssue]
|
|
488
512
|
for param_name, param_value in subquery.parameters.items(): # pyright: ignore[reportAttributeAccessIssue]
|
|
489
|
-
builder.
|
|
513
|
+
unique_name = builder._generate_unique_parameter_name(param_name)
|
|
514
|
+
builder.add_parameter(param_value, name=unique_name)
|
|
490
515
|
else:
|
|
491
516
|
subquery_exp = values # type: ignore[assignment]
|
|
492
517
|
condition = col_expr.isin(subquery_exp)
|
|
@@ -666,6 +691,600 @@ class WhereClauseMixin:
|
|
|
666
691
|
condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
|
|
667
692
|
return self.where(condition)
|
|
668
693
|
|
|
694
|
+
def or_where(
|
|
695
|
+
self,
|
|
696
|
+
condition: Union[
|
|
697
|
+
str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression", "SQL"
|
|
698
|
+
],
|
|
699
|
+
*values: Any,
|
|
700
|
+
operator: Optional[str] = None,
|
|
701
|
+
**kwargs: Any,
|
|
702
|
+
) -> Self:
|
|
703
|
+
"""Add an OR condition to the existing WHERE clause.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
condition: The condition for the OR WHERE clause. Can be:
|
|
707
|
+
- A string condition with or without parameter placeholders
|
|
708
|
+
- A string column name (when values are provided)
|
|
709
|
+
- A sqlglot Expression or Condition
|
|
710
|
+
- A 2-tuple (column, value) for equality comparison
|
|
711
|
+
- A 3-tuple (column, operator, value) for custom comparison
|
|
712
|
+
*values: Positional values for parameter binding (when condition contains placeholders or is a column name)
|
|
713
|
+
operator: Operator for comparison (when condition is a column name)
|
|
714
|
+
**kwargs: Named parameters for parameter binding (when condition contains named placeholders)
|
|
715
|
+
|
|
716
|
+
Raises:
|
|
717
|
+
SQLBuilderError: If the current expression is not a supported statement type or no existing WHERE clause.
|
|
718
|
+
|
|
719
|
+
Returns:
|
|
720
|
+
The current builder instance for method chaining.
|
|
721
|
+
"""
|
|
722
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
723
|
+
if builder._expression is None:
|
|
724
|
+
msg = "Cannot add OR WHERE clause: expression is not initialized."
|
|
725
|
+
raise SQLBuilderError(msg)
|
|
726
|
+
|
|
727
|
+
# Get the existing WHERE condition
|
|
728
|
+
existing_where = None
|
|
729
|
+
if isinstance(builder._expression, (exp.Select, exp.Update, exp.Delete)):
|
|
730
|
+
existing_where = builder._expression.find(exp.Where)
|
|
731
|
+
if existing_where:
|
|
732
|
+
existing_where = existing_where.this
|
|
733
|
+
|
|
734
|
+
if existing_where is None:
|
|
735
|
+
msg = "Cannot add OR WHERE clause: no existing WHERE clause found. Use where() first."
|
|
736
|
+
raise SQLBuilderError(msg)
|
|
737
|
+
|
|
738
|
+
# Process the new condition (reuse existing logic from where method)
|
|
739
|
+
new_condition = self._process_where_condition(condition, values, operator, kwargs)
|
|
740
|
+
|
|
741
|
+
# Combine with existing WHERE using OR
|
|
742
|
+
or_condition = exp.Or(this=existing_where, expression=new_condition)
|
|
743
|
+
|
|
744
|
+
# Update the WHERE clause by modifying the existing WHERE node
|
|
745
|
+
if isinstance(builder._expression, (exp.Select, exp.Update, exp.Delete)):
|
|
746
|
+
where_node = builder._expression.find(exp.Where)
|
|
747
|
+
if where_node:
|
|
748
|
+
where_node.set("this", or_condition)
|
|
749
|
+
else:
|
|
750
|
+
# This shouldn't happen since we checked for existing_where above
|
|
751
|
+
builder._expression = builder._expression.where(or_condition, copy=False)
|
|
752
|
+
else:
|
|
753
|
+
msg = f"OR WHERE clause not supported for {type(builder._expression).__name__}"
|
|
754
|
+
raise SQLBuilderError(msg)
|
|
755
|
+
|
|
756
|
+
return self
|
|
757
|
+
|
|
758
|
+
def where_or(self, *conditions: Union[str, "tuple[Any, ...]", exp.Expression]) -> Self:
|
|
759
|
+
"""Combine multiple conditions with OR logic.
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
*conditions: Multiple conditions to combine with OR. Each condition can be:
|
|
763
|
+
- A string condition
|
|
764
|
+
- A 2-tuple (column, value) for equality comparison
|
|
765
|
+
- A 3-tuple (column, operator, value) for custom comparison
|
|
766
|
+
- A sqlglot Expression or Condition
|
|
767
|
+
|
|
768
|
+
Raises:
|
|
769
|
+
SQLBuilderError: If no conditions provided or current expression not supported.
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
The current builder instance for method chaining.
|
|
773
|
+
|
|
774
|
+
Examples:
|
|
775
|
+
query.where_or(
|
|
776
|
+
("name", "John"),
|
|
777
|
+
("email", "john@email.com"),
|
|
778
|
+
"age > 25"
|
|
779
|
+
)
|
|
780
|
+
# Produces: WHERE (name = :name OR email = :email OR age > 25)
|
|
781
|
+
"""
|
|
782
|
+
if not conditions:
|
|
783
|
+
msg = "where_or() requires at least one condition"
|
|
784
|
+
raise SQLBuilderError(msg)
|
|
785
|
+
|
|
786
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
787
|
+
if builder._expression is None:
|
|
788
|
+
msg = "Cannot add WHERE OR clause: expression is not initialized."
|
|
789
|
+
raise SQLBuilderError(msg)
|
|
790
|
+
|
|
791
|
+
# Process all conditions
|
|
792
|
+
processed_conditions = []
|
|
793
|
+
for condition in conditions:
|
|
794
|
+
processed_condition = self._process_where_condition(condition, (), None, {})
|
|
795
|
+
processed_conditions.append(processed_condition)
|
|
796
|
+
|
|
797
|
+
# Create OR expression from all conditions
|
|
798
|
+
or_condition = self._create_or_expression(processed_conditions)
|
|
799
|
+
|
|
800
|
+
# Apply the OR condition
|
|
801
|
+
if isinstance(builder._expression, (exp.Select, exp.Update, exp.Delete)):
|
|
802
|
+
builder._expression = builder._expression.where(or_condition, copy=False)
|
|
803
|
+
else:
|
|
804
|
+
msg = f"WHERE OR clause not supported for {type(builder._expression).__name__}"
|
|
805
|
+
raise SQLBuilderError(msg)
|
|
806
|
+
|
|
807
|
+
return self
|
|
808
|
+
|
|
809
|
+
def _process_where_condition(
|
|
810
|
+
self,
|
|
811
|
+
condition: Union[
|
|
812
|
+
str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression", "SQL"
|
|
813
|
+
],
|
|
814
|
+
values: tuple[Any, ...],
|
|
815
|
+
operator: Optional[str],
|
|
816
|
+
kwargs: dict[str, Any],
|
|
817
|
+
) -> exp.Expression:
|
|
818
|
+
"""Process a WHERE condition into a sqlglot expression.
|
|
819
|
+
|
|
820
|
+
This is extracted from the where() method to be reusable by OR methods.
|
|
821
|
+
|
|
822
|
+
Args:
|
|
823
|
+
condition: The condition to process
|
|
824
|
+
values: Positional values for parameter binding
|
|
825
|
+
operator: Operator for comparison
|
|
826
|
+
kwargs: Named parameters for parameter binding
|
|
827
|
+
|
|
828
|
+
Returns:
|
|
829
|
+
Processed sqlglot expression
|
|
830
|
+
"""
|
|
831
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
832
|
+
|
|
833
|
+
# Handle string conditions with external parameters
|
|
834
|
+
if values or kwargs:
|
|
835
|
+
if not isinstance(condition, str):
|
|
836
|
+
msg = "When values are provided, condition must be a string"
|
|
837
|
+
raise SQLBuilderError(msg)
|
|
838
|
+
|
|
839
|
+
# Check if condition contains parameter placeholders
|
|
840
|
+
from sqlspec.core.parameters import ParameterStyle, ParameterValidator
|
|
841
|
+
|
|
842
|
+
validator = ParameterValidator()
|
|
843
|
+
param_info = validator.extract_parameters(condition)
|
|
844
|
+
|
|
845
|
+
if param_info:
|
|
846
|
+
# String condition with placeholders - create SQL object with parameters
|
|
847
|
+
from sqlspec import sql as sql_factory
|
|
848
|
+
|
|
849
|
+
# Create parameter mapping based on the detected parameter info
|
|
850
|
+
param_dict = dict(kwargs) # Start with named parameters
|
|
851
|
+
|
|
852
|
+
# Handle positional parameters - these are ordinal-based ($1, $2, :1, :2, ?)
|
|
853
|
+
positional_params = [
|
|
854
|
+
param
|
|
855
|
+
for param in param_info
|
|
856
|
+
if param.style in {ParameterStyle.NUMERIC, ParameterStyle.POSITIONAL_COLON, ParameterStyle.QMARK}
|
|
857
|
+
]
|
|
858
|
+
|
|
859
|
+
# Map positional values to positional parameters
|
|
860
|
+
if len(values) != len(positional_params):
|
|
861
|
+
msg = f"Parameter count mismatch: condition has {len(positional_params)} positional placeholders, got {len(values)} values"
|
|
862
|
+
raise SQLBuilderError(msg)
|
|
863
|
+
|
|
864
|
+
for i, value in enumerate(values):
|
|
865
|
+
param_dict[f"param_{i}"] = value
|
|
866
|
+
|
|
867
|
+
# Create SQL object with parameters that will be processed correctly
|
|
868
|
+
condition = sql_factory.raw(condition, **param_dict)
|
|
869
|
+
# Fall through to existing SQL object handling logic
|
|
870
|
+
|
|
871
|
+
elif len(values) == 1 and not kwargs:
|
|
872
|
+
# Single value - treat as column = value
|
|
873
|
+
if operator is not None:
|
|
874
|
+
return self._process_tuple_condition((condition, operator, values[0]))
|
|
875
|
+
return self._process_tuple_condition((condition, values[0]))
|
|
876
|
+
else:
|
|
877
|
+
msg = f"Cannot bind parameters to condition without placeholders: {condition}"
|
|
878
|
+
raise SQLBuilderError(msg)
|
|
879
|
+
|
|
880
|
+
# Handle all condition types (including SQL objects created above)
|
|
881
|
+
if isinstance(condition, str):
|
|
882
|
+
return parse_condition_expression(condition)
|
|
883
|
+
if isinstance(condition, (exp.Expression, exp.Condition)):
|
|
884
|
+
return condition
|
|
885
|
+
if isinstance(condition, tuple):
|
|
886
|
+
return self._process_tuple_condition(condition)
|
|
887
|
+
if has_query_builder_parameters(condition):
|
|
888
|
+
column_expr_obj = cast("ColumnExpression", condition)
|
|
889
|
+
return column_expr_obj._expression # pyright: ignore
|
|
890
|
+
if has_sqlglot_expression(condition):
|
|
891
|
+
raw_expr = condition.sqlglot_expression # pyright: ignore[attr-defined]
|
|
892
|
+
if raw_expr is not None:
|
|
893
|
+
return builder._parameterize_expression(raw_expr)
|
|
894
|
+
return parse_condition_expression(str(condition))
|
|
895
|
+
if hasattr(condition, "expression") and hasattr(condition, "sql"):
|
|
896
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
897
|
+
expression = getattr(condition, "expression", None)
|
|
898
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
899
|
+
# Merge parameters from SQL object into builder
|
|
900
|
+
if hasattr(condition, "parameters") and hasattr(builder, "add_parameter"):
|
|
901
|
+
sql_parameters = getattr(condition, "parameters", {})
|
|
902
|
+
for param_name, param_value in sql_parameters.items():
|
|
903
|
+
unique_name = builder._generate_unique_parameter_name(param_name)
|
|
904
|
+
builder.add_parameter(param_value, name=unique_name)
|
|
905
|
+
return cast("exp.Expression", expression)
|
|
906
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
907
|
+
sql_text = getattr(condition, "sql", "")
|
|
908
|
+
# Merge parameters even when parsing raw SQL
|
|
909
|
+
if hasattr(condition, "parameters") and hasattr(builder, "add_parameter"):
|
|
910
|
+
sql_parameters = getattr(condition, "parameters", {})
|
|
911
|
+
for param_name, param_value in sql_parameters.items():
|
|
912
|
+
unique_name = builder._generate_unique_parameter_name(param_name)
|
|
913
|
+
builder.add_parameter(param_value, name=unique_name)
|
|
914
|
+
return parse_condition_expression(sql_text)
|
|
915
|
+
msg = f"Unsupported condition type: {type(condition).__name__}"
|
|
916
|
+
raise SQLBuilderError(msg)
|
|
917
|
+
|
|
918
|
+
def _create_or_expression(self, conditions: list[exp.Expression]) -> exp.Expression:
|
|
919
|
+
"""Create OR expression from multiple conditions.
|
|
920
|
+
|
|
921
|
+
Args:
|
|
922
|
+
conditions: List of sqlglot expressions to combine with OR
|
|
923
|
+
|
|
924
|
+
Returns:
|
|
925
|
+
Combined OR expression, or single condition if only one provided
|
|
926
|
+
"""
|
|
927
|
+
if len(conditions) == 1:
|
|
928
|
+
return conditions[0]
|
|
929
|
+
|
|
930
|
+
result = conditions[0]
|
|
931
|
+
for condition in conditions[1:]:
|
|
932
|
+
result = exp.Or(this=result, expression=condition)
|
|
933
|
+
return result
|
|
934
|
+
|
|
935
|
+
# OR helper methods for consistency with existing where_* methods
|
|
936
|
+
def or_where_eq(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
937
|
+
"""Add OR column = value clause."""
|
|
938
|
+
condition = self._create_parameterized_condition(column, value, lambda col, placeholder: col.eq(placeholder))
|
|
939
|
+
return self.or_where(condition)
|
|
940
|
+
|
|
941
|
+
def or_where_neq(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
942
|
+
"""Add OR column != value clause."""
|
|
943
|
+
condition = self._create_parameterized_condition(column, value, lambda col, placeholder: col.neq(placeholder))
|
|
944
|
+
return self.or_where(condition)
|
|
945
|
+
|
|
946
|
+
def or_where_in(self, column: Union[str, exp.Column], values: Any) -> Self:
|
|
947
|
+
"""Add OR column IN (values) clause."""
|
|
948
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
949
|
+
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
950
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
951
|
+
subquery_exp: exp.Expression
|
|
952
|
+
if has_query_builder_parameters(values):
|
|
953
|
+
subquery_builder_parameters: dict[str, Any] = values.parameters # pyright: ignore
|
|
954
|
+
param_mapping = {}
|
|
955
|
+
if subquery_builder_parameters:
|
|
956
|
+
for p_name, p_value in subquery_builder_parameters.items():
|
|
957
|
+
unique_name = builder._generate_unique_parameter_name(p_name)
|
|
958
|
+
param_mapping[p_name] = unique_name
|
|
959
|
+
builder.add_parameter(p_value, name=unique_name)
|
|
960
|
+
subquery = values.build() # pyright: ignore
|
|
961
|
+
subquery_parsed = exp.maybe_parse(subquery.sql, dialect=builder.dialect_name) # pyright: ignore
|
|
962
|
+
if param_mapping and subquery_parsed:
|
|
963
|
+
subquery_parsed = cast("Any", builder)._update_placeholders_in_expression(
|
|
964
|
+
subquery_parsed, param_mapping
|
|
965
|
+
)
|
|
966
|
+
subquery_exp = (
|
|
967
|
+
exp.paren(subquery_parsed)
|
|
968
|
+
if subquery_parsed
|
|
969
|
+
else exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
|
|
970
|
+
) # pyright: ignore
|
|
971
|
+
else:
|
|
972
|
+
subquery_exp = values # type: ignore[assignment]
|
|
973
|
+
condition = col_expr.isin(subquery_exp)
|
|
974
|
+
return self.or_where(condition)
|
|
975
|
+
if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
|
|
976
|
+
msg = "Unsupported type for 'values' in OR WHERE IN"
|
|
977
|
+
raise SQLBuilderError(msg)
|
|
978
|
+
column_name = _extract_column_name(column)
|
|
979
|
+
parameters = []
|
|
980
|
+
for i, v in enumerate(values):
|
|
981
|
+
if len(values) == 1:
|
|
982
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
983
|
+
else:
|
|
984
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
|
|
985
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
986
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
987
|
+
condition = col_expr.isin(*parameters)
|
|
988
|
+
return self.or_where(condition)
|
|
989
|
+
|
|
990
|
+
def or_where_like(self, column: Union[str, exp.Column], pattern: str, escape: Optional[str] = None) -> Self:
|
|
991
|
+
"""Add OR column LIKE pattern clause."""
|
|
992
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
993
|
+
column_name = _extract_column_name(column)
|
|
994
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
995
|
+
_, param_name = builder.add_parameter(pattern, name=param_name)
|
|
996
|
+
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
997
|
+
if escape is not None:
|
|
998
|
+
cond = exp.Like(this=col_expr, expression=exp.Placeholder(this=param_name), escape=exp.convert(str(escape)))
|
|
999
|
+
else:
|
|
1000
|
+
cond = col_expr.like(exp.Placeholder(this=param_name))
|
|
1001
|
+
condition: exp.Expression = cond
|
|
1002
|
+
return self.or_where(condition)
|
|
1003
|
+
|
|
1004
|
+
def or_where_is_null(self, column: Union[str, exp.Column]) -> Self:
|
|
1005
|
+
"""Add OR column IS NULL clause."""
|
|
1006
|
+
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
1007
|
+
condition: exp.Expression = col_expr.is_(exp.null())
|
|
1008
|
+
return self.or_where(condition)
|
|
1009
|
+
|
|
1010
|
+
def or_where_is_not_null(self, column: Union[str, exp.Column]) -> Self:
|
|
1011
|
+
"""Add OR column IS NOT NULL clause."""
|
|
1012
|
+
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
1013
|
+
condition: exp.Expression = col_expr.is_(exp.null()).not_()
|
|
1014
|
+
return self.or_where(condition)
|
|
1015
|
+
|
|
1016
|
+
def or_where_lt(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
1017
|
+
"""Add OR column < value clause."""
|
|
1018
|
+
condition = self._create_parameterized_condition(
|
|
1019
|
+
column, value, lambda col, placeholder: exp.LT(this=col, expression=placeholder)
|
|
1020
|
+
)
|
|
1021
|
+
return self.or_where(condition)
|
|
1022
|
+
|
|
1023
|
+
def or_where_lte(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
1024
|
+
"""Add OR column <= value clause."""
|
|
1025
|
+
condition = self._create_parameterized_condition(
|
|
1026
|
+
column, value, lambda col, placeholder: exp.LTE(this=col, expression=placeholder)
|
|
1027
|
+
)
|
|
1028
|
+
return self.or_where(condition)
|
|
1029
|
+
|
|
1030
|
+
def or_where_gt(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
1031
|
+
"""Add OR column > value clause."""
|
|
1032
|
+
condition = self._create_parameterized_condition(
|
|
1033
|
+
column, value, lambda col, placeholder: exp.GT(this=col, expression=placeholder)
|
|
1034
|
+
)
|
|
1035
|
+
return self.or_where(condition)
|
|
1036
|
+
|
|
1037
|
+
def or_where_gte(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
1038
|
+
"""Add OR column >= value clause."""
|
|
1039
|
+
condition = self._create_parameterized_condition(
|
|
1040
|
+
column, value, lambda col, placeholder: exp.GTE(this=col, expression=placeholder)
|
|
1041
|
+
)
|
|
1042
|
+
return self.or_where(condition)
|
|
1043
|
+
|
|
1044
|
+
def or_where_between(self, column: Union[str, exp.Column], low: Any, high: Any) -> Self:
|
|
1045
|
+
"""Add OR column BETWEEN low AND high clause."""
|
|
1046
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
1047
|
+
column_name = _extract_column_name(column)
|
|
1048
|
+
low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
|
|
1049
|
+
high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
|
|
1050
|
+
_, low_param = builder.add_parameter(low, name=low_param)
|
|
1051
|
+
_, high_param = builder.add_parameter(high, name=high_param)
|
|
1052
|
+
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
1053
|
+
condition: exp.Expression = col_expr.between(exp.Placeholder(this=low_param), exp.Placeholder(this=high_param))
|
|
1054
|
+
return self.or_where(condition)
|
|
1055
|
+
|
|
1056
|
+
def or_where_not_like(self, column: Union[str, exp.Column], pattern: str) -> Self:
|
|
1057
|
+
"""Add OR column NOT LIKE pattern clause."""
|
|
1058
|
+
condition = self._create_parameterized_condition(
|
|
1059
|
+
column, pattern, lambda col, placeholder: col.like(placeholder).not_()
|
|
1060
|
+
)
|
|
1061
|
+
return self.or_where(condition)
|
|
1062
|
+
|
|
1063
|
+
def or_where_not_in(self, column: Union[str, exp.Column], values: Any) -> Self:
|
|
1064
|
+
"""Add OR column NOT IN (values) clause."""
|
|
1065
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
1066
|
+
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
1067
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
1068
|
+
subquery_exp: exp.Expression
|
|
1069
|
+
if has_query_builder_parameters(values):
|
|
1070
|
+
subquery_builder_parameters: dict[str, Any] = values.parameters # pyright: ignore
|
|
1071
|
+
param_mapping = {}
|
|
1072
|
+
if subquery_builder_parameters:
|
|
1073
|
+
for p_name, p_value in subquery_builder_parameters.items():
|
|
1074
|
+
unique_name = builder._generate_unique_parameter_name(p_name)
|
|
1075
|
+
param_mapping[p_name] = unique_name
|
|
1076
|
+
builder.add_parameter(p_value, name=unique_name)
|
|
1077
|
+
subquery = values.build() # pyright: ignore
|
|
1078
|
+
subquery_parsed = exp.maybe_parse(subquery.sql, dialect=builder.dialect_name) # pyright: ignore
|
|
1079
|
+
if param_mapping and subquery_parsed:
|
|
1080
|
+
subquery_parsed = cast("Any", builder)._update_placeholders_in_expression(
|
|
1081
|
+
subquery_parsed, param_mapping
|
|
1082
|
+
)
|
|
1083
|
+
subquery_exp = (
|
|
1084
|
+
exp.paren(subquery_parsed)
|
|
1085
|
+
if subquery_parsed
|
|
1086
|
+
else exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
|
|
1087
|
+
) # pyright: ignore
|
|
1088
|
+
else:
|
|
1089
|
+
subquery_exp = values # type: ignore[assignment]
|
|
1090
|
+
condition = exp.Not(this=col_expr.isin(subquery_exp))
|
|
1091
|
+
return self.or_where(condition)
|
|
1092
|
+
if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
|
|
1093
|
+
msg = "Values for or_where_not_in must be a non-string iterable or subquery."
|
|
1094
|
+
raise SQLBuilderError(msg)
|
|
1095
|
+
column_name = _extract_column_name(column)
|
|
1096
|
+
parameters = []
|
|
1097
|
+
for i, v in enumerate(values):
|
|
1098
|
+
if len(values) == 1:
|
|
1099
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
1100
|
+
else:
|
|
1101
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
|
|
1102
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
1103
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
1104
|
+
condition = exp.Not(this=col_expr.isin(*parameters))
|
|
1105
|
+
return self.or_where(condition)
|
|
1106
|
+
|
|
1107
|
+
def or_where_ilike(self, column: Union[str, exp.Column], pattern: str) -> Self:
|
|
1108
|
+
"""Add OR column ILIKE pattern clause."""
|
|
1109
|
+
condition = self._create_parameterized_condition(
|
|
1110
|
+
column, pattern, lambda col, placeholder: col.ilike(placeholder)
|
|
1111
|
+
)
|
|
1112
|
+
return self.or_where(condition)
|
|
1113
|
+
|
|
1114
|
+
def or_where_null(self, column: Union[str, exp.Column]) -> Self:
|
|
1115
|
+
"""Add OR column IS NULL clause."""
|
|
1116
|
+
return self.or_where_is_null(column)
|
|
1117
|
+
|
|
1118
|
+
def or_where_not_null(self, column: Union[str, exp.Column]) -> Self:
|
|
1119
|
+
"""Add OR column IS NOT NULL clause."""
|
|
1120
|
+
return self.or_where_is_not_null(column)
|
|
1121
|
+
|
|
1122
|
+
def or_where_exists(self, subquery: Union[str, Any]) -> Self:
|
|
1123
|
+
"""Add OR EXISTS (subquery) clause."""
|
|
1124
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
1125
|
+
sub_expr: exp.Expression
|
|
1126
|
+
if has_query_builder_parameters(subquery):
|
|
1127
|
+
subquery_builder_parameters: dict[str, Any] = subquery.parameters
|
|
1128
|
+
param_mapping = {}
|
|
1129
|
+
if subquery_builder_parameters:
|
|
1130
|
+
for p_name, p_value in subquery_builder_parameters.items():
|
|
1131
|
+
unique_name = builder._generate_unique_parameter_name(p_name)
|
|
1132
|
+
param_mapping[p_name] = unique_name
|
|
1133
|
+
builder.add_parameter(p_value, name=unique_name)
|
|
1134
|
+
sub_sql_obj = subquery.build() # pyright: ignore
|
|
1135
|
+
sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=builder.dialect_name) # pyright: ignore
|
|
1136
|
+
# Update placeholders to use unique parameter names
|
|
1137
|
+
if param_mapping and sub_expr:
|
|
1138
|
+
sub_expr = cast("Any", builder)._update_placeholders_in_expression(sub_expr, param_mapping)
|
|
1139
|
+
else:
|
|
1140
|
+
sub_expr = exp.maybe_parse(str(subquery), dialect=builder.dialect_name)
|
|
1141
|
+
|
|
1142
|
+
if sub_expr is None:
|
|
1143
|
+
msg = "Could not parse subquery for OR EXISTS"
|
|
1144
|
+
raise SQLBuilderError(msg)
|
|
1145
|
+
|
|
1146
|
+
exists_expr = exp.Exists(this=sub_expr)
|
|
1147
|
+
return self.or_where(exists_expr)
|
|
1148
|
+
|
|
1149
|
+
def or_where_not_exists(self, subquery: Union[str, Any]) -> Self:
|
|
1150
|
+
"""Add OR NOT EXISTS (subquery) clause."""
|
|
1151
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
1152
|
+
sub_expr: exp.Expression
|
|
1153
|
+
if has_query_builder_parameters(subquery):
|
|
1154
|
+
subquery_builder_parameters: dict[str, Any] = subquery.parameters
|
|
1155
|
+
param_mapping = {}
|
|
1156
|
+
if subquery_builder_parameters:
|
|
1157
|
+
for p_name, p_value in subquery_builder_parameters.items():
|
|
1158
|
+
unique_name = builder._generate_unique_parameter_name(p_name)
|
|
1159
|
+
param_mapping[p_name] = unique_name
|
|
1160
|
+
builder.add_parameter(p_value, name=unique_name)
|
|
1161
|
+
sub_sql_obj = subquery.build() # pyright: ignore
|
|
1162
|
+
sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=builder.dialect_name) # pyright: ignore
|
|
1163
|
+
# Update placeholders to use unique parameter names
|
|
1164
|
+
if param_mapping and sub_expr:
|
|
1165
|
+
sub_expr = cast("Any", builder)._update_placeholders_in_expression(sub_expr, param_mapping)
|
|
1166
|
+
else:
|
|
1167
|
+
sub_expr = exp.maybe_parse(str(subquery), dialect=builder.dialect_name)
|
|
1168
|
+
|
|
1169
|
+
if sub_expr is None:
|
|
1170
|
+
msg = "Could not parse subquery for OR NOT EXISTS"
|
|
1171
|
+
raise SQLBuilderError(msg)
|
|
1172
|
+
|
|
1173
|
+
not_exists_expr = exp.Not(this=exp.Exists(this=sub_expr))
|
|
1174
|
+
return self.or_where(not_exists_expr)
|
|
1175
|
+
|
|
1176
|
+
def or_where_any(self, column: Union[str, exp.Column], values: Any) -> Self:
|
|
1177
|
+
"""Add OR column = ANY(values) clause."""
|
|
1178
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
1179
|
+
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
1180
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
1181
|
+
subquery_exp: exp.Expression
|
|
1182
|
+
if has_query_builder_parameters(values):
|
|
1183
|
+
subquery_builder_parameters: dict[str, Any] = values.parameters # pyright: ignore
|
|
1184
|
+
param_mapping = {}
|
|
1185
|
+
if subquery_builder_parameters:
|
|
1186
|
+
for p_name, p_value in subquery_builder_parameters.items():
|
|
1187
|
+
unique_name = builder._generate_unique_parameter_name(p_name)
|
|
1188
|
+
param_mapping[p_name] = unique_name
|
|
1189
|
+
builder.add_parameter(p_value, name=unique_name)
|
|
1190
|
+
subquery = values.build() # pyright: ignore
|
|
1191
|
+
subquery_parsed = exp.maybe_parse(subquery.sql, dialect=builder.dialect_name) # pyright: ignore
|
|
1192
|
+
if param_mapping and subquery_parsed:
|
|
1193
|
+
subquery_parsed = cast("Any", builder)._update_placeholders_in_expression(
|
|
1194
|
+
subquery_parsed, param_mapping
|
|
1195
|
+
)
|
|
1196
|
+
subquery_exp = (
|
|
1197
|
+
exp.paren(subquery_parsed)
|
|
1198
|
+
if subquery_parsed
|
|
1199
|
+
else exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
|
|
1200
|
+
) # pyright: ignore
|
|
1201
|
+
else:
|
|
1202
|
+
subquery_exp = values # type: ignore[assignment]
|
|
1203
|
+
condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
|
|
1204
|
+
return self.or_where(condition)
|
|
1205
|
+
if isinstance(values, str):
|
|
1206
|
+
try:
|
|
1207
|
+
parsed_expr: Optional[exp.Expression] = exp.maybe_parse(values)
|
|
1208
|
+
if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
|
|
1209
|
+
subquery_exp = exp.paren(parsed_expr)
|
|
1210
|
+
condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
|
|
1211
|
+
return self.or_where(condition)
|
|
1212
|
+
except Exception: # noqa: S110
|
|
1213
|
+
pass
|
|
1214
|
+
msg = "Unsupported type for 'values' in OR WHERE ANY"
|
|
1215
|
+
raise SQLBuilderError(msg)
|
|
1216
|
+
if not is_iterable_parameters(values) or isinstance(values, bytes):
|
|
1217
|
+
msg = "Unsupported type for 'values' in OR WHERE ANY"
|
|
1218
|
+
raise SQLBuilderError(msg)
|
|
1219
|
+
column_name = _extract_column_name(column)
|
|
1220
|
+
parameters = []
|
|
1221
|
+
for i, v in enumerate(values):
|
|
1222
|
+
if len(values) == 1:
|
|
1223
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
1224
|
+
else:
|
|
1225
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_any_{i + 1}")
|
|
1226
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
1227
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
1228
|
+
tuple_expr = exp.Tuple(expressions=parameters)
|
|
1229
|
+
condition = exp.EQ(this=col_expr, expression=exp.Any(this=tuple_expr))
|
|
1230
|
+
return self.or_where(condition)
|
|
1231
|
+
|
|
1232
|
+
def or_where_not_any(self, column: Union[str, exp.Column], values: Any) -> Self:
|
|
1233
|
+
"""Add OR column <> ANY(values) clause."""
|
|
1234
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
1235
|
+
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
1236
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
1237
|
+
subquery_exp: exp.Expression
|
|
1238
|
+
if has_query_builder_parameters(values):
|
|
1239
|
+
subquery_builder_parameters: dict[str, Any] = values.parameters # pyright: ignore
|
|
1240
|
+
param_mapping = {}
|
|
1241
|
+
if subquery_builder_parameters:
|
|
1242
|
+
for p_name, p_value in subquery_builder_parameters.items():
|
|
1243
|
+
unique_name = builder._generate_unique_parameter_name(p_name)
|
|
1244
|
+
param_mapping[p_name] = unique_name
|
|
1245
|
+
builder.add_parameter(p_value, name=unique_name)
|
|
1246
|
+
subquery = values.build() # pyright: ignore
|
|
1247
|
+
subquery_parsed = exp.maybe_parse(subquery.sql, dialect=builder.dialect_name) # pyright: ignore
|
|
1248
|
+
if param_mapping and subquery_parsed:
|
|
1249
|
+
subquery_parsed = cast("Any", builder)._update_placeholders_in_expression(
|
|
1250
|
+
subquery_parsed, param_mapping
|
|
1251
|
+
)
|
|
1252
|
+
subquery_exp = (
|
|
1253
|
+
exp.paren(subquery_parsed)
|
|
1254
|
+
if subquery_parsed
|
|
1255
|
+
else exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
|
|
1256
|
+
) # pyright: ignore
|
|
1257
|
+
else:
|
|
1258
|
+
subquery_exp = values # type: ignore[assignment]
|
|
1259
|
+
condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
|
|
1260
|
+
return self.or_where(condition)
|
|
1261
|
+
if isinstance(values, str):
|
|
1262
|
+
try:
|
|
1263
|
+
parsed_expr: Optional[exp.Expression] = exp.maybe_parse(values)
|
|
1264
|
+
if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
|
|
1265
|
+
subquery_exp = exp.paren(parsed_expr)
|
|
1266
|
+
condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
|
|
1267
|
+
return self.or_where(condition)
|
|
1268
|
+
except Exception: # noqa: S110
|
|
1269
|
+
pass
|
|
1270
|
+
msg = "Unsupported type for 'values' in OR WHERE NOT ANY"
|
|
1271
|
+
raise SQLBuilderError(msg)
|
|
1272
|
+
if not is_iterable_parameters(values) or isinstance(values, bytes):
|
|
1273
|
+
msg = "Unsupported type for 'values' in OR WHERE NOT ANY"
|
|
1274
|
+
raise SQLBuilderError(msg)
|
|
1275
|
+
column_name = _extract_column_name(column)
|
|
1276
|
+
parameters = []
|
|
1277
|
+
for i, v in enumerate(values):
|
|
1278
|
+
if len(values) == 1:
|
|
1279
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
1280
|
+
else:
|
|
1281
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_not_any_{i + 1}")
|
|
1282
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
1283
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
1284
|
+
tuple_expr = exp.Tuple(expressions=parameters)
|
|
1285
|
+
condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
|
|
1286
|
+
return self.or_where(condition)
|
|
1287
|
+
|
|
669
1288
|
|
|
670
1289
|
@trait
|
|
671
1290
|
class HavingClauseMixin:
|