sqlspec 0.15.0__py3-none-any.whl → 0.16.1__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 +699 -43
- 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 +61 -35
- sqlspec/builder/_merge.py +17 -2
- sqlspec/builder/_parsing_utils.py +16 -12
- 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 +11 -4
- sqlspec/builder/mixins/_merge_operations.py +91 -19
- sqlspec/builder/mixins/_order_limit_operations.py +15 -3
- sqlspec/builder/mixins/_pivot_operations.py +11 -2
- sqlspec/builder/mixins/_select_operations.py +16 -10
- sqlspec/builder/mixins/_update_operations.py +43 -10
- sqlspec/builder/mixins/_where_clause.py +177 -65
- 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.1.dist-info}/METADATA +1 -1
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/RECORD +43 -43
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/WHEEL +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
5
5
|
|
|
6
|
+
from mypy_extensions import trait
|
|
6
7
|
from sqlglot import exp
|
|
7
8
|
from typing_extensions import Self
|
|
8
9
|
|
|
@@ -10,6 +11,30 @@ from sqlspec.builder._parsing_utils import parse_column_expression, parse_condit
|
|
|
10
11
|
from sqlspec.exceptions import SQLBuilderError
|
|
11
12
|
from sqlspec.utils.type_guards import has_query_builder_parameters, has_sqlglot_expression, is_iterable_parameters
|
|
12
13
|
|
|
14
|
+
|
|
15
|
+
def _extract_column_name(column: Union[str, exp.Column]) -> str:
|
|
16
|
+
"""Extract column name from column expression for parameter naming.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
column: Column expression (string or SQLGlot Column)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Column name as string for use as parameter name
|
|
23
|
+
"""
|
|
24
|
+
if isinstance(column, str):
|
|
25
|
+
# Handle simple column names and table.column references
|
|
26
|
+
if "." in column:
|
|
27
|
+
return column.split(".")[-1] # Return just the column part
|
|
28
|
+
return column
|
|
29
|
+
if isinstance(column, exp.Column):
|
|
30
|
+
# Extract the column name from SQLGlot Column expression
|
|
31
|
+
try:
|
|
32
|
+
return str(column.this.this)
|
|
33
|
+
except AttributeError:
|
|
34
|
+
return str(column.this) if column.this else "column"
|
|
35
|
+
return "column"
|
|
36
|
+
|
|
37
|
+
|
|
13
38
|
if TYPE_CHECKING:
|
|
14
39
|
from sqlspec.builder._column import ColumnExpression
|
|
15
40
|
from sqlspec.protocols import SQLBuilderProtocol
|
|
@@ -17,31 +42,51 @@ if TYPE_CHECKING:
|
|
|
17
42
|
__all__ = ("HavingClauseMixin", "WhereClauseMixin")
|
|
18
43
|
|
|
19
44
|
|
|
45
|
+
@trait
|
|
20
46
|
class WhereClauseMixin:
|
|
21
47
|
"""Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
|
|
22
48
|
|
|
23
|
-
|
|
49
|
+
__slots__ = ()
|
|
50
|
+
|
|
51
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
52
|
+
_expression: Optional[exp.Expression]
|
|
53
|
+
|
|
54
|
+
def _handle_in_operator(
|
|
55
|
+
self, column_exp: exp.Expression, value: Any, column_name: str = "column"
|
|
56
|
+
) -> exp.Expression:
|
|
24
57
|
"""Handle IN operator."""
|
|
25
58
|
builder = cast("SQLBuilderProtocol", self)
|
|
26
59
|
if is_iterable_parameters(value):
|
|
27
60
|
placeholders = []
|
|
28
|
-
for v in value:
|
|
29
|
-
|
|
61
|
+
for i, v in enumerate(value):
|
|
62
|
+
if len(value) == 1:
|
|
63
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
64
|
+
else:
|
|
65
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
|
|
66
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
30
67
|
placeholders.append(exp.Placeholder(this=param_name))
|
|
31
68
|
return exp.In(this=column_exp, expressions=placeholders)
|
|
32
|
-
|
|
69
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
70
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
33
71
|
return exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)])
|
|
34
72
|
|
|
35
|
-
def _handle_not_in_operator(
|
|
73
|
+
def _handle_not_in_operator(
|
|
74
|
+
self, column_exp: exp.Expression, value: Any, column_name: str = "column"
|
|
75
|
+
) -> exp.Expression:
|
|
36
76
|
"""Handle NOT IN operator."""
|
|
37
77
|
builder = cast("SQLBuilderProtocol", self)
|
|
38
78
|
if is_iterable_parameters(value):
|
|
39
79
|
placeholders = []
|
|
40
|
-
for v in value:
|
|
41
|
-
|
|
80
|
+
for i, v in enumerate(value):
|
|
81
|
+
if len(value) == 1:
|
|
82
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
83
|
+
else:
|
|
84
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
|
|
85
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
42
86
|
placeholders.append(exp.Placeholder(this=param_name))
|
|
43
87
|
return exp.Not(this=exp.In(this=column_exp, expressions=placeholders))
|
|
44
|
-
|
|
88
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
89
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
45
90
|
return exp.Not(this=exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)]))
|
|
46
91
|
|
|
47
92
|
def _handle_is_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
|
|
@@ -54,26 +99,34 @@ class WhereClauseMixin:
|
|
|
54
99
|
value_expr = exp.Null() if value is None else exp.convert(value)
|
|
55
100
|
return exp.Not(this=exp.Is(this=column_exp, expression=value_expr))
|
|
56
101
|
|
|
57
|
-
def _handle_between_operator(
|
|
102
|
+
def _handle_between_operator(
|
|
103
|
+
self, column_exp: exp.Expression, value: Any, column_name: str = "column"
|
|
104
|
+
) -> exp.Expression:
|
|
58
105
|
"""Handle BETWEEN operator."""
|
|
59
106
|
if is_iterable_parameters(value) and len(value) == 2:
|
|
60
107
|
builder = cast("SQLBuilderProtocol", self)
|
|
61
108
|
low, high = value
|
|
62
|
-
|
|
63
|
-
|
|
109
|
+
low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
|
|
110
|
+
high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
|
|
111
|
+
_, low_param = builder.add_parameter(low, name=low_param)
|
|
112
|
+
_, high_param = builder.add_parameter(high, name=high_param)
|
|
64
113
|
return exp.Between(
|
|
65
114
|
this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
|
|
66
115
|
)
|
|
67
116
|
msg = f"BETWEEN operator requires a tuple of two values, got {type(value).__name__}"
|
|
68
117
|
raise SQLBuilderError(msg)
|
|
69
118
|
|
|
70
|
-
def _handle_not_between_operator(
|
|
119
|
+
def _handle_not_between_operator(
|
|
120
|
+
self, column_exp: exp.Expression, value: Any, column_name: str = "column"
|
|
121
|
+
) -> exp.Expression:
|
|
71
122
|
"""Handle NOT BETWEEN operator."""
|
|
72
123
|
if is_iterable_parameters(value) and len(value) == 2:
|
|
73
124
|
builder = cast("SQLBuilderProtocol", self)
|
|
74
125
|
low, high = value
|
|
75
|
-
|
|
76
|
-
|
|
126
|
+
low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
|
|
127
|
+
high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
|
|
128
|
+
_, low_param = builder.add_parameter(low, name=low_param)
|
|
129
|
+
_, high_param = builder.add_parameter(high, name=high_param)
|
|
77
130
|
return exp.Not(
|
|
78
131
|
this=exp.Between(
|
|
79
132
|
this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
|
|
@@ -85,13 +138,15 @@ class WhereClauseMixin:
|
|
|
85
138
|
def _process_tuple_condition(self, condition: tuple) -> exp.Expression:
|
|
86
139
|
"""Process tuple-based WHERE conditions."""
|
|
87
140
|
builder = cast("SQLBuilderProtocol", self)
|
|
88
|
-
|
|
89
|
-
column_exp = parse_column_expression(
|
|
141
|
+
column_name_raw = str(condition[0])
|
|
142
|
+
column_exp = parse_column_expression(column_name_raw)
|
|
143
|
+
column_name = _extract_column_name(column_name_raw)
|
|
90
144
|
|
|
91
145
|
if len(condition) == 2:
|
|
92
146
|
# (column, value) tuple for equality
|
|
93
147
|
value = condition[1]
|
|
94
|
-
|
|
148
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
149
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
95
150
|
return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
96
151
|
|
|
97
152
|
if len(condition) == 3:
|
|
@@ -100,42 +155,50 @@ class WhereClauseMixin:
|
|
|
100
155
|
value = condition[2]
|
|
101
156
|
|
|
102
157
|
if operator == "=":
|
|
103
|
-
|
|
158
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
159
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
104
160
|
return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
105
161
|
if operator in {"!=", "<>"}:
|
|
106
|
-
|
|
162
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
163
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
107
164
|
return exp.NEQ(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
108
165
|
if operator == ">":
|
|
109
|
-
|
|
166
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
167
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
110
168
|
return exp.GT(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
111
169
|
if operator == ">=":
|
|
112
|
-
|
|
170
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
171
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
113
172
|
return exp.GTE(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
114
173
|
if operator == "<":
|
|
115
|
-
|
|
174
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
175
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
116
176
|
return exp.LT(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
117
177
|
if operator == "<=":
|
|
118
|
-
|
|
178
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
179
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
119
180
|
return exp.LTE(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
120
181
|
if operator == "LIKE":
|
|
121
|
-
|
|
182
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
183
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
122
184
|
return exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name))
|
|
123
185
|
if operator == "NOT LIKE":
|
|
124
|
-
|
|
186
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
187
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
125
188
|
return exp.Not(this=exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name)))
|
|
126
189
|
|
|
127
190
|
if operator == "IN":
|
|
128
|
-
return self._handle_in_operator(column_exp, value)
|
|
191
|
+
return self._handle_in_operator(column_exp, value, column_name)
|
|
129
192
|
if operator == "NOT IN":
|
|
130
|
-
return self._handle_not_in_operator(column_exp, value)
|
|
193
|
+
return self._handle_not_in_operator(column_exp, value, column_name)
|
|
131
194
|
if operator == "IS":
|
|
132
195
|
return self._handle_is_operator(column_exp, value)
|
|
133
196
|
if operator == "IS NOT":
|
|
134
197
|
return self._handle_is_not_operator(column_exp, value)
|
|
135
198
|
if operator == "BETWEEN":
|
|
136
|
-
return self._handle_between_operator(column_exp, value)
|
|
199
|
+
return self._handle_between_operator(column_exp, value, column_name)
|
|
137
200
|
if operator == "NOT BETWEEN":
|
|
138
|
-
return self._handle_not_between_operator(column_exp, value)
|
|
201
|
+
return self._handle_not_between_operator(column_exp, value, column_name)
|
|
139
202
|
|
|
140
203
|
msg = f"Unsupported operator: {operator}"
|
|
141
204
|
raise SQLBuilderError(msg)
|
|
@@ -167,7 +230,7 @@ class WhereClauseMixin:
|
|
|
167
230
|
Returns:
|
|
168
231
|
The current builder instance for method chaining.
|
|
169
232
|
"""
|
|
170
|
-
if self.__class__.__name__ == "Update" and not isinstance(self._expression, exp.Update):
|
|
233
|
+
if self.__class__.__name__ == "Update" and not isinstance(self._expression, exp.Update):
|
|
171
234
|
msg = "Cannot add WHERE clause to non-UPDATE expression"
|
|
172
235
|
raise SQLBuilderError(msg)
|
|
173
236
|
|
|
@@ -218,86 +281,107 @@ class WhereClauseMixin:
|
|
|
218
281
|
def where_eq(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
219
282
|
"""Add WHERE column = value clause."""
|
|
220
283
|
builder = cast("SQLBuilderProtocol", self)
|
|
221
|
-
|
|
284
|
+
column_name = _extract_column_name(column)
|
|
285
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
286
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
222
287
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
223
|
-
condition: exp.Expression = col_expr.eq(exp.
|
|
288
|
+
condition: exp.Expression = col_expr.eq(exp.Placeholder(this=param_name))
|
|
224
289
|
return self.where(condition)
|
|
225
290
|
|
|
226
291
|
def where_neq(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
227
292
|
"""Add WHERE column != value clause."""
|
|
228
293
|
builder = cast("SQLBuilderProtocol", self)
|
|
229
|
-
|
|
294
|
+
column_name = _extract_column_name(column)
|
|
295
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
296
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
230
297
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
231
|
-
condition: exp.Expression = col_expr.neq(exp.
|
|
298
|
+
condition: exp.Expression = col_expr.neq(exp.Placeholder(this=param_name))
|
|
232
299
|
return self.where(condition)
|
|
233
300
|
|
|
234
301
|
def where_lt(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
235
302
|
"""Add WHERE column < value clause."""
|
|
236
303
|
builder = cast("SQLBuilderProtocol", self)
|
|
237
|
-
|
|
304
|
+
column_name = _extract_column_name(column)
|
|
305
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
306
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
238
307
|
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.
|
|
308
|
+
condition: exp.Expression = exp.LT(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
240
309
|
return self.where(condition)
|
|
241
310
|
|
|
242
311
|
def where_lte(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
243
312
|
"""Add WHERE column <= value clause."""
|
|
244
313
|
builder = cast("SQLBuilderProtocol", self)
|
|
245
|
-
|
|
314
|
+
column_name = _extract_column_name(column)
|
|
315
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
316
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
246
317
|
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.
|
|
318
|
+
condition: exp.Expression = exp.LTE(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
248
319
|
return self.where(condition)
|
|
249
320
|
|
|
250
321
|
def where_gt(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
251
322
|
"""Add WHERE column > value clause."""
|
|
252
323
|
builder = cast("SQLBuilderProtocol", self)
|
|
253
|
-
|
|
324
|
+
column_name = _extract_column_name(column)
|
|
325
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
326
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
254
327
|
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.
|
|
328
|
+
condition: exp.Expression = exp.GT(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
256
329
|
return self.where(condition)
|
|
257
330
|
|
|
258
331
|
def where_gte(self, column: Union[str, exp.Column], value: Any) -> Self:
|
|
259
332
|
"""Add WHERE column >= value clause."""
|
|
260
333
|
builder = cast("SQLBuilderProtocol", self)
|
|
261
|
-
|
|
334
|
+
column_name = _extract_column_name(column)
|
|
335
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
336
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
262
337
|
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.
|
|
338
|
+
condition: exp.Expression = exp.GTE(this=col_expr, expression=exp.Placeholder(this=param_name))
|
|
264
339
|
return self.where(condition)
|
|
265
340
|
|
|
266
341
|
def where_between(self, column: Union[str, exp.Column], low: Any, high: Any) -> Self:
|
|
267
342
|
"""Add WHERE column BETWEEN low AND high clause."""
|
|
268
343
|
builder = cast("SQLBuilderProtocol", self)
|
|
269
|
-
|
|
270
|
-
|
|
344
|
+
column_name = _extract_column_name(column)
|
|
345
|
+
low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
|
|
346
|
+
high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
|
|
347
|
+
_, low_param = builder.add_parameter(low, name=low_param)
|
|
348
|
+
_, high_param = builder.add_parameter(high, name=high_param)
|
|
271
349
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
272
|
-
condition: exp.Expression = col_expr.between(exp.
|
|
350
|
+
condition: exp.Expression = col_expr.between(exp.Placeholder(this=low_param), exp.Placeholder(this=high_param))
|
|
273
351
|
return self.where(condition)
|
|
274
352
|
|
|
275
353
|
def where_like(self, column: Union[str, exp.Column], pattern: str, escape: Optional[str] = None) -> Self:
|
|
276
354
|
"""Add WHERE column LIKE pattern clause."""
|
|
277
355
|
builder = cast("SQLBuilderProtocol", self)
|
|
278
|
-
|
|
356
|
+
column_name = _extract_column_name(column)
|
|
357
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
358
|
+
_, param_name = builder.add_parameter(pattern, name=param_name)
|
|
279
359
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
280
360
|
if escape is not None:
|
|
281
|
-
cond = exp.Like(this=col_expr, expression=exp.
|
|
361
|
+
cond = exp.Like(this=col_expr, expression=exp.Placeholder(this=param_name), escape=exp.convert(str(escape)))
|
|
282
362
|
else:
|
|
283
|
-
cond = col_expr.like(exp.
|
|
363
|
+
cond = col_expr.like(exp.Placeholder(this=param_name))
|
|
284
364
|
condition: exp.Expression = cond
|
|
285
365
|
return self.where(condition)
|
|
286
366
|
|
|
287
367
|
def where_not_like(self, column: Union[str, exp.Column], pattern: str) -> Self:
|
|
288
368
|
"""Add WHERE column NOT LIKE pattern clause."""
|
|
289
369
|
builder = cast("SQLBuilderProtocol", self)
|
|
290
|
-
|
|
370
|
+
column_name = _extract_column_name(column)
|
|
371
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
372
|
+
_, param_name = builder.add_parameter(pattern, name=param_name)
|
|
291
373
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
292
|
-
condition: exp.Expression = col_expr.like(exp.
|
|
374
|
+
condition: exp.Expression = col_expr.like(exp.Placeholder(this=param_name)).not_()
|
|
293
375
|
return self.where(condition)
|
|
294
376
|
|
|
295
377
|
def where_ilike(self, column: Union[str, exp.Column], pattern: str) -> Self:
|
|
296
378
|
"""Add WHERE column ILIKE pattern clause."""
|
|
297
379
|
builder = cast("SQLBuilderProtocol", self)
|
|
298
|
-
|
|
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)
|
|
299
383
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
300
|
-
condition: exp.Expression = col_expr.ilike(exp.
|
|
384
|
+
condition: exp.Expression = col_expr.ilike(exp.Placeholder(this=param_name))
|
|
301
385
|
return self.where(condition)
|
|
302
386
|
|
|
303
387
|
def where_is_null(self, column: Union[str, exp.Column]) -> Self:
|
|
@@ -322,6 +406,10 @@ class WhereClauseMixin:
|
|
|
322
406
|
subquery = values.build() # pyright: ignore
|
|
323
407
|
sql_str = subquery.sql
|
|
324
408
|
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect_name)) # pyright: ignore
|
|
409
|
+
# Merge subquery parameters into parent builder
|
|
410
|
+
if hasattr(subquery, "parameters") and isinstance(subquery.parameters, dict): # pyright: ignore[reportAttributeAccessIssue]
|
|
411
|
+
for param_name, param_value in subquery.parameters.items(): # pyright: ignore[reportAttributeAccessIssue]
|
|
412
|
+
builder.add_parameter(param_value, name=param_name)
|
|
325
413
|
else:
|
|
326
414
|
subquery_exp = values # type: ignore[assignment]
|
|
327
415
|
condition = col_expr.isin(subquery_exp)
|
|
@@ -329,10 +417,15 @@ class WhereClauseMixin:
|
|
|
329
417
|
if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
|
|
330
418
|
msg = "Unsupported type for 'values' in WHERE IN"
|
|
331
419
|
raise SQLBuilderError(msg)
|
|
420
|
+
column_name = _extract_column_name(column)
|
|
332
421
|
parameters = []
|
|
333
|
-
for v in values:
|
|
334
|
-
|
|
335
|
-
|
|
422
|
+
for i, v in enumerate(values):
|
|
423
|
+
if len(values) == 1:
|
|
424
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
425
|
+
else:
|
|
426
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
|
|
427
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
428
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
336
429
|
condition = col_expr.isin(*parameters)
|
|
337
430
|
return self.where(condition)
|
|
338
431
|
|
|
@@ -353,10 +446,15 @@ class WhereClauseMixin:
|
|
|
353
446
|
if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
|
|
354
447
|
msg = "Values for where_not_in must be a non-string iterable or subquery."
|
|
355
448
|
raise SQLBuilderError(msg)
|
|
449
|
+
column_name = _extract_column_name(column)
|
|
356
450
|
parameters = []
|
|
357
|
-
for v in values:
|
|
358
|
-
|
|
359
|
-
|
|
451
|
+
for i, v in enumerate(values):
|
|
452
|
+
if len(values) == 1:
|
|
453
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
454
|
+
else:
|
|
455
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
|
|
456
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
457
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
360
458
|
condition = exp.Not(this=col_expr.isin(*parameters))
|
|
361
459
|
return self.where(condition)
|
|
362
460
|
|
|
@@ -438,10 +536,15 @@ class WhereClauseMixin:
|
|
|
438
536
|
if not is_iterable_parameters(values) or isinstance(values, bytes):
|
|
439
537
|
msg = "Unsupported type for 'values' in WHERE ANY"
|
|
440
538
|
raise SQLBuilderError(msg)
|
|
539
|
+
column_name = _extract_column_name(column)
|
|
441
540
|
parameters = []
|
|
442
|
-
for v in values:
|
|
443
|
-
|
|
444
|
-
|
|
541
|
+
for i, v in enumerate(values):
|
|
542
|
+
if len(values) == 1:
|
|
543
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
544
|
+
else:
|
|
545
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_any_{i + 1}")
|
|
546
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
547
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
445
548
|
tuple_expr = exp.Tuple(expressions=parameters)
|
|
446
549
|
condition = exp.EQ(this=col_expr, expression=exp.Any(this=tuple_expr))
|
|
447
550
|
return self.where(condition)
|
|
@@ -473,19 +576,28 @@ class WhereClauseMixin:
|
|
|
473
576
|
if not is_iterable_parameters(values) or isinstance(values, bytes):
|
|
474
577
|
msg = "Unsupported type for 'values' in WHERE NOT ANY"
|
|
475
578
|
raise SQLBuilderError(msg)
|
|
579
|
+
column_name = _extract_column_name(column)
|
|
476
580
|
parameters = []
|
|
477
|
-
for v in values:
|
|
478
|
-
|
|
479
|
-
|
|
581
|
+
for i, v in enumerate(values):
|
|
582
|
+
if len(values) == 1:
|
|
583
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
584
|
+
else:
|
|
585
|
+
param_name = builder._generate_unique_parameter_name(f"{column_name}_not_any_{i + 1}")
|
|
586
|
+
_, param_name = builder.add_parameter(v, name=param_name)
|
|
587
|
+
parameters.append(exp.Placeholder(this=param_name))
|
|
480
588
|
tuple_expr = exp.Tuple(expressions=parameters)
|
|
481
589
|
condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
|
|
482
590
|
return self.where(condition)
|
|
483
591
|
|
|
484
592
|
|
|
593
|
+
@trait
|
|
485
594
|
class HavingClauseMixin:
|
|
486
595
|
"""Mixin providing HAVING clause for SELECT builders."""
|
|
487
596
|
|
|
488
|
-
|
|
597
|
+
__slots__ = ()
|
|
598
|
+
|
|
599
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
600
|
+
_expression: Optional[exp.Expression]
|
|
489
601
|
|
|
490
602
|
def having(self, condition: Union[str, exp.Expression]) -> Self:
|
|
491
603
|
"""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."""
|