sqlspec 0.13.1__py3-none-any.whl → 0.16.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/__init__.py +71 -8
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +1 -3
- sqlspec/_serialization.py +1 -2
- sqlspec/_sql.py +930 -136
- sqlspec/_typing.py +278 -142
- sqlspec/adapters/adbc/__init__.py +4 -3
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +116 -285
- sqlspec/adapters/adbc/driver.py +462 -340
- sqlspec/adapters/aiosqlite/__init__.py +18 -3
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +202 -150
- sqlspec/adapters/aiosqlite/driver.py +226 -247
- sqlspec/adapters/asyncmy/__init__.py +18 -3
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +80 -199
- sqlspec/adapters/asyncmy/driver.py +257 -215
- sqlspec/adapters/asyncpg/__init__.py +19 -4
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +81 -214
- sqlspec/adapters/asyncpg/driver.py +284 -359
- sqlspec/adapters/bigquery/__init__.py +17 -3
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +191 -299
- sqlspec/adapters/bigquery/driver.py +474 -634
- sqlspec/adapters/duckdb/__init__.py +14 -3
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +414 -397
- sqlspec/adapters/duckdb/driver.py +342 -393
- sqlspec/adapters/oracledb/__init__.py +19 -5
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +123 -458
- sqlspec/adapters/oracledb/driver.py +505 -531
- sqlspec/adapters/psqlpy/__init__.py +13 -3
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +93 -307
- sqlspec/adapters/psqlpy/driver.py +504 -213
- sqlspec/adapters/psycopg/__init__.py +19 -5
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +143 -472
- sqlspec/adapters/psycopg/driver.py +704 -825
- sqlspec/adapters/sqlite/__init__.py +14 -3
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +208 -142
- sqlspec/adapters/sqlite/driver.py +263 -278
- sqlspec/base.py +105 -9
- sqlspec/{statement/builder → builder}/__init__.py +12 -14
- sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
- sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
- sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
- sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
- sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
- sqlspec/builder/_insert.py +421 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
- sqlspec/builder/_select.py +170 -0
- sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
- sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +604 -0
- sqlspec/builder/mixins/_update_operations.py +202 -0
- sqlspec/builder/mixins/_where_clause.py +644 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +183 -138
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.py +677 -0
- sqlspec/{statement → core}/splitter.py +321 -191
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +7 -10
- sqlspec/driver/_async.py +422 -163
- sqlspec/driver/_common.py +545 -287
- sqlspec/driver/_sync.py +426 -160
- sqlspec/driver/mixins/__init__.py +2 -13
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +65 -14
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/__init__.py +2 -1
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +21 -16
- sqlspec/extensions/litestar/providers.py +17 -52
- sqlspec/loader.py +423 -104
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +51 -186
- sqlspec/storage/__init__.py +1 -1
- sqlspec/storage/backends/base.py +37 -40
- sqlspec/storage/backends/fsspec.py +136 -112
- sqlspec/storage/backends/obstore.py +138 -160
- sqlspec/storage/capabilities.py +5 -4
- sqlspec/storage/registry.py +57 -106
- sqlspec/typing.py +136 -115
- sqlspec/utils/__init__.py +2 -2
- sqlspec/utils/correlation.py +0 -3
- sqlspec/utils/deprecation.py +6 -6
- sqlspec/utils/fixtures.py +6 -6
- sqlspec/utils/logging.py +0 -2
- sqlspec/utils/module_loader.py +7 -12
- sqlspec/utils/singleton.py +0 -1
- sqlspec/utils/sync_tools.py +17 -38
- sqlspec/utils/text.py +12 -51
- sqlspec/utils/type_guards.py +482 -235
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
- sqlspec-0.16.2.dist-info/RECORD +134 -0
- sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
- sqlspec/driver/connection.py +0 -207
- sqlspec/driver/mixins/_csv_writer.py +0 -91
- sqlspec/driver/mixins/_pipeline.py +0 -512
- sqlspec/driver/mixins/_result_utils.py +0 -140
- sqlspec/driver/mixins/_storage.py +0 -926
- sqlspec/driver/mixins/_type_coercion.py +0 -130
- sqlspec/driver/parameters.py +0 -138
- sqlspec/service/__init__.py +0 -4
- sqlspec/service/_util.py +0 -147
- sqlspec/service/base.py +0 -1131
- sqlspec/service/pagination.py +0 -26
- sqlspec/statement/__init__.py +0 -21
- sqlspec/statement/builder/insert.py +0 -288
- sqlspec/statement/builder/merge.py +0 -95
- sqlspec/statement/builder/mixins/__init__.py +0 -65
- sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
- sqlspec/statement/builder/mixins/_case_builder.py +0 -91
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
- sqlspec/statement/builder/mixins/_from.py +0 -63
- sqlspec/statement/builder/mixins/_group_by.py +0 -118
- sqlspec/statement/builder/mixins/_having.py +0 -35
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
- sqlspec/statement/builder/mixins/_insert_into.py +0 -36
- sqlspec/statement/builder/mixins/_insert_values.py +0 -67
- sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
- sqlspec/statement/builder/mixins/_order_by.py +0 -46
- sqlspec/statement/builder/mixins/_pivot.py +0 -79
- sqlspec/statement/builder/mixins/_returning.py +0 -37
- sqlspec/statement/builder/mixins/_select_columns.py +0 -61
- sqlspec/statement/builder/mixins/_set_ops.py +0 -122
- sqlspec/statement/builder/mixins/_unpivot.py +0 -77
- sqlspec/statement/builder/mixins/_update_from.py +0 -55
- sqlspec/statement/builder/mixins/_update_set.py +0 -94
- sqlspec/statement/builder/mixins/_update_table.py +0 -29
- sqlspec/statement/builder/mixins/_where.py +0 -401
- sqlspec/statement/builder/mixins/_window_functions.py +0 -86
- sqlspec/statement/builder/select.py +0 -221
- sqlspec/statement/filters.py +0 -596
- sqlspec/statement/parameter_manager.py +0 -220
- sqlspec/statement/parameters.py +0 -867
- sqlspec/statement/pipelines/__init__.py +0 -210
- sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
- sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
- sqlspec/statement/pipelines/context.py +0 -115
- sqlspec/statement/pipelines/transformers/__init__.py +0 -7
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
- sqlspec/statement/pipelines/validators/__init__.py +0 -23
- sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
- sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
- sqlspec/statement/pipelines/validators/_performance.py +0 -718
- sqlspec/statement/pipelines/validators/_security.py +0 -967
- sqlspec/statement/result.py +0 -435
- sqlspec/statement/sql.py +0 -1704
- sqlspec/statement/sql_compiler.py +0 -140
- sqlspec/utils/cached_property.py +0 -25
- sqlspec-0.13.1.dist-info/RECORD +0 -150
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
"""Merge operation mixins for SQL builders."""
|
|
2
|
+
|
|
1
3
|
from typing import Any, Optional, Union
|
|
2
4
|
|
|
5
|
+
from mypy_extensions import trait
|
|
3
6
|
from sqlglot import exp
|
|
4
7
|
from typing_extensions import Self
|
|
5
8
|
|
|
@@ -16,10 +19,12 @@ __all__ = (
|
|
|
16
19
|
)
|
|
17
20
|
|
|
18
21
|
|
|
22
|
+
@trait
|
|
19
23
|
class MergeIntoClauseMixin:
|
|
20
24
|
"""Mixin providing INTO clause for MERGE builders."""
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
__slots__ = ()
|
|
27
|
+
_expression: Optional[exp.Expression]
|
|
23
28
|
|
|
24
29
|
def into(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
25
30
|
"""Set the target table for the MERGE operation (INTO clause).
|
|
@@ -33,17 +38,24 @@ class MergeIntoClauseMixin:
|
|
|
33
38
|
The current builder instance for method chaining.
|
|
34
39
|
"""
|
|
35
40
|
if self._expression is None:
|
|
36
|
-
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
37
|
-
if not isinstance(self._expression, exp.Merge):
|
|
38
|
-
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
41
|
+
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
42
|
+
if not isinstance(self._expression, exp.Merge):
|
|
43
|
+
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
39
44
|
self._expression.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
|
|
40
45
|
return self
|
|
41
46
|
|
|
42
47
|
|
|
48
|
+
@trait
|
|
43
49
|
class MergeUsingClauseMixin:
|
|
44
50
|
"""Mixin providing USING clause for MERGE builders."""
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
__slots__ = ()
|
|
53
|
+
_expression: Optional[exp.Expression]
|
|
54
|
+
|
|
55
|
+
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
56
|
+
"""Add parameter - provided by QueryBuilder."""
|
|
57
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
58
|
+
raise NotImplementedError(msg)
|
|
47
59
|
|
|
48
60
|
def using(self, source: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
|
|
49
61
|
"""Set the source data for the MERGE operation (USING clause).
|
|
@@ -68,11 +80,10 @@ class MergeUsingClauseMixin:
|
|
|
68
80
|
if isinstance(source, str):
|
|
69
81
|
source_expr = exp.to_table(source, alias=alias)
|
|
70
82
|
elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
83
|
+
subquery_builder_parameters = source.parameters
|
|
84
|
+
if subquery_builder_parameters:
|
|
85
|
+
for p_name, p_value in subquery_builder_parameters.items():
|
|
86
|
+
self.add_parameter(p_value, name=p_name)
|
|
76
87
|
|
|
77
88
|
subquery_exp = exp.paren(getattr(source, "_expression", exp.select()))
|
|
78
89
|
source_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
@@ -88,10 +99,12 @@ class MergeUsingClauseMixin:
|
|
|
88
99
|
return self
|
|
89
100
|
|
|
90
101
|
|
|
102
|
+
@trait
|
|
91
103
|
class MergeOnClauseMixin:
|
|
92
104
|
"""Mixin providing ON clause for MERGE builders."""
|
|
93
105
|
|
|
94
|
-
|
|
106
|
+
__slots__ = ()
|
|
107
|
+
_expression: Optional[exp.Expression]
|
|
95
108
|
|
|
96
109
|
def on(self, condition: Union[str, exp.Expression]) -> Self:
|
|
97
110
|
"""Set the join condition for the MERGE operation (ON clause).
|
|
@@ -130,10 +143,22 @@ class MergeOnClauseMixin:
|
|
|
130
143
|
return self
|
|
131
144
|
|
|
132
145
|
|
|
146
|
+
@trait
|
|
133
147
|
class MergeMatchedClauseMixin:
|
|
134
148
|
"""Mixin providing WHEN MATCHED THEN ... clauses for MERGE builders."""
|
|
135
149
|
|
|
136
|
-
|
|
150
|
+
__slots__ = ()
|
|
151
|
+
_expression: Optional[exp.Expression]
|
|
152
|
+
|
|
153
|
+
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
154
|
+
"""Add parameter - provided by QueryBuilder."""
|
|
155
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
156
|
+
raise NotImplementedError(msg)
|
|
157
|
+
|
|
158
|
+
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
159
|
+
"""Generate unique parameter name - provided by QueryBuilder."""
|
|
160
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
161
|
+
raise NotImplementedError(msg)
|
|
137
162
|
|
|
138
163
|
def _add_when_clause(self, when_clause: exp.When) -> None:
|
|
139
164
|
"""Helper to add a WHEN clause to the MERGE statement.
|
|
@@ -142,9 +167,9 @@ class MergeMatchedClauseMixin:
|
|
|
142
167
|
when_clause: The WHEN clause to add.
|
|
143
168
|
"""
|
|
144
169
|
if self._expression is None:
|
|
145
|
-
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
170
|
+
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # type: ignore[misc]
|
|
146
171
|
if not isinstance(self._expression, exp.Merge):
|
|
147
|
-
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
172
|
+
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # type: ignore[misc]
|
|
148
173
|
|
|
149
174
|
whens = self._expression.args.get("whens")
|
|
150
175
|
if not whens:
|
|
@@ -154,14 +179,23 @@ class MergeMatchedClauseMixin:
|
|
|
154
179
|
whens.append("expressions", when_clause)
|
|
155
180
|
|
|
156
181
|
def when_matched_then_update(
|
|
157
|
-
self,
|
|
182
|
+
self,
|
|
183
|
+
set_values: Optional[dict[str, Any]] = None,
|
|
184
|
+
condition: Optional[Union[str, exp.Expression]] = None,
|
|
185
|
+
**kwargs: Any,
|
|
158
186
|
) -> Self:
|
|
159
187
|
"""Define the UPDATE action for matched rows.
|
|
160
188
|
|
|
189
|
+
Supports:
|
|
190
|
+
- when_matched_then_update({"column": value})
|
|
191
|
+
- when_matched_then_update(column=value, other_column=other_value)
|
|
192
|
+
- when_matched_then_update({"column": value}, other_column=other_value)
|
|
193
|
+
|
|
161
194
|
Args:
|
|
162
195
|
set_values: A dictionary of column names and their new values to set.
|
|
163
196
|
The values will be parameterized.
|
|
164
197
|
condition: An optional additional condition for this specific action.
|
|
198
|
+
**kwargs: Column-value pairs to update on match.
|
|
165
199
|
|
|
166
200
|
Raises:
|
|
167
201
|
SQLBuilderError: If the condition type is unsupported.
|
|
@@ -169,10 +203,48 @@ class MergeMatchedClauseMixin:
|
|
|
169
203
|
Returns:
|
|
170
204
|
The current builder instance for method chaining.
|
|
171
205
|
"""
|
|
206
|
+
# Combine set_values dict and kwargs
|
|
207
|
+
all_values = dict(set_values or {}, **kwargs)
|
|
208
|
+
|
|
209
|
+
if not all_values:
|
|
210
|
+
msg = "No update values provided. Use set_values dict or kwargs."
|
|
211
|
+
raise SQLBuilderError(msg)
|
|
212
|
+
|
|
172
213
|
update_expressions: list[exp.EQ] = []
|
|
173
|
-
for col, val in
|
|
174
|
-
|
|
175
|
-
|
|
214
|
+
for col, val in all_values.items():
|
|
215
|
+
if hasattr(val, "expression") and hasattr(val, "sql"):
|
|
216
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
217
|
+
expression = getattr(val, "expression", None)
|
|
218
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
219
|
+
# Merge parameters from SQL object into builder
|
|
220
|
+
if hasattr(val, "parameters"):
|
|
221
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
222
|
+
for param_name, param_value in sql_parameters.items():
|
|
223
|
+
self.add_parameter(param_value, name=param_name)
|
|
224
|
+
value_expr = expression
|
|
225
|
+
else:
|
|
226
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
227
|
+
sql_text = getattr(val, "sql", "")
|
|
228
|
+
# Merge parameters even when parsing raw SQL
|
|
229
|
+
if hasattr(val, "parameters"):
|
|
230
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
231
|
+
for param_name, param_value in sql_parameters.items():
|
|
232
|
+
self.add_parameter(param_value, name=param_name)
|
|
233
|
+
# Check if sql_text is callable (like Expression.sql method)
|
|
234
|
+
if callable(sql_text):
|
|
235
|
+
sql_text = str(val)
|
|
236
|
+
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
237
|
+
elif isinstance(val, exp.Expression):
|
|
238
|
+
value_expr = val
|
|
239
|
+
else:
|
|
240
|
+
column_name = col if isinstance(col, str) else str(col)
|
|
241
|
+
if "." in column_name:
|
|
242
|
+
column_name = column_name.split(".")[-1]
|
|
243
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
244
|
+
param_name = self.add_parameter(val, name=param_name)[1]
|
|
245
|
+
value_expr = exp.Placeholder(this=param_name)
|
|
246
|
+
|
|
247
|
+
update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
176
248
|
|
|
177
249
|
when_args: dict[str, Any] = {"matched": True, "then": exp.Update(expressions=update_expressions)}
|
|
178
250
|
|
|
@@ -233,10 +305,28 @@ class MergeMatchedClauseMixin:
|
|
|
233
305
|
return self
|
|
234
306
|
|
|
235
307
|
|
|
308
|
+
@trait
|
|
236
309
|
class MergeNotMatchedClauseMixin:
|
|
237
310
|
"""Mixin providing WHEN NOT MATCHED THEN ... clauses for MERGE builders."""
|
|
238
311
|
|
|
239
|
-
|
|
312
|
+
__slots__ = ()
|
|
313
|
+
|
|
314
|
+
_expression: Optional[exp.Expression]
|
|
315
|
+
|
|
316
|
+
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
317
|
+
"""Add parameter - provided by QueryBuilder."""
|
|
318
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
319
|
+
raise NotImplementedError(msg)
|
|
320
|
+
|
|
321
|
+
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
322
|
+
"""Generate unique parameter name - provided by QueryBuilder."""
|
|
323
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
324
|
+
raise NotImplementedError(msg)
|
|
325
|
+
|
|
326
|
+
def _add_when_clause(self, when_clause: exp.When) -> None:
|
|
327
|
+
"""Helper to add a WHEN clause to the MERGE statement - provided by QueryBuilder."""
|
|
328
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
329
|
+
raise NotImplementedError(msg)
|
|
240
330
|
|
|
241
331
|
def when_not_matched_then_insert(
|
|
242
332
|
self,
|
|
@@ -269,8 +359,12 @@ class MergeNotMatchedClauseMixin:
|
|
|
269
359
|
raise SQLBuilderError(msg)
|
|
270
360
|
|
|
271
361
|
parameterized_values: list[exp.Expression] = []
|
|
272
|
-
for val in values:
|
|
273
|
-
|
|
362
|
+
for i, val in enumerate(values):
|
|
363
|
+
column_name = columns[i] if isinstance(columns[i], str) else str(columns[i])
|
|
364
|
+
if "." in column_name:
|
|
365
|
+
column_name = column_name.split(".")[-1]
|
|
366
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
367
|
+
param_name = self.add_parameter(val, name=param_name)[1]
|
|
274
368
|
parameterized_values.append(exp.var(param_name))
|
|
275
369
|
|
|
276
370
|
insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
|
|
@@ -279,7 +373,6 @@ class MergeNotMatchedClauseMixin:
|
|
|
279
373
|
msg = "Specifying columns without values for INSERT action is complex and not fully supported yet. Consider providing full expressions."
|
|
280
374
|
raise SQLBuilderError(msg)
|
|
281
375
|
elif not columns and not values:
|
|
282
|
-
# INSERT DEFAULT VALUES case
|
|
283
376
|
pass
|
|
284
377
|
else:
|
|
285
378
|
msg = "Cannot specify values without columns for INSERT action."
|
|
@@ -308,25 +401,52 @@ class MergeNotMatchedClauseMixin:
|
|
|
308
401
|
when_args["this"] = condition_expr
|
|
309
402
|
|
|
310
403
|
when_clause = exp.When(**when_args)
|
|
311
|
-
self._add_when_clause(when_clause)
|
|
404
|
+
self._add_when_clause(when_clause)
|
|
312
405
|
return self
|
|
313
406
|
|
|
314
407
|
|
|
408
|
+
@trait
|
|
315
409
|
class MergeNotMatchedBySourceClauseMixin:
|
|
316
410
|
"""Mixin providing WHEN NOT MATCHED BY SOURCE THEN ... clauses for MERGE builders."""
|
|
317
411
|
|
|
318
|
-
|
|
412
|
+
__slots__ = ()
|
|
413
|
+
|
|
414
|
+
_expression: Optional[exp.Expression]
|
|
415
|
+
|
|
416
|
+
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
417
|
+
"""Add parameter - provided by QueryBuilder."""
|
|
418
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
419
|
+
raise NotImplementedError(msg)
|
|
420
|
+
|
|
421
|
+
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
422
|
+
"""Generate unique parameter name - provided by QueryBuilder."""
|
|
423
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
424
|
+
raise NotImplementedError(msg)
|
|
425
|
+
|
|
426
|
+
def _add_when_clause(self, when_clause: exp.When) -> None:
|
|
427
|
+
"""Helper to add a WHEN clause to the MERGE statement - provided by QueryBuilder."""
|
|
428
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
429
|
+
raise NotImplementedError(msg)
|
|
319
430
|
|
|
320
431
|
def when_not_matched_by_source_then_update(
|
|
321
|
-
self,
|
|
432
|
+
self,
|
|
433
|
+
set_values: Optional[dict[str, Any]] = None,
|
|
434
|
+
condition: Optional[Union[str, exp.Expression]] = None,
|
|
435
|
+
**kwargs: Any,
|
|
322
436
|
) -> Self:
|
|
323
437
|
"""Define the UPDATE action for rows not matched by source.
|
|
324
438
|
|
|
325
439
|
This is useful for handling rows that exist in the target but not in the source.
|
|
326
440
|
|
|
441
|
+
Supports:
|
|
442
|
+
- when_not_matched_by_source_then_update({"column": value})
|
|
443
|
+
- when_not_matched_by_source_then_update(column=value, other_column=other_value)
|
|
444
|
+
- when_not_matched_by_source_then_update({"column": value}, other_column=other_value)
|
|
445
|
+
|
|
327
446
|
Args:
|
|
328
447
|
set_values: A dictionary of column names and their new values to set.
|
|
329
448
|
condition: An optional additional condition for this specific action.
|
|
449
|
+
**kwargs: Column-value pairs to update when not matched by source.
|
|
330
450
|
|
|
331
451
|
Raises:
|
|
332
452
|
SQLBuilderError: If the condition type is unsupported.
|
|
@@ -334,10 +454,48 @@ class MergeNotMatchedBySourceClauseMixin:
|
|
|
334
454
|
Returns:
|
|
335
455
|
The current builder instance for method chaining.
|
|
336
456
|
"""
|
|
457
|
+
# Combine set_values dict and kwargs
|
|
458
|
+
all_values = dict(set_values or {}, **kwargs)
|
|
459
|
+
|
|
460
|
+
if not all_values:
|
|
461
|
+
msg = "No update values provided. Use set_values dict or kwargs."
|
|
462
|
+
raise SQLBuilderError(msg)
|
|
463
|
+
|
|
337
464
|
update_expressions: list[exp.EQ] = []
|
|
338
|
-
for col, val in
|
|
339
|
-
|
|
340
|
-
|
|
465
|
+
for col, val in all_values.items():
|
|
466
|
+
if hasattr(val, "expression") and hasattr(val, "sql"):
|
|
467
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
468
|
+
expression = getattr(val, "expression", None)
|
|
469
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
470
|
+
# Merge parameters from SQL object into builder
|
|
471
|
+
if hasattr(val, "parameters"):
|
|
472
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
473
|
+
for param_name, param_value in sql_parameters.items():
|
|
474
|
+
self.add_parameter(param_value, name=param_name)
|
|
475
|
+
value_expr = expression
|
|
476
|
+
else:
|
|
477
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
478
|
+
sql_text = getattr(val, "sql", "")
|
|
479
|
+
# Merge parameters even when parsing raw SQL
|
|
480
|
+
if hasattr(val, "parameters"):
|
|
481
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
482
|
+
for param_name, param_value in sql_parameters.items():
|
|
483
|
+
self.add_parameter(param_value, name=param_name)
|
|
484
|
+
# Check if sql_text is callable (like Expression.sql method)
|
|
485
|
+
if callable(sql_text):
|
|
486
|
+
sql_text = str(val)
|
|
487
|
+
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
488
|
+
elif isinstance(val, exp.Expression):
|
|
489
|
+
value_expr = val
|
|
490
|
+
else:
|
|
491
|
+
column_name = col if isinstance(col, str) else str(col)
|
|
492
|
+
if "." in column_name:
|
|
493
|
+
column_name = column_name.split(".")[-1]
|
|
494
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
495
|
+
param_name = self.add_parameter(val, name=param_name)[1]
|
|
496
|
+
value_expr = exp.Placeholder(this=param_name)
|
|
497
|
+
|
|
498
|
+
update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
341
499
|
|
|
342
500
|
when_args: dict[str, Any] = {
|
|
343
501
|
"matched": False,
|
|
@@ -363,7 +521,7 @@ class MergeNotMatchedBySourceClauseMixin:
|
|
|
363
521
|
when_args["this"] = condition_expr
|
|
364
522
|
|
|
365
523
|
when_clause = exp.When(**when_args)
|
|
366
|
-
self._add_when_clause(when_clause)
|
|
524
|
+
self._add_when_clause(when_clause)
|
|
367
525
|
return self
|
|
368
526
|
|
|
369
527
|
def when_not_matched_by_source_then_delete(self, condition: Optional[Union[str, exp.Expression]] = None) -> Self:
|
|
@@ -400,5 +558,5 @@ class MergeNotMatchedBySourceClauseMixin:
|
|
|
400
558
|
when_args["this"] = condition_expr
|
|
401
559
|
|
|
402
560
|
when_clause = exp.When(**when_args)
|
|
403
|
-
self._add_when_clause(when_clause)
|
|
561
|
+
self._add_when_clause(when_clause)
|
|
404
562
|
return self
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Order, Limit, Offset and Returning operations mixins for SQL builders."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Optional, Union, cast
|
|
4
|
+
|
|
5
|
+
from mypy_extensions import trait
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from sqlspec.builder._parsing_utils import parse_order_expression
|
|
10
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
14
|
+
|
|
15
|
+
__all__ = ("LimitOffsetClauseMixin", "OrderByClauseMixin", "ReturningClauseMixin")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@trait
|
|
19
|
+
class OrderByClauseMixin:
|
|
20
|
+
"""Mixin providing ORDER BY clause."""
|
|
21
|
+
|
|
22
|
+
__slots__ = ()
|
|
23
|
+
|
|
24
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
25
|
+
_expression: Optional[exp.Expression]
|
|
26
|
+
|
|
27
|
+
def order_by(self, *items: Union[str, exp.Ordered], desc: bool = False) -> Self:
|
|
28
|
+
"""Add ORDER BY clause.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
*items: Columns to order by. Can be strings (column names) or sqlglot.exp.Ordered instances for specific directions (e.g., exp.column("name").desc()).
|
|
32
|
+
desc: Whether to order in descending order (applies to all items if they are strings).
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
SQLBuilderError: If the current expression is not a SELECT statement or if the item type is unsupported.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
The current builder instance for method chaining.
|
|
39
|
+
"""
|
|
40
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
41
|
+
if not isinstance(builder._expression, exp.Select):
|
|
42
|
+
msg = "ORDER BY is only supported for SELECT statements."
|
|
43
|
+
raise SQLBuilderError(msg)
|
|
44
|
+
|
|
45
|
+
current_expr = builder._expression
|
|
46
|
+
for item in items:
|
|
47
|
+
if isinstance(item, str):
|
|
48
|
+
order_item = parse_order_expression(item)
|
|
49
|
+
if desc:
|
|
50
|
+
order_item = order_item.desc()
|
|
51
|
+
else:
|
|
52
|
+
order_item = item
|
|
53
|
+
current_expr = current_expr.order_by(order_item, copy=False)
|
|
54
|
+
builder._expression = current_expr
|
|
55
|
+
return cast("Self", builder)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@trait
|
|
59
|
+
class LimitOffsetClauseMixin:
|
|
60
|
+
"""Mixin providing LIMIT and OFFSET clauses."""
|
|
61
|
+
|
|
62
|
+
__slots__ = ()
|
|
63
|
+
|
|
64
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
65
|
+
_expression: Optional[exp.Expression]
|
|
66
|
+
|
|
67
|
+
def limit(self, value: int) -> Self:
|
|
68
|
+
"""Add LIMIT clause.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
value: The maximum number of rows to return.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The current builder instance for method chaining.
|
|
78
|
+
"""
|
|
79
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
80
|
+
if not isinstance(builder._expression, exp.Select):
|
|
81
|
+
msg = "LIMIT is only supported for SELECT statements."
|
|
82
|
+
raise SQLBuilderError(msg)
|
|
83
|
+
builder._expression = builder._expression.limit(exp.convert(value), copy=False)
|
|
84
|
+
return cast("Self", builder)
|
|
85
|
+
|
|
86
|
+
def offset(self, value: int) -> Self:
|
|
87
|
+
"""Add OFFSET clause.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
value: The number of rows to skip before starting to return rows.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The current builder instance for method chaining.
|
|
97
|
+
"""
|
|
98
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
99
|
+
if not isinstance(builder._expression, exp.Select):
|
|
100
|
+
msg = "OFFSET is only supported for SELECT statements."
|
|
101
|
+
raise SQLBuilderError(msg)
|
|
102
|
+
builder._expression = builder._expression.offset(exp.convert(value), copy=False)
|
|
103
|
+
return cast("Self", builder)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@trait
|
|
107
|
+
class ReturningClauseMixin:
|
|
108
|
+
"""Mixin providing RETURNING clause."""
|
|
109
|
+
|
|
110
|
+
__slots__ = ()
|
|
111
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
112
|
+
_expression: Optional[exp.Expression]
|
|
113
|
+
|
|
114
|
+
def returning(self, *columns: Union[str, exp.Expression]) -> Self:
|
|
115
|
+
"""Add RETURNING clause to the statement.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
*columns: Columns to return. Can be strings or sqlglot expressions.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
SQLBuilderError: If the current expression is not INSERT, UPDATE, or DELETE.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The current builder instance for method chaining.
|
|
125
|
+
"""
|
|
126
|
+
if self._expression is None:
|
|
127
|
+
msg = "Cannot add RETURNING: expression is not initialized."
|
|
128
|
+
raise SQLBuilderError(msg)
|
|
129
|
+
valid_types = (exp.Insert, exp.Update, exp.Delete)
|
|
130
|
+
if not isinstance(self._expression, valid_types):
|
|
131
|
+
msg = "RETURNING is only supported for INSERT, UPDATE, and DELETE statements."
|
|
132
|
+
raise SQLBuilderError(msg)
|
|
133
|
+
returning_exprs = [exp.column(c) if isinstance(c, str) else c for c in columns]
|
|
134
|
+
self._expression.set("returning", exp.Returning(expressions=returning_exprs))
|
|
135
|
+
return self
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Pivot and Unpivot operations mixins for SQL builders."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Optional, Union, cast
|
|
4
|
+
|
|
5
|
+
from mypy_extensions import trait
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from sqlglot.dialects.dialect import DialectType
|
|
10
|
+
|
|
11
|
+
from sqlspec.builder._select import Select
|
|
12
|
+
|
|
13
|
+
__all__ = ("PivotClauseMixin", "UnpivotClauseMixin")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@trait
|
|
17
|
+
class PivotClauseMixin:
|
|
18
|
+
"""Mixin class to add PIVOT functionality to a Select."""
|
|
19
|
+
|
|
20
|
+
__slots__ = ()
|
|
21
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
22
|
+
_expression: Optional[exp.Expression]
|
|
23
|
+
|
|
24
|
+
dialect: "DialectType" = None
|
|
25
|
+
|
|
26
|
+
def pivot(
|
|
27
|
+
self: "PivotClauseMixin",
|
|
28
|
+
aggregate_function: Union[str, exp.Expression],
|
|
29
|
+
aggregate_column: Union[str, exp.Expression],
|
|
30
|
+
pivot_column: Union[str, exp.Expression],
|
|
31
|
+
pivot_values: list[Union[str, int, float, exp.Expression]],
|
|
32
|
+
alias: Optional[str] = None,
|
|
33
|
+
) -> "Select":
|
|
34
|
+
"""Adds a PIVOT clause to the SELECT statement.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
`query.pivot(aggregate_function="SUM", aggregate_column="Sales", pivot_column="Quarter", pivot_values=["Q1", "Q2", "Q3", "Q4"], alias="PivotTable")`
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
aggregate_function: The aggregate function to use (e.g., "SUM", "AVG").
|
|
41
|
+
aggregate_column: The column to be aggregated.
|
|
42
|
+
pivot_column: The column whose unique values will become new column headers.
|
|
43
|
+
pivot_values: A list of specific values from the pivot_column to be turned into columns.
|
|
44
|
+
alias: Optional alias for the pivoted table/subquery.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The SelectBuilder instance for chaining.
|
|
48
|
+
"""
|
|
49
|
+
current_expr = self._expression
|
|
50
|
+
if not isinstance(current_expr, exp.Select):
|
|
51
|
+
msg = "Pivot can only be applied to a Select expression managed by SelectBuilder."
|
|
52
|
+
raise TypeError(msg)
|
|
53
|
+
|
|
54
|
+
agg_func_name = aggregate_function if isinstance(aggregate_function, str) else aggregate_function.name
|
|
55
|
+
agg_col_expr = exp.column(aggregate_column) if isinstance(aggregate_column, str) else aggregate_column
|
|
56
|
+
pivot_col_expr = exp.column(pivot_column) if isinstance(pivot_column, str) else pivot_column
|
|
57
|
+
|
|
58
|
+
pivot_agg_expr = exp.func(agg_func_name, agg_col_expr)
|
|
59
|
+
|
|
60
|
+
pivot_value_exprs: list[exp.Expression] = []
|
|
61
|
+
for val in pivot_values:
|
|
62
|
+
if isinstance(val, exp.Expression):
|
|
63
|
+
pivot_value_exprs.append(val)
|
|
64
|
+
elif isinstance(val, (str, int, float)):
|
|
65
|
+
pivot_value_exprs.append(exp.convert(val))
|
|
66
|
+
else:
|
|
67
|
+
pivot_value_exprs.append(exp.convert(str(val)))
|
|
68
|
+
|
|
69
|
+
in_expr = exp.In(this=pivot_col_expr, expressions=pivot_value_exprs)
|
|
70
|
+
|
|
71
|
+
pivot_node = exp.Pivot(expressions=[pivot_agg_expr], fields=[in_expr], unpivot=False)
|
|
72
|
+
|
|
73
|
+
if alias:
|
|
74
|
+
pivot_node.set("alias", exp.TableAlias(this=exp.to_identifier(alias)))
|
|
75
|
+
|
|
76
|
+
from_clause = current_expr.args.get("from")
|
|
77
|
+
if from_clause and isinstance(from_clause, exp.From):
|
|
78
|
+
table = from_clause.this
|
|
79
|
+
if isinstance(table, exp.Table):
|
|
80
|
+
existing_pivots = table.args.get("pivots", [])
|
|
81
|
+
existing_pivots.append(pivot_node)
|
|
82
|
+
table.set("pivots", existing_pivots)
|
|
83
|
+
|
|
84
|
+
return cast("Select", self)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@trait
|
|
88
|
+
class UnpivotClauseMixin:
|
|
89
|
+
"""Mixin class to add UNPIVOT functionality to a Select."""
|
|
90
|
+
|
|
91
|
+
__slots__ = ()
|
|
92
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
93
|
+
_expression: Optional[exp.Expression]
|
|
94
|
+
|
|
95
|
+
dialect: "DialectType" = None
|
|
96
|
+
|
|
97
|
+
def unpivot(
|
|
98
|
+
self: "UnpivotClauseMixin",
|
|
99
|
+
value_column_name: str,
|
|
100
|
+
name_column_name: str,
|
|
101
|
+
columns_to_unpivot: list[Union[str, exp.Expression]],
|
|
102
|
+
alias: Optional[str] = None,
|
|
103
|
+
) -> "Select":
|
|
104
|
+
"""Adds an UNPIVOT clause to the SELECT statement.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
`query.unpivot(value_column_name="Sales", name_column_name="Quarter", columns_to_unpivot=["Q1Sales", "Q2Sales"], alias="UnpivotTable")`
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
value_column_name: The name for the new column that will hold the values from the unpivoted columns.
|
|
111
|
+
name_column_name: The name for the new column that will hold the names of the original unpivoted columns.
|
|
112
|
+
columns_to_unpivot: A list of columns to be unpivoted into rows.
|
|
113
|
+
alias: Optional alias for the unpivoted table/subquery.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
TypeError: If the current expression is not a Select expression.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The Select instance for chaining.
|
|
120
|
+
"""
|
|
121
|
+
current_expr = self._expression
|
|
122
|
+
if not isinstance(current_expr, exp.Select):
|
|
123
|
+
msg = "Unpivot can only be applied to a Select expression managed by Select."
|
|
124
|
+
raise TypeError(msg)
|
|
125
|
+
|
|
126
|
+
value_col_ident = exp.to_identifier(value_column_name)
|
|
127
|
+
name_col_ident = exp.to_identifier(name_column_name)
|
|
128
|
+
|
|
129
|
+
unpivot_cols_exprs: list[exp.Expression] = []
|
|
130
|
+
for col_name_or_expr in columns_to_unpivot:
|
|
131
|
+
if isinstance(col_name_or_expr, exp.Expression):
|
|
132
|
+
unpivot_cols_exprs.append(col_name_or_expr)
|
|
133
|
+
elif isinstance(col_name_or_expr, str):
|
|
134
|
+
unpivot_cols_exprs.append(exp.column(col_name_or_expr))
|
|
135
|
+
else:
|
|
136
|
+
unpivot_cols_exprs.append(exp.column(str(col_name_or_expr)))
|
|
137
|
+
|
|
138
|
+
in_expr = exp.In(this=name_col_ident, expressions=unpivot_cols_exprs)
|
|
139
|
+
|
|
140
|
+
unpivot_node = exp.Pivot(expressions=[value_col_ident], fields=[in_expr], unpivot=True)
|
|
141
|
+
|
|
142
|
+
if alias:
|
|
143
|
+
unpivot_node.set("alias", exp.TableAlias(this=exp.to_identifier(alias)))
|
|
144
|
+
|
|
145
|
+
from_clause = current_expr.args.get("from")
|
|
146
|
+
if from_clause and isinstance(from_clause, exp.From):
|
|
147
|
+
table = from_clause.this
|
|
148
|
+
if isinstance(table, exp.Table):
|
|
149
|
+
existing_pivots = table.args.get("pivots", [])
|
|
150
|
+
existing_pivots.append(unpivot_node)
|
|
151
|
+
table.set("pivots", existing_pivots)
|
|
152
|
+
|
|
153
|
+
return cast("Select", self)
|