sqlspec 0.24.1__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 +20 -62
- sqlspec/_typing.py +11 -0
- sqlspec/adapters/adbc/config.py +8 -1
- sqlspec/adapters/adbc/data_dictionary.py +290 -0
- sqlspec/adapters/adbc/driver.py +129 -20
- 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 +68 -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 +8 -4
- 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/base.py +3 -4
- sqlspec/builder/_base.py +130 -48
- sqlspec/builder/_column.py +66 -24
- sqlspec/builder/_ddl.py +91 -41
- sqlspec/builder/_insert.py +40 -58
- sqlspec/builder/_parsing_utils.py +127 -12
- sqlspec/builder/_select.py +147 -2
- sqlspec/builder/_update.py +1 -1
- sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
- sqlspec/builder/mixins/_delete_operations.py +12 -7
- sqlspec/builder/mixins/_insert_operations.py +50 -36
- sqlspec/builder/mixins/_join_operations.py +15 -30
- sqlspec/builder/mixins/_merge_operations.py +210 -78
- sqlspec/builder/mixins/_order_limit_operations.py +4 -10
- sqlspec/builder/mixins/_pivot_operations.py +1 -0
- sqlspec/builder/mixins/_select_operations.py +44 -22
- sqlspec/builder/mixins/_update_operations.py +30 -37
- sqlspec/builder/mixins/_where_clause.py +52 -70
- sqlspec/cli.py +246 -140
- sqlspec/config.py +33 -19
- sqlspec/core/__init__.py +3 -2
- sqlspec/core/cache.py +298 -352
- sqlspec/core/compiler.py +61 -4
- sqlspec/core/filters.py +246 -213
- sqlspec/core/hashing.py +9 -11
- sqlspec/core/parameters.py +27 -10
- sqlspec/core/statement.py +72 -12
- sqlspec/core/type_conversion.py +234 -0
- sqlspec/driver/__init__.py +6 -3
- sqlspec/driver/_async.py +108 -5
- sqlspec/driver/_common.py +186 -17
- sqlspec/driver/_sync.py +108 -5
- sqlspec/driver/mixins/_result_tools.py +60 -7
- sqlspec/exceptions.py +5 -0
- sqlspec/loader.py +8 -9
- 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/storage/backends/fsspec.py +1 -0
- sqlspec/typing.py +4 -0
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/serializers.py +50 -2
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
- sqlspec-0.26.0.dist-info/RECORD +157 -0
- sqlspec-0.24.1.dist-info/RECORD +0 -139
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
1
2
|
"""MERGE operation mixins.
|
|
2
3
|
|
|
3
4
|
Provides mixins for MERGE statement functionality including INTO,
|
|
@@ -10,6 +11,7 @@ from mypy_extensions import trait
|
|
|
10
11
|
from sqlglot import exp
|
|
11
12
|
from typing_extensions import Self
|
|
12
13
|
|
|
14
|
+
from sqlspec.builder._parsing_utils import extract_sql_object_expression
|
|
13
15
|
from sqlspec.exceptions import SQLBuilderError
|
|
14
16
|
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
15
17
|
|
|
@@ -28,7 +30,9 @@ class MergeIntoClauseMixin:
|
|
|
28
30
|
"""Mixin providing INTO clause for MERGE builders."""
|
|
29
31
|
|
|
30
32
|
__slots__ = ()
|
|
31
|
-
|
|
33
|
+
|
|
34
|
+
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
35
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
32
36
|
|
|
33
37
|
def into(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
34
38
|
"""Set the target table for the MERGE operation (INTO clause).
|
|
@@ -41,11 +45,13 @@ class MergeIntoClauseMixin:
|
|
|
41
45
|
Returns:
|
|
42
46
|
The current builder instance for method chaining.
|
|
43
47
|
"""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
current_expr = self.get_expression()
|
|
49
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
50
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
51
|
+
current_expr = self.get_expression()
|
|
52
|
+
|
|
53
|
+
assert current_expr is not None
|
|
54
|
+
current_expr.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
|
|
49
55
|
return self
|
|
50
56
|
|
|
51
57
|
|
|
@@ -54,13 +60,20 @@ class MergeUsingClauseMixin:
|
|
|
54
60
|
"""Mixin providing USING clause for MERGE builders."""
|
|
55
61
|
|
|
56
62
|
__slots__ = ()
|
|
57
|
-
|
|
63
|
+
|
|
64
|
+
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
65
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
58
66
|
|
|
59
67
|
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
60
68
|
"""Add parameter - provided by QueryBuilder."""
|
|
61
69
|
msg = "Method must be provided by QueryBuilder subclass"
|
|
62
70
|
raise NotImplementedError(msg)
|
|
63
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
|
+
|
|
64
77
|
def using(self, source: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
|
|
65
78
|
"""Set the source data for the MERGE operation (USING clause).
|
|
66
79
|
|
|
@@ -75,14 +88,40 @@ class MergeUsingClauseMixin:
|
|
|
75
88
|
Raises:
|
|
76
89
|
SQLBuilderError: If the current expression is not a MERGE statement or if the source type is unsupported.
|
|
77
90
|
"""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
current_expr = self.get_expression()
|
|
92
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
93
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
94
|
+
current_expr = self.get_expression()
|
|
82
95
|
|
|
96
|
+
assert current_expr is not None
|
|
83
97
|
source_expr: exp.Expression
|
|
84
98
|
if isinstance(source, str):
|
|
85
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)
|
|
86
125
|
elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
|
|
87
126
|
subquery_builder_parameters = source.parameters
|
|
88
127
|
if subquery_builder_parameters:
|
|
@@ -99,7 +138,7 @@ class MergeUsingClauseMixin:
|
|
|
99
138
|
msg = f"Unsupported source type for USING clause: {type(source)}"
|
|
100
139
|
raise SQLBuilderError(msg)
|
|
101
140
|
|
|
102
|
-
|
|
141
|
+
current_expr.set("using", source_expr)
|
|
103
142
|
return self
|
|
104
143
|
|
|
105
144
|
|
|
@@ -108,7 +147,9 @@ class MergeOnClauseMixin:
|
|
|
108
147
|
"""Mixin providing ON clause for MERGE builders."""
|
|
109
148
|
|
|
110
149
|
__slots__ = ()
|
|
111
|
-
|
|
150
|
+
|
|
151
|
+
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
152
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
112
153
|
|
|
113
154
|
def on(self, condition: Union[str, exp.Expression]) -> Self:
|
|
114
155
|
"""Set the join condition for the MERGE operation (ON clause).
|
|
@@ -123,11 +164,12 @@ class MergeOnClauseMixin:
|
|
|
123
164
|
Raises:
|
|
124
165
|
SQLBuilderError: If the current expression is not a MERGE statement or if the condition type is unsupported.
|
|
125
166
|
"""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
167
|
+
current_expr = self.get_expression()
|
|
168
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
169
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
170
|
+
current_expr = self.get_expression()
|
|
130
171
|
|
|
172
|
+
assert current_expr is not None
|
|
131
173
|
condition_expr: exp.Expression
|
|
132
174
|
if isinstance(condition, str):
|
|
133
175
|
parsed_condition: Optional[exp.Expression] = exp.maybe_parse(
|
|
@@ -143,7 +185,7 @@ class MergeOnClauseMixin:
|
|
|
143
185
|
msg = f"Unsupported condition type for ON clause: {type(condition)}"
|
|
144
186
|
raise SQLBuilderError(msg)
|
|
145
187
|
|
|
146
|
-
|
|
188
|
+
current_expr.set("on", condition_expr)
|
|
147
189
|
return self
|
|
148
190
|
|
|
149
191
|
|
|
@@ -152,7 +194,9 @@ class MergeMatchedClauseMixin:
|
|
|
152
194
|
"""Mixin providing WHEN MATCHED THEN ... clauses for MERGE builders."""
|
|
153
195
|
|
|
154
196
|
__slots__ = ()
|
|
155
|
-
|
|
197
|
+
|
|
198
|
+
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
199
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
156
200
|
|
|
157
201
|
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
158
202
|
"""Add parameter - provided by QueryBuilder."""
|
|
@@ -164,21 +208,58 @@ class MergeMatchedClauseMixin:
|
|
|
164
208
|
msg = "Method must be provided by QueryBuilder subclass"
|
|
165
209
|
raise NotImplementedError(msg)
|
|
166
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
|
+
|
|
167
247
|
def _add_when_clause(self, when_clause: exp.When) -> None:
|
|
168
248
|
"""Helper to add a WHEN clause to the MERGE statement.
|
|
169
249
|
|
|
170
250
|
Args:
|
|
171
251
|
when_clause: The WHEN clause to add.
|
|
172
252
|
"""
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
253
|
+
current_expr = self.get_expression()
|
|
254
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
255
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
256
|
+
current_expr = self.get_expression()
|
|
177
257
|
|
|
178
|
-
|
|
258
|
+
assert current_expr is not None
|
|
259
|
+
whens = current_expr.args.get("whens")
|
|
179
260
|
if not whens:
|
|
180
261
|
whens = exp.Whens(expressions=[])
|
|
181
|
-
|
|
262
|
+
current_expr.set("whens", whens)
|
|
182
263
|
|
|
183
264
|
whens.append("expressions", when_clause)
|
|
184
265
|
|
|
@@ -208,7 +289,11 @@ class MergeMatchedClauseMixin:
|
|
|
208
289
|
The current builder instance for method chaining.
|
|
209
290
|
"""
|
|
210
291
|
# Combine set_values dict and kwargs
|
|
211
|
-
all_values =
|
|
292
|
+
all_values = {}
|
|
293
|
+
if set_values:
|
|
294
|
+
all_values.update(set_values)
|
|
295
|
+
if kwargs:
|
|
296
|
+
all_values.update(kwargs)
|
|
212
297
|
|
|
213
298
|
if not all_values:
|
|
214
299
|
msg = "No update values provided. Use set_values dict or kwargs."
|
|
@@ -217,35 +302,17 @@ class MergeMatchedClauseMixin:
|
|
|
217
302
|
update_expressions: list[exp.EQ] = []
|
|
218
303
|
for col, val in all_values.items():
|
|
219
304
|
if hasattr(val, "expression") and hasattr(val, "sql"):
|
|
220
|
-
|
|
221
|
-
expression = getattr(val, "expression", None)
|
|
222
|
-
if expression is not None and isinstance(expression, exp.Expression):
|
|
223
|
-
# Merge parameters from SQL object into builder
|
|
224
|
-
if hasattr(val, "parameters"):
|
|
225
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
226
|
-
for param_name, param_value in sql_parameters.items():
|
|
227
|
-
self.add_parameter(param_value, name=param_name)
|
|
228
|
-
value_expr = expression
|
|
229
|
-
else:
|
|
230
|
-
# If expression is None, fall back to parsing the raw SQL
|
|
231
|
-
sql_text = getattr(val, "sql", "")
|
|
232
|
-
# Merge parameters even when parsing raw SQL
|
|
233
|
-
if hasattr(val, "parameters"):
|
|
234
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
235
|
-
for param_name, param_value in sql_parameters.items():
|
|
236
|
-
self.add_parameter(param_value, name=param_name)
|
|
237
|
-
# Check if sql_text is callable (like Expression.sql method)
|
|
238
|
-
if callable(sql_text):
|
|
239
|
-
sql_text = str(val)
|
|
240
|
-
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
305
|
+
value_expr = extract_sql_object_expression(val, builder=self)
|
|
241
306
|
elif isinstance(val, exp.Expression):
|
|
242
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)
|
|
243
310
|
else:
|
|
244
311
|
column_name = col if isinstance(col, str) else str(col)
|
|
245
312
|
if "." in column_name:
|
|
246
313
|
column_name = column_name.split(".")[-1]
|
|
247
314
|
param_name = self._generate_unique_parameter_name(column_name)
|
|
248
|
-
param_name = self.add_parameter(val, name=param_name)
|
|
315
|
+
_, param_name = self.add_parameter(val, name=param_name)
|
|
249
316
|
value_expr = exp.Placeholder(this=param_name)
|
|
250
317
|
|
|
251
318
|
update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
@@ -315,7 +382,8 @@ class MergeNotMatchedClauseMixin:
|
|
|
315
382
|
|
|
316
383
|
__slots__ = ()
|
|
317
384
|
|
|
318
|
-
|
|
385
|
+
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
386
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
319
387
|
|
|
320
388
|
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
321
389
|
"""Add parameter - provided by QueryBuilder."""
|
|
@@ -327,6 +395,54 @@ class MergeNotMatchedClauseMixin:
|
|
|
327
395
|
msg = "Method must be provided by QueryBuilder subclass"
|
|
328
396
|
raise NotImplementedError(msg)
|
|
329
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
|
+
|
|
330
446
|
def _add_when_clause(self, when_clause: exp.When) -> None:
|
|
331
447
|
"""Helper to add a WHEN clause to the MERGE statement - provided by QueryBuilder."""
|
|
332
448
|
msg = "Method must be provided by QueryBuilder subclass"
|
|
@@ -364,12 +480,16 @@ class MergeNotMatchedClauseMixin:
|
|
|
364
480
|
|
|
365
481
|
parameterized_values: list[exp.Expression] = []
|
|
366
482
|
for i, val in enumerate(values):
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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))
|
|
373
493
|
|
|
374
494
|
insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
|
|
375
495
|
insert_args["expression"] = exp.Tuple(expressions=parameterized_values)
|
|
@@ -415,7 +535,8 @@ class MergeNotMatchedBySourceClauseMixin:
|
|
|
415
535
|
|
|
416
536
|
__slots__ = ()
|
|
417
537
|
|
|
418
|
-
|
|
538
|
+
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
539
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
419
540
|
|
|
420
541
|
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
421
542
|
"""Add parameter - provided by QueryBuilder."""
|
|
@@ -432,6 +553,35 @@ class MergeNotMatchedBySourceClauseMixin:
|
|
|
432
553
|
msg = "Method must be provided by QueryBuilder subclass"
|
|
433
554
|
raise NotImplementedError(msg)
|
|
434
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
|
+
|
|
435
585
|
def when_not_matched_by_source_then_update(
|
|
436
586
|
self,
|
|
437
587
|
set_values: Optional[dict[str, Any]] = None,
|
|
@@ -468,35 +618,17 @@ class MergeNotMatchedBySourceClauseMixin:
|
|
|
468
618
|
update_expressions: list[exp.EQ] = []
|
|
469
619
|
for col, val in all_values.items():
|
|
470
620
|
if hasattr(val, "expression") and hasattr(val, "sql"):
|
|
471
|
-
|
|
472
|
-
expression = getattr(val, "expression", None)
|
|
473
|
-
if expression is not None and isinstance(expression, exp.Expression):
|
|
474
|
-
# Merge parameters from SQL object into builder
|
|
475
|
-
if hasattr(val, "parameters"):
|
|
476
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
477
|
-
for param_name, param_value in sql_parameters.items():
|
|
478
|
-
self.add_parameter(param_value, name=param_name)
|
|
479
|
-
value_expr = expression
|
|
480
|
-
else:
|
|
481
|
-
# If expression is None, fall back to parsing the raw SQL
|
|
482
|
-
sql_text = getattr(val, "sql", "")
|
|
483
|
-
# Merge parameters even when parsing raw SQL
|
|
484
|
-
if hasattr(val, "parameters"):
|
|
485
|
-
sql_parameters = getattr(val, "parameters", {})
|
|
486
|
-
for param_name, param_value in sql_parameters.items():
|
|
487
|
-
self.add_parameter(param_value, name=param_name)
|
|
488
|
-
# Check if sql_text is callable (like Expression.sql method)
|
|
489
|
-
if callable(sql_text):
|
|
490
|
-
sql_text = str(val)
|
|
491
|
-
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
621
|
+
value_expr = extract_sql_object_expression(val, builder=self)
|
|
492
622
|
elif isinstance(val, exp.Expression):
|
|
493
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)
|
|
494
626
|
else:
|
|
495
627
|
column_name = col if isinstance(col, str) else str(col)
|
|
496
628
|
if "." in column_name:
|
|
497
629
|
column_name = column_name.split(".")[-1]
|
|
498
630
|
param_name = self._generate_unique_parameter_name(column_name)
|
|
499
|
-
param_name = self.add_parameter(val, name=param_name)
|
|
631
|
+
_, param_name = self.add_parameter(val, name=param_name)
|
|
500
632
|
value_expr = exp.Placeholder(this=param_name)
|
|
501
633
|
|
|
502
634
|
update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
1
2
|
"""ORDER BY, LIMIT, OFFSET, and RETURNING clause mixins.
|
|
2
3
|
|
|
3
4
|
Provides mixins for query result ordering, limiting, and result
|
|
@@ -10,7 +11,7 @@ from mypy_extensions import trait
|
|
|
10
11
|
from sqlglot import exp
|
|
11
12
|
from typing_extensions import Self
|
|
12
13
|
|
|
13
|
-
from sqlspec.builder._parsing_utils import parse_order_expression
|
|
14
|
+
from sqlspec.builder._parsing_utils import extract_expression, parse_order_expression
|
|
14
15
|
from sqlspec.exceptions import SQLBuilderError
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
@@ -28,7 +29,6 @@ class OrderByClauseMixin:
|
|
|
28
29
|
|
|
29
30
|
__slots__ = ()
|
|
30
31
|
|
|
31
|
-
# Type annotation for PyRight - this will be provided by the base class
|
|
32
32
|
_expression: Optional[exp.Expression]
|
|
33
33
|
|
|
34
34
|
def order_by(self, *items: Union[str, exp.Ordered, "Column"], desc: bool = False) -> Self:
|
|
@@ -57,9 +57,7 @@ class OrderByClauseMixin:
|
|
|
57
57
|
order_item = order_item.desc()
|
|
58
58
|
else:
|
|
59
59
|
# Extract expression from Column objects or use as-is for sqlglot expressions
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
extracted_item = SQLFactory._extract_expression(item)
|
|
60
|
+
extracted_item = extract_expression(item)
|
|
63
61
|
order_item = extracted_item
|
|
64
62
|
if desc and not isinstance(item, exp.Ordered):
|
|
65
63
|
order_item = order_item.desc()
|
|
@@ -74,7 +72,6 @@ class LimitOffsetClauseMixin:
|
|
|
74
72
|
|
|
75
73
|
__slots__ = ()
|
|
76
74
|
|
|
77
|
-
# Type annotation for PyRight - this will be provided by the base class
|
|
78
75
|
_expression: Optional[exp.Expression]
|
|
79
76
|
|
|
80
77
|
def limit(self, value: int) -> Self:
|
|
@@ -121,7 +118,6 @@ class ReturningClauseMixin:
|
|
|
121
118
|
"""Mixin providing RETURNING clause."""
|
|
122
119
|
|
|
123
120
|
__slots__ = ()
|
|
124
|
-
# Type annotation for PyRight - this will be provided by the base class
|
|
125
121
|
_expression: Optional[exp.Expression]
|
|
126
122
|
|
|
127
123
|
def returning(self, *columns: Union[str, exp.Expression, "Column", "ExpressionWrapper", "Case"]) -> Self:
|
|
@@ -144,8 +140,6 @@ class ReturningClauseMixin:
|
|
|
144
140
|
msg = "RETURNING is only supported for INSERT, UPDATE, and DELETE statements."
|
|
145
141
|
raise SQLBuilderError(msg)
|
|
146
142
|
# Extract expressions from various wrapper types
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
returning_exprs = [SQLFactory._extract_expression(c) for c in columns]
|
|
143
|
+
returning_exprs = [extract_expression(c) for c in columns]
|
|
150
144
|
self._expression.set("returning", exp.Returning(expressions=returning_exprs))
|
|
151
145
|
return self
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
1
2
|
"""SELECT clause mixins.
|
|
2
3
|
|
|
3
4
|
Provides mixins for SELECT statement functionality including column selection,
|
|
@@ -10,7 +11,7 @@ from mypy_extensions import trait
|
|
|
10
11
|
from sqlglot import exp
|
|
11
12
|
from typing_extensions import Self
|
|
12
13
|
|
|
13
|
-
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
|
|
14
15
|
from sqlspec.exceptions import SQLBuilderError
|
|
15
16
|
from sqlspec.utils.type_guards import has_query_builder_parameters, is_expression
|
|
16
17
|
|
|
@@ -28,8 +29,8 @@ class SelectClauseMixin:
|
|
|
28
29
|
|
|
29
30
|
__slots__ = ()
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
def get_expression(self) -> Optional[exp.Expression]: ...
|
|
33
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
33
34
|
|
|
34
35
|
def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn", "SQL", "Case"]) -> Self:
|
|
35
36
|
"""Add columns to SELECT clause.
|
|
@@ -41,13 +42,17 @@ class SelectClauseMixin:
|
|
|
41
42
|
The current builder instance for method chaining.
|
|
42
43
|
"""
|
|
43
44
|
builder = cast("SQLBuilderProtocol", self)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
current_expr = self.get_expression()
|
|
46
|
+
if current_expr is None:
|
|
47
|
+
self.set_expression(exp.Select())
|
|
48
|
+
current_expr = self.get_expression()
|
|
49
|
+
|
|
50
|
+
if not isinstance(current_expr, exp.Select):
|
|
47
51
|
msg = "Cannot add select columns to a non-SELECT expression."
|
|
48
52
|
raise SQLBuilderError(msg)
|
|
49
53
|
for column in columns:
|
|
50
|
-
|
|
54
|
+
current_expr = current_expr.select(parse_column_expression(column, builder), copy=False)
|
|
55
|
+
self.set_expression(current_expr)
|
|
51
56
|
return cast("Self", builder)
|
|
52
57
|
|
|
53
58
|
def distinct(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn", "SQL"]) -> Self:
|
|
@@ -129,13 +134,13 @@ class SelectClauseMixin:
|
|
|
129
134
|
Returns:
|
|
130
135
|
The current builder instance for method chaining.
|
|
131
136
|
"""
|
|
132
|
-
|
|
137
|
+
current_expr = self.get_expression()
|
|
138
|
+
if current_expr is None or not isinstance(current_expr, exp.Select):
|
|
133
139
|
return self
|
|
134
140
|
|
|
135
141
|
for column in columns:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
142
|
+
current_expr = current_expr.group_by(exp.column(column) if isinstance(column, str) else column, copy=False)
|
|
143
|
+
self.set_expression(current_expr)
|
|
139
144
|
return self
|
|
140
145
|
|
|
141
146
|
def group_by_rollup(self, *columns: Union[str, exp.Expression]) -> Self:
|
|
@@ -480,9 +485,12 @@ class SelectClauseMixin:
|
|
|
480
485
|
Returns:
|
|
481
486
|
The current builder instance for method chaining.
|
|
482
487
|
"""
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
488
|
+
current_expr = self.get_expression()
|
|
489
|
+
if current_expr is None:
|
|
490
|
+
self.set_expression(exp.Select())
|
|
491
|
+
current_expr = self.get_expression()
|
|
492
|
+
|
|
493
|
+
if not isinstance(current_expr, exp.Select):
|
|
486
494
|
msg = "Cannot add window function to a non-SELECT expression."
|
|
487
495
|
raise SQLBuilderError(msg)
|
|
488
496
|
|
|
@@ -525,7 +533,8 @@ class SelectClauseMixin:
|
|
|
525
533
|
over_args["frame"] = frame_expr
|
|
526
534
|
|
|
527
535
|
window_expr = exp.Window(this=func_expr_parsed, **over_args)
|
|
528
|
-
|
|
536
|
+
current_expr = current_expr.select(exp.alias_(window_expr, alias) if alias else window_expr, copy=False)
|
|
537
|
+
self.set_expression(current_expr)
|
|
529
538
|
return self
|
|
530
539
|
|
|
531
540
|
def case_(self, alias: "Optional[str]" = None) -> "CaseBuilder":
|
|
@@ -855,12 +864,9 @@ class Case:
|
|
|
855
864
|
Returns:
|
|
856
865
|
Self for method chaining.
|
|
857
866
|
"""
|
|
858
|
-
from sqlspec._sql import SQLFactory
|
|
859
|
-
|
|
860
867
|
cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
|
|
861
|
-
val_expr =
|
|
868
|
+
val_expr = to_expression(value)
|
|
862
869
|
|
|
863
|
-
# SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
|
|
864
870
|
when_clause = exp.If(this=cond_expr, true=val_expr)
|
|
865
871
|
self._conditions.append(when_clause)
|
|
866
872
|
return self
|
|
@@ -874,9 +880,7 @@ class Case:
|
|
|
874
880
|
Returns:
|
|
875
881
|
Self for method chaining.
|
|
876
882
|
"""
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
self._default = SQLFactory._to_expression(value)
|
|
883
|
+
self._default = to_expression(value)
|
|
880
884
|
return self
|
|
881
885
|
|
|
882
886
|
def end(self) -> Self:
|
|
@@ -906,3 +910,21 @@ class Case:
|
|
|
906
910
|
"""
|
|
907
911
|
case_expr = exp.Case(ifs=self._conditions, default=self._default)
|
|
908
912
|
return cast("exp.Alias", exp.alias_(case_expr, alias))
|
|
913
|
+
|
|
914
|
+
@property
|
|
915
|
+
def conditions(self) -> "list[exp.If]":
|
|
916
|
+
"""Get CASE conditions (public API).
|
|
917
|
+
|
|
918
|
+
Returns:
|
|
919
|
+
List of If expressions representing WHEN clauses
|
|
920
|
+
"""
|
|
921
|
+
return self._conditions
|
|
922
|
+
|
|
923
|
+
@property
|
|
924
|
+
def default(self) -> Optional[exp.Expression]:
|
|
925
|
+
"""Get CASE default value (public API).
|
|
926
|
+
|
|
927
|
+
Returns:
|
|
928
|
+
Default expression for the ELSE clause, or None
|
|
929
|
+
"""
|
|
930
|
+
return self._default
|