sqlspec 0.25.0__py3-none-any.whl → 0.27.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/__init__.py +7 -15
- sqlspec/_serialization.py +256 -24
- sqlspec/_typing.py +71 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +870 -0
- sqlspec/adapters/adbc/config.py +69 -12
- sqlspec/adapters/adbc/data_dictionary.py +340 -0
- sqlspec/adapters/adbc/driver.py +266 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +153 -0
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +527 -0
- sqlspec/adapters/aiosqlite/config.py +88 -15
- sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
- sqlspec/adapters/aiosqlite/driver.py +143 -40
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +2 -2
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +68 -23
- sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
- sqlspec/adapters/asyncmy/driver.py +313 -58
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +450 -0
- sqlspec/adapters/asyncpg/config.py +59 -35
- sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
- sqlspec/adapters/asyncpg/driver.py +170 -25
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +576 -0
- sqlspec/adapters/bigquery/config.py +27 -10
- sqlspec/adapters/bigquery/data_dictionary.py +149 -0
- sqlspec/adapters/bigquery/driver.py +368 -142
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +125 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +80 -20
- sqlspec/adapters/duckdb/data_dictionary.py +163 -0
- sqlspec/adapters/duckdb/driver.py +167 -45
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +4 -4
- sqlspec/adapters/duckdb/type_converter.py +133 -0
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1745 -0
- sqlspec/adapters/oracledb/config.py +122 -32
- sqlspec/adapters/oracledb/data_dictionary.py +509 -0
- sqlspec/adapters/oracledb/driver.py +353 -91
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +348 -73
- sqlspec/adapters/oracledb/type_converter.py +207 -0
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +482 -0
- sqlspec/adapters/psqlpy/config.py +46 -17
- sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
- sqlspec/adapters/psqlpy/driver.py +123 -209
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +102 -0
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +944 -0
- sqlspec/adapters/psycopg/config.py +69 -35
- sqlspec/adapters/psycopg/data_dictionary.py +331 -0
- sqlspec/adapters/psycopg/driver.py +238 -81
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +572 -0
- sqlspec/adapters/sqlite/config.py +87 -15
- sqlspec/adapters/sqlite/data_dictionary.py +149 -0
- sqlspec/adapters/sqlite/driver.py +137 -54
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +18 -9
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +162 -89
- sqlspec/builder/_column.py +62 -29
- sqlspec/builder/_ddl.py +180 -121
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +53 -94
- sqlspec/builder/_insert.py +32 -131
- sqlspec/builder/_join.py +375 -0
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +111 -17
- sqlspec/builder/_select.py +1457 -24
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +307 -194
- sqlspec/config.py +252 -67
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +17 -17
- sqlspec/core/compiler.py +62 -9
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +83 -48
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +36 -30
- sqlspec/core/type_conversion.py +235 -0
- sqlspec/driver/__init__.py +7 -6
- sqlspec/driver/_async.py +188 -151
- sqlspec/driver/_common.py +285 -80
- sqlspec/driver/_sync.py +188 -152
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +75 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +73 -53
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +59 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +324 -223
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +302 -39
- sqlspec/migrations/commands.py +611 -144
- sqlspec/migrations/context.py +142 -0
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +68 -23
- sqlspec/migrations/runner.py +543 -107
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +66 -36
- sqlspec/storage/_utils.py +98 -0
- sqlspec/storage/backends/fsspec.py +134 -106
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +278 -162
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +16 -84
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +2 -2
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +50 -2
- sqlspec/utils/sync_tools.py +21 -17
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +111 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
- sqlspec-0.27.0.dist-info/RECORD +207 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_join_operations.py +0 -389
- sqlspec/builder/mixins/_merge_operations.py +0 -592
- sqlspec/builder/mixins/_order_limit_operations.py +0 -152
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -936
- sqlspec/builder/mixins/_update_operations.py +0 -218
- sqlspec/builder/mixins/_where_clause.py +0 -1304
- sqlspec-0.25.0.dist-info/RECORD +0 -139
- sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/builder/_merge.py
CHANGED
|
@@ -4,24 +4,459 @@ Provides a fluent interface for building SQL MERGE queries with
|
|
|
4
4
|
parameter binding and validation.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from itertools import starmap
|
|
9
|
+
from typing import Any
|
|
8
10
|
|
|
11
|
+
from mypy_extensions import trait
|
|
9
12
|
from sqlglot import exp
|
|
13
|
+
from typing_extensions import Self
|
|
10
14
|
|
|
11
15
|
from sqlspec.builder._base import QueryBuilder
|
|
12
|
-
from sqlspec.builder.
|
|
13
|
-
MergeIntoClauseMixin,
|
|
14
|
-
MergeMatchedClauseMixin,
|
|
15
|
-
MergeNotMatchedBySourceClauseMixin,
|
|
16
|
-
MergeNotMatchedClauseMixin,
|
|
17
|
-
MergeOnClauseMixin,
|
|
18
|
-
MergeUsingClauseMixin,
|
|
19
|
-
)
|
|
16
|
+
from sqlspec.builder._parsing_utils import extract_sql_object_expression
|
|
20
17
|
from sqlspec.core.result import SQLResult
|
|
18
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
19
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
21
20
|
|
|
22
21
|
__all__ = ("Merge",)
|
|
23
22
|
|
|
24
23
|
|
|
24
|
+
class _MergeAssignmentMixin:
|
|
25
|
+
"""Shared assignment helpers for MERGE clause mixins."""
|
|
26
|
+
|
|
27
|
+
__slots__ = ()
|
|
28
|
+
|
|
29
|
+
def add_parameter(self, value: Any, name: str | None = None) -> tuple[Any, str]:
|
|
30
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
31
|
+
raise NotImplementedError(msg)
|
|
32
|
+
|
|
33
|
+
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
34
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
35
|
+
raise NotImplementedError(msg)
|
|
36
|
+
|
|
37
|
+
def _is_column_reference(self, value: str) -> bool:
|
|
38
|
+
if not isinstance(value, str):
|
|
39
|
+
return False
|
|
40
|
+
candidate = value.strip()
|
|
41
|
+
sql_keywords = {"NULL", "CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME", "DEFAULT"}
|
|
42
|
+
if candidate.upper() in sql_keywords:
|
|
43
|
+
return True
|
|
44
|
+
if "(" not in candidate and ")" not in candidate:
|
|
45
|
+
return False
|
|
46
|
+
try:
|
|
47
|
+
parsed: exp.Expression | None = exp.maybe_parse(candidate)
|
|
48
|
+
except Exception:
|
|
49
|
+
return False
|
|
50
|
+
if parsed is None:
|
|
51
|
+
return False
|
|
52
|
+
return isinstance(
|
|
53
|
+
parsed, (exp.Dot, exp.Anonymous, exp.Func, exp.Null, exp.CurrentTimestamp, exp.CurrentDate, exp.CurrentTime)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _process_assignment(self, target_column: str, value: Any) -> exp.Expression:
|
|
57
|
+
column_identifier = exp.column(target_column) if isinstance(target_column, str) else target_column
|
|
58
|
+
|
|
59
|
+
if hasattr(value, "expression") and hasattr(value, "sql"):
|
|
60
|
+
value_expr = extract_sql_object_expression(value, builder=self)
|
|
61
|
+
return exp.EQ(this=column_identifier, expression=value_expr)
|
|
62
|
+
if isinstance(value, exp.Expression):
|
|
63
|
+
return exp.EQ(this=column_identifier, expression=value)
|
|
64
|
+
if isinstance(value, str) and self._is_column_reference(value):
|
|
65
|
+
parsed_expression: exp.Expression | None = exp.maybe_parse(value)
|
|
66
|
+
if parsed_expression is None:
|
|
67
|
+
msg = f"Could not parse assignment expression: {value}"
|
|
68
|
+
raise SQLBuilderError(msg)
|
|
69
|
+
return exp.EQ(this=column_identifier, expression=parsed_expression)
|
|
70
|
+
|
|
71
|
+
column_name = target_column if isinstance(target_column, str) else str(target_column)
|
|
72
|
+
column_leaf = column_name.split(".")[-1]
|
|
73
|
+
param_name = self._generate_unique_parameter_name(column_leaf)
|
|
74
|
+
_, param_name = self.add_parameter(value, name=param_name)
|
|
75
|
+
placeholder = exp.Placeholder(this=param_name)
|
|
76
|
+
return exp.EQ(this=column_identifier, expression=placeholder)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@trait
|
|
80
|
+
class MergeIntoClauseMixin:
|
|
81
|
+
"""Mixin providing INTO clause for MERGE builders."""
|
|
82
|
+
|
|
83
|
+
__slots__ = ()
|
|
84
|
+
|
|
85
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
86
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
87
|
+
|
|
88
|
+
def into(self, table: str | exp.Expression, alias: str | None = None) -> Self:
|
|
89
|
+
current_expr = self.get_expression()
|
|
90
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
91
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
92
|
+
current_expr = self.get_expression()
|
|
93
|
+
|
|
94
|
+
assert current_expr is not None
|
|
95
|
+
current_expr.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@trait
|
|
100
|
+
class MergeUsingClauseMixin(_MergeAssignmentMixin):
|
|
101
|
+
"""Mixin providing USING clause for MERGE builders."""
|
|
102
|
+
|
|
103
|
+
__slots__ = ()
|
|
104
|
+
|
|
105
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
106
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
107
|
+
|
|
108
|
+
def add_parameter(self, value: Any, name: str | None = None) -> tuple[Any, str]:
|
|
109
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
110
|
+
raise NotImplementedError(msg)
|
|
111
|
+
|
|
112
|
+
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
113
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
114
|
+
raise NotImplementedError(msg)
|
|
115
|
+
|
|
116
|
+
def using(self, source: str | exp.Expression | Any, alias: str | None = None) -> Self:
|
|
117
|
+
current_expr = self.get_expression()
|
|
118
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
119
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
120
|
+
current_expr = self.get_expression()
|
|
121
|
+
|
|
122
|
+
assert current_expr is not None
|
|
123
|
+
source_expr: exp.Expression
|
|
124
|
+
if isinstance(source, str):
|
|
125
|
+
source_expr = exp.to_table(source, alias=alias)
|
|
126
|
+
elif isinstance(source, dict):
|
|
127
|
+
columns = list(source.keys())
|
|
128
|
+
values = list(source.values())
|
|
129
|
+
|
|
130
|
+
parameterized_values: list[exp.Expression] = []
|
|
131
|
+
for column, value in zip(columns, values, strict=False):
|
|
132
|
+
column_name = column if isinstance(column, str) else str(column)
|
|
133
|
+
if "." in column_name:
|
|
134
|
+
column_name = column_name.split(".")[-1]
|
|
135
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
136
|
+
_, param_name = self.add_parameter(value, name=param_name)
|
|
137
|
+
parameterized_values.append(exp.Placeholder(this=param_name))
|
|
138
|
+
|
|
139
|
+
select_expr = exp.Select()
|
|
140
|
+
select_expr.set(
|
|
141
|
+
"expressions", [exp.alias_(parameterized_values[index], column) for index, column in enumerate(columns)]
|
|
142
|
+
)
|
|
143
|
+
select_expr.set("from", exp.From(this=exp.to_table("DUAL")))
|
|
144
|
+
|
|
145
|
+
source_expr = exp.paren(select_expr)
|
|
146
|
+
if alias:
|
|
147
|
+
source_expr = exp.alias_(source_expr, alias, table=False)
|
|
148
|
+
elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
|
|
149
|
+
parameters_obj = getattr(source, "parameters", None)
|
|
150
|
+
if isinstance(parameters_obj, dict):
|
|
151
|
+
for param_name, param_value in parameters_obj.items():
|
|
152
|
+
self.add_parameter(param_value, name=param_name)
|
|
153
|
+
elif isinstance(parameters_obj, (list, tuple)):
|
|
154
|
+
for param_value in parameters_obj:
|
|
155
|
+
self.add_parameter(param_value)
|
|
156
|
+
elif parameters_obj is not None:
|
|
157
|
+
self.add_parameter(parameters_obj)
|
|
158
|
+
subquery_expression_source = getattr(source, "_expression", None)
|
|
159
|
+
subquery_expression = (
|
|
160
|
+
exp.paren(subquery_expression_source)
|
|
161
|
+
if isinstance(subquery_expression_source, exp.Expression)
|
|
162
|
+
else exp.paren(exp.select())
|
|
163
|
+
)
|
|
164
|
+
source_expr = exp.alias_(subquery_expression, alias) if alias else subquery_expression
|
|
165
|
+
elif isinstance(source, exp.Expression):
|
|
166
|
+
source_expr = exp.alias_(source, alias) if alias else source
|
|
167
|
+
else:
|
|
168
|
+
msg = f"Unsupported source type for USING clause: {type(source)}"
|
|
169
|
+
raise SQLBuilderError(msg)
|
|
170
|
+
|
|
171
|
+
current_expr.set("using", source_expr)
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@trait
|
|
176
|
+
class MergeOnClauseMixin:
|
|
177
|
+
"""Mixin providing ON clause for MERGE builders."""
|
|
178
|
+
|
|
179
|
+
__slots__ = ()
|
|
180
|
+
|
|
181
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
182
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
183
|
+
|
|
184
|
+
def on(self, condition: str | exp.Expression) -> Self:
|
|
185
|
+
current_expr = self.get_expression()
|
|
186
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
187
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
188
|
+
current_expr = self.get_expression()
|
|
189
|
+
|
|
190
|
+
assert current_expr is not None
|
|
191
|
+
if isinstance(condition, str):
|
|
192
|
+
parsed_condition: exp.Expression | None = exp.maybe_parse(condition, dialect=getattr(self, "dialect", None))
|
|
193
|
+
if parsed_condition is None:
|
|
194
|
+
msg = f"Could not parse ON condition: {condition}"
|
|
195
|
+
raise SQLBuilderError(msg)
|
|
196
|
+
condition_expr = parsed_condition
|
|
197
|
+
elif isinstance(condition, exp.Expression):
|
|
198
|
+
condition_expr = condition
|
|
199
|
+
else:
|
|
200
|
+
msg = f"Unsupported condition type for ON clause: {type(condition)}"
|
|
201
|
+
raise SQLBuilderError(msg)
|
|
202
|
+
|
|
203
|
+
current_expr.set("on", condition_expr)
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@trait
|
|
208
|
+
class MergeMatchedClauseMixin(_MergeAssignmentMixin):
|
|
209
|
+
"""Mixin providing WHEN MATCHED THEN ... clauses for MERGE builders."""
|
|
210
|
+
|
|
211
|
+
__slots__ = ()
|
|
212
|
+
|
|
213
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
214
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
215
|
+
|
|
216
|
+
def add_parameter(self, value: Any, name: str | None = None) -> tuple[Any, str]:
|
|
217
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
218
|
+
raise NotImplementedError(msg)
|
|
219
|
+
|
|
220
|
+
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
221
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
222
|
+
raise NotImplementedError(msg)
|
|
223
|
+
|
|
224
|
+
def when_matched_then_update(
|
|
225
|
+
self,
|
|
226
|
+
set_values: dict[str, Any] | None = None,
|
|
227
|
+
condition: str | exp.Expression | None = None,
|
|
228
|
+
**assignments: Any,
|
|
229
|
+
) -> Self:
|
|
230
|
+
current_expr = self.get_expression()
|
|
231
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
232
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
233
|
+
current_expr = self.get_expression()
|
|
234
|
+
|
|
235
|
+
assert current_expr is not None
|
|
236
|
+
combined_assignments: dict[str, Any] = {}
|
|
237
|
+
if set_values:
|
|
238
|
+
combined_assignments.update(set_values)
|
|
239
|
+
if assignments:
|
|
240
|
+
combined_assignments.update(assignments)
|
|
241
|
+
|
|
242
|
+
if not combined_assignments:
|
|
243
|
+
msg = "No update values provided. Use set_values or keyword arguments."
|
|
244
|
+
raise SQLBuilderError(msg)
|
|
245
|
+
|
|
246
|
+
set_expressions = list(starmap(self._process_assignment, combined_assignments.items()))
|
|
247
|
+
update_expression = exp.Update(expressions=set_expressions)
|
|
248
|
+
|
|
249
|
+
when_kwargs: dict[str, Any] = {"matched": True, "then": update_expression}
|
|
250
|
+
if condition is not None:
|
|
251
|
+
if isinstance(condition, str):
|
|
252
|
+
parsed_condition: exp.Expression | None = exp.maybe_parse(
|
|
253
|
+
condition, dialect=getattr(self, "dialect", None)
|
|
254
|
+
)
|
|
255
|
+
if parsed_condition is None:
|
|
256
|
+
msg = f"Could not parse WHEN clause condition: {condition}"
|
|
257
|
+
raise SQLBuilderError(msg)
|
|
258
|
+
when_kwargs["this"] = parsed_condition
|
|
259
|
+
elif isinstance(condition, exp.Expression):
|
|
260
|
+
when_kwargs["this"] = condition
|
|
261
|
+
else:
|
|
262
|
+
msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
|
|
263
|
+
raise SQLBuilderError(msg)
|
|
264
|
+
|
|
265
|
+
whens = current_expr.args.get("whens")
|
|
266
|
+
if not isinstance(whens, exp.Whens):
|
|
267
|
+
whens = exp.Whens(expressions=[])
|
|
268
|
+
current_expr.set("whens", whens)
|
|
269
|
+
whens.append("expressions", exp.When(**when_kwargs))
|
|
270
|
+
return self
|
|
271
|
+
|
|
272
|
+
def when_matched_then_delete(self, condition: str | exp.Expression | None = None) -> Self:
|
|
273
|
+
current_expr = self.get_expression()
|
|
274
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
275
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
276
|
+
current_expr = self.get_expression()
|
|
277
|
+
|
|
278
|
+
assert current_expr is not None
|
|
279
|
+
when_kwargs: dict[str, Any] = {"matched": True, "then": exp.Delete()}
|
|
280
|
+
if condition is not None:
|
|
281
|
+
if isinstance(condition, str):
|
|
282
|
+
parsed_condition: exp.Expression | None = exp.maybe_parse(
|
|
283
|
+
condition, dialect=getattr(self, "dialect", None)
|
|
284
|
+
)
|
|
285
|
+
if parsed_condition is None:
|
|
286
|
+
msg = f"Could not parse WHEN clause condition: {condition}"
|
|
287
|
+
raise SQLBuilderError(msg)
|
|
288
|
+
when_kwargs["this"] = parsed_condition
|
|
289
|
+
elif isinstance(condition, exp.Expression):
|
|
290
|
+
when_kwargs["this"] = condition
|
|
291
|
+
else:
|
|
292
|
+
msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
|
|
293
|
+
raise SQLBuilderError(msg)
|
|
294
|
+
|
|
295
|
+
whens = current_expr.args.get("whens")
|
|
296
|
+
if not isinstance(whens, exp.Whens):
|
|
297
|
+
whens = exp.Whens(expressions=[])
|
|
298
|
+
current_expr.set("whens", whens)
|
|
299
|
+
whens.append("expressions", exp.When(**when_kwargs))
|
|
300
|
+
return self
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@trait
|
|
304
|
+
class MergeNotMatchedClauseMixin(_MergeAssignmentMixin):
|
|
305
|
+
"""Mixin providing WHEN NOT MATCHED THEN ... clauses for MERGE builders."""
|
|
306
|
+
|
|
307
|
+
__slots__ = ()
|
|
308
|
+
|
|
309
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
310
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
311
|
+
|
|
312
|
+
def add_parameter(self, value: Any, name: str | None = None) -> tuple[Any, str]:
|
|
313
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
314
|
+
raise NotImplementedError(msg)
|
|
315
|
+
|
|
316
|
+
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
317
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
318
|
+
raise NotImplementedError(msg)
|
|
319
|
+
|
|
320
|
+
def when_not_matched_then_insert(
|
|
321
|
+
self,
|
|
322
|
+
columns: Mapping[str, Any] | Sequence[str] | None = None,
|
|
323
|
+
values: Sequence[Any] | None = None,
|
|
324
|
+
**value_kwargs: Any,
|
|
325
|
+
) -> Self:
|
|
326
|
+
current_expr = self.get_expression()
|
|
327
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
328
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
329
|
+
current_expr = self.get_expression()
|
|
330
|
+
|
|
331
|
+
assert current_expr is not None
|
|
332
|
+
insert_expr = exp.Insert()
|
|
333
|
+
column_names: list[str]
|
|
334
|
+
column_values: list[Any]
|
|
335
|
+
|
|
336
|
+
if isinstance(columns, Mapping):
|
|
337
|
+
combined = dict(columns)
|
|
338
|
+
if value_kwargs:
|
|
339
|
+
combined.update(value_kwargs)
|
|
340
|
+
column_names = list(combined.keys())
|
|
341
|
+
column_values = list(combined.values())
|
|
342
|
+
elif value_kwargs:
|
|
343
|
+
column_names = list(value_kwargs.keys())
|
|
344
|
+
column_values = list(value_kwargs.values())
|
|
345
|
+
else:
|
|
346
|
+
if columns is None or values is None:
|
|
347
|
+
msg = "Columns and values must be provided when not using keyword arguments."
|
|
348
|
+
raise SQLBuilderError(msg)
|
|
349
|
+
column_names = [str(column) for column in columns]
|
|
350
|
+
column_values = list(values)
|
|
351
|
+
if len(column_names) != len(column_values):
|
|
352
|
+
msg = "Number of columns must match number of values for MERGE insert"
|
|
353
|
+
raise SQLBuilderError(msg)
|
|
354
|
+
|
|
355
|
+
insert_columns = [exp.column(name) for name in column_names]
|
|
356
|
+
|
|
357
|
+
insert_values: list[exp.Expression] = []
|
|
358
|
+
for column_name, value in zip(column_names, column_values, strict=True):
|
|
359
|
+
if hasattr(value, "expression") and hasattr(value, "sql"):
|
|
360
|
+
insert_values.append(extract_sql_object_expression(value, builder=self))
|
|
361
|
+
elif isinstance(value, exp.Expression):
|
|
362
|
+
insert_values.append(value)
|
|
363
|
+
elif isinstance(value, str):
|
|
364
|
+
param_name = self._generate_unique_parameter_name(column_name.split(".")[-1])
|
|
365
|
+
_, param_name = self.add_parameter(value, name=param_name)
|
|
366
|
+
insert_values.append(exp.Placeholder(this=param_name))
|
|
367
|
+
else:
|
|
368
|
+
param_name = self._generate_unique_parameter_name(column_name.split(".")[-1])
|
|
369
|
+
_, param_name = self.add_parameter(value, name=param_name)
|
|
370
|
+
insert_values.append(exp.Placeholder(this=param_name))
|
|
371
|
+
|
|
372
|
+
insert_expr.set("this", exp.Tuple(expressions=insert_columns))
|
|
373
|
+
insert_expr.set("expression", exp.Tuple(expressions=insert_values))
|
|
374
|
+
whens = current_expr.args.get("whens")
|
|
375
|
+
if not isinstance(whens, exp.Whens):
|
|
376
|
+
whens = exp.Whens(expressions=[])
|
|
377
|
+
current_expr.set("whens", whens)
|
|
378
|
+
whens.append("expressions", exp.When(matched=False, then=insert_expr))
|
|
379
|
+
return self
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@trait
|
|
383
|
+
class MergeNotMatchedBySourceClauseMixin(_MergeAssignmentMixin):
|
|
384
|
+
"""Mixin providing WHEN NOT MATCHED BY SOURCE THEN ... clauses."""
|
|
385
|
+
|
|
386
|
+
__slots__ = ()
|
|
387
|
+
|
|
388
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
389
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
390
|
+
|
|
391
|
+
def add_parameter(self, value: Any, name: str | None = None) -> tuple[Any, str]:
|
|
392
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
393
|
+
raise NotImplementedError(msg)
|
|
394
|
+
|
|
395
|
+
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
396
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
397
|
+
raise NotImplementedError(msg)
|
|
398
|
+
|
|
399
|
+
def when_not_matched_by_source_then_update(
|
|
400
|
+
self, set_values: dict[str, Any] | None = None, **assignments: Any
|
|
401
|
+
) -> Self:
|
|
402
|
+
current_expr = self.get_expression()
|
|
403
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
404
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
405
|
+
current_expr = self.get_expression()
|
|
406
|
+
|
|
407
|
+
assert current_expr is not None
|
|
408
|
+
combined_assignments: dict[str, Any] = {}
|
|
409
|
+
if set_values:
|
|
410
|
+
combined_assignments.update(set_values)
|
|
411
|
+
if assignments:
|
|
412
|
+
combined_assignments.update(assignments)
|
|
413
|
+
|
|
414
|
+
if not combined_assignments:
|
|
415
|
+
msg = "No update values provided. Use set_values or keyword arguments."
|
|
416
|
+
raise SQLBuilderError(msg)
|
|
417
|
+
|
|
418
|
+
set_expressions: list[exp.Expression] = []
|
|
419
|
+
for column_name, value in combined_assignments.items():
|
|
420
|
+
column_identifier = exp.column(column_name)
|
|
421
|
+
if hasattr(value, "expression") and hasattr(value, "sql"):
|
|
422
|
+
value_expr = extract_sql_object_expression(value, builder=self)
|
|
423
|
+
elif isinstance(value, exp.Expression):
|
|
424
|
+
value_expr = value
|
|
425
|
+
elif isinstance(value, str) and self._is_column_reference(value):
|
|
426
|
+
parsed_value: exp.Expression | None = exp.maybe_parse(value)
|
|
427
|
+
if parsed_value is None:
|
|
428
|
+
msg = f"Could not parse assignment expression: {value}"
|
|
429
|
+
raise SQLBuilderError(msg)
|
|
430
|
+
value_expr = parsed_value
|
|
431
|
+
else:
|
|
432
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
433
|
+
_, param_name = self.add_parameter(value, name=param_name)
|
|
434
|
+
value_expr = exp.Placeholder(this=param_name)
|
|
435
|
+
set_expressions.append(exp.EQ(this=column_identifier, expression=value_expr))
|
|
436
|
+
|
|
437
|
+
update_expr = exp.Update(expressions=set_expressions)
|
|
438
|
+
whens = current_expr.args.get("whens")
|
|
439
|
+
if not isinstance(whens, exp.Whens):
|
|
440
|
+
whens = exp.Whens(expressions=[])
|
|
441
|
+
current_expr.set("whens", whens)
|
|
442
|
+
whens.append("expressions", exp.When(matched=False, source=True, then=update_expr))
|
|
443
|
+
return self
|
|
444
|
+
|
|
445
|
+
def when_not_matched_by_source_then_delete(self) -> Self:
|
|
446
|
+
current_expr = self.get_expression()
|
|
447
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
448
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
449
|
+
current_expr = self.get_expression()
|
|
450
|
+
|
|
451
|
+
assert current_expr is not None
|
|
452
|
+
whens = current_expr.args.get("whens")
|
|
453
|
+
if not isinstance(whens, exp.Whens):
|
|
454
|
+
whens = exp.Whens(expressions=[])
|
|
455
|
+
current_expr.set("whens", whens)
|
|
456
|
+
whens.append("expressions", exp.When(matched=False, source=True, then=exp.Delete()))
|
|
457
|
+
return self
|
|
458
|
+
|
|
459
|
+
|
|
25
460
|
class Merge(
|
|
26
461
|
QueryBuilder,
|
|
27
462
|
MergeUsingClauseMixin,
|
|
@@ -38,9 +473,9 @@ class Merge(
|
|
|
38
473
|
"""
|
|
39
474
|
|
|
40
475
|
__slots__ = ()
|
|
41
|
-
_expression:
|
|
476
|
+
_expression: exp.Expression | None
|
|
42
477
|
|
|
43
|
-
def __init__(self, target_table:
|
|
478
|
+
def __init__(self, target_table: str | None = None, **kwargs: Any) -> None:
|
|
44
479
|
"""Initialize MERGE with optional target table.
|
|
45
480
|
|
|
46
481
|
Args:
|
|
@@ -5,11 +5,11 @@ passed as strings to builder methods.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import contextlib
|
|
8
|
-
from typing import Any, Final,
|
|
8
|
+
from typing import Any, Final, cast
|
|
9
9
|
|
|
10
10
|
from sqlglot import exp, maybe_parse, parse_one
|
|
11
11
|
|
|
12
|
-
from sqlspec.core.parameters import ParameterStyle
|
|
12
|
+
from sqlspec.core.parameters import ParameterStyle, ParameterValidator
|
|
13
13
|
from sqlspec.utils.type_guards import (
|
|
14
14
|
has_expression_and_parameters,
|
|
15
15
|
has_expression_and_sql,
|
|
@@ -18,7 +18,7 @@ from sqlspec.utils.type_guards import (
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def extract_column_name(column:
|
|
21
|
+
def extract_column_name(column: str | exp.Column) -> str:
|
|
22
22
|
"""Extract column name from column expression for parameter naming.
|
|
23
23
|
|
|
24
24
|
Args:
|
|
@@ -39,9 +39,7 @@ def extract_column_name(column: Union[str, exp.Column]) -> str:
|
|
|
39
39
|
return "column"
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def parse_column_expression(
|
|
43
|
-
column_input: Union[str, exp.Expression, Any], builder: Optional[Any] = None
|
|
44
|
-
) -> exp.Expression:
|
|
42
|
+
def parse_column_expression(column_input: str | exp.Expression | Any, builder: Any | None = None) -> exp.Expression:
|
|
45
43
|
"""Parse a column input that might be a complex expression.
|
|
46
44
|
|
|
47
45
|
Handles cases like:
|
|
@@ -63,9 +61,7 @@ def parse_column_expression(
|
|
|
63
61
|
if isinstance(column_input, exp.Expression):
|
|
64
62
|
return column_input
|
|
65
63
|
|
|
66
|
-
# Handle SQL objects (from sql.raw with parameters)
|
|
67
64
|
if has_expression_and_sql(column_input):
|
|
68
|
-
# This is likely a SQL object
|
|
69
65
|
expression = getattr(column_input, "expression", None)
|
|
70
66
|
if expression is not None and isinstance(expression, exp.Expression):
|
|
71
67
|
# Merge parameters from SQL object into builder if available
|
|
@@ -74,9 +70,7 @@ def parse_column_expression(
|
|
|
74
70
|
for param_name, param_value in sql_parameters.items():
|
|
75
71
|
builder.add_parameter(param_value, name=param_name)
|
|
76
72
|
return cast("exp.Expression", expression)
|
|
77
|
-
# If expression is None, fall back to parsing the raw SQL
|
|
78
73
|
sql_text = getattr(column_input, "sql", "")
|
|
79
|
-
# Merge parameters even when parsing raw SQL
|
|
80
74
|
if builder and has_expression_and_parameters(column_input) and hasattr(builder, "add_parameter"):
|
|
81
75
|
sql_parameters = getattr(column_input, "parameters", {})
|
|
82
76
|
for param_name, param_value in sql_parameters.items():
|
|
@@ -91,7 +85,7 @@ def parse_column_expression(
|
|
|
91
85
|
return exp.maybe_parse(column_input) or exp.column(str(column_input))
|
|
92
86
|
|
|
93
87
|
|
|
94
|
-
def parse_table_expression(table_input: str, explicit_alias:
|
|
88
|
+
def parse_table_expression(table_input: str, explicit_alias: str | None = None) -> exp.Expression:
|
|
95
89
|
"""Parses a table string that can be a name, a name with an alias, or a subquery string."""
|
|
96
90
|
with contextlib.suppress(Exception):
|
|
97
91
|
parsed = parse_one(f"SELECT * FROM {table_input}")
|
|
@@ -106,7 +100,7 @@ def parse_table_expression(table_input: str, explicit_alias: Optional[str] = Non
|
|
|
106
100
|
return exp.to_table(table_input, alias=explicit_alias)
|
|
107
101
|
|
|
108
102
|
|
|
109
|
-
def parse_order_expression(order_input:
|
|
103
|
+
def parse_order_expression(order_input: str | exp.Expression) -> exp.Expression:
|
|
110
104
|
"""Parse an ORDER BY expression that might include direction.
|
|
111
105
|
|
|
112
106
|
Handles cases like:
|
|
@@ -133,7 +127,7 @@ def parse_order_expression(order_input: Union[str, exp.Expression]) -> exp.Expre
|
|
|
133
127
|
|
|
134
128
|
|
|
135
129
|
def parse_condition_expression(
|
|
136
|
-
condition_input:
|
|
130
|
+
condition_input: str | exp.Expression | tuple[str, Any], builder: "Any" = None
|
|
137
131
|
) -> exp.Expression:
|
|
138
132
|
"""Parse a condition that might be complex SQL.
|
|
139
133
|
|
|
@@ -175,8 +169,6 @@ def parse_condition_expression(
|
|
|
175
169
|
|
|
176
170
|
# Convert database-specific parameter styles to SQLGlot-compatible format
|
|
177
171
|
# This ensures that placeholders like $1, %s, :1 are properly recognized as parameters
|
|
178
|
-
from sqlspec.core.parameters import ParameterValidator
|
|
179
|
-
|
|
180
172
|
validator = ParameterValidator()
|
|
181
173
|
param_info = validator.extract_parameters(condition_input)
|
|
182
174
|
|
|
@@ -199,10 +191,112 @@ def parse_condition_expression(
|
|
|
199
191
|
)
|
|
200
192
|
condition_input = converted_condition
|
|
201
193
|
|
|
202
|
-
parsed:
|
|
194
|
+
parsed: exp.Expression | None = exp.maybe_parse(condition_input)
|
|
203
195
|
if parsed:
|
|
204
196
|
return parsed
|
|
205
197
|
return exp.condition(condition_input)
|
|
206
198
|
|
|
207
199
|
|
|
208
|
-
|
|
200
|
+
def extract_sql_object_expression(value: Any, builder: Any | None = None) -> exp.Expression:
|
|
201
|
+
"""Extract SQLGlot expression from SQL object value with parameter merging.
|
|
202
|
+
|
|
203
|
+
Handles the common pattern of:
|
|
204
|
+
1. Check if value has expression and SQL attributes
|
|
205
|
+
2. Try to get expression first, merge parameters if available
|
|
206
|
+
3. Fall back to parsing raw SQL text if expression is None
|
|
207
|
+
4. Merge parameters in both cases
|
|
208
|
+
5. Handle callable SQL text
|
|
209
|
+
|
|
210
|
+
This consolidates duplicated logic across builder files that process
|
|
211
|
+
SQL objects (like those from sql.raw() calls).
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
value: The SQL object value to process
|
|
215
|
+
builder: Optional builder instance for parameter merging (must have add_parameter method)
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
SQLGlot Expression extracted from the SQL object
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
ValueError: If the value doesn't appear to be a SQL object
|
|
222
|
+
"""
|
|
223
|
+
if not has_expression_and_sql(value):
|
|
224
|
+
msg = f"Value does not have both expression and sql attributes: {type(value)}"
|
|
225
|
+
raise ValueError(msg)
|
|
226
|
+
|
|
227
|
+
# Try expression attribute first
|
|
228
|
+
expression = getattr(value, "expression", None)
|
|
229
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
230
|
+
# Merge parameters if available and builder supports it
|
|
231
|
+
if builder and hasattr(value, "parameters") and hasattr(builder, "add_parameter"):
|
|
232
|
+
sql_parameters = getattr(value, "parameters", {})
|
|
233
|
+
for param_name, param_value in sql_parameters.items():
|
|
234
|
+
builder.add_parameter(param_value, name=param_name)
|
|
235
|
+
return cast("exp.Expression", expression)
|
|
236
|
+
|
|
237
|
+
# Fall back to parsing raw SQL text
|
|
238
|
+
sql_text = getattr(value, "sql", "")
|
|
239
|
+
|
|
240
|
+
# Merge parameters even when parsing raw SQL
|
|
241
|
+
if builder and hasattr(value, "parameters") and hasattr(builder, "add_parameter"):
|
|
242
|
+
sql_parameters = getattr(value, "parameters", {})
|
|
243
|
+
for param_name, param_value in sql_parameters.items():
|
|
244
|
+
builder.add_parameter(param_value, name=param_name)
|
|
245
|
+
|
|
246
|
+
# Handle callable SQL text
|
|
247
|
+
if callable(sql_text):
|
|
248
|
+
sql_text = str(value)
|
|
249
|
+
|
|
250
|
+
# Parse SQL text and return as expression
|
|
251
|
+
return exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def extract_expression(value: Any) -> exp.Expression:
|
|
255
|
+
"""Extract SQLGlot expression from value, handling wrapper types.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
value: String, SQLGlot expression, or wrapper type.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Raw SQLGlot expression.
|
|
262
|
+
"""
|
|
263
|
+
from sqlspec.builder._column import Column
|
|
264
|
+
from sqlspec.builder._expression_wrappers import ExpressionWrapper
|
|
265
|
+
from sqlspec.builder._select import Case
|
|
266
|
+
|
|
267
|
+
if isinstance(value, str):
|
|
268
|
+
return exp.column(value)
|
|
269
|
+
if isinstance(value, Column):
|
|
270
|
+
return value.sqlglot_expression
|
|
271
|
+
if isinstance(value, ExpressionWrapper):
|
|
272
|
+
return value.expression
|
|
273
|
+
if isinstance(value, Case):
|
|
274
|
+
return exp.Case(ifs=value.conditions, default=value.default)
|
|
275
|
+
if isinstance(value, exp.Expression):
|
|
276
|
+
return value
|
|
277
|
+
return exp.convert(value)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def to_expression(value: Any) -> exp.Expression:
|
|
281
|
+
"""Convert a Python value to a raw SQLGlot expression.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
value: Python value or SQLGlot expression to convert.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Raw SQLGlot expression.
|
|
288
|
+
"""
|
|
289
|
+
if isinstance(value, exp.Expression):
|
|
290
|
+
return value
|
|
291
|
+
return exp.convert(value)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
__all__ = (
|
|
295
|
+
"extract_expression",
|
|
296
|
+
"extract_sql_object_expression",
|
|
297
|
+
"parse_column_expression",
|
|
298
|
+
"parse_condition_expression",
|
|
299
|
+
"parse_order_expression",
|
|
300
|
+
"parse_table_expression",
|
|
301
|
+
"to_expression",
|
|
302
|
+
)
|