sqlspec 0.26.0__py3-none-any.whl → 0.28.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 +55 -25
- sqlspec/_typing.py +155 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +880 -0
- sqlspec/adapters/adbc/config.py +62 -12
- sqlspec/adapters/adbc/data_dictionary.py +74 -2
- sqlspec/adapters/adbc/driver.py +226 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +44 -50
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +536 -0
- sqlspec/adapters/aiosqlite/config.py +86 -16
- sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
- sqlspec/adapters/aiosqlite/driver.py +127 -38
- 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 +1 -1
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +503 -0
- sqlspec/adapters/asyncmy/config.py +59 -17
- sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
- sqlspec/adapters/asyncmy/driver.py +293 -62
- 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 +460 -0
- sqlspec/adapters/asyncpg/config.py +57 -36
- sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
- sqlspec/adapters/asyncpg/driver.py +153 -23
- 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 +585 -0
- sqlspec/adapters/bigquery/config.py +36 -11
- sqlspec/adapters/bigquery/data_dictionary.py +42 -2
- sqlspec/adapters/bigquery/driver.py +489 -144
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +55 -23
- sqlspec/adapters/duckdb/_types.py +2 -2
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +563 -0
- sqlspec/adapters/duckdb/config.py +79 -21
- sqlspec/adapters/duckdb/data_dictionary.py +41 -2
- sqlspec/adapters/duckdb/driver.py +225 -44
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +5 -5
- sqlspec/adapters/duckdb/type_converter.py +51 -21
- 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 +1628 -0
- sqlspec/adapters/oracledb/config.py +120 -36
- sqlspec/adapters/oracledb/data_dictionary.py +87 -20
- sqlspec/adapters/oracledb/driver.py +475 -86
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +765 -0
- sqlspec/adapters/oracledb/migrations.py +316 -25
- sqlspec/adapters/oracledb/type_converter.py +91 -16
- 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 +483 -0
- sqlspec/adapters/psqlpy/config.py +45 -19
- sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
- sqlspec/adapters/psqlpy/driver.py +108 -41
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +40 -11
- 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 +962 -0
- sqlspec/adapters/psycopg/config.py +65 -37
- sqlspec/adapters/psycopg/data_dictionary.py +91 -3
- sqlspec/adapters/psycopg/driver.py +200 -78
- 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 +582 -0
- sqlspec/adapters/sqlite/config.py +85 -16
- sqlspec/adapters/sqlite/data_dictionary.py +34 -2
- sqlspec/adapters/sqlite/driver.py +120 -52
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +5 -5
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +91 -58
- sqlspec/builder/_column.py +5 -5
- sqlspec/builder/_ddl.py +98 -89
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +41 -44
- sqlspec/builder/_insert.py +5 -82
- sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +9 -11
- sqlspec/builder/_select.py +1313 -25
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +76 -69
- sqlspec/config.py +331 -62
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +18 -18
- sqlspec/core/compiler.py +6 -8
- sqlspec/core/filters.py +55 -47
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +76 -45
- sqlspec/core/result.py +234 -47
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +32 -31
- sqlspec/core/type_conversion.py +3 -2
- sqlspec/driver/__init__.py +1 -3
- sqlspec/driver/_async.py +183 -160
- sqlspec/driver/_common.py +197 -109
- sqlspec/driver/_sync.py +189 -161
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +70 -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 +69 -61
- sqlspec/extensions/fastapi/__init__.py +21 -0
- sqlspec/extensions/fastapi/extension.py +331 -0
- sqlspec/extensions/fastapi/providers.py +543 -0
- sqlspec/extensions/flask/__init__.py +36 -0
- sqlspec/extensions/flask/_state.py +71 -0
- sqlspec/extensions/flask/_utils.py +40 -0
- sqlspec/extensions/flask/extension.py +389 -0
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +56 -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 +349 -224
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/extensions/starlette/__init__.py +10 -0
- sqlspec/extensions/starlette/_state.py +25 -0
- sqlspec/extensions/starlette/_utils.py +52 -0
- sqlspec/extensions/starlette/extension.py +254 -0
- sqlspec/extensions/starlette/middleware.py +154 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/base.py +200 -76
- sqlspec/migrations/commands.py +591 -62
- sqlspec/migrations/context.py +6 -9
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +47 -19
- sqlspec/migrations/runner.py +241 -75
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +106 -36
- sqlspec/storage/_utils.py +85 -0
- sqlspec/storage/backends/fsspec.py +133 -107
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +276 -168
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +30 -84
- sqlspec/utils/__init__.py +25 -4
- sqlspec/utils/arrow_helpers.py +81 -0
- sqlspec/utils/config_resolver.py +6 -6
- 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 +205 -5
- sqlspec/utils/portal.py +311 -0
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +113 -4
- sqlspec/utils/sync_tools.py +36 -22
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +136 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
- sqlspec-0.28.0.dist-info/RECORD +221 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_merge_operations.py +0 -698
- sqlspec/builder/mixins/_order_limit_operations.py +0 -145
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -930
- sqlspec/builder/mixins/_update_operations.py +0 -199
- sqlspec/builder/mixins/_where_clause.py +0 -1298
- sqlspec-0.26.0.dist-info/RECORD +0 -157
- sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.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,7 +5,7 @@ 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
|
|
|
@@ -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:
|
|
@@ -87,7 +85,7 @@ def parse_column_expression(
|
|
|
87
85
|
return exp.maybe_parse(column_input) or exp.column(str(column_input))
|
|
88
86
|
|
|
89
87
|
|
|
90
|
-
def parse_table_expression(table_input: str, explicit_alias:
|
|
88
|
+
def parse_table_expression(table_input: str, explicit_alias: str | None = None) -> exp.Expression:
|
|
91
89
|
"""Parses a table string that can be a name, a name with an alias, or a subquery string."""
|
|
92
90
|
with contextlib.suppress(Exception):
|
|
93
91
|
parsed = parse_one(f"SELECT * FROM {table_input}")
|
|
@@ -102,7 +100,7 @@ def parse_table_expression(table_input: str, explicit_alias: Optional[str] = Non
|
|
|
102
100
|
return exp.to_table(table_input, alias=explicit_alias)
|
|
103
101
|
|
|
104
102
|
|
|
105
|
-
def parse_order_expression(order_input:
|
|
103
|
+
def parse_order_expression(order_input: str | exp.Expression) -> exp.Expression:
|
|
106
104
|
"""Parse an ORDER BY expression that might include direction.
|
|
107
105
|
|
|
108
106
|
Handles cases like:
|
|
@@ -129,7 +127,7 @@ def parse_order_expression(order_input: Union[str, exp.Expression]) -> exp.Expre
|
|
|
129
127
|
|
|
130
128
|
|
|
131
129
|
def parse_condition_expression(
|
|
132
|
-
condition_input:
|
|
130
|
+
condition_input: str | exp.Expression | tuple[str, Any], builder: "Any" = None
|
|
133
131
|
) -> exp.Expression:
|
|
134
132
|
"""Parse a condition that might be complex SQL.
|
|
135
133
|
|
|
@@ -193,13 +191,13 @@ def parse_condition_expression(
|
|
|
193
191
|
)
|
|
194
192
|
condition_input = converted_condition
|
|
195
193
|
|
|
196
|
-
parsed:
|
|
194
|
+
parsed: exp.Expression | None = exp.maybe_parse(condition_input)
|
|
197
195
|
if parsed:
|
|
198
196
|
return parsed
|
|
199
197
|
return exp.condition(condition_input)
|
|
200
198
|
|
|
201
199
|
|
|
202
|
-
def extract_sql_object_expression(value: Any, builder:
|
|
200
|
+
def extract_sql_object_expression(value: Any, builder: Any | None = None) -> exp.Expression:
|
|
203
201
|
"""Extract SQLGlot expression from SQL object value with parameter merging.
|
|
204
202
|
|
|
205
203
|
Handles the common pattern of:
|
|
@@ -264,7 +262,7 @@ def extract_expression(value: Any) -> exp.Expression:
|
|
|
264
262
|
"""
|
|
265
263
|
from sqlspec.builder._column import Column
|
|
266
264
|
from sqlspec.builder._expression_wrappers import ExpressionWrapper
|
|
267
|
-
from sqlspec.builder.
|
|
265
|
+
from sqlspec.builder._select import Case
|
|
268
266
|
|
|
269
267
|
if isinstance(value, str):
|
|
270
268
|
return exp.column(value)
|