sqlspec 0.16.2__cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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.
- 51ff5a9eadfdefd49f98__mypyc.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/__init__.py +92 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +77 -0
- sqlspec/_sql.py +1782 -0
- sqlspec/_typing.py +680 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +361 -0
- sqlspec/adapters/adbc/driver.py +512 -0
- sqlspec/adapters/aiosqlite/__init__.py +19 -0
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +253 -0
- sqlspec/adapters/aiosqlite/driver.py +248 -0
- sqlspec/adapters/asyncmy/__init__.py +19 -0
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +180 -0
- sqlspec/adapters/asyncmy/driver.py +274 -0
- sqlspec/adapters/asyncpg/__init__.py +21 -0
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +229 -0
- sqlspec/adapters/asyncpg/driver.py +344 -0
- sqlspec/adapters/bigquery/__init__.py +18 -0
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +298 -0
- sqlspec/adapters/bigquery/driver.py +558 -0
- sqlspec/adapters/duckdb/__init__.py +22 -0
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +504 -0
- sqlspec/adapters/duckdb/driver.py +368 -0
- sqlspec/adapters/oracledb/__init__.py +32 -0
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +317 -0
- sqlspec/adapters/oracledb/driver.py +538 -0
- sqlspec/adapters/psqlpy/__init__.py +16 -0
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +214 -0
- sqlspec/adapters/psqlpy/driver.py +530 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +426 -0
- sqlspec/adapters/psycopg/driver.py +796 -0
- sqlspec/adapters/sqlite/__init__.py +15 -0
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +240 -0
- sqlspec/adapters/sqlite/driver.py +294 -0
- sqlspec/base.py +571 -0
- sqlspec/builder/__init__.py +62 -0
- sqlspec/builder/_base.py +473 -0
- sqlspec/builder/_column.py +320 -0
- sqlspec/builder/_ddl.py +1346 -0
- sqlspec/builder/_ddl_utils.py +103 -0
- sqlspec/builder/_delete.py +76 -0
- sqlspec/builder/_insert.py +421 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/builder/_parsing_utils.py +164 -0
- sqlspec/builder/_select.py +170 -0
- sqlspec/builder/_update.py +188 -0
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/builder/mixins/_delete_operations.py +41 -0
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/builder/mixins/_join_operations.py +149 -0
- sqlspec/builder/mixins/_merge_operations.py +562 -0
- 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 +395 -0
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result.py +677 -0
- sqlspec/core/splitter.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +819 -0
- sqlspec/core/statement.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +19 -0
- sqlspec/driver/_async.py +502 -0
- sqlspec/driver/_common.py +631 -0
- sqlspec/driver/_sync.py +503 -0
- sqlspec/driver/mixins/__init__.py +6 -0
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +86 -0
- sqlspec/exceptions.py +193 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +461 -0
- sqlspec/extensions/litestar/__init__.py +6 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +92 -0
- sqlspec/extensions/litestar/handlers.py +260 -0
- sqlspec/extensions/litestar/plugin.py +145 -0
- sqlspec/extensions/litestar/providers.py +454 -0
- sqlspec/loader.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/loader.py +760 -0
- 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 +407 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +23 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +163 -0
- sqlspec/storage/backends/fsspec.py +386 -0
- sqlspec/storage/backends/obstore.py +459 -0
- sqlspec/storage/capabilities.py +102 -0
- sqlspec/storage/registry.py +239 -0
- sqlspec/typing.py +299 -0
- sqlspec/utils/__init__.py +3 -0
- sqlspec/utils/correlation.py +150 -0
- sqlspec/utils/deprecation.py +106 -0
- sqlspec/utils/fixtures.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/fixtures.py +58 -0
- sqlspec/utils/logging.py +127 -0
- sqlspec/utils/module_loader.py +89 -0
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +32 -0
- sqlspec/utils/sync_tools.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +237 -0
- sqlspec/utils/text.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +96 -0
- sqlspec/utils/type_guards.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_guards.py +1139 -0
- sqlspec-0.16.2.dist-info/METADATA +365 -0
- sqlspec-0.16.2.dist-info/RECORD +148 -0
- sqlspec-0.16.2.dist-info/WHEEL +7 -0
- sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
- sqlspec-0.16.2.dist-info/licenses/LICENSE +21 -0
- sqlspec-0.16.2.dist-info/licenses/NOTICE +29 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""CTE (Common Table Expression) and Set Operations mixins for SQL builders."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
|
+
|
|
5
|
+
from mypy_extensions import trait
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
10
|
+
|
|
11
|
+
__all__ = ("CommonTableExpressionMixin", "SetOperationMixin")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@trait
|
|
15
|
+
class CommonTableExpressionMixin:
|
|
16
|
+
"""Mixin providing WITH clause (Common Table Expressions) support for SQL builders."""
|
|
17
|
+
|
|
18
|
+
__slots__ = ()
|
|
19
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
20
|
+
_expression: Optional[exp.Expression]
|
|
21
|
+
|
|
22
|
+
_with_ctes: Any # Provided by QueryBuilder
|
|
23
|
+
dialect: Any # Provided by QueryBuilder
|
|
24
|
+
|
|
25
|
+
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
26
|
+
"""Add parameter - provided by QueryBuilder."""
|
|
27
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
28
|
+
raise NotImplementedError(msg)
|
|
29
|
+
|
|
30
|
+
def with_(
|
|
31
|
+
self, name: str, query: Union[Any, str], recursive: bool = False, columns: Optional[list[str]] = None
|
|
32
|
+
) -> Self:
|
|
33
|
+
"""Add WITH clause (Common Table Expression).
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
name: The name of the CTE.
|
|
37
|
+
query: The query for the CTE (builder instance or SQL string).
|
|
38
|
+
recursive: Whether this is a recursive CTE.
|
|
39
|
+
columns: Optional column names for the CTE.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
SQLBuilderError: If the query type is unsupported.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The current builder instance for method chaining.
|
|
46
|
+
"""
|
|
47
|
+
if self._expression is None:
|
|
48
|
+
msg = "Cannot add WITH clause: expression not initialized."
|
|
49
|
+
raise SQLBuilderError(msg)
|
|
50
|
+
|
|
51
|
+
if not isinstance(self._expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)):
|
|
52
|
+
msg = f"Cannot add WITH clause to {type(self._expression).__name__} expression."
|
|
53
|
+
raise SQLBuilderError(msg)
|
|
54
|
+
|
|
55
|
+
cte_expr: Optional[exp.Expression] = None
|
|
56
|
+
if isinstance(query, str):
|
|
57
|
+
cte_expr = exp.maybe_parse(query, dialect=self.dialect)
|
|
58
|
+
elif isinstance(query, exp.Expression):
|
|
59
|
+
cte_expr = query
|
|
60
|
+
else:
|
|
61
|
+
built_query = query.to_statement()
|
|
62
|
+
cte_sql = built_query.sql
|
|
63
|
+
cte_expr = exp.maybe_parse(cte_sql, dialect=self.dialect)
|
|
64
|
+
|
|
65
|
+
parameters = built_query.parameters
|
|
66
|
+
if parameters:
|
|
67
|
+
if isinstance(parameters, dict):
|
|
68
|
+
for param_name, param_value in parameters.items():
|
|
69
|
+
self.add_parameter(param_value, name=param_name)
|
|
70
|
+
elif isinstance(parameters, (list, tuple)):
|
|
71
|
+
for param_value in parameters:
|
|
72
|
+
self.add_parameter(param_value)
|
|
73
|
+
|
|
74
|
+
if not cte_expr:
|
|
75
|
+
msg = f"Could not parse CTE query: {query}"
|
|
76
|
+
raise SQLBuilderError(msg)
|
|
77
|
+
|
|
78
|
+
if columns:
|
|
79
|
+
cte_alias_expr = exp.alias_(cte_expr, name, table=[exp.to_identifier(col) for col in columns])
|
|
80
|
+
else:
|
|
81
|
+
cte_alias_expr = exp.alias_(cte_expr, name)
|
|
82
|
+
|
|
83
|
+
existing_with = self._expression.args.get("with")
|
|
84
|
+
if existing_with:
|
|
85
|
+
existing_with.expressions.append(cte_alias_expr)
|
|
86
|
+
if recursive:
|
|
87
|
+
existing_with.set("recursive", recursive)
|
|
88
|
+
else:
|
|
89
|
+
# Only SELECT, INSERT, UPDATE support WITH clauses
|
|
90
|
+
if hasattr(self._expression, "with_") and isinstance(
|
|
91
|
+
self._expression, (exp.Select, exp.Insert, exp.Update)
|
|
92
|
+
):
|
|
93
|
+
self._expression = self._expression.with_(cte_alias_expr, as_=name, copy=False)
|
|
94
|
+
if recursive:
|
|
95
|
+
with_clause = self._expression.find(exp.With)
|
|
96
|
+
if with_clause:
|
|
97
|
+
with_clause.set("recursive", recursive)
|
|
98
|
+
self._with_ctes[name] = exp.CTE(this=cte_expr, alias=exp.to_table(name))
|
|
99
|
+
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@trait
|
|
104
|
+
class SetOperationMixin:
|
|
105
|
+
"""Mixin providing set operations (UNION, INTERSECT, EXCEPT) for SELECT builders."""
|
|
106
|
+
|
|
107
|
+
__slots__ = ()
|
|
108
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
109
|
+
_expression: Optional[exp.Expression]
|
|
110
|
+
|
|
111
|
+
_parameters: dict[str, Any]
|
|
112
|
+
dialect: Any = None
|
|
113
|
+
|
|
114
|
+
def build(self) -> Any:
|
|
115
|
+
"""Build the query - provided by QueryBuilder."""
|
|
116
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
117
|
+
raise NotImplementedError(msg)
|
|
118
|
+
|
|
119
|
+
def union(self, other: Any, all_: bool = False) -> Self:
|
|
120
|
+
"""Combine this query with another using UNION.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
other: Another SelectBuilder or compatible builder to union with.
|
|
124
|
+
all_: If True, use UNION ALL instead of UNION.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The new builder instance for the union query.
|
|
131
|
+
"""
|
|
132
|
+
left_query = self.build()
|
|
133
|
+
right_query = other.build()
|
|
134
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
|
|
135
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
|
|
136
|
+
if not left_expr or not right_expr:
|
|
137
|
+
msg = "Could not parse queries for UNION operation"
|
|
138
|
+
raise SQLBuilderError(msg)
|
|
139
|
+
union_expr = exp.union(left_expr, right_expr, distinct=not all_)
|
|
140
|
+
new_builder = type(self)()
|
|
141
|
+
new_builder.dialect = self.dialect
|
|
142
|
+
new_builder._expression = union_expr
|
|
143
|
+
merged_parameters = dict(left_query.parameters)
|
|
144
|
+
for param_name, param_value in right_query.parameters.items():
|
|
145
|
+
if param_name in merged_parameters:
|
|
146
|
+
counter = 1
|
|
147
|
+
new_param_name = f"{param_name}_right_{counter}"
|
|
148
|
+
while new_param_name in merged_parameters:
|
|
149
|
+
counter += 1
|
|
150
|
+
new_param_name = f"{param_name}_right_{counter}"
|
|
151
|
+
|
|
152
|
+
def rename_parameter(
|
|
153
|
+
node: exp.Expression, old_name: str = param_name, new_name: str = new_param_name
|
|
154
|
+
) -> exp.Expression:
|
|
155
|
+
if isinstance(node, exp.Placeholder) and node.name == old_name:
|
|
156
|
+
return exp.Placeholder(this=new_name)
|
|
157
|
+
return node
|
|
158
|
+
|
|
159
|
+
right_expr = right_expr.transform(rename_parameter)
|
|
160
|
+
union_expr = exp.union(left_expr, right_expr, distinct=not all_)
|
|
161
|
+
new_builder._expression = union_expr
|
|
162
|
+
merged_parameters[new_param_name] = param_value
|
|
163
|
+
else:
|
|
164
|
+
merged_parameters[param_name] = param_value
|
|
165
|
+
new_builder._parameters = merged_parameters
|
|
166
|
+
return new_builder
|
|
167
|
+
|
|
168
|
+
def intersect(self, other: Any) -> Self:
|
|
169
|
+
"""Add INTERSECT clause.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
other: Another SelectBuilder or compatible builder to intersect with.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
The new builder instance for the intersect query.
|
|
179
|
+
"""
|
|
180
|
+
left_query = self.build()
|
|
181
|
+
right_query = other.build()
|
|
182
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
|
|
183
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
|
|
184
|
+
if not left_expr or not right_expr:
|
|
185
|
+
msg = "Could not parse queries for INTERSECT operation"
|
|
186
|
+
raise SQLBuilderError(msg)
|
|
187
|
+
intersect_expr = exp.intersect(left_expr, right_expr, distinct=True)
|
|
188
|
+
new_builder = type(self)()
|
|
189
|
+
new_builder.dialect = self.dialect
|
|
190
|
+
new_builder._expression = intersect_expr
|
|
191
|
+
merged_parameters = dict(left_query.parameters)
|
|
192
|
+
merged_parameters.update(right_query.parameters)
|
|
193
|
+
new_builder._parameters = merged_parameters
|
|
194
|
+
return new_builder
|
|
195
|
+
|
|
196
|
+
def except_(self, other: Any) -> Self:
|
|
197
|
+
"""Combine this query with another using EXCEPT.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
other: Another SelectBuilder or compatible builder to except with.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The new builder instance for the except query.
|
|
207
|
+
"""
|
|
208
|
+
left_query = self.build()
|
|
209
|
+
right_query = other.build()
|
|
210
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
|
|
211
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
|
|
212
|
+
if not left_expr or not right_expr:
|
|
213
|
+
msg = "Could not parse queries for EXCEPT operation"
|
|
214
|
+
raise SQLBuilderError(msg)
|
|
215
|
+
except_expr = exp.except_(left_expr, right_expr)
|
|
216
|
+
new_builder = type(self)()
|
|
217
|
+
new_builder.dialect = self.dialect
|
|
218
|
+
new_builder._expression = except_expr
|
|
219
|
+
merged_parameters = dict(left_query.parameters)
|
|
220
|
+
merged_parameters.update(right_query.parameters)
|
|
221
|
+
new_builder._parameters = merged_parameters
|
|
222
|
+
return new_builder
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Delete operation mixins for SQL builders."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mypy_extensions import trait
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
10
|
+
|
|
11
|
+
__all__ = ("DeleteFromClauseMixin",)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@trait
|
|
15
|
+
class DeleteFromClauseMixin:
|
|
16
|
+
"""Mixin providing FROM clause for DELETE builders."""
|
|
17
|
+
|
|
18
|
+
__slots__ = ()
|
|
19
|
+
|
|
20
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
21
|
+
_expression: Optional[exp.Expression]
|
|
22
|
+
|
|
23
|
+
def from_(self, table: str) -> Self:
|
|
24
|
+
"""Set the target table for the DELETE statement.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
table: The table name to delete from.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The current builder instance for method chaining.
|
|
31
|
+
"""
|
|
32
|
+
if self._expression is None:
|
|
33
|
+
self._expression = exp.Delete()
|
|
34
|
+
if not isinstance(self._expression, exp.Delete):
|
|
35
|
+
current_expr_type = type(self._expression).__name__
|
|
36
|
+
msg = f"Base expression for Delete is {current_expr_type}, expected Delete."
|
|
37
|
+
raise SQLBuilderError(msg)
|
|
38
|
+
|
|
39
|
+
setattr(self, "_table", table)
|
|
40
|
+
self._expression.set("this", exp.to_table(table))
|
|
41
|
+
return self
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Insert operation mixins for SQL builders."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any, Optional, TypeVar, Union
|
|
5
|
+
|
|
6
|
+
from mypy_extensions import trait
|
|
7
|
+
from sqlglot import exp
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
11
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
12
|
+
|
|
13
|
+
BuilderT = TypeVar("BuilderT", bound=SQLBuilderProtocol)
|
|
14
|
+
|
|
15
|
+
__all__ = ("InsertFromSelectMixin", "InsertIntoClauseMixin", "InsertValuesMixin")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@trait
|
|
19
|
+
class InsertIntoClauseMixin:
|
|
20
|
+
"""Mixin providing INTO clause for INSERT builders."""
|
|
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 into(self, table: str) -> Self:
|
|
28
|
+
"""Set the target table for the INSERT statement.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
table: The name of the table to insert data into.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
SQLBuilderError: If the current expression is not an INSERT statement.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The current builder instance for method chaining.
|
|
38
|
+
"""
|
|
39
|
+
if self._expression is None:
|
|
40
|
+
self._expression = exp.Insert()
|
|
41
|
+
if not isinstance(self._expression, exp.Insert):
|
|
42
|
+
msg = "Cannot set target table on a non-INSERT expression."
|
|
43
|
+
raise SQLBuilderError(msg)
|
|
44
|
+
|
|
45
|
+
setattr(self, "_table", table)
|
|
46
|
+
self._expression.set("this", exp.to_table(table))
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@trait
|
|
51
|
+
class InsertValuesMixin:
|
|
52
|
+
"""Mixin providing VALUES and columns methods for INSERT builders."""
|
|
53
|
+
|
|
54
|
+
__slots__ = ()
|
|
55
|
+
|
|
56
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
57
|
+
_expression: Optional[exp.Expression]
|
|
58
|
+
|
|
59
|
+
_columns: Any # Provided by QueryBuilder
|
|
60
|
+
|
|
61
|
+
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
62
|
+
"""Add parameter - provided by QueryBuilder."""
|
|
63
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
64
|
+
raise NotImplementedError(msg)
|
|
65
|
+
|
|
66
|
+
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
67
|
+
"""Generate unique parameter name - provided by QueryBuilder."""
|
|
68
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
69
|
+
raise NotImplementedError(msg)
|
|
70
|
+
|
|
71
|
+
def columns(self, *columns: Union[str, exp.Expression]) -> Self:
|
|
72
|
+
"""Set the columns for the INSERT statement and synchronize the _columns attribute on the builder."""
|
|
73
|
+
if self._expression is None:
|
|
74
|
+
self._expression = exp.Insert()
|
|
75
|
+
if not isinstance(self._expression, exp.Insert):
|
|
76
|
+
msg = "Cannot set columns on a non-INSERT expression."
|
|
77
|
+
raise SQLBuilderError(msg)
|
|
78
|
+
column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
|
|
79
|
+
self._expression.set("columns", column_exprs)
|
|
80
|
+
try:
|
|
81
|
+
cols = self._columns
|
|
82
|
+
if not columns:
|
|
83
|
+
cols.clear()
|
|
84
|
+
else:
|
|
85
|
+
cols[:] = [col.name if isinstance(col, exp.Column) else str(col) for col in columns]
|
|
86
|
+
except AttributeError:
|
|
87
|
+
pass
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def values(self, *values: Any, **kwargs: Any) -> Self:
|
|
91
|
+
"""Add a row of values to the INSERT statement.
|
|
92
|
+
|
|
93
|
+
Supports:
|
|
94
|
+
- values(val1, val2, val3)
|
|
95
|
+
- values(col1=val1, col2=val2)
|
|
96
|
+
- values(mapping)
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
*values: Either positional values or a single mapping.
|
|
100
|
+
**kwargs: Column-value pairs.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
The current builder instance for method chaining.
|
|
104
|
+
"""
|
|
105
|
+
if self._expression is None:
|
|
106
|
+
self._expression = exp.Insert()
|
|
107
|
+
if not isinstance(self._expression, exp.Insert):
|
|
108
|
+
msg = "Cannot add values to a non-INSERT expression."
|
|
109
|
+
raise SQLBuilderError(msg)
|
|
110
|
+
|
|
111
|
+
if kwargs:
|
|
112
|
+
if values:
|
|
113
|
+
msg = "Cannot mix positional values with keyword values."
|
|
114
|
+
raise SQLBuilderError(msg)
|
|
115
|
+
try:
|
|
116
|
+
_columns = self._columns
|
|
117
|
+
if not _columns:
|
|
118
|
+
self.columns(*kwargs.keys())
|
|
119
|
+
except AttributeError:
|
|
120
|
+
pass
|
|
121
|
+
row_exprs = []
|
|
122
|
+
for col, val in kwargs.items():
|
|
123
|
+
if isinstance(val, exp.Expression):
|
|
124
|
+
row_exprs.append(val)
|
|
125
|
+
else:
|
|
126
|
+
column_name = col if isinstance(col, str) else str(col)
|
|
127
|
+
if "." in column_name:
|
|
128
|
+
column_name = column_name.split(".")[-1]
|
|
129
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
130
|
+
_, param_name = self.add_parameter(val, name=param_name)
|
|
131
|
+
row_exprs.append(exp.var(param_name))
|
|
132
|
+
elif len(values) == 1 and hasattr(values[0], "items"):
|
|
133
|
+
mapping = values[0]
|
|
134
|
+
try:
|
|
135
|
+
_columns = self._columns
|
|
136
|
+
if not _columns:
|
|
137
|
+
self.columns(*mapping.keys())
|
|
138
|
+
except AttributeError:
|
|
139
|
+
pass
|
|
140
|
+
row_exprs = []
|
|
141
|
+
for col, val in mapping.items():
|
|
142
|
+
if isinstance(val, exp.Expression):
|
|
143
|
+
row_exprs.append(val)
|
|
144
|
+
else:
|
|
145
|
+
column_name = col if isinstance(col, str) else str(col)
|
|
146
|
+
if "." in column_name:
|
|
147
|
+
column_name = column_name.split(".")[-1]
|
|
148
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
149
|
+
_, param_name = self.add_parameter(val, name=param_name)
|
|
150
|
+
row_exprs.append(exp.var(param_name))
|
|
151
|
+
else:
|
|
152
|
+
try:
|
|
153
|
+
_columns = self._columns
|
|
154
|
+
if _columns and len(values) != len(_columns):
|
|
155
|
+
msg = f"Number of values ({len(values)}) does not match the number of specified columns ({len(_columns)})."
|
|
156
|
+
raise SQLBuilderError(msg)
|
|
157
|
+
except AttributeError:
|
|
158
|
+
pass
|
|
159
|
+
row_exprs = []
|
|
160
|
+
for i, v in enumerate(values):
|
|
161
|
+
if isinstance(v, exp.Expression):
|
|
162
|
+
row_exprs.append(v)
|
|
163
|
+
else:
|
|
164
|
+
try:
|
|
165
|
+
_columns = self._columns
|
|
166
|
+
if _columns and i < len(_columns):
|
|
167
|
+
column_name = (
|
|
168
|
+
str(_columns[i]).split(".")[-1] if "." in str(_columns[i]) else str(_columns[i])
|
|
169
|
+
)
|
|
170
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
171
|
+
else:
|
|
172
|
+
param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
|
|
173
|
+
except AttributeError:
|
|
174
|
+
param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
|
|
175
|
+
_, param_name = self.add_parameter(v, name=param_name)
|
|
176
|
+
row_exprs.append(exp.var(param_name))
|
|
177
|
+
|
|
178
|
+
values_expr = exp.Values(expressions=[row_exprs])
|
|
179
|
+
self._expression.set("expression", values_expr)
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
def add_values(self, values: Sequence[Any]) -> Self:
|
|
183
|
+
"""Add a row of values to the INSERT statement (alternative signature).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
values: Sequence of values for the row.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The current builder instance for method chaining.
|
|
190
|
+
"""
|
|
191
|
+
return self.values(*values)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@trait
|
|
195
|
+
class InsertFromSelectMixin:
|
|
196
|
+
"""Mixin providing INSERT ... SELECT support for INSERT builders."""
|
|
197
|
+
|
|
198
|
+
__slots__ = ()
|
|
199
|
+
|
|
200
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
201
|
+
_expression: Optional[exp.Expression]
|
|
202
|
+
|
|
203
|
+
_table: Any # Provided by QueryBuilder
|
|
204
|
+
|
|
205
|
+
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
206
|
+
"""Add parameter - provided by QueryBuilder."""
|
|
207
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
208
|
+
raise NotImplementedError(msg)
|
|
209
|
+
|
|
210
|
+
def from_select(self, select_builder: Any) -> Self:
|
|
211
|
+
"""Sets the INSERT source to a SELECT statement.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
select_builder: A SelectBuilder instance representing the SELECT query.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
The current builder instance for method chaining.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
SQLBuilderError: If the table is not set or the select_builder is invalid.
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
if not self._table:
|
|
224
|
+
msg = "The target table must be set using .into() before adding values."
|
|
225
|
+
raise SQLBuilderError(msg)
|
|
226
|
+
except AttributeError:
|
|
227
|
+
msg = "The target table must be set using .into() before adding values."
|
|
228
|
+
raise SQLBuilderError(msg)
|
|
229
|
+
if self._expression is None:
|
|
230
|
+
self._expression = exp.Insert()
|
|
231
|
+
if not isinstance(self._expression, exp.Insert):
|
|
232
|
+
msg = "Cannot set INSERT source on a non-INSERT expression."
|
|
233
|
+
raise SQLBuilderError(msg)
|
|
234
|
+
subquery_parameters = select_builder._parameters
|
|
235
|
+
if subquery_parameters:
|
|
236
|
+
for p_name, p_value in subquery_parameters.items():
|
|
237
|
+
self.add_parameter(p_value, name=p_name)
|
|
238
|
+
select_expr = select_builder._expression
|
|
239
|
+
if select_expr and isinstance(select_expr, exp.Select):
|
|
240
|
+
self._expression.set("expression", select_expr.copy())
|
|
241
|
+
else:
|
|
242
|
+
msg = "SelectBuilder must have a valid SELECT expression."
|
|
243
|
+
raise SQLBuilderError(msg)
|
|
244
|
+
return self
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
2
|
+
|
|
3
|
+
from mypy_extensions import trait
|
|
4
|
+
from sqlglot import exp
|
|
5
|
+
from typing_extensions import Self
|
|
6
|
+
|
|
7
|
+
from sqlspec.builder._parsing_utils import parse_table_expression
|
|
8
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
9
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from sqlspec.core.statement import SQL
|
|
13
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
14
|
+
|
|
15
|
+
__all__ = ("JoinClauseMixin",)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@trait
|
|
19
|
+
class JoinClauseMixin:
|
|
20
|
+
"""Mixin providing JOIN clause methods for SELECT builders."""
|
|
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 join(
|
|
28
|
+
self,
|
|
29
|
+
table: Union[str, exp.Expression, Any],
|
|
30
|
+
on: Optional[Union[str, exp.Expression, "SQL"]] = None,
|
|
31
|
+
alias: Optional[str] = None,
|
|
32
|
+
join_type: str = "INNER",
|
|
33
|
+
) -> Self:
|
|
34
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
35
|
+
if builder._expression is None:
|
|
36
|
+
builder._expression = exp.Select()
|
|
37
|
+
if not isinstance(builder._expression, exp.Select):
|
|
38
|
+
msg = "JOIN clause is only supported for SELECT statements."
|
|
39
|
+
raise SQLBuilderError(msg)
|
|
40
|
+
table_expr: exp.Expression
|
|
41
|
+
if isinstance(table, str):
|
|
42
|
+
table_expr = parse_table_expression(table, alias)
|
|
43
|
+
elif has_query_builder_parameters(table):
|
|
44
|
+
if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
|
|
45
|
+
table_expr_value = getattr(table, "_expression", None)
|
|
46
|
+
if table_expr_value is not None:
|
|
47
|
+
subquery_exp = exp.paren(table_expr_value)
|
|
48
|
+
else:
|
|
49
|
+
subquery_exp = exp.paren(exp.Anonymous(this=""))
|
|
50
|
+
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
51
|
+
else:
|
|
52
|
+
subquery = table.build()
|
|
53
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
54
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
|
|
55
|
+
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
56
|
+
else:
|
|
57
|
+
table_expr = table
|
|
58
|
+
on_expr: Optional[exp.Expression] = None
|
|
59
|
+
if on is not None:
|
|
60
|
+
if isinstance(on, str):
|
|
61
|
+
on_expr = exp.condition(on)
|
|
62
|
+
elif hasattr(on, "expression") and hasattr(on, "sql"):
|
|
63
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
64
|
+
expression = getattr(on, "expression", None)
|
|
65
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
66
|
+
# Merge parameters from SQL object into builder
|
|
67
|
+
if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
|
|
68
|
+
sql_parameters = getattr(on, "parameters", {})
|
|
69
|
+
for param_name, param_value in sql_parameters.items():
|
|
70
|
+
builder.add_parameter(param_value, name=param_name)
|
|
71
|
+
on_expr = expression
|
|
72
|
+
else:
|
|
73
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
74
|
+
sql_text = getattr(on, "sql", "")
|
|
75
|
+
# Merge parameters even when parsing raw SQL
|
|
76
|
+
if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
|
|
77
|
+
sql_parameters = getattr(on, "parameters", {})
|
|
78
|
+
for param_name, param_value in sql_parameters.items():
|
|
79
|
+
builder.add_parameter(param_value, name=param_name)
|
|
80
|
+
on_expr = exp.maybe_parse(sql_text) or exp.condition(str(sql_text))
|
|
81
|
+
# For other types (should be exp.Expression)
|
|
82
|
+
elif isinstance(on, exp.Expression):
|
|
83
|
+
on_expr = on
|
|
84
|
+
else:
|
|
85
|
+
# Last resort - convert to string and parse
|
|
86
|
+
on_expr = exp.condition(str(on))
|
|
87
|
+
join_type_upper = join_type.upper()
|
|
88
|
+
if join_type_upper == "INNER":
|
|
89
|
+
join_expr = exp.Join(this=table_expr, on=on_expr)
|
|
90
|
+
elif join_type_upper == "LEFT":
|
|
91
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="LEFT")
|
|
92
|
+
elif join_type_upper == "RIGHT":
|
|
93
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="RIGHT")
|
|
94
|
+
elif join_type_upper == "FULL":
|
|
95
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="FULL", kind="OUTER")
|
|
96
|
+
else:
|
|
97
|
+
msg = f"Unsupported join type: {join_type}"
|
|
98
|
+
raise SQLBuilderError(msg)
|
|
99
|
+
builder._expression = builder._expression.join(join_expr, copy=False)
|
|
100
|
+
return cast("Self", builder)
|
|
101
|
+
|
|
102
|
+
def inner_join(
|
|
103
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
|
|
104
|
+
) -> Self:
|
|
105
|
+
return self.join(table, on, alias, "INNER")
|
|
106
|
+
|
|
107
|
+
def left_join(
|
|
108
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
|
|
109
|
+
) -> Self:
|
|
110
|
+
return self.join(table, on, alias, "LEFT")
|
|
111
|
+
|
|
112
|
+
def right_join(
|
|
113
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
|
|
114
|
+
) -> Self:
|
|
115
|
+
return self.join(table, on, alias, "RIGHT")
|
|
116
|
+
|
|
117
|
+
def full_join(
|
|
118
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
|
|
119
|
+
) -> Self:
|
|
120
|
+
return self.join(table, on, alias, "FULL")
|
|
121
|
+
|
|
122
|
+
def cross_join(self, table: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
|
|
123
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
124
|
+
if builder._expression is None:
|
|
125
|
+
builder._expression = exp.Select()
|
|
126
|
+
if not isinstance(builder._expression, exp.Select):
|
|
127
|
+
msg = "Cannot add cross join to a non-SELECT expression."
|
|
128
|
+
raise SQLBuilderError(msg)
|
|
129
|
+
table_expr: exp.Expression
|
|
130
|
+
if isinstance(table, str):
|
|
131
|
+
table_expr = parse_table_expression(table, alias)
|
|
132
|
+
elif has_query_builder_parameters(table):
|
|
133
|
+
if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
|
|
134
|
+
table_expr_value = getattr(table, "_expression", None)
|
|
135
|
+
if table_expr_value is not None:
|
|
136
|
+
subquery_exp = exp.paren(table_expr_value)
|
|
137
|
+
else:
|
|
138
|
+
subquery_exp = exp.paren(exp.Anonymous(this=""))
|
|
139
|
+
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
140
|
+
else:
|
|
141
|
+
subquery = table.build()
|
|
142
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
143
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
|
|
144
|
+
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
145
|
+
else:
|
|
146
|
+
table_expr = table
|
|
147
|
+
join_expr = exp.Join(this=table_expr, kind="CROSS")
|
|
148
|
+
builder._expression = builder._expression.join(join_expr, copy=False)
|
|
149
|
+
return cast("Self", builder)
|