sqlspec 0.12.2__py3-none-any.whl → 0.13.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 +21 -180
- sqlspec/adapters/adbc/config.py +10 -12
- sqlspec/adapters/adbc/driver.py +120 -118
- sqlspec/adapters/aiosqlite/config.py +16 -3
- sqlspec/adapters/aiosqlite/driver.py +100 -130
- sqlspec/adapters/asyncmy/config.py +17 -4
- sqlspec/adapters/asyncmy/driver.py +123 -135
- sqlspec/adapters/asyncpg/config.py +17 -29
- sqlspec/adapters/asyncpg/driver.py +98 -140
- sqlspec/adapters/bigquery/config.py +4 -5
- sqlspec/adapters/bigquery/driver.py +125 -167
- sqlspec/adapters/duckdb/config.py +3 -6
- sqlspec/adapters/duckdb/driver.py +114 -111
- sqlspec/adapters/oracledb/config.py +32 -5
- sqlspec/adapters/oracledb/driver.py +242 -259
- sqlspec/adapters/psqlpy/config.py +18 -9
- sqlspec/adapters/psqlpy/driver.py +118 -93
- sqlspec/adapters/psycopg/config.py +44 -31
- sqlspec/adapters/psycopg/driver.py +283 -236
- sqlspec/adapters/sqlite/config.py +3 -3
- sqlspec/adapters/sqlite/driver.py +103 -97
- sqlspec/config.py +0 -4
- sqlspec/driver/_async.py +89 -98
- sqlspec/driver/_common.py +52 -17
- sqlspec/driver/_sync.py +81 -105
- sqlspec/driver/connection.py +207 -0
- sqlspec/driver/mixins/_csv_writer.py +91 -0
- sqlspec/driver/mixins/_pipeline.py +38 -49
- sqlspec/driver/mixins/_result_utils.py +27 -9
- sqlspec/driver/mixins/_storage.py +67 -181
- sqlspec/driver/mixins/_type_coercion.py +3 -4
- sqlspec/driver/parameters.py +138 -0
- sqlspec/exceptions.py +10 -2
- sqlspec/extensions/aiosql/adapter.py +0 -10
- sqlspec/extensions/litestar/handlers.py +0 -1
- sqlspec/extensions/litestar/plugin.py +0 -3
- sqlspec/extensions/litestar/providers.py +0 -14
- sqlspec/loader.py +25 -90
- sqlspec/protocols.py +542 -0
- sqlspec/service/__init__.py +3 -2
- sqlspec/service/_util.py +147 -0
- sqlspec/service/base.py +1116 -9
- sqlspec/statement/builder/__init__.py +42 -32
- sqlspec/statement/builder/_ddl_utils.py +0 -10
- sqlspec/statement/builder/_parsing_utils.py +10 -4
- sqlspec/statement/builder/base.py +67 -22
- sqlspec/statement/builder/column.py +283 -0
- sqlspec/statement/builder/ddl.py +91 -67
- sqlspec/statement/builder/delete.py +23 -7
- sqlspec/statement/builder/insert.py +29 -15
- sqlspec/statement/builder/merge.py +4 -4
- sqlspec/statement/builder/mixins/_aggregate_functions.py +113 -14
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -1
- sqlspec/statement/builder/mixins/_delete_from.py +1 -1
- sqlspec/statement/builder/mixins/_from.py +10 -8
- sqlspec/statement/builder/mixins/_group_by.py +0 -1
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -1
- sqlspec/statement/builder/mixins/_insert_values.py +0 -2
- sqlspec/statement/builder/mixins/_join.py +20 -13
- sqlspec/statement/builder/mixins/_limit_offset.py +3 -3
- sqlspec/statement/builder/mixins/_merge_clauses.py +3 -4
- sqlspec/statement/builder/mixins/_order_by.py +2 -2
- sqlspec/statement/builder/mixins/_pivot.py +4 -7
- sqlspec/statement/builder/mixins/_select_columns.py +6 -5
- sqlspec/statement/builder/mixins/_unpivot.py +6 -9
- sqlspec/statement/builder/mixins/_update_from.py +2 -1
- sqlspec/statement/builder/mixins/_update_set.py +11 -8
- sqlspec/statement/builder/mixins/_where.py +61 -34
- sqlspec/statement/builder/select.py +32 -17
- sqlspec/statement/builder/update.py +25 -11
- sqlspec/statement/filters.py +39 -14
- sqlspec/statement/parameter_manager.py +220 -0
- sqlspec/statement/parameters.py +210 -79
- sqlspec/statement/pipelines/__init__.py +166 -23
- sqlspec/statement/pipelines/analyzers/_analyzer.py +21 -20
- sqlspec/statement/pipelines/context.py +35 -39
- sqlspec/statement/pipelines/transformers/__init__.py +2 -3
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +19 -187
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +628 -58
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +76 -0
- sqlspec/statement/pipelines/validators/_dml_safety.py +33 -18
- sqlspec/statement/pipelines/validators/_parameter_style.py +87 -14
- sqlspec/statement/pipelines/validators/_performance.py +38 -23
- sqlspec/statement/pipelines/validators/_security.py +39 -62
- sqlspec/statement/result.py +37 -129
- sqlspec/statement/splitter.py +0 -12
- sqlspec/statement/sql.py +863 -391
- sqlspec/statement/sql_compiler.py +140 -0
- sqlspec/storage/__init__.py +10 -2
- sqlspec/storage/backends/fsspec.py +53 -8
- sqlspec/storage/backends/obstore.py +15 -19
- sqlspec/storage/capabilities.py +101 -0
- sqlspec/storage/registry.py +56 -83
- sqlspec/typing.py +6 -434
- sqlspec/utils/cached_property.py +25 -0
- sqlspec/utils/correlation.py +0 -2
- sqlspec/utils/logging.py +0 -6
- sqlspec/utils/sync_tools.py +0 -4
- sqlspec/utils/text.py +0 -5
- sqlspec/utils/type_guards.py +892 -0
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/METADATA +1 -1
- sqlspec-0.13.1.dist-info/RECORD +150 -0
- sqlspec/statement/builder/protocols.py +0 -20
- sqlspec/statement/pipelines/base.py +0 -315
- sqlspec/statement/pipelines/result_types.py +0 -41
- sqlspec/statement/pipelines/transformers/_remove_comments.py +0 -66
- sqlspec/statement/pipelines/transformers/_remove_hints.py +0 -81
- sqlspec/statement/pipelines/validators/base.py +0 -67
- sqlspec/storage/protocol.py +0 -173
- sqlspec-0.12.2.dist-info/RECORD +0 -145
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/WHEEL +0 -0
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -5,6 +5,7 @@ from sqlglot import exp
|
|
|
5
5
|
from typing_extensions import Self
|
|
6
6
|
|
|
7
7
|
from sqlspec.exceptions import SQLBuilderError
|
|
8
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
8
9
|
|
|
9
10
|
__all__ = ("UpdateSetClauseMixin",)
|
|
10
11
|
|
|
@@ -49,14 +50,15 @@ class UpdateSetClauseMixin:
|
|
|
49
50
|
# If value is an expression, use it directly
|
|
50
51
|
if isinstance(val, exp.Expression):
|
|
51
52
|
value_expr = val
|
|
52
|
-
elif
|
|
53
|
+
elif has_query_builder_parameters(val):
|
|
53
54
|
# It's a builder (like SelectBuilder), convert to subquery
|
|
54
55
|
subquery = val.build()
|
|
55
56
|
# Parse the SQL and use as expression
|
|
56
|
-
|
|
57
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
58
|
+
value_expr = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect", None)))
|
|
57
59
|
# Merge parameters from subquery
|
|
58
|
-
if
|
|
59
|
-
for p_name, p_value in val.
|
|
60
|
+
if has_query_builder_parameters(val):
|
|
61
|
+
for p_name, p_value in val.parameters.items():
|
|
60
62
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
61
63
|
else:
|
|
62
64
|
param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
|
|
@@ -69,14 +71,15 @@ class UpdateSetClauseMixin:
|
|
|
69
71
|
# If value is an expression, use it directly
|
|
70
72
|
if isinstance(val, exp.Expression):
|
|
71
73
|
value_expr = val
|
|
72
|
-
elif
|
|
74
|
+
elif has_query_builder_parameters(val):
|
|
73
75
|
# It's a builder (like SelectBuilder), convert to subquery
|
|
74
76
|
subquery = val.build()
|
|
75
77
|
# Parse the SQL and use as expression
|
|
76
|
-
|
|
78
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
79
|
+
value_expr = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect", None)))
|
|
77
80
|
# Merge parameters from subquery
|
|
78
|
-
if
|
|
79
|
-
for p_name, p_value in val.
|
|
81
|
+
if has_query_builder_parameters(val):
|
|
82
|
+
for p_name, p_value in val.parameters.items():
|
|
80
83
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
81
84
|
else:
|
|
82
85
|
param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
|
|
@@ -6,9 +6,11 @@ from typing_extensions import Self
|
|
|
6
6
|
|
|
7
7
|
from sqlspec.exceptions import SQLBuilderError
|
|
8
8
|
from sqlspec.statement.builder._parsing_utils import parse_column_expression, parse_condition_expression
|
|
9
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters, is_iterable_parameters
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
|
-
from sqlspec.
|
|
12
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
13
|
+
from sqlspec.statement.builder.column import ColumnExpression
|
|
12
14
|
|
|
13
15
|
__all__ = ("WhereClauseMixin",)
|
|
14
16
|
|
|
@@ -17,7 +19,8 @@ class WhereClauseMixin:
|
|
|
17
19
|
"""Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
|
|
18
20
|
|
|
19
21
|
def where(
|
|
20
|
-
self,
|
|
22
|
+
self,
|
|
23
|
+
condition: Union[str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression"],
|
|
21
24
|
) -> Self:
|
|
22
25
|
"""Add a WHERE clause to the statement.
|
|
23
26
|
|
|
@@ -34,14 +37,14 @@ class WhereClauseMixin:
|
|
|
34
37
|
Returns:
|
|
35
38
|
The current builder instance for method chaining.
|
|
36
39
|
"""
|
|
37
|
-
# Special case: if this is an
|
|
40
|
+
# Special case: if this is an Update and _expression is not exp.Update, raise the expected error for test coverage
|
|
38
41
|
|
|
39
|
-
if self.__class__.__name__ == "
|
|
42
|
+
if self.__class__.__name__ == "Update" and not (
|
|
40
43
|
hasattr(self, "_expression") and isinstance(getattr(self, "_expression", None), exp.Update)
|
|
41
44
|
):
|
|
42
45
|
msg = "Cannot add WHERE clause to non-UPDATE expression"
|
|
43
46
|
raise SQLBuilderError(msg)
|
|
44
|
-
builder = cast("
|
|
47
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
45
48
|
if builder._expression is None:
|
|
46
49
|
msg = "Cannot add WHERE clause: expression is not initialized."
|
|
47
50
|
raise SQLBuilderError(msg)
|
|
@@ -50,7 +53,6 @@ class WhereClauseMixin:
|
|
|
50
53
|
msg = f"Cannot add WHERE clause to unsupported expression type: {type(builder._expression).__name__}."
|
|
51
54
|
raise SQLBuilderError(msg)
|
|
52
55
|
|
|
53
|
-
# Check if table is set for DELETE queries
|
|
54
56
|
if isinstance(builder._expression, exp.Delete) and not builder._expression.args.get("this"):
|
|
55
57
|
msg = "WHERE clause requires a table to be set. Use from() to set the table first."
|
|
56
58
|
raise SQLBuilderError(msg)
|
|
@@ -58,7 +60,6 @@ class WhereClauseMixin:
|
|
|
58
60
|
# Normalize the condition using enhanced parsing
|
|
59
61
|
condition_expr: exp.Expression
|
|
60
62
|
if isinstance(condition, tuple):
|
|
61
|
-
# Handle tuple format with proper parameter binding
|
|
62
63
|
if len(condition) == 2:
|
|
63
64
|
# 2-tuple: (column, value) -> column = value
|
|
64
65
|
param_name = builder.add_parameter(condition[1])[1]
|
|
@@ -87,7 +88,6 @@ class WhereClauseMixin:
|
|
|
87
88
|
"any": exp.Any,
|
|
88
89
|
}
|
|
89
90
|
operator = operator.lower()
|
|
90
|
-
# Handle special cases for NOT operators
|
|
91
91
|
if operator == "not like":
|
|
92
92
|
condition_expr = exp.Not(this=exp.Like(this=col_expr, expression=placeholder_expr))
|
|
93
93
|
elif operator == "not in":
|
|
@@ -104,7 +104,20 @@ class WhereClauseMixin:
|
|
|
104
104
|
else:
|
|
105
105
|
msg = f"WHERE tuple must have 2 or 3 elements, got {len(condition)}"
|
|
106
106
|
raise SQLBuilderError(msg)
|
|
107
|
+
# Handle ColumnExpression objects
|
|
108
|
+
elif hasattr(condition, "sqlglot_expression"):
|
|
109
|
+
# This is a ColumnExpression from our new Column syntax
|
|
110
|
+
raw_expr = getattr(condition, "sqlglot_expression", None)
|
|
111
|
+
if raw_expr is not None:
|
|
112
|
+
condition_expr = builder._parameterize_expression(raw_expr)
|
|
113
|
+
else:
|
|
114
|
+
# Fallback if attribute exists but is None
|
|
115
|
+
condition_expr = parse_condition_expression(str(condition))
|
|
107
116
|
else:
|
|
117
|
+
# Existing logic for strings and raw SQLGlot expressions
|
|
118
|
+
# Convert to string if it's not a recognized type
|
|
119
|
+
if not isinstance(condition, (str, exp.Expression, tuple)):
|
|
120
|
+
condition = str(condition)
|
|
108
121
|
condition_expr = parse_condition_expression(condition)
|
|
109
122
|
|
|
110
123
|
# Use dialect if available for Delete
|
|
@@ -194,13 +207,16 @@ class WhereClauseMixin:
|
|
|
194
207
|
|
|
195
208
|
def where_exists(self, subquery: "Union[str, Any]") -> "Self":
|
|
196
209
|
sub_expr: exp.Expression
|
|
197
|
-
if
|
|
198
|
-
subquery_builder_params: dict[str, Any] = subquery.
|
|
210
|
+
if has_query_builder_parameters(subquery):
|
|
211
|
+
subquery_builder_params: dict[str, Any] = subquery.parameters
|
|
199
212
|
if subquery_builder_params:
|
|
200
213
|
for p_name, p_value in subquery_builder_params.items():
|
|
201
214
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
202
215
|
sub_sql_obj = subquery.build() # pyright: ignore
|
|
203
|
-
|
|
216
|
+
sql_str = (
|
|
217
|
+
sub_sql_obj.sql if hasattr(sub_sql_obj, "sql") and not callable(sub_sql_obj.sql) else str(sub_sql_obj)
|
|
218
|
+
)
|
|
219
|
+
sub_expr = exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None))
|
|
204
220
|
else:
|
|
205
221
|
sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(self, "dialect_name", None))
|
|
206
222
|
|
|
@@ -213,13 +229,16 @@ class WhereClauseMixin:
|
|
|
213
229
|
|
|
214
230
|
def where_not_exists(self, subquery: "Union[str, Any]") -> "Self":
|
|
215
231
|
sub_expr: exp.Expression
|
|
216
|
-
if
|
|
217
|
-
subquery_builder_params: dict[str, Any] = subquery.
|
|
232
|
+
if has_query_builder_parameters(subquery):
|
|
233
|
+
subquery_builder_params: dict[str, Any] = subquery.parameters
|
|
218
234
|
if subquery_builder_params:
|
|
219
235
|
for p_name, p_value in subquery_builder_params.items():
|
|
220
236
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
221
237
|
sub_sql_obj = subquery.build() # pyright: ignore
|
|
222
|
-
|
|
238
|
+
sql_str = (
|
|
239
|
+
sub_sql_obj.sql if hasattr(sub_sql_obj, "sql") and not callable(sub_sql_obj.sql) else str(sub_sql_obj)
|
|
240
|
+
)
|
|
241
|
+
sub_expr = exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None))
|
|
223
242
|
else:
|
|
224
243
|
sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(self, "dialect_name", None))
|
|
225
244
|
|
|
@@ -238,16 +257,18 @@ class WhereClauseMixin:
|
|
|
238
257
|
"""Add a WHERE ... IN (...) clause. Supports subqueries and iterables."""
|
|
239
258
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
240
259
|
# Subquery support
|
|
241
|
-
if
|
|
242
|
-
|
|
260
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
261
|
+
subquery_exp: exp.Expression
|
|
262
|
+
if has_query_builder_parameters(values):
|
|
243
263
|
subquery = values.build() # pyright: ignore
|
|
244
|
-
|
|
264
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
265
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
|
|
245
266
|
else:
|
|
246
|
-
subquery_exp = values
|
|
267
|
+
subquery_exp = values # type: ignore[assignment]
|
|
247
268
|
condition = col_expr.isin(subquery_exp)
|
|
248
269
|
return self.where(condition)
|
|
249
270
|
# Iterable of values
|
|
250
|
-
if not
|
|
271
|
+
if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
|
|
251
272
|
msg = "Unsupported type for 'values' in WHERE IN"
|
|
252
273
|
raise SQLBuilderError(msg)
|
|
253
274
|
params = []
|
|
@@ -260,15 +281,17 @@ class WhereClauseMixin:
|
|
|
260
281
|
def where_not_in(self, column: "Union[str, exp.Column]", values: Any) -> "Self":
|
|
261
282
|
"""Add a WHERE ... NOT IN (...) clause. Supports subqueries and iterables."""
|
|
262
283
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
263
|
-
if
|
|
264
|
-
|
|
284
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
285
|
+
subquery_exp: exp.Expression
|
|
286
|
+
if has_query_builder_parameters(values):
|
|
265
287
|
subquery = values.build() # pyright: ignore
|
|
266
|
-
|
|
288
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
289
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
|
|
267
290
|
else:
|
|
268
|
-
subquery_exp = values
|
|
291
|
+
subquery_exp = values # type: ignore[assignment]
|
|
269
292
|
condition = exp.Not(this=col_expr.isin(subquery_exp))
|
|
270
293
|
return self.where(condition)
|
|
271
|
-
if not
|
|
294
|
+
if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
|
|
272
295
|
msg = "Values for where_not_in must be a non-string iterable or subquery."
|
|
273
296
|
raise SQLBuilderError(msg)
|
|
274
297
|
params = []
|
|
@@ -294,12 +317,14 @@ class WhereClauseMixin:
|
|
|
294
317
|
The current builder instance for method chaining.
|
|
295
318
|
"""
|
|
296
319
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
297
|
-
if
|
|
298
|
-
|
|
320
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
321
|
+
subquery_exp: exp.Expression
|
|
322
|
+
if has_query_builder_parameters(values):
|
|
299
323
|
subquery = values.build() # pyright: ignore
|
|
300
|
-
|
|
324
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
325
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
|
|
301
326
|
else:
|
|
302
|
-
subquery_exp = values
|
|
327
|
+
subquery_exp = values # type: ignore[assignment]
|
|
303
328
|
condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
|
|
304
329
|
return self.where(condition)
|
|
305
330
|
if isinstance(values, str):
|
|
@@ -317,7 +342,7 @@ class WhereClauseMixin:
|
|
|
317
342
|
# If parsing fails, fall through to error
|
|
318
343
|
msg = "Unsupported type for 'values' in WHERE ANY"
|
|
319
344
|
raise SQLBuilderError(msg)
|
|
320
|
-
if not
|
|
345
|
+
if not is_iterable_parameters(values) or isinstance(values, bytes):
|
|
321
346
|
msg = "Unsupported type for 'values' in WHERE ANY"
|
|
322
347
|
raise SQLBuilderError(msg)
|
|
323
348
|
params = []
|
|
@@ -339,12 +364,14 @@ class WhereClauseMixin:
|
|
|
339
364
|
The current builder instance for method chaining.
|
|
340
365
|
"""
|
|
341
366
|
col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
|
|
342
|
-
if
|
|
343
|
-
|
|
367
|
+
if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
|
|
368
|
+
subquery_exp: exp.Expression
|
|
369
|
+
if has_query_builder_parameters(values):
|
|
344
370
|
subquery = values.build() # pyright: ignore
|
|
345
|
-
|
|
371
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
372
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
|
|
346
373
|
else:
|
|
347
|
-
subquery_exp = values
|
|
374
|
+
subquery_exp = values # type: ignore[assignment]
|
|
348
375
|
condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
|
|
349
376
|
return self.where(condition)
|
|
350
377
|
if isinstance(values, str):
|
|
@@ -362,7 +389,7 @@ class WhereClauseMixin:
|
|
|
362
389
|
# If parsing fails, fall through to error
|
|
363
390
|
msg = "Unsupported type for 'values' in WHERE NOT ANY"
|
|
364
391
|
raise SQLBuilderError(msg)
|
|
365
|
-
if not
|
|
392
|
+
if not is_iterable_parameters(values) or isinstance(values, bytes):
|
|
366
393
|
msg = "Unsupported type for 'values' in WHERE NOT ANY"
|
|
367
394
|
raise SQLBuilderError(msg)
|
|
368
395
|
params = []
|
|
@@ -6,7 +6,7 @@ with automatic parameter binding and validation.
|
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
|
-
from typing import Optional, Union, cast
|
|
9
|
+
from typing import Any, Optional, Union, cast
|
|
10
10
|
|
|
11
11
|
from sqlglot import exp
|
|
12
12
|
from typing_extensions import Self
|
|
@@ -32,11 +32,11 @@ from sqlspec.statement.builder.mixins import (
|
|
|
32
32
|
from sqlspec.statement.result import SQLResult
|
|
33
33
|
from sqlspec.typing import RowT
|
|
34
34
|
|
|
35
|
-
__all__ = ("
|
|
35
|
+
__all__ = ("Select",)
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
@dataclass
|
|
39
|
-
class
|
|
39
|
+
class Select(
|
|
40
40
|
QueryBuilder[RowT],
|
|
41
41
|
WhereClauseMixin,
|
|
42
42
|
OrderByClauseMixin,
|
|
@@ -77,16 +77,37 @@ class SelectBuilder(
|
|
|
77
77
|
_schema: The schema/model class for row typing, if set via as_schema().
|
|
78
78
|
"""
|
|
79
79
|
|
|
80
|
-
_with_parts: "dict[str, Union[exp.CTE,
|
|
80
|
+
_with_parts: "dict[str, Union[exp.CTE, Select]]" = field(default_factory=dict, init=False)
|
|
81
81
|
_expression: Optional[exp.Expression] = field(default=None, init=False, repr=False, compare=False, hash=False)
|
|
82
82
|
_schema: Optional[type[RowT]] = None
|
|
83
83
|
_hints: "list[dict[str, object]]" = field(default_factory=list, init=False, repr=False)
|
|
84
84
|
|
|
85
|
-
def
|
|
86
|
-
|
|
85
|
+
def __init__(self, *columns: str, **kwargs: Any) -> None:
|
|
86
|
+
"""Initialize SELECT with optional columns.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
*columns: Column names to select (e.g., "id", "name", "u.email")
|
|
90
|
+
**kwargs: Additional QueryBuilder arguments (dialect, schema, etc.)
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
Select("id", "name") # Shorthand for Select().select("id", "name")
|
|
94
|
+
Select() # Same as SelectBuilder() - start empty
|
|
95
|
+
"""
|
|
96
|
+
super().__init__(**kwargs)
|
|
97
|
+
|
|
98
|
+
# Initialize fields from dataclass
|
|
99
|
+
self._with_parts = {}
|
|
100
|
+
self._expression = None
|
|
101
|
+
self._schema = None
|
|
102
|
+
self._hints = []
|
|
103
|
+
|
|
87
104
|
if self._expression is None:
|
|
88
105
|
self._create_base_expression()
|
|
89
106
|
|
|
107
|
+
# Add columns if provided - just a shorthand for .select()
|
|
108
|
+
if columns:
|
|
109
|
+
self.select(*columns)
|
|
110
|
+
|
|
90
111
|
@property
|
|
91
112
|
def _expected_result_type(self) -> "type[SQLResult[RowT]]":
|
|
92
113
|
"""Get the expected result type for SELECT operations.
|
|
@@ -102,8 +123,8 @@ class SelectBuilder(
|
|
|
102
123
|
# At this point, self._expression is exp.Select
|
|
103
124
|
return self._expression
|
|
104
125
|
|
|
105
|
-
def as_schema(self, schema: "type[RowT]") -> "
|
|
106
|
-
"""Return a new
|
|
126
|
+
def as_schema(self, schema: "type[RowT]") -> "Select[RowT]":
|
|
127
|
+
"""Return a new Select instance parameterized with the given schema/model type.
|
|
107
128
|
|
|
108
129
|
This enables type-safe result mapping: the returned builder will carry the schema type
|
|
109
130
|
for static analysis and IDE autocompletion. The schema should be a class such as a Pydantic
|
|
@@ -113,15 +134,15 @@ class SelectBuilder(
|
|
|
113
134
|
schema: The schema/model class to use for row typing (e.g., a Pydantic model, dataclass, or msgspec.Struct).
|
|
114
135
|
|
|
115
136
|
Returns:
|
|
116
|
-
|
|
137
|
+
Select[RowT]: A new Select instance with RowT set to the provided schema/model type.
|
|
117
138
|
"""
|
|
118
|
-
new_builder =
|
|
139
|
+
new_builder = Select()
|
|
119
140
|
new_builder._expression = self._expression.copy() if self._expression is not None else None
|
|
120
141
|
new_builder._parameters = self._parameters.copy()
|
|
121
142
|
new_builder._parameter_counter = self._parameter_counter
|
|
122
143
|
new_builder.dialect = self.dialect
|
|
123
144
|
new_builder._schema = schema # type: ignore[assignment]
|
|
124
|
-
return cast("
|
|
145
|
+
return cast("Select[RowT]", new_builder)
|
|
125
146
|
|
|
126
147
|
def with_hint(
|
|
127
148
|
self,
|
|
@@ -151,15 +172,12 @@ class SelectBuilder(
|
|
|
151
172
|
Returns:
|
|
152
173
|
SafeQuery: A dataclass containing the SQL string and parameters.
|
|
153
174
|
"""
|
|
154
|
-
# Call parent build method which handles CTEs and optimization
|
|
155
175
|
safe_query = super().build()
|
|
156
176
|
|
|
157
|
-
# Apply hints using SQLGlot's proper hint support (more robust than regex)
|
|
158
177
|
if hasattr(self, "_hints") and self._hints:
|
|
159
178
|
modified_expr = self._expression.copy() if self._expression else None
|
|
160
179
|
|
|
161
180
|
if modified_expr and isinstance(modified_expr, exp.Select):
|
|
162
|
-
# Apply statement-level hints using SQLGlot's Hint expression
|
|
163
181
|
statement_hints = [h["hint"] for h in self._hints if h.get("location") == "statement"]
|
|
164
182
|
if statement_hints:
|
|
165
183
|
# Parse each hint and create proper hint expressions
|
|
@@ -172,12 +190,10 @@ class SelectBuilder(
|
|
|
172
190
|
if hint_expr:
|
|
173
191
|
hint_expressions.append(hint_expr)
|
|
174
192
|
else:
|
|
175
|
-
# Create a raw identifier for unparsable hints
|
|
176
193
|
hint_expressions.append(exp.Anonymous(this=hint_str))
|
|
177
194
|
except Exception: # noqa: PERF203
|
|
178
195
|
hint_expressions.append(exp.Anonymous(this=str(hint)))
|
|
179
196
|
|
|
180
|
-
# Create a Hint node and attach to SELECT
|
|
181
197
|
if hint_expressions:
|
|
182
198
|
hint_node = exp.Hint(expressions=hint_expressions)
|
|
183
199
|
modified_expr.set("hint", hint_node)
|
|
@@ -186,7 +202,6 @@ class SelectBuilder(
|
|
|
186
202
|
# since SQLGlot doesn't have a standard way to attach hints to individual tables
|
|
187
203
|
modified_sql = modified_expr.sql(dialect=self.dialect_name, pretty=True)
|
|
188
204
|
|
|
189
|
-
# Apply table-level hints via string manipulation (as fallback)
|
|
190
205
|
table_hints = [h for h in self._hints if h.get("location") == "table" and h.get("table")]
|
|
191
206
|
if table_hints:
|
|
192
207
|
for th in table_hints:
|
|
@@ -5,7 +5,7 @@ with automatic parameter binding and validation.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import TYPE_CHECKING, Optional, Union
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
9
9
|
|
|
10
10
|
from sqlglot import exp
|
|
11
11
|
from typing_extensions import Self
|
|
@@ -23,13 +23,13 @@ from sqlspec.statement.result import SQLResult
|
|
|
23
23
|
from sqlspec.typing import RowT
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
-
from sqlspec.statement.builder.select import
|
|
26
|
+
from sqlspec.statement.builder.select import Select
|
|
27
27
|
|
|
28
|
-
__all__ = ("
|
|
28
|
+
__all__ = ("Update",)
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@dataclass(unsafe_hash=True)
|
|
32
|
-
class
|
|
32
|
+
class Update(
|
|
33
33
|
QueryBuilder[RowT],
|
|
34
34
|
WhereClauseMixin,
|
|
35
35
|
ReturningClauseMixin,
|
|
@@ -46,16 +46,21 @@ class UpdateBuilder(
|
|
|
46
46
|
```python
|
|
47
47
|
# Basic UPDATE
|
|
48
48
|
update_query = (
|
|
49
|
-
|
|
49
|
+
Update()
|
|
50
50
|
.table("users")
|
|
51
51
|
.set(name="John Doe")
|
|
52
52
|
.set(email="john@example.com")
|
|
53
53
|
.where("id = 1")
|
|
54
54
|
)
|
|
55
55
|
|
|
56
|
+
# Even more concise with constructor
|
|
57
|
+
update_query = (
|
|
58
|
+
Update("users").set(name="John Doe").where("id = 1")
|
|
59
|
+
)
|
|
60
|
+
|
|
56
61
|
# UPDATE with parameterized conditions
|
|
57
62
|
update_query = (
|
|
58
|
-
|
|
63
|
+
Update()
|
|
59
64
|
.table("users")
|
|
60
65
|
.set(status="active")
|
|
61
66
|
.where_eq("id", 123)
|
|
@@ -63,7 +68,7 @@ class UpdateBuilder(
|
|
|
63
68
|
|
|
64
69
|
# UPDATE with FROM clause (PostgreSQL style)
|
|
65
70
|
update_query = (
|
|
66
|
-
|
|
71
|
+
Update()
|
|
67
72
|
.table("users", "u")
|
|
68
73
|
.set(name="Updated Name")
|
|
69
74
|
.from_("profiles", "p")
|
|
@@ -72,6 +77,18 @@ class UpdateBuilder(
|
|
|
72
77
|
```
|
|
73
78
|
"""
|
|
74
79
|
|
|
80
|
+
def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
|
|
81
|
+
"""Initialize UPDATE with optional table.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
table: Target table name
|
|
85
|
+
**kwargs: Additional QueryBuilder arguments
|
|
86
|
+
"""
|
|
87
|
+
super().__init__(**kwargs)
|
|
88
|
+
|
|
89
|
+
if table:
|
|
90
|
+
self.table(table)
|
|
91
|
+
|
|
75
92
|
@property
|
|
76
93
|
def _expected_result_type(self) -> "type[SQLResult[RowT]]":
|
|
77
94
|
"""Return the expected result type for this builder."""
|
|
@@ -87,7 +104,7 @@ class UpdateBuilder(
|
|
|
87
104
|
|
|
88
105
|
def join(
|
|
89
106
|
self,
|
|
90
|
-
table: "Union[str, exp.Expression,
|
|
107
|
+
table: "Union[str, exp.Expression, Select[RowT]]",
|
|
91
108
|
on: "Union[str, exp.Expression]",
|
|
92
109
|
alias: "Optional[str]" = None,
|
|
93
110
|
join_type: str = "INNER",
|
|
@@ -141,7 +158,6 @@ class UpdateBuilder(
|
|
|
141
158
|
msg = f"Unsupported join type: {join_type}"
|
|
142
159
|
raise SQLBuilderError(msg)
|
|
143
160
|
|
|
144
|
-
# Add join to the UPDATE expression
|
|
145
161
|
if not self._expression.args.get("joins"):
|
|
146
162
|
self._expression.set("joins", [])
|
|
147
163
|
self._expression.args["joins"].append(join_expr)
|
|
@@ -165,12 +181,10 @@ class UpdateBuilder(
|
|
|
165
181
|
msg = "No UPDATE expression to build or expression is of the wrong type."
|
|
166
182
|
raise SQLBuilderError(msg)
|
|
167
183
|
|
|
168
|
-
# Check that the table is set
|
|
169
184
|
if getattr(self._expression, "this", None) is None:
|
|
170
185
|
msg = "No table specified for UPDATE statement."
|
|
171
186
|
raise SQLBuilderError(msg)
|
|
172
187
|
|
|
173
|
-
# Check that at least one SET expression exists
|
|
174
188
|
if not self._expression.args.get("expressions"):
|
|
175
189
|
msg = "At least one SET clause must be specified for UPDATE statement."
|
|
176
190
|
raise SQLBuilderError(msg)
|
sqlspec/statement/filters.py
CHANGED
|
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
|
|
|
17
17
|
__all__ = (
|
|
18
18
|
"AnyCollectionFilter",
|
|
19
19
|
"BeforeAfterFilter",
|
|
20
|
+
"FilterTypeT",
|
|
20
21
|
"FilterTypes",
|
|
21
22
|
"InAnyFilter",
|
|
22
23
|
"InCollectionFilter",
|
|
@@ -112,9 +113,7 @@ class BeforeAfterFilter(StatementFilter):
|
|
|
112
113
|
final_condition = conditions[0]
|
|
113
114
|
for cond in conditions[1:]:
|
|
114
115
|
final_condition = exp.And(this=final_condition, expression=cond)
|
|
115
|
-
# Use the SQL object's where method which handles all cases
|
|
116
116
|
result = statement.where(final_condition)
|
|
117
|
-
# Add the filter's parameters to the result
|
|
118
117
|
_, named_params = self.extract_parameters()
|
|
119
118
|
for name, value in named_params.items():
|
|
120
119
|
result = result.add_named_parameter(name, value)
|
|
@@ -171,7 +170,6 @@ class OnBeforeAfterFilter(StatementFilter):
|
|
|
171
170
|
for cond in conditions[1:]:
|
|
172
171
|
final_condition = exp.And(this=final_condition, expression=cond)
|
|
173
172
|
result = statement.where(final_condition)
|
|
174
|
-
# Add the filter's parameters to the result
|
|
175
173
|
_, named_params = self.extract_parameters()
|
|
176
174
|
for name, value in named_params.items():
|
|
177
175
|
result = result.add_named_parameter(name, value)
|
|
@@ -229,7 +227,6 @@ class InCollectionFilter(InAnyFilter[T]):
|
|
|
229
227
|
]
|
|
230
228
|
|
|
231
229
|
result = statement.where(exp.In(this=exp.column(self.field_name), expressions=placeholder_expressions))
|
|
232
|
-
# Add the filter's parameters to the result
|
|
233
230
|
_, named_params = self.extract_parameters()
|
|
234
231
|
for name, value in named_params.items():
|
|
235
232
|
result = result.add_named_parameter(name, value)
|
|
@@ -273,7 +270,6 @@ class NotInCollectionFilter(InAnyFilter[T]):
|
|
|
273
270
|
result = statement.where(
|
|
274
271
|
exp.Not(this=exp.In(this=exp.column(self.field_name), expressions=placeholder_expressions))
|
|
275
272
|
)
|
|
276
|
-
# Add the filter's parameters to the result
|
|
277
273
|
_, named_params = self.extract_parameters()
|
|
278
274
|
for name, value in named_params.items():
|
|
279
275
|
result = result.add_named_parameter(name, value)
|
|
@@ -323,7 +319,6 @@ class AnyCollectionFilter(InAnyFilter[T]):
|
|
|
323
319
|
array_expr = exp.Array(expressions=placeholder_expressions)
|
|
324
320
|
# Generates SQL like: self.field_name = ANY(ARRAY[?, ?, ...])
|
|
325
321
|
result = statement.where(exp.EQ(this=exp.column(self.field_name), expression=exp.Any(this=array_expr)))
|
|
326
|
-
# Add the filter's parameters to the result
|
|
327
322
|
_, named_params = self.extract_parameters()
|
|
328
323
|
for name, value in named_params.items():
|
|
329
324
|
result = result.add_named_parameter(name, value)
|
|
@@ -372,7 +367,6 @@ class NotAnyCollectionFilter(InAnyFilter[T]):
|
|
|
372
367
|
# Generates SQL like: NOT (self.field_name = ANY(ARRAY[?, ?, ...]))
|
|
373
368
|
condition = exp.EQ(this=exp.column(self.field_name), expression=exp.Any(this=array_expr))
|
|
374
369
|
result = statement.where(exp.Not(this=condition))
|
|
375
|
-
# Add the filter's parameters to the result
|
|
376
370
|
_, named_params = self.extract_parameters()
|
|
377
371
|
for name, value in named_params.items():
|
|
378
372
|
result = result.add_named_parameter(name, value)
|
|
@@ -396,13 +390,48 @@ class LimitOffsetFilter(PaginationFilter):
|
|
|
396
390
|
offset: int
|
|
397
391
|
"""Value for ``OFFSET`` clause of query."""
|
|
398
392
|
|
|
393
|
+
def __post_init__(self) -> None:
|
|
394
|
+
"""Initialize parameter names."""
|
|
395
|
+
# Generate unique parameter names to avoid conflicts
|
|
396
|
+
import uuid
|
|
397
|
+
|
|
398
|
+
unique_suffix = str(uuid.uuid4()).replace("-", "")[:8]
|
|
399
|
+
self._limit_param_name = f"limit_{unique_suffix}"
|
|
400
|
+
self._offset_param_name = f"offset_{unique_suffix}"
|
|
401
|
+
|
|
399
402
|
def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
|
|
400
403
|
"""Extract filter parameters."""
|
|
401
|
-
|
|
402
|
-
return [], {"limit": self.limit, "offset": self.offset}
|
|
404
|
+
return [], {self._limit_param_name: self.limit, self._offset_param_name: self.offset}
|
|
403
405
|
|
|
404
406
|
def append_to_statement(self, statement: "SQL") -> "SQL":
|
|
405
|
-
|
|
407
|
+
# Create limit and offset expressions using our pre-generated parameter names
|
|
408
|
+
from sqlglot import exp
|
|
409
|
+
|
|
410
|
+
limit_placeholder = exp.Placeholder(this=self._limit_param_name)
|
|
411
|
+
offset_placeholder = exp.Placeholder(this=self._offset_param_name)
|
|
412
|
+
|
|
413
|
+
# Apply LIMIT and OFFSET to the statement
|
|
414
|
+
result = statement
|
|
415
|
+
|
|
416
|
+
# Check if the statement supports LIMIT directly
|
|
417
|
+
if isinstance(result._statement, exp.Select):
|
|
418
|
+
new_statement = result._statement.limit(limit_placeholder)
|
|
419
|
+
else:
|
|
420
|
+
# Wrap in a SELECT if the statement doesn't support LIMIT directly
|
|
421
|
+
new_statement = exp.Select().from_(result._statement).limit(limit_placeholder)
|
|
422
|
+
|
|
423
|
+
# Add OFFSET
|
|
424
|
+
if isinstance(new_statement, exp.Select):
|
|
425
|
+
new_statement = new_statement.offset(offset_placeholder)
|
|
426
|
+
|
|
427
|
+
result = result.copy(statement=new_statement)
|
|
428
|
+
|
|
429
|
+
# Add the parameters to the result
|
|
430
|
+
_, named_params = self.extract_parameters()
|
|
431
|
+
for name, value in named_params.items():
|
|
432
|
+
result = result.add_named_parameter(name, value)
|
|
433
|
+
|
|
434
|
+
return result
|
|
406
435
|
|
|
407
436
|
|
|
408
437
|
@dataclass
|
|
@@ -450,7 +479,6 @@ class SearchFilter(StatementFilter):
|
|
|
450
479
|
if isinstance(self.field_name, str):
|
|
451
480
|
self._param_name = f"{self.field_name}_search"
|
|
452
481
|
else:
|
|
453
|
-
# For multiple fields, use a generic search parameter name
|
|
454
482
|
self._param_name = "search_value"
|
|
455
483
|
|
|
456
484
|
def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
|
|
@@ -484,7 +512,6 @@ class SearchFilter(StatementFilter):
|
|
|
484
512
|
final_condition = exp.Or(this=final_condition, expression=cond)
|
|
485
513
|
result = statement.where(final_condition)
|
|
486
514
|
|
|
487
|
-
# Add the filter's parameters to the result
|
|
488
515
|
_, named_params = self.extract_parameters()
|
|
489
516
|
for name, value in named_params.items():
|
|
490
517
|
result = result.add_named_parameter(name, value)
|
|
@@ -502,7 +529,6 @@ class NotInSearchFilter(SearchFilter):
|
|
|
502
529
|
if isinstance(self.field_name, str):
|
|
503
530
|
self._param_name = f"{self.field_name}_not_search"
|
|
504
531
|
else:
|
|
505
|
-
# For multiple fields, use a generic search parameter name
|
|
506
532
|
self._param_name = "not_search_value"
|
|
507
533
|
|
|
508
534
|
def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
|
|
@@ -536,7 +562,6 @@ class NotInSearchFilter(SearchFilter):
|
|
|
536
562
|
final_condition = exp.And(this=final_condition, expression=cond)
|
|
537
563
|
result = statement.where(final_condition)
|
|
538
564
|
|
|
539
|
-
# Add the filter's parameters to the result
|
|
540
565
|
_, named_params = self.extract_parameters()
|
|
541
566
|
for name, value in named_params.items():
|
|
542
567
|
result = result.add_named_parameter(name, value)
|