sqlspec 0.15.0__py3-none-any.whl → 0.16.2__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 +702 -44
- sqlspec/builder/_base.py +77 -44
- sqlspec/builder/_column.py +0 -4
- sqlspec/builder/_ddl.py +15 -52
- sqlspec/builder/_ddl_utils.py +0 -1
- sqlspec/builder/_delete.py +4 -5
- sqlspec/builder/_insert.py +235 -44
- sqlspec/builder/_merge.py +17 -2
- sqlspec/builder/_parsing_utils.py +42 -14
- sqlspec/builder/_select.py +29 -33
- sqlspec/builder/_update.py +4 -2
- sqlspec/builder/mixins/_cte_and_set_ops.py +47 -20
- sqlspec/builder/mixins/_delete_operations.py +6 -1
- sqlspec/builder/mixins/_insert_operations.py +126 -24
- sqlspec/builder/mixins/_join_operations.py +44 -10
- sqlspec/builder/mixins/_merge_operations.py +183 -25
- sqlspec/builder/mixins/_order_limit_operations.py +15 -3
- sqlspec/builder/mixins/_pivot_operations.py +11 -2
- sqlspec/builder/mixins/_select_operations.py +21 -14
- sqlspec/builder/mixins/_update_operations.py +80 -32
- sqlspec/builder/mixins/_where_clause.py +201 -66
- sqlspec/core/cache.py +26 -28
- sqlspec/core/compiler.py +58 -37
- sqlspec/core/filters.py +12 -10
- sqlspec/core/parameters.py +80 -52
- sqlspec/core/result.py +30 -17
- sqlspec/core/statement.py +47 -22
- sqlspec/driver/_async.py +76 -46
- sqlspec/driver/_common.py +25 -6
- sqlspec/driver/_sync.py +73 -43
- sqlspec/driver/mixins/_result_tools.py +62 -37
- sqlspec/driver/mixins/_sql_translator.py +61 -11
- sqlspec/extensions/litestar/cli.py +1 -1
- sqlspec/extensions/litestar/plugin.py +2 -2
- sqlspec/protocols.py +7 -0
- sqlspec/utils/sync_tools.py +1 -1
- sqlspec/utils/type_guards.py +7 -3
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/METADATA +1 -1
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/RECORD +43 -43
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
5
5
|
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from sqlspec.core.statement import SQL
|
|
8
|
+
|
|
9
|
+
from mypy_extensions import trait
|
|
6
10
|
from sqlglot import exp
|
|
7
11
|
from typing_extensions import Self
|
|
8
12
|
|
|
@@ -10,6 +14,30 @@ from sqlspec.builder._parsing_utils import parse_column_expression, parse_condit
|
|
|
10
14
|
from sqlspec.exceptions import SQLBuilderError
|
|
11
15
|
from sqlspec.utils.type_guards import has_query_builder_parameters, has_sqlglot_expression, is_iterable_parameters
|
|
12
16
|
|
|
17
|
+
|
|
18
|
+
def _extract_column_name(column: Union[str, exp.Column]) -> str:
|
|
19
|
+
"""Extract column name from column expression for parameter naming.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
column: Column expression (string or SQLGlot Column)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Column name as string for use as parameter name
|
|
26
|
+
"""
|
|
27
|
+
if isinstance(column, str):
|
|
28
|
+
# Handle simple column names and table.column references
|
|
29
|
+
if "." in column:
|
|
30
|
+
return column.split(".")[-1] # Return just the column part
|
|
31
|
+
return column
|
|
32
|
+
if isinstance(column, exp.Column):
|
|
33
|
+
# Extract the column name from SQLGlot Column expression
|
|
34
|
+
try:
|
|
35
|
+
return str(column.this.this)
|
|
36
|
+
except AttributeError:
|
|
37
|
+
return str(column.this) if column.this else "column"
|
|
38
|
+
return "column"
|
|
39
|
+
|
|
40
|
+
|
|
13
41
|
if TYPE_CHECKING:
|
|
14
42
|
from sqlspec.builder._column import ColumnExpression
|
|
15
43
|
from sqlspec.protocols import SQLBuilderProtocol
|
|
@@ -17,31 +45,51 @@ if TYPE_CHECKING:
|
|
|
17
45
|
__all__ = ("HavingClauseMixin", "WhereClauseMixin")
|
|
18
46
|
|
|
19
47
|
|
|
48
|
+
@trait
|
|
20
49
|
class WhereClauseMixin:
|
|
21
50
|
"""Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
|
|
22
51
|
|
|
23
|
-
|
|
52
|
+
__slots__ = ()
|
|
53
|
+
|
|
54
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
55
|
+
_expression: Optional[exp.Expression]
|
|
56
|
+
|
|
57
|
+
def _handle_in_operator(
|
|
58
|
+
self, column_exp: exp.Expression, value: Any, column_name: str = "column"
|
|
59
|
+
) -> exp.Expression:
|
|
24
60
|
"""Handle IN operator."""
|
|
25
61
|
builder = cast("SQLBuilderProtocol", self)
|
|
26
62
|
if is_iterable_parameters(value):
|
|
27
63
|
placeholders = []
|
|
28
|
-
for v in value:
|
|
29
|
-
|
|
64
|
+
for i, v in enumerate(value):
|
|
65
|
+
if len(value) == 1:
|
|
66
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
67
|
+
else:
|
|
68
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
|
|
69
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
30
70
|
placeholders.append(exp.Placeholder(this=param_name))
|
|
31
71
|
return exp.In(this=column_exp, expressions=placeholders)
|
|
32
|
-
|
|
72
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
73
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
33
74
|
return exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)])
|
|
34
75
|
|
|
35
|
-
def _handle_not_in_operator(
|
|
76
|
+
def _handle_not_in_operator(
|
|
77
|
+
self, column_exp: exp.Expression, value: Any, column_name: str = "column"
|
|
78
|
+
) -> exp.Expression:
|
|
36
79
|
"""Handle NOT IN operator."""
|
|
37
80
|
builder = cast("SQLBuilderProtocol", self)
|
|
38
81
|
if is_iterable_parameters(value):
|
|
39
82
|
placeholders = []
|
|
40
|
-
for v in value:
|
|
41
|
-
|
|
83
|
+
for i, v in enumerate(value):
|
|
84
|
+
if len(value) == 1:
|
|
85
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
86
|
+
else:
|
|
87
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
|
|
88
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
42
89
|
placeholders.append(exp.Placeholder(this=param_name))
|
|
43
90
|
return exp.Not(this=exp.In(this=column_exp, expressions=placeholders))
|
|
44
|
-
|
|
91
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
92
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
45
93
|
return exp.Not(this=exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)]))
|
|
46
94
|
|
|
47
95
|
def _handle_is_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
|
|
@@ -54,26 +102,34 @@ class WhereClauseMixin:
|
|
|
54
102
|
value_expr = exp.Null() if value is None else exp.convert(value)
|
|
55
103
|
return exp.Not(this=exp.Is(this=column_exp, expression=value_expr))
|
|
56
104
|
|
|
57
|
-
def _handle_between_operator(
|
|
105
|
+
def _handle_between_operator(
|
|
106
|
+
self, column_exp: exp.Expression, value: Any, column_name: str = "column"
|
|
107
|
+
) -> exp.Expression:
|
|
58
108
|
"""Handle BETWEEN operator."""
|
|
59
109
|
if is_iterable_parameters(value) and len(value) == 2:
|
|
60
110
|
builder = cast("SQLBuilderProtocol", self)
|
|
61
111
|
low, high = value
|
|
62
|
-
|
|
63
|
-
|
|
112
|
+
low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
|
|
113
|
+
high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
|
|
114
|
+
_, low_param = builder.add_parameter(low, name=low_param)
|
|
115
|
+
_, high_param = builder.add_parameter(high, name=high_param)
|
|
64
116
|
return exp.Between(
|
|
65
117
|
this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
|
|
66
118
|
)
|
|
67
119
|
msg = f"BETWEEN operator requires a tuple of two values, got {type(value).__name__}"
|
|
68
120
|
raise SQLBuilderError(msg)
|
|
69
121
|
|
|
70
|
-
def _handle_not_between_operator(
|
|
122
|
+
def _handle_not_between_operator(
|
|
123
|
+
self, column_exp: exp.Expression, value: Any, column_name: str = "column"
|
|
124
|
+
) -> exp.Expression:
|
|
71
125
|
"""Handle NOT BETWEEN operator."""
|
|
72
126
|
if is_iterable_parameters(value) and len(value) == 2:
|
|
73
127
|
builder = cast("SQLBuilderProtocol", self)
|
|
74
128
|
low, high = value
|
|
75
|
-
|
|
76
|
-
|
|
129
|
+
low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
|
|
130
|
+
high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
|
|
131
|
+
_, low_param = builder.add_parameter(low, name=low_param)
|
|
132
|
+
_, high_param = builder.add_parameter(high, name=high_param)
|
|
77
133
|
return exp.Not(
|
|
78
134
|
this=exp.Between(
|
|
79
135
|
this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
|
|
@@ -85,13 +141,15 @@ class WhereClauseMixin:
|
|
|
85
141
|
def _process_tuple_condition(self, condition: tuple) -> exp.Expression:
|
|
86
142
|
"""Process tuple-based WHERE conditions."""
|
|
87
143
|
builder = cast("SQLBuilderProtocol", self)
|
|
88
|
-
|
|
89
|
-
column_exp = parse_column_expression(
|
|
144
|
+
column_name_raw = str(condition[0])
|
|
145
|
+
column_exp = parse_column_expression(column_name_raw)
|
|
146
|
+
column_name = _extract_column_name(column_name_raw)
|
|
90
147
|
|
|
91
148
|
if len(condition) == 2:
|
|
92
149
|
# (column, value) tuple for equality
|
|
93
150
|
value = condition[1]
|
|
94
|
-
|
|
151
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
152
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
95
153
|
return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
96
154
|
|
|
97
155
|
if len(condition) == 3:
|
|
@@ -100,42 +158,50 @@ class WhereClauseMixin:
|
|
|
100
158
|
value = condition[2]
|
|
101
159
|
|
|
102
160
|
if operator == "=":
|
|
103
|
-
|
|
161
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
162
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
104
163
|
return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
105
164
|
if operator in {"!=", "<>"}:
|
|
106
|
-
|
|
165
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
166
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
107
167
|
return exp.NEQ(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
108
168
|
if operator == ">":
|
|
109
|
-
|
|
169
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
170
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
110
171
|
return exp.GT(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
111
172
|
if operator == ">=":
|
|
112
|
-
|
|
173
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
174
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
113
175
|
return exp.GTE(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
114
176
|
if operator == "<":
|
|
115
|
-
|
|
177
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
178
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
116
179
|
return exp.LT(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
117
180
|
if operator == "<=":
|
|
118
|
-
|
|
181
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
182
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
119
183
|
return exp.LTE(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
120
184
|
if operator == "LIKE":
|
|
121
|
-
|
|
185
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
186
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
122
187
|
return exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
123
188
|
if operator == "NOT LIKE":
|
|
124
|
-
|
|
189
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
190
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
125
191
|
return exp.Not(this=exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name)))
|
|
126
192
|
|
|
127
193
|
if operator == "IN":
|
|
128
|
-
return self._handle_in_operator(column_exp, value)
|
|
194
|
+
return self._handle_in_operator(column_exp, value, column_name)
|
|
129
195
|
if operator == "NOT IN":
|
|
130
|
-
return self._handle_not_in_operator(column_exp, value)
|
|
196
|
+
return self._handle_not_in_operator(column_exp, value, column_name)
|
|
131
197
|
if operator == "IS":
|
|
132
198
|
return self._handle_is_operator(column_exp, value)
|
|
133
199
|
if operator == "IS NOT":
|
|
134
200
|
return self._handle_is_not_operator(column_exp, value)
|
|
135
201
|
if operator == "BETWEEN":
|
|
136
|
-
return self._handle_between_operator(column_exp, value)
|
|
202
|
+
return self._handle_between_operator(column_exp, value, column_name)
|
|
137
203
|
if operator == "NOT BETWEEN":
|
|
138
|
-
return self._handle_not_between_operator(column_exp, value)
|
|
204
|
+
return self._handle_not_between_operator(column_exp, value, column_name)
|
|
139
205
|
|
|
140
206
|
msg = f"Unsupported operator: {operator}"
|
|
141
207
|
raise SQLBuilderError(msg)
|
|
@@ -145,7 +211,9 @@ class WhereClauseMixin:
|
|
|
145
211
|
|
|
146
212
|
def where(
|
|
147
213
|
self,
|
|
148
|
-
condition: Union[
|
|
214
|
+
condition: Union[
|
|
215
|
+
str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression", "SQL"
|
|
216
|
+
],
|
|
149
217
|
value: Optional[Any] = None,
|
|
150
218
|
operator: Optional[str] = None,
|
|
151
219
|
) -> Self:
|
|
@@ -167,7 +235,7 @@ class WhereClauseMixin:
|
|
|
167
235
|
Returns:
|
|
168
236
|
The current builder instance for method chaining.
|
|
169
237
|
"""
|
|
170
|
-
if self.__class__.__name__ == "Update" and not isinstance(self._expression, exp.Update):
|
|
238
|
+
if self.__class__.__name__ == "Update" and not isinstance(self._expression, exp.Update):
|
|
171
239
|
msg = "Cannot add WHERE clause to non-UPDATE expression"
|
|
172
240
|
raise SQLBuilderError(msg)
|
|
173
241
|
|
|
@@ -204,6 +272,25 @@ class WhereClauseMixin:
|
|
|
204
272
|
where_expr = builder._parameterize_expression(raw_expr)
|
|
205
273
|
else:
|
|
206
274
|
where_expr = parse_condition_expression(str(condition))
|
|
275
|
+
elif hasattr(condition, "expression") and hasattr(condition, "sql"):
|
|
276
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
277
|
+
expression = getattr(condition, "expression", None)
|
|
278
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
279
|
+
# Merge parameters from SQL object into builder
|
|
280
|
+
if hasattr(condition, "parameters") and hasattr(builder, "add_parameter"):
|
|
281
|
+
sql_parameters = getattr(condition, "parameters", {})
|
|
282
|
+
for param_name, param_value in sql_parameters.items():
|
|
283
|
+
builder.add_parameter(param_value, name=param_name)
|
|
284
|
+
where_expr = expression
|
|
285
|
+
else:
|
|
286
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
287
|
+
sql_text = getattr(condition, "sql", "")
|
|
288
|
+
# Merge parameters even when parsing raw SQL
|
|
289
|
+
if hasattr(condition, "parameters") and hasattr(builder, "add_parameter"):
|
|
290
|
+
sql_parameters = getattr(condition, "parameters", {})
|
|
291
|
+
for param_name, param_value in sql_parameters.items():
|
|
292
|
+
builder.add_parameter(param_value, name=param_name)
|
|
293
|
+
where_expr = parse_condition_expression(sql_text)
|
|
207
294
|
else:
|
|
208
295
|
msg = f"Unsupported condition type: {type(condition).__name__}"
|
|
209
296
|
raise SQLBuilderError(msg)
|
|
@@ -218,86 +305,107 @@ class WhereClauseMixin:
|
|
|
218
305
|
def where_eq(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
219
306
|
"""Add WHERE column = value clause."""
|
|
220
307
|
builder = cast("SQLBuilderProtocol", self)
|
|
221
|
-
|
|
308
|
+
column_name = _extract_column_name(column)
|
|
309
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
310
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
222
311
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
223
|
-
condition: exp.Expression = col_expr.eq(exp.
|
|
312
|
+
condition: exp.Expression = col_expr.eq(exp.Placeholder(this=param_name))
|
|
224
313
|
return self.where(condition)
|
|
225
314
|
|
|
226
315
|
def where_neq(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
227
316
|
"""Add WHERE column != value clause."""
|
|
228
317
|
builder = cast("SQLBuilderProtocol", self)
|
|
229
|
-
|
|
318
|
+
column_name = _extract_column_name(column)
|
|
319
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
320
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
230
321
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
231
|
-
condition: exp.Expression = col_expr.neq(exp.
|
|
322
|
+
condition: exp.Expression = col_expr.neq(exp.Placeholder(this=param_name))
|
|
232
323
|
return self.where(condition)
|
|
233
324
|
|
|
234
325
|
def where_lt(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
235
326
|
"""Add WHERE column < value clause."""
|
|
236
327
|
builder = cast("SQLBuilderProtocol", self)
|
|
237
|
-
|
|
328
|
+
column_name = _extract_column_name(column)
|
|
329
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
330
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
238
331
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
239
|
-
condition: exp.Expression = exp.LT(this=col_expr, expression=exp.
|
|
332
|
+
condition: exp.Expression = exp.LT(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
240
333
|
return self.where(condition)
|
|
241
334
|
|
|
242
335
|
def where_lte(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
243
336
|
"""Add WHERE column <= value clause."""
|
|
244
337
|
builder = cast("SQLBuilderProtocol", self)
|
|
245
|
-
|
|
338
|
+
column_name = _extract_column_name(column)
|
|
339
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
340
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
246
341
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
247
|
-
condition: exp.Expression = exp.LTE(this=col_expr, expression=exp.
|
|
342
|
+
condition: exp.Expression = exp.LTE(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
248
343
|
return self.where(condition)
|
|
249
344
|
|
|
250
345
|
def where_gt(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
251
346
|
"""Add WHERE column > value clause."""
|
|
252
347
|
builder = cast("SQLBuilderProtocol", self)
|
|
253
|
-
|
|
348
|
+
column_name = _extract_column_name(column)
|
|
349
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
350
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
254
351
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
255
|
-
condition: exp.Expression = exp.GT(this=col_expr, expression=exp.
|
|
352
|
+
condition: exp.Expression = exp.GT(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
256
353
|
return self.where(condition)
|
|
257
354
|
|
|
258
355
|
def where_gte(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
259
356
|
"""Add WHERE column >= value clause."""
|
|
260
357
|
builder = cast("SQLBuilderProtocol", self)
|
|
261
|
-
|
|
358
|
+
column_name = _extract_column_name(column)
|
|
359
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
360
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
262
361
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
263
|
-
condition: exp.Expression = exp.GTE(this=col_expr, expression=exp.
|
|
362
|
+
condition: exp.Expression = exp.GTE(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
264
363
|
return self.where(condition)
|
|
265
364
|
|
|
266
365
|
def where_between(self, column: Union[str, exp.Column], low: Any, high: Any) -> Self:
|
|
267
366
|
"""Add WHERE column BETWEEN low AND high clause."""
|
|
268
367
|
builder = cast("SQLBuilderProtocol", self)
|
|
269
|
-
|
|
270
|
-
|
|
368
|
+
column_name = _extract_column_name(column)
|
|
369
|
+
low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
|
|
370
|
+
high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
|
|
371
|
+
_, low_param = builder.add_parameter(low, name=low_param)
|
|
372
|
+
_, high_param = builder.add_parameter(high, name=high_param)
|
|
271
373
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
272
|
-
condition: exp.Expression = col_expr.between(exp.
|
|
374
|
+
condition: exp.Expression = col_expr.between(exp.Placeholder(this=low_param), exp.Placeholder(this=high_param))
|
|
273
375
|
return self.where(condition)
|
|
274
376
|
|
|
275
377
|
def where_like(self, column: Union[str, exp.Column], pattern: str, escape: Optional[str] = None) -> Self:
|
|
276
378
|
"""Add WHERE column LIKE pattern clause."""
|
|
277
379
|
builder = cast("SQLBuilderProtocol", self)
|
|
278
|
-
|
|
380
|
+
column_name = _extract_column_name(column)
|
|
381
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
382
|
+
_, param_name = builder.add_parameter(pattern, name=param_name)
|
|
279
383
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
280
384
|
if escape is not None:
|
|
281
|
-
cond = exp.Like(this=col_expr, expression=exp.
|
|
385
|
+
cond = exp.Like(this=col_expr, expression=exp.Placeholder(this=param_name), escape=exp.convert(str(escape)))
|
|
282
386
|
else:
|
|
283
|
-
cond = col_expr.like(exp.
|
|
387
|
+
cond = col_expr.like(exp.Placeholder(this=param_name))
|
|
284
388
|
condition: exp.Expression = cond
|
|
285
389
|
return self.where(condition)
|
|
286
390
|
|
|
287
391
|
def where_not_like(self, column: Union[str, exp.Column], pattern: str) -> Self:
|
|
288
392
|
"""Add WHERE column NOT LIKE pattern clause."""
|
|
289
393
|
builder = cast("SQLBuilderProtocol", self)
|
|
290
|
-
|
|
394
|
+
column_name = _extract_column_name(column)
|
|
395
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
396
|
+
_, param_name = builder.add_parameter(pattern, name=param_name)
|
|
291
397
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
292
|
-
condition: exp.Expression = col_expr.like(exp.
|
|
398
|
+
condition: exp.Expression = col_expr.like(exp.Placeholder(this=param_name)).not_()
|
|
293
399
|
return self.where(condition)
|
|
294
400
|
|
|
295
401
|
def where_ilike(self, column: Union[str, exp.Column], pattern: str) -> Self:
|
|
296
402
|
"""Add WHERE column ILIKE pattern clause."""
|
|
297
403
|
builder = cast("SQLBuilderProtocol", self)
|
|
298
|
-
|
|
404
|
+
column_name = _extract_column_name(column)
|
|
405
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
406
|
+
_, param_name = builder.add_parameter(pattern, name=param_name)
|
|
299
407
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
300
|
-
condition: exp.Expression = col_expr.ilike(exp.
|
|
408
|
+
condition: exp.Expression = col_expr.ilike(exp.Placeholder(this=param_name))
|
|
301
409
|
return self.where(condition)
|
|
302
410
|
|
|
303
411
|
def where_is_null(self, column: Union[str, exp.Column]) -> Self:
|
|
@@ -322,6 +430,10 @@ class WhereClauseMixin:
|
|
|
322
430
|
subquery = values.build() # pyright: ignore
|
|
323
431
|
sql_str = subquery.sql
|
|
324
432
|
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect_name)) # pyright: ignore
|
|
433
|
+
# Merge subquery parameters into parent builder
|
|
434
|
+
if hasattr(subquery, "parameters") and isinstance(subquery.parameters, dict): # pyright: ignore[reportAttributeAccessIssue]
|
|
435
|
+
for param_name, param_value in subquery.parameters.items(): # pyright: ignore[reportAttributeAccessIssue]
|
|
436
|
+
builder.add_parameter(param_value, name=param_name)
|
|
325
437
|
else:
|
|
326
438
|
subquery_exp = values # type: ignore[assignment]
|
|
327
439
|
condition = col_expr.isin(subquery_exp)
|
|
@@ -329,10 +441,15 @@ class WhereClauseMixin:
|
|
|
329
441
|
if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
|
|
330
442
|
msg = "Unsupported type for 'values' in WHERE IN"
|
|
331
443
|
raise SQLBuilderError(msg)
|
|
444
|
+
column_name = _extract_column_name(column)
|
|
332
445
|
parameters = []
|
|
333
|
-
for v in values:
|
|
334
|
-
|
|
335
|
-
|
|
446
|
+
for i, v in enumerate(values):
|
|
447
|
+
if len(values) == 1:
|
|
448
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
449
|
+
else:
|
|
450
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
|
|
451
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
452
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
336
453
|
condition = col_expr.isin(*parameters)
|
|
337
454
|
return self.where(condition)
|
|
338
455
|
|
|
@@ -353,10 +470,15 @@ class WhereClauseMixin:
|
|
|
353
470
|
if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
|
|
354
471
|
msg = "Values for where_not_in must be a non-string iterable or subquery."
|
|
355
472
|
raise SQLBuilderError(msg)
|
|
473
|
+
column_name = _extract_column_name(column)
|
|
356
474
|
parameters = []
|
|
357
|
-
for v in values:
|
|
358
|
-
|
|
359
|
-
|
|
475
|
+
for i, v in enumerate(values):
|
|
476
|
+
if len(values) == 1:
|
|
477
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
478
|
+
else:
|
|
479
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
|
|
480
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
481
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
360
482
|
condition = exp.Not(this=col_expr.isin(*parameters))
|
|
361
483
|
return self.where(condition)
|
|
362
484
|
|
|
@@ -438,10 +560,15 @@ class WhereClauseMixin:
|
|
|
438
560
|
if not is_iterable_parameters(values) or isinstance(values, bytes):
|
|
439
561
|
msg = "Unsupported type for 'values' in WHERE ANY"
|
|
440
562
|
raise SQLBuilderError(msg)
|
|
563
|
+
column_name = _extract_column_name(column)
|
|
441
564
|
parameters = []
|
|
442
|
-
for v in values:
|
|
443
|
-
|
|
444
|
-
|
|
565
|
+
for i, v in enumerate(values):
|
|
566
|
+
if len(values) == 1:
|
|
567
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
568
|
+
else:
|
|
569
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_any_{i + 1}")
|
|
570
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
571
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
445
572
|
tuple_expr = exp.Tuple(expressions=parameters)
|
|
446
573
|
condition = exp.EQ(this=col_expr, expression=exp.Any(this=tuple_expr))
|
|
447
574
|
return self.where(condition)
|
|
@@ -473,19 +600,27 @@ class WhereClauseMixin:
|
|
|
473
600
|
if not is_iterable_parameters(values) or isinstance(values, bytes):
|
|
474
601
|
msg = "Unsupported type for 'values' in WHERE NOT ANY"
|
|
475
602
|
raise SQLBuilderError(msg)
|
|
603
|
+
column_name = _extract_column_name(column)
|
|
476
604
|
parameters = []
|
|
477
|
-
for v in values:
|
|
478
|
-
|
|
479
|
-
|
|
605
|
+
for i, v in enumerate(values):
|
|
606
|
+
if len(values) == 1:
|
|
607
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
608
|
+
else:
|
|
609
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_not_any_{i + 1}")
|
|
610
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
611
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
480
612
|
tuple_expr = exp.Tuple(expressions=parameters)
|
|
481
613
|
condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
|
|
482
614
|
return self.where(condition)
|
|
483
615
|
|
|
484
616
|
|
|
617
|
+
@trait
|
|
485
618
|
class HavingClauseMixin:
|
|
486
619
|
"""Mixin providing HAVING clause for SELECT builders."""
|
|
487
620
|
|
|
488
|
-
|
|
621
|
+
__slots__ = ()
|
|
622
|
+
|
|
623
|
+
_expression: Optional[exp.Expression]
|
|
489
624
|
|
|
490
625
|
def having(self, condition: Union[str, exp.Expression]) -> Self:
|
|
491
626
|
"""Add HAVING clause.
|
sqlspec/core/cache.py
CHANGED
|
@@ -245,19 +245,21 @@ class UnifiedCache(Generic[CacheValueT]):
|
|
|
245
245
|
Cached value or None if not found or expired
|
|
246
246
|
"""
|
|
247
247
|
with self._lock:
|
|
248
|
-
node
|
|
248
|
+
node = self._cache.get(key)
|
|
249
249
|
if node is None:
|
|
250
250
|
self._stats.record_miss()
|
|
251
251
|
return None
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
ttl
|
|
255
|
-
if ttl is not None
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
253
|
+
# Optimize TTL check with early variable assignment
|
|
254
|
+
ttl = self._ttl
|
|
255
|
+
if ttl is not None:
|
|
256
|
+
current_time = time.time()
|
|
257
|
+
if (current_time - node.timestamp) > ttl:
|
|
258
|
+
self._remove_node(node)
|
|
259
|
+
del self._cache[key]
|
|
260
|
+
self._stats.record_miss()
|
|
261
|
+
self._stats.record_eviction()
|
|
262
|
+
return None
|
|
261
263
|
|
|
262
264
|
self._move_to_head(node)
|
|
263
265
|
node.access_count += 1
|
|
@@ -272,7 +274,7 @@ class UnifiedCache(Generic[CacheValueT]):
|
|
|
272
274
|
value: Value to cache
|
|
273
275
|
"""
|
|
274
276
|
with self._lock:
|
|
275
|
-
existing_node
|
|
277
|
+
existing_node = self._cache.get(key)
|
|
276
278
|
if existing_node is not None:
|
|
277
279
|
existing_node.value = value
|
|
278
280
|
existing_node.timestamp = time.time()
|
|
@@ -280,14 +282,13 @@ class UnifiedCache(Generic[CacheValueT]):
|
|
|
280
282
|
self._move_to_head(existing_node)
|
|
281
283
|
return
|
|
282
284
|
|
|
283
|
-
new_node
|
|
285
|
+
new_node = CacheNode(key, value)
|
|
284
286
|
self._cache[key] = new_node
|
|
285
287
|
self._add_to_head(new_node)
|
|
286
288
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
tail_node: Optional[CacheNode] = self._tail.prev
|
|
289
|
+
# Optimize size check with cached length
|
|
290
|
+
if len(self._cache) > self._max_size:
|
|
291
|
+
tail_node = self._tail.prev
|
|
291
292
|
if tail_node is not None and tail_node is not self._head:
|
|
292
293
|
self._remove_node(tail_node)
|
|
293
294
|
del self._cache[tail_node.key]
|
|
@@ -361,17 +362,13 @@ class UnifiedCache(Generic[CacheValueT]):
|
|
|
361
362
|
def __contains__(self, key: CacheKey) -> bool:
|
|
362
363
|
"""Check if key exists in cache."""
|
|
363
364
|
with self._lock:
|
|
364
|
-
node
|
|
365
|
+
node = self._cache.get(key)
|
|
365
366
|
if node is None:
|
|
366
367
|
return False
|
|
367
368
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (current_time - node.timestamp) > ttl:
|
|
372
|
-
return False
|
|
373
|
-
|
|
374
|
-
return True
|
|
369
|
+
# Optimize TTL check
|
|
370
|
+
ttl = self._ttl
|
|
371
|
+
return not (ttl is not None and time.time() - node.timestamp > ttl)
|
|
375
372
|
|
|
376
373
|
|
|
377
374
|
@mypyc_attr(allow_interpreted_subclasses=False)
|
|
@@ -553,6 +550,8 @@ class ParameterCache:
|
|
|
553
550
|
"""
|
|
554
551
|
# Create stable key from parameters and configuration
|
|
555
552
|
try:
|
|
553
|
+
# Optimize type checking order
|
|
554
|
+
param_key: tuple[Any, ...]
|
|
556
555
|
if isinstance(params, dict):
|
|
557
556
|
param_key = tuple(sorted(params.items()))
|
|
558
557
|
elif isinstance(params, (list, tuple)):
|
|
@@ -560,12 +559,11 @@ class ParameterCache:
|
|
|
560
559
|
else:
|
|
561
560
|
param_key = (params,)
|
|
562
561
|
|
|
563
|
-
|
|
564
|
-
return CacheKey(key_data)
|
|
562
|
+
return CacheKey(("parameters", param_key, config_hash))
|
|
565
563
|
except (TypeError, ValueError):
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
return CacheKey(
|
|
564
|
+
# Fallback for unhashable types
|
|
565
|
+
param_key_fallback = (str(params), type(params).__name__)
|
|
566
|
+
return CacheKey(("parameters", param_key_fallback, config_hash))
|
|
569
567
|
|
|
570
568
|
def clear(self) -> None:
|
|
571
569
|
"""Clear parameter cache."""
|