sqlspec 0.25.0__py3-none-any.whl → 0.26.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/_serialization.py +223 -21
- sqlspec/_sql.py +12 -50
- sqlspec/_typing.py +9 -0
- sqlspec/adapters/adbc/config.py +8 -1
- sqlspec/adapters/adbc/data_dictionary.py +290 -0
- sqlspec/adapters/adbc/driver.py +127 -18
- sqlspec/adapters/adbc/type_converter.py +159 -0
- sqlspec/adapters/aiosqlite/config.py +3 -0
- sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
- sqlspec/adapters/aiosqlite/driver.py +17 -3
- sqlspec/adapters/asyncmy/_types.py +1 -1
- sqlspec/adapters/asyncmy/config.py +11 -8
- sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
- sqlspec/adapters/asyncmy/driver.py +31 -7
- sqlspec/adapters/asyncpg/config.py +3 -0
- sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
- sqlspec/adapters/asyncpg/driver.py +19 -4
- sqlspec/adapters/bigquery/config.py +3 -0
- sqlspec/adapters/bigquery/data_dictionary.py +109 -0
- sqlspec/adapters/bigquery/driver.py +21 -3
- sqlspec/adapters/bigquery/type_converter.py +93 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/config.py +2 -0
- sqlspec/adapters/duckdb/data_dictionary.py +124 -0
- sqlspec/adapters/duckdb/driver.py +32 -5
- sqlspec/adapters/duckdb/pool.py +1 -1
- sqlspec/adapters/duckdb/type_converter.py +103 -0
- sqlspec/adapters/oracledb/config.py +6 -0
- sqlspec/adapters/oracledb/data_dictionary.py +442 -0
- sqlspec/adapters/oracledb/driver.py +63 -9
- sqlspec/adapters/oracledb/migrations.py +51 -67
- sqlspec/adapters/oracledb/type_converter.py +132 -0
- sqlspec/adapters/psqlpy/config.py +3 -0
- sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
- sqlspec/adapters/psqlpy/driver.py +23 -179
- sqlspec/adapters/psqlpy/type_converter.py +73 -0
- sqlspec/adapters/psycopg/config.py +6 -0
- sqlspec/adapters/psycopg/data_dictionary.py +257 -0
- sqlspec/adapters/psycopg/driver.py +40 -5
- sqlspec/adapters/sqlite/config.py +3 -0
- sqlspec/adapters/sqlite/data_dictionary.py +117 -0
- sqlspec/adapters/sqlite/driver.py +18 -3
- sqlspec/adapters/sqlite/pool.py +13 -4
- sqlspec/builder/_base.py +82 -42
- sqlspec/builder/_column.py +57 -24
- sqlspec/builder/_ddl.py +84 -34
- sqlspec/builder/_insert.py +30 -52
- sqlspec/builder/_parsing_utils.py +104 -8
- sqlspec/builder/_select.py +147 -2
- sqlspec/builder/mixins/_cte_and_set_ops.py +1 -2
- sqlspec/builder/mixins/_join_operations.py +14 -30
- sqlspec/builder/mixins/_merge_operations.py +167 -61
- sqlspec/builder/mixins/_order_limit_operations.py +3 -10
- sqlspec/builder/mixins/_select_operations.py +3 -9
- sqlspec/builder/mixins/_update_operations.py +3 -22
- sqlspec/builder/mixins/_where_clause.py +4 -10
- sqlspec/cli.py +246 -140
- sqlspec/config.py +33 -19
- sqlspec/core/cache.py +2 -2
- sqlspec/core/compiler.py +56 -1
- sqlspec/core/parameters.py +7 -3
- sqlspec/core/statement.py +5 -0
- sqlspec/core/type_conversion.py +234 -0
- sqlspec/driver/__init__.py +6 -3
- sqlspec/driver/_async.py +106 -3
- sqlspec/driver/_common.py +156 -4
- sqlspec/driver/_sync.py +106 -3
- sqlspec/exceptions.py +5 -0
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +153 -14
- sqlspec/migrations/commands.py +34 -96
- sqlspec/migrations/context.py +145 -0
- sqlspec/migrations/loaders.py +25 -8
- sqlspec/migrations/runner.py +352 -82
- sqlspec/typing.py +2 -0
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/serializers.py +50 -2
- {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
- sqlspec-0.26.0.dist-info/RECORD +157 -0
- sqlspec-0.25.0.dist-info/RECORD +0 -139
- {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -11,6 +11,7 @@ from mypy_extensions import trait
|
|
|
11
11
|
from sqlglot import exp
|
|
12
12
|
from typing_extensions import Self
|
|
13
13
|
|
|
14
|
+
from sqlspec.builder._parsing_utils import extract_sql_object_expression
|
|
14
15
|
from sqlspec.exceptions import SQLBuilderError
|
|
15
16
|
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
16
17
|
|
|
@@ -30,7 +31,6 @@ class MergeIntoClauseMixin:
|
|
|
30
31
|
|
|
31
32
|
__slots__ = ()
|
|
32
33
|
|
|
33
|
-
# Type annotations for PyRight - these will be provided by the base class
|
|
34
34
|
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
35
35
|
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
36
36
|
|
|
@@ -50,7 +50,6 @@ class MergeIntoClauseMixin:
|
|
|
50
50
|
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
51
51
|
current_expr = self.get_expression()
|
|
52
52
|
|
|
53
|
-
# Type guard: current_expr is now guaranteed to be an Expression
|
|
54
53
|
assert current_expr is not None
|
|
55
54
|
current_expr.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
|
|
56
55
|
return self
|
|
@@ -62,7 +61,6 @@ class MergeUsingClauseMixin:
|
|
|
62
61
|
|
|
63
62
|
__slots__ = ()
|
|
64
63
|
|
|
65
|
-
# Type annotations for PyRight - these will be provided by the base class
|
|
66
64
|
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
67
65
|
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
68
66
|
|
|
@@ -71,6 +69,11 @@ class MergeUsingClauseMixin:
|
|
|
71
69
|
msg = "Method must be provided by QueryBuilder subclass"
|
|
72
70
|
raise NotImplementedError(msg)
|
|
73
71
|
|
|
72
|
+
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
73
|
+
"""Generate unique parameter name - provided by QueryBuilder."""
|
|
74
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
75
|
+
raise NotImplementedError(msg)
|
|
76
|
+
|
|
74
77
|
def using(self, source: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
|
|
75
78
|
"""Set the source data for the MERGE operation (USING clause).
|
|
76
79
|
|
|
@@ -90,11 +93,35 @@ class MergeUsingClauseMixin:
|
|
|
90
93
|
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
91
94
|
current_expr = self.get_expression()
|
|
92
95
|
|
|
93
|
-
# Type guard: current_expr is now guaranteed to be an Expression
|
|
94
96
|
assert current_expr is not None
|
|
95
97
|
source_expr: exp.Expression
|
|
96
98
|
if isinstance(source, str):
|
|
97
99
|
source_expr = exp.to_table(source, alias=alias)
|
|
100
|
+
elif isinstance(source, dict):
|
|
101
|
+
columns = list(source.keys())
|
|
102
|
+
values = list(source.values())
|
|
103
|
+
|
|
104
|
+
parameterized_values: list[exp.Expression] = []
|
|
105
|
+
for col, val in zip(columns, values):
|
|
106
|
+
column_name = col if isinstance(col, str) else str(col)
|
|
107
|
+
if "." in column_name:
|
|
108
|
+
column_name = column_name.split(".")[-1]
|
|
109
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
110
|
+
_, param_name = self.add_parameter(val, name=param_name)
|
|
111
|
+
parameterized_values.append(exp.Placeholder(this=param_name))
|
|
112
|
+
|
|
113
|
+
select_expr = exp.Select()
|
|
114
|
+
select_expressions = []
|
|
115
|
+
for i, col in enumerate(columns):
|
|
116
|
+
select_expressions.append(exp.alias_(parameterized_values[i], col))
|
|
117
|
+
select_expr.set("expressions", select_expressions)
|
|
118
|
+
|
|
119
|
+
from_expr = exp.From(this=exp.to_table("DUAL"))
|
|
120
|
+
select_expr.set("from", from_expr)
|
|
121
|
+
|
|
122
|
+
source_expr = exp.paren(select_expr)
|
|
123
|
+
if alias:
|
|
124
|
+
source_expr = exp.alias_(source_expr, alias, table=False)
|
|
98
125
|
elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
|
|
99
126
|
subquery_builder_parameters = source.parameters
|
|
100
127
|
if subquery_builder_parameters:
|
|
@@ -121,7 +148,6 @@ class MergeOnClauseMixin:
|
|
|
121
148
|
|
|
122
149
|
__slots__ = ()
|
|
123
150
|
|
|
124
|
-
# Type annotations for PyRight - these will be provided by the base class
|
|
125
151
|
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
126
152
|
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
127
153
|
|
|
@@ -143,7 +169,6 @@ class MergeOnClauseMixin:
|
|
|
143
169
|
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
144
170
|
current_expr = self.get_expression()
|
|
145
171
|
|
|
146
|
-
# Type guard: current_expr is now guaranteed to be an Expression
|
|
147
172
|
assert current_expr is not None
|
|
148
173
|
condition_expr: exp.Expression
|
|
149
174
|
if isinstance(condition, str):
|
|
@@ -170,7 +195,6 @@ class MergeMatchedClauseMixin:
|
|
|
170
195
|
|
|
171
196
|
__slots__ = ()
|
|
172
197
|
|
|
173
|
-
# Type annotations for PyRight - these will be provided by the base class
|
|
174
198
|
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
175
199
|
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
176
200
|
|
|
@@ -184,6 +208,42 @@ class MergeMatchedClauseMixin:
|
|
|
184
208
|
msg = "Method must be provided by QueryBuilder subclass"
|
|
185
209
|
raise NotImplementedError(msg)
|
|
186
210
|
|
|
211
|
+
def _is_column_reference(self, value: str) -> bool:
|
|
212
|
+
"""Check if a string value is a column reference rather than a literal.
|
|
213
|
+
|
|
214
|
+
Uses sqlglot to parse the value and determine if it represents a column
|
|
215
|
+
reference, function call, or other SQL expression rather than a literal.
|
|
216
|
+
"""
|
|
217
|
+
if not isinstance(value, str):
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
# If the string contains spaces and no SQL-like syntax, treat as literal
|
|
221
|
+
if " " in value and not any(x in value for x in [".", "(", ")", "*", "="]):
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
# Only consider strings with dots (table.column), functions, or SQL keywords as column references
|
|
225
|
+
# Simple identifiers are treated as literals
|
|
226
|
+
if not any(x in value for x in [".", "(", ")"]):
|
|
227
|
+
# Check if it's a SQL keyword/function that should be treated as expression
|
|
228
|
+
sql_keywords = {"NULL", "CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME", "DEFAULT"}
|
|
229
|
+
if value.upper() not in sql_keywords:
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
# Try to parse as SQL expression
|
|
234
|
+
parsed: Optional[exp.Expression] = exp.maybe_parse(value)
|
|
235
|
+
if parsed is None:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
# Check for SQL literals that should be treated as expressions
|
|
239
|
+
return isinstance(
|
|
240
|
+
parsed,
|
|
241
|
+
(exp.Dot, exp.Anonymous, exp.Func, exp.Null, exp.CurrentTimestamp, exp.CurrentDate, exp.CurrentTime),
|
|
242
|
+
)
|
|
243
|
+
except Exception:
|
|
244
|
+
# If parsing fails, treat as literal
|
|
245
|
+
return False
|
|
246
|
+
|
|
187
247
|
def _add_when_clause(self, when_clause: exp.When) -> None:
|
|
188
248
|
"""Helper to add a WHEN clause to the MERGE statement.
|
|
189
249
|
|
|
@@ -195,7 +255,6 @@ class MergeMatchedClauseMixin:
|
|
|
195
255
|
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
196
256
|
current_expr = self.get_expression()
|
|
197
257
|
|
|
198
|
-
# Type guard: current_expr is now guaranteed to be an Expression
|
|
199
258
|
assert current_expr is not None
|
|
200
259
|
whens = current_expr.args.get("whens")
|
|
201
260
|
if not whens:
|
|
@@ -230,7 +289,11 @@ class MergeMatchedClauseMixin:
|
|
|
230
289
|
The current builder instance for method chaining.
|
|
231
290
|
"""
|
|
232
291
|
# Combine set_values dict and kwargs
|
|
233
|
-
all_values =
|
|
292
|
+
all_values = {}
|
|
293
|
+
if set_values:
|
|
294
|
+
all_values.update(set_values)
|
|
295
|
+
if kwargs:
|
|
296
|
+
all_values.update(kwargs)
|
|
234
297
|
|
|
235
298
|
if not all_values:
|
|
236
299
|
msg = "No update values provided. Use set_values dict or kwargs."
|
|
@@ -239,35 +302,17 @@ class MergeMatchedClauseMixin:
|
|
|
239
302
|
update_expressions: list[exp.EQ] = []
|
|
240
303
|
for col, val in all_values.items():
|
|
241
304
|
if hasattr(val, "expression") and hasattr(val, "sql"):
|
|
242
|
-
|
|
243
|
-
expression = getattr(val, "expression", None)
|
|
244
|
-
if expression is not None and isinstance(expression, exp.Expression):
|
|
245
|
-
# Merge parameters from SQL object into builder
|
|
246
|
-
if hasattr(val, "parameters"):
|
|
247
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
248
|
-
for param_name, param_value in sql_parameters.items():
|
|
249
|
-
self.add_parameter(param_value, name=param_name)
|
|
250
|
-
value_expr = expression
|
|
251
|
-
else:
|
|
252
|
-
# If expression is None, fall back to parsing the raw SQL
|
|
253
|
-
sql_text = getattr(val, "sql", "")
|
|
254
|
-
# Merge parameters even when parsing raw SQL
|
|
255
|
-
if hasattr(val, "parameters"):
|
|
256
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
257
|
-
for param_name, param_value in sql_parameters.items():
|
|
258
|
-
self.add_parameter(param_value, name=param_name)
|
|
259
|
-
# Check if sql_text is callable (like Expression.sql method)
|
|
260
|
-
if callable(sql_text):
|
|
261
|
-
sql_text = str(val)
|
|
262
|
-
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
305
|
+
value_expr = extract_sql_object_expression(val, builder=self)
|
|
263
306
|
elif isinstance(val, exp.Expression):
|
|
264
307
|
value_expr = val
|
|
308
|
+
elif isinstance(val, str) and self._is_column_reference(val):
|
|
309
|
+
value_expr = exp.maybe_parse(val) or exp.column(val)
|
|
265
310
|
else:
|
|
266
311
|
column_name = col if isinstance(col, str) else str(col)
|
|
267
312
|
if "." in column_name:
|
|
268
313
|
column_name = column_name.split(".")[-1]
|
|
269
314
|
param_name = self._generate_unique_parameter_name(column_name)
|
|
270
|
-
param_name = self.add_parameter(val, name=param_name)
|
|
315
|
+
_, param_name = self.add_parameter(val, name=param_name)
|
|
271
316
|
value_expr = exp.Placeholder(this=param_name)
|
|
272
317
|
|
|
273
318
|
update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
@@ -337,7 +382,6 @@ class MergeNotMatchedClauseMixin:
|
|
|
337
382
|
|
|
338
383
|
__slots__ = ()
|
|
339
384
|
|
|
340
|
-
# Type annotations for PyRight - these will be provided by the base class
|
|
341
385
|
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
342
386
|
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
343
387
|
|
|
@@ -351,6 +395,54 @@ class MergeNotMatchedClauseMixin:
|
|
|
351
395
|
msg = "Method must be provided by QueryBuilder subclass"
|
|
352
396
|
raise NotImplementedError(msg)
|
|
353
397
|
|
|
398
|
+
def _is_column_reference(self, value: str) -> bool:
|
|
399
|
+
"""Check if a string value is a column reference rather than a literal.
|
|
400
|
+
|
|
401
|
+
Uses sqlglot to parse the value and determine if it represents a column
|
|
402
|
+
reference, function call, or other SQL expression rather than a literal.
|
|
403
|
+
"""
|
|
404
|
+
if not isinstance(value, str):
|
|
405
|
+
return False
|
|
406
|
+
|
|
407
|
+
# If the string contains spaces and no SQL-like syntax, treat as literal
|
|
408
|
+
if " " in value and not any(x in value for x in [".", "(", ")", "*", "="]):
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
# Try to parse as SQL expression
|
|
413
|
+
parsed: Optional[exp.Expression] = exp.maybe_parse(value)
|
|
414
|
+
if parsed is None:
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
# If it parses to a Column, Dot (table.column), Identifier, or other SQL constructs
|
|
418
|
+
|
|
419
|
+
except Exception:
|
|
420
|
+
# If parsing fails, fall back to conservative approach
|
|
421
|
+
# Only treat simple identifiers as column references
|
|
422
|
+
return (
|
|
423
|
+
value.replace("_", "").replace(".", "").isalnum()
|
|
424
|
+
and (value[0].isalpha() or value[0] == "_")
|
|
425
|
+
and " " not in value
|
|
426
|
+
and "'" not in value
|
|
427
|
+
and '"' not in value
|
|
428
|
+
)
|
|
429
|
+
return bool(
|
|
430
|
+
isinstance(
|
|
431
|
+
parsed,
|
|
432
|
+
(
|
|
433
|
+
exp.Column,
|
|
434
|
+
exp.Dot,
|
|
435
|
+
exp.Identifier,
|
|
436
|
+
exp.Anonymous,
|
|
437
|
+
exp.Func,
|
|
438
|
+
exp.Null,
|
|
439
|
+
exp.CurrentTimestamp,
|
|
440
|
+
exp.CurrentDate,
|
|
441
|
+
exp.CurrentTime,
|
|
442
|
+
),
|
|
443
|
+
)
|
|
444
|
+
)
|
|
445
|
+
|
|
354
446
|
def _add_when_clause(self, when_clause: exp.When) -> None:
|
|
355
447
|
"""Helper to add a WHEN clause to the MERGE statement - provided by QueryBuilder."""
|
|
356
448
|
msg = "Method must be provided by QueryBuilder subclass"
|
|
@@ -388,12 +480,16 @@ class MergeNotMatchedClauseMixin:
|
|
|
388
480
|
|
|
389
481
|
parameterized_values: list[exp.Expression] = []
|
|
390
482
|
for i, val in enumerate(values):
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
483
|
+
if isinstance(val, str) and self._is_column_reference(val):
|
|
484
|
+
# Handle column references (like "s.data") as column expressions, not parameters
|
|
485
|
+
parameterized_values.append(exp.maybe_parse(val) or exp.column(val))
|
|
486
|
+
else:
|
|
487
|
+
column_name = columns[i] if isinstance(columns[i], str) else str(columns[i])
|
|
488
|
+
if "." in column_name:
|
|
489
|
+
column_name = column_name.split(".")[-1]
|
|
490
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
491
|
+
_, param_name = self.add_parameter(val, name=param_name)
|
|
492
|
+
parameterized_values.append(exp.Placeholder(this=param_name))
|
|
397
493
|
|
|
398
494
|
insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
|
|
399
495
|
insert_args["expression"] = exp.Tuple(expressions=parameterized_values)
|
|
@@ -439,7 +535,6 @@ class MergeNotMatchedBySourceClauseMixin:
|
|
|
439
535
|
|
|
440
536
|
__slots__ = ()
|
|
441
537
|
|
|
442
|
-
# Type annotations for PyRight - these will be provided by the base class
|
|
443
538
|
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
444
539
|
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
445
540
|
|
|
@@ -458,6 +553,35 @@ class MergeNotMatchedBySourceClauseMixin:
|
|
|
458
553
|
msg = "Method must be provided by QueryBuilder subclass"
|
|
459
554
|
raise NotImplementedError(msg)
|
|
460
555
|
|
|
556
|
+
def _is_column_reference(self, value: str) -> bool:
|
|
557
|
+
"""Check if a string value is a column reference rather than a literal.
|
|
558
|
+
|
|
559
|
+
Uses sqlglot to parse the value and determine if it represents a column
|
|
560
|
+
reference, function call, or other SQL expression rather than a literal.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
value: The string value to check
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
True if the value is a column reference, False if it's a literal
|
|
567
|
+
"""
|
|
568
|
+
if not isinstance(value, str):
|
|
569
|
+
return False
|
|
570
|
+
|
|
571
|
+
try:
|
|
572
|
+
parsed: Optional[exp.Expression] = exp.maybe_parse(value)
|
|
573
|
+
if parsed is None:
|
|
574
|
+
return False
|
|
575
|
+
|
|
576
|
+
except Exception:
|
|
577
|
+
return False
|
|
578
|
+
return bool(
|
|
579
|
+
isinstance(
|
|
580
|
+
parsed,
|
|
581
|
+
(exp.Dot, exp.Anonymous, exp.Func, exp.Null, exp.CurrentTimestamp, exp.CurrentDate, exp.CurrentTime),
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
|
|
461
585
|
def when_not_matched_by_source_then_update(
|
|
462
586
|
self,
|
|
463
587
|
set_values: Optional[dict[str, Any]] = None,
|
|
@@ -494,35 +618,17 @@ class MergeNotMatchedBySourceClauseMixin:
|
|
|
494
618
|
update_expressions: list[exp.EQ] = []
|
|
495
619
|
for col, val in all_values.items():
|
|
496
620
|
if hasattr(val, "expression") and hasattr(val, "sql"):
|
|
497
|
-
|
|
498
|
-
expression = getattr(val, "expression", None)
|
|
499
|
-
if expression is not None and isinstance(expression, exp.Expression):
|
|
500
|
-
# Merge parameters from SQL object into builder
|
|
501
|
-
if hasattr(val, "parameters"):
|
|
502
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
503
|
-
for param_name, param_value in sql_parameters.items():
|
|
504
|
-
self.add_parameter(param_value, name=param_name)
|
|
505
|
-
value_expr = expression
|
|
506
|
-
else:
|
|
507
|
-
# If expression is None, fall back to parsing the raw SQL
|
|
508
|
-
sql_text = getattr(val, "sql", "")
|
|
509
|
-
# Merge parameters even when parsing raw SQL
|
|
510
|
-
if hasattr(val, "parameters"):
|
|
511
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
512
|
-
for param_name, param_value in sql_parameters.items():
|
|
513
|
-
self.add_parameter(param_value, name=param_name)
|
|
514
|
-
# Check if sql_text is callable (like Expression.sql method)
|
|
515
|
-
if callable(sql_text):
|
|
516
|
-
sql_text = str(val)
|
|
517
|
-
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
621
|
+
value_expr = extract_sql_object_expression(val, builder=self)
|
|
518
622
|
elif isinstance(val, exp.Expression):
|
|
519
623
|
value_expr = val
|
|
624
|
+
elif isinstance(val, str) and self._is_column_reference(val):
|
|
625
|
+
value_expr = exp.maybe_parse(val) or exp.column(val)
|
|
520
626
|
else:
|
|
521
627
|
column_name = col if isinstance(col, str) else str(col)
|
|
522
628
|
if "." in column_name:
|
|
523
629
|
column_name = column_name.split(".")[-1]
|
|
524
630
|
param_name = self._generate_unique_parameter_name(column_name)
|
|
525
|
-
param_name = self.add_parameter(val, name=param_name)
|
|
631
|
+
_, param_name = self.add_parameter(val, name=param_name)
|
|
526
632
|
value_expr = exp.Placeholder(this=param_name)
|
|
527
633
|
|
|
528
634
|
update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
@@ -11,7 +11,7 @@ from mypy_extensions import trait
|
|
|
11
11
|
from sqlglot import exp
|
|
12
12
|
from typing_extensions import Self
|
|
13
13
|
|
|
14
|
-
from sqlspec.builder._parsing_utils import parse_order_expression
|
|
14
|
+
from sqlspec.builder._parsing_utils import extract_expression, parse_order_expression
|
|
15
15
|
from sqlspec.exceptions import SQLBuilderError
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
@@ -29,7 +29,6 @@ class OrderByClauseMixin:
|
|
|
29
29
|
|
|
30
30
|
__slots__ = ()
|
|
31
31
|
|
|
32
|
-
# Type annotation for PyRight - this will be provided by the base class
|
|
33
32
|
_expression: Optional[exp.Expression]
|
|
34
33
|
|
|
35
34
|
def order_by(self, *items: Union[str, exp.Ordered, "Column"], desc: bool = False) -> Self:
|
|
@@ -58,9 +57,7 @@ class OrderByClauseMixin:
|
|
|
58
57
|
order_item = order_item.desc()
|
|
59
58
|
else:
|
|
60
59
|
# Extract expression from Column objects or use as-is for sqlglot expressions
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
extracted_item = SQLFactory._extract_expression(item)
|
|
60
|
+
extracted_item = extract_expression(item)
|
|
64
61
|
order_item = extracted_item
|
|
65
62
|
if desc and not isinstance(item, exp.Ordered):
|
|
66
63
|
order_item = order_item.desc()
|
|
@@ -75,7 +72,6 @@ class LimitOffsetClauseMixin:
|
|
|
75
72
|
|
|
76
73
|
__slots__ = ()
|
|
77
74
|
|
|
78
|
-
# Type annotation for PyRight - this will be provided by the base class
|
|
79
75
|
_expression: Optional[exp.Expression]
|
|
80
76
|
|
|
81
77
|
def limit(self, value: int) -> Self:
|
|
@@ -122,7 +118,6 @@ class ReturningClauseMixin:
|
|
|
122
118
|
"""Mixin providing RETURNING clause."""
|
|
123
119
|
|
|
124
120
|
__slots__ = ()
|
|
125
|
-
# Type annotation for PyRight - this will be provided by the base class
|
|
126
121
|
_expression: Optional[exp.Expression]
|
|
127
122
|
|
|
128
123
|
def returning(self, *columns: Union[str, exp.Expression, "Column", "ExpressionWrapper", "Case"]) -> Self:
|
|
@@ -145,8 +140,6 @@ class ReturningClauseMixin:
|
|
|
145
140
|
msg = "RETURNING is only supported for INSERT, UPDATE, and DELETE statements."
|
|
146
141
|
raise SQLBuilderError(msg)
|
|
147
142
|
# Extract expressions from various wrapper types
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
returning_exprs = [SQLFactory._extract_expression(c) for c in columns]
|
|
143
|
+
returning_exprs = [extract_expression(c) for c in columns]
|
|
151
144
|
self._expression.set("returning", exp.Returning(expressions=returning_exprs))
|
|
152
145
|
return self
|
|
@@ -11,7 +11,7 @@ from mypy_extensions import trait
|
|
|
11
11
|
from sqlglot import exp
|
|
12
12
|
from typing_extensions import Self
|
|
13
13
|
|
|
14
|
-
from sqlspec.builder._parsing_utils import parse_column_expression, parse_table_expression
|
|
14
|
+
from sqlspec.builder._parsing_utils import parse_column_expression, parse_table_expression, to_expression
|
|
15
15
|
from sqlspec.exceptions import SQLBuilderError
|
|
16
16
|
from sqlspec.utils.type_guards import has_query_builder_parameters, is_expression
|
|
17
17
|
|
|
@@ -29,7 +29,6 @@ class SelectClauseMixin:
|
|
|
29
29
|
|
|
30
30
|
__slots__ = ()
|
|
31
31
|
|
|
32
|
-
# Type annotations for PyRight - these will be provided by the base class
|
|
33
32
|
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
34
33
|
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
35
34
|
|
|
@@ -865,12 +864,9 @@ class Case:
|
|
|
865
864
|
Returns:
|
|
866
865
|
Self for method chaining.
|
|
867
866
|
"""
|
|
868
|
-
from sqlspec._sql import SQLFactory
|
|
869
|
-
|
|
870
867
|
cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
|
|
871
|
-
val_expr =
|
|
868
|
+
val_expr = to_expression(value)
|
|
872
869
|
|
|
873
|
-
# SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
|
|
874
870
|
when_clause = exp.If(this=cond_expr, true=val_expr)
|
|
875
871
|
self._conditions.append(when_clause)
|
|
876
872
|
return self
|
|
@@ -884,9 +880,7 @@ class Case:
|
|
|
884
880
|
Returns:
|
|
885
881
|
Self for method chaining.
|
|
886
882
|
"""
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
self._default = SQLFactory._to_expression(value)
|
|
883
|
+
self._default = to_expression(value)
|
|
890
884
|
return self
|
|
891
885
|
|
|
892
886
|
def end(self) -> Self:
|
|
@@ -6,12 +6,13 @@ table specification, SET clauses, and FROM clauses.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from collections.abc import Mapping
|
|
9
|
-
from typing import Any, Optional, Union
|
|
9
|
+
from typing import Any, Optional, Union
|
|
10
10
|
|
|
11
11
|
from mypy_extensions import trait
|
|
12
12
|
from sqlglot import exp
|
|
13
13
|
from typing_extensions import Self
|
|
14
14
|
|
|
15
|
+
from sqlspec.builder._parsing_utils import extract_sql_object_expression
|
|
15
16
|
from sqlspec.exceptions import SQLBuilderError
|
|
16
17
|
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
17
18
|
|
|
@@ -26,7 +27,6 @@ class UpdateTableClauseMixin:
|
|
|
26
27
|
|
|
27
28
|
__slots__ = ()
|
|
28
29
|
|
|
29
|
-
# Type annotations for PyRight - these will be provided by the base class
|
|
30
30
|
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
31
31
|
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
32
32
|
|
|
@@ -58,7 +58,6 @@ class UpdateSetClauseMixin:
|
|
|
58
58
|
|
|
59
59
|
__slots__ = ()
|
|
60
60
|
|
|
61
|
-
# Type annotations for PyRight - these will be provided by the base class
|
|
62
61
|
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
63
62
|
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
64
63
|
|
|
@@ -93,24 +92,7 @@ class UpdateSetClauseMixin:
|
|
|
93
92
|
self.add_parameter(p_value, name=p_name)
|
|
94
93
|
return value_expr
|
|
95
94
|
if hasattr(val, "expression") and hasattr(val, "sql"):
|
|
96
|
-
|
|
97
|
-
expression = getattr(val, "expression", None)
|
|
98
|
-
if expression is not None and isinstance(expression, exp.Expression):
|
|
99
|
-
# Merge parameters from SQL object into builder
|
|
100
|
-
if hasattr(val, "parameters"):
|
|
101
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
102
|
-
for param_name, param_value in sql_parameters.items():
|
|
103
|
-
self.add_parameter(param_value, name=param_name)
|
|
104
|
-
return cast("exp.Expression", expression)
|
|
105
|
-
# If expression is None, fall back to parsing the raw SQL
|
|
106
|
-
sql_text = getattr(val, "sql", "")
|
|
107
|
-
# Merge parameters even when parsing raw SQL
|
|
108
|
-
if hasattr(val, "parameters"):
|
|
109
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
110
|
-
for param_name, param_value in sql_parameters.items():
|
|
111
|
-
self.add_parameter(param_value, name=param_name)
|
|
112
|
-
parsed_expr = exp.maybe_parse(sql_text)
|
|
113
|
-
return parsed_expr if parsed_expr is not None else exp.convert(str(sql_text))
|
|
95
|
+
return extract_sql_object_expression(val, builder=self)
|
|
114
96
|
column_name = col if isinstance(col, str) else str(col)
|
|
115
97
|
if "." in column_name:
|
|
116
98
|
column_name = column_name.split(".")[-1]
|
|
@@ -170,7 +152,6 @@ class UpdateFromClauseMixin:
|
|
|
170
152
|
|
|
171
153
|
__slots__ = ()
|
|
172
154
|
|
|
173
|
-
# Type annotations for PyRight - these will be provided by the base class
|
|
174
155
|
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
175
156
|
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
176
157
|
|
|
@@ -16,6 +16,8 @@ from sqlglot import exp
|
|
|
16
16
|
from typing_extensions import Self
|
|
17
17
|
|
|
18
18
|
from sqlspec.builder._parsing_utils import extract_column_name, parse_column_expression, parse_condition_expression
|
|
19
|
+
from sqlspec.core.parameters import ParameterStyle, ParameterValidator
|
|
20
|
+
from sqlspec.core.statement import SQL
|
|
19
21
|
from sqlspec.exceptions import SQLBuilderError
|
|
20
22
|
from sqlspec.utils.type_guards import (
|
|
21
23
|
has_expression_and_parameters,
|
|
@@ -299,15 +301,11 @@ class WhereClauseMixin:
|
|
|
299
301
|
raise SQLBuilderError(msg)
|
|
300
302
|
|
|
301
303
|
# Check if condition contains parameter placeholders
|
|
302
|
-
from sqlspec.core.parameters import ParameterStyle, ParameterValidator
|
|
303
|
-
|
|
304
304
|
validator = ParameterValidator()
|
|
305
305
|
param_info = validator.extract_parameters(condition)
|
|
306
306
|
|
|
307
307
|
if param_info:
|
|
308
308
|
# String condition with placeholders - create SQL object with parameters
|
|
309
|
-
from sqlspec import sql as sql_factory
|
|
310
|
-
|
|
311
309
|
# Create parameter mapping based on the detected parameter info
|
|
312
310
|
param_dict = dict(kwargs) # Start with named parameters
|
|
313
311
|
|
|
@@ -327,7 +325,7 @@ class WhereClauseMixin:
|
|
|
327
325
|
param_dict[f"param_{i}"] = value
|
|
328
326
|
|
|
329
327
|
# Create SQL object with parameters that will be processed correctly
|
|
330
|
-
condition =
|
|
328
|
+
condition = SQL(condition, param_dict)
|
|
331
329
|
# Fall through to existing SQL object handling logic
|
|
332
330
|
|
|
333
331
|
elif len(values) == 1 and not kwargs:
|
|
@@ -819,15 +817,11 @@ class WhereClauseMixin:
|
|
|
819
817
|
raise SQLBuilderError(msg)
|
|
820
818
|
|
|
821
819
|
# Check if condition contains parameter placeholders
|
|
822
|
-
from sqlspec.core.parameters import ParameterStyle, ParameterValidator
|
|
823
|
-
|
|
824
820
|
validator = ParameterValidator()
|
|
825
821
|
param_info = validator.extract_parameters(condition)
|
|
826
822
|
|
|
827
823
|
if param_info:
|
|
828
824
|
# String condition with placeholders - create SQL object with parameters
|
|
829
|
-
from sqlspec import sql as sql_factory
|
|
830
|
-
|
|
831
825
|
# Create parameter mapping based on the detected parameter info
|
|
832
826
|
param_dict = dict(kwargs) # Start with named parameters
|
|
833
827
|
|
|
@@ -847,7 +841,7 @@ class WhereClauseMixin:
|
|
|
847
841
|
param_dict[f"param_{i}"] = value
|
|
848
842
|
|
|
849
843
|
# Create SQL object with parameters that will be processed correctly
|
|
850
|
-
condition =
|
|
844
|
+
condition = SQL(condition, param_dict)
|
|
851
845
|
# Fall through to existing SQL object handling logic
|
|
852
846
|
|
|
853
847
|
elif len(values) == 1 and not kwargs:
|