sqlspec 0.12.2__py3-none-any.whl → 0.13.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/_sql.py +21 -180
- sqlspec/adapters/adbc/config.py +10 -12
- sqlspec/adapters/adbc/driver.py +120 -118
- sqlspec/adapters/aiosqlite/config.py +3 -3
- sqlspec/adapters/aiosqlite/driver.py +100 -130
- sqlspec/adapters/asyncmy/config.py +3 -4
- sqlspec/adapters/asyncmy/driver.py +123 -135
- sqlspec/adapters/asyncpg/config.py +3 -7
- sqlspec/adapters/asyncpg/driver.py +98 -140
- sqlspec/adapters/bigquery/config.py +4 -5
- sqlspec/adapters/bigquery/driver.py +125 -167
- sqlspec/adapters/duckdb/config.py +3 -6
- sqlspec/adapters/duckdb/driver.py +114 -111
- sqlspec/adapters/oracledb/config.py +6 -5
- sqlspec/adapters/oracledb/driver.py +242 -259
- sqlspec/adapters/psqlpy/config.py +3 -7
- sqlspec/adapters/psqlpy/driver.py +118 -93
- sqlspec/adapters/psycopg/config.py +18 -31
- sqlspec/adapters/psycopg/driver.py +283 -236
- sqlspec/adapters/sqlite/config.py +3 -3
- sqlspec/adapters/sqlite/driver.py +103 -97
- sqlspec/config.py +0 -4
- sqlspec/driver/_async.py +89 -98
- sqlspec/driver/_common.py +52 -17
- sqlspec/driver/_sync.py +81 -105
- sqlspec/driver/connection.py +207 -0
- sqlspec/driver/mixins/_csv_writer.py +91 -0
- sqlspec/driver/mixins/_pipeline.py +38 -49
- sqlspec/driver/mixins/_result_utils.py +27 -9
- sqlspec/driver/mixins/_storage.py +67 -181
- sqlspec/driver/mixins/_type_coercion.py +3 -4
- sqlspec/driver/parameters.py +138 -0
- sqlspec/exceptions.py +10 -2
- sqlspec/extensions/aiosql/adapter.py +0 -10
- sqlspec/extensions/litestar/handlers.py +0 -1
- sqlspec/extensions/litestar/plugin.py +0 -3
- sqlspec/extensions/litestar/providers.py +0 -14
- sqlspec/loader.py +25 -90
- sqlspec/protocols.py +542 -0
- sqlspec/service/__init__.py +3 -2
- sqlspec/service/_util.py +147 -0
- sqlspec/service/base.py +1116 -9
- sqlspec/statement/builder/__init__.py +42 -32
- sqlspec/statement/builder/_ddl_utils.py +0 -10
- sqlspec/statement/builder/_parsing_utils.py +10 -4
- sqlspec/statement/builder/base.py +67 -22
- sqlspec/statement/builder/column.py +283 -0
- sqlspec/statement/builder/ddl.py +91 -67
- sqlspec/statement/builder/delete.py +23 -7
- sqlspec/statement/builder/insert.py +29 -15
- sqlspec/statement/builder/merge.py +4 -4
- sqlspec/statement/builder/mixins/_aggregate_functions.py +113 -14
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -1
- sqlspec/statement/builder/mixins/_delete_from.py +1 -1
- sqlspec/statement/builder/mixins/_from.py +10 -8
- sqlspec/statement/builder/mixins/_group_by.py +0 -1
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -1
- sqlspec/statement/builder/mixins/_insert_values.py +0 -2
- sqlspec/statement/builder/mixins/_join.py +20 -13
- sqlspec/statement/builder/mixins/_limit_offset.py +3 -3
- sqlspec/statement/builder/mixins/_merge_clauses.py +3 -4
- sqlspec/statement/builder/mixins/_order_by.py +2 -2
- sqlspec/statement/builder/mixins/_pivot.py +4 -7
- sqlspec/statement/builder/mixins/_select_columns.py +6 -5
- sqlspec/statement/builder/mixins/_unpivot.py +6 -9
- sqlspec/statement/builder/mixins/_update_from.py +2 -1
- sqlspec/statement/builder/mixins/_update_set.py +11 -8
- sqlspec/statement/builder/mixins/_where.py +61 -34
- sqlspec/statement/builder/select.py +32 -17
- sqlspec/statement/builder/update.py +25 -11
- sqlspec/statement/filters.py +39 -14
- sqlspec/statement/parameter_manager.py +220 -0
- sqlspec/statement/parameters.py +210 -79
- sqlspec/statement/pipelines/__init__.py +166 -23
- sqlspec/statement/pipelines/analyzers/_analyzer.py +21 -20
- sqlspec/statement/pipelines/context.py +35 -39
- sqlspec/statement/pipelines/transformers/__init__.py +2 -3
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +19 -187
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +628 -58
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +76 -0
- sqlspec/statement/pipelines/validators/_dml_safety.py +33 -18
- sqlspec/statement/pipelines/validators/_parameter_style.py +87 -14
- sqlspec/statement/pipelines/validators/_performance.py +38 -23
- sqlspec/statement/pipelines/validators/_security.py +39 -62
- sqlspec/statement/result.py +37 -129
- sqlspec/statement/splitter.py +0 -12
- sqlspec/statement/sql.py +863 -391
- sqlspec/statement/sql_compiler.py +140 -0
- sqlspec/storage/__init__.py +10 -2
- sqlspec/storage/backends/fsspec.py +53 -8
- sqlspec/storage/backends/obstore.py +15 -19
- sqlspec/storage/capabilities.py +101 -0
- sqlspec/storage/registry.py +56 -83
- sqlspec/typing.py +6 -434
- sqlspec/utils/cached_property.py +25 -0
- sqlspec/utils/correlation.py +0 -2
- sqlspec/utils/logging.py +0 -6
- sqlspec/utils/sync_tools.py +0 -4
- sqlspec/utils/text.py +0 -5
- sqlspec/utils/type_guards.py +892 -0
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.0.dist-info}/METADATA +1 -1
- sqlspec-0.13.0.dist-info/RECORD +150 -0
- sqlspec/statement/builder/protocols.py +0 -20
- sqlspec/statement/pipelines/base.py +0 -315
- sqlspec/statement/pipelines/result_types.py +0 -41
- sqlspec/statement/pipelines/transformers/_remove_comments.py +0 -66
- sqlspec/statement/pipelines/transformers/_remove_hints.py +0 -81
- sqlspec/statement/pipelines/validators/base.py +0 -67
- sqlspec/storage/protocol.py +0 -173
- sqlspec-0.12.2.dist-info/RECORD +0 -145
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -20,11 +20,11 @@ from sqlspec.statement.builder.mixins import (
|
|
|
20
20
|
from sqlspec.statement.result import SQLResult
|
|
21
21
|
from sqlspec.typing import RowT
|
|
22
22
|
|
|
23
|
-
__all__ = ("
|
|
23
|
+
__all__ = ("Merge",)
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
@dataclass(unsafe_hash=True)
|
|
27
|
-
class
|
|
27
|
+
class Merge(
|
|
28
28
|
QueryBuilder[RowT],
|
|
29
29
|
MergeUsingClauseMixin,
|
|
30
30
|
MergeOnClauseMixin,
|
|
@@ -42,7 +42,7 @@ class MergeBuilder(
|
|
|
42
42
|
```python
|
|
43
43
|
# Basic MERGE statement
|
|
44
44
|
merge_query = (
|
|
45
|
-
|
|
45
|
+
Merge()
|
|
46
46
|
.into("target_table")
|
|
47
47
|
.using("source_table", "src")
|
|
48
48
|
.on("target_table.id = src.id")
|
|
@@ -64,7 +64,7 @@ class MergeBuilder(
|
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
merge_query = (
|
|
67
|
-
|
|
67
|
+
Merge()
|
|
68
68
|
.into("users")
|
|
69
69
|
.using(source_query, "src")
|
|
70
70
|
.on("users.email = src.email")
|
|
@@ -4,7 +4,7 @@ from sqlglot import exp
|
|
|
4
4
|
from typing_extensions import Self
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
|
-
from sqlspec.
|
|
7
|
+
from sqlspec.protocols import SelectBuilderProtocol
|
|
8
8
|
|
|
9
9
|
__all__ = ("AggregateFunctionsMixin",)
|
|
10
10
|
|
|
@@ -112,40 +112,139 @@ class AggregateFunctionsMixin:
|
|
|
112
112
|
select_expr = exp.alias_(array_agg_expr, alias) if alias else array_agg_expr
|
|
113
113
|
return cast("Self", builder.select(select_expr))
|
|
114
114
|
|
|
115
|
-
def
|
|
116
|
-
"""Add
|
|
115
|
+
def count_distinct(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
116
|
+
"""Add COUNT(DISTINCT column) to SELECT clause.
|
|
117
117
|
|
|
118
118
|
Args:
|
|
119
|
-
column: The
|
|
119
|
+
column: The column to count distinct values of.
|
|
120
|
+
alias: Optional alias for the count.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The current builder instance for method chaining.
|
|
124
|
+
"""
|
|
125
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
126
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
127
|
+
count_expr = exp.Count(this=exp.Distinct(expressions=[col_expr]))
|
|
128
|
+
select_expr = exp.alias_(count_expr, alias) if alias else count_expr
|
|
129
|
+
return cast("Self", builder.select(select_expr))
|
|
130
|
+
|
|
131
|
+
def stddev(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
132
|
+
"""Add STDDEV aggregate function to SELECT clause.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
column: The column to calculate standard deviation of.
|
|
120
136
|
alias: Optional alias for the result.
|
|
121
137
|
|
|
122
138
|
Returns:
|
|
123
139
|
The current builder instance for method chaining.
|
|
140
|
+
"""
|
|
141
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
142
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
143
|
+
stddev_expr = exp.Stddev(this=col_expr)
|
|
144
|
+
select_expr = exp.alias_(stddev_expr, alias) if alias else stddev_expr
|
|
145
|
+
return cast("Self", builder.select(select_expr))
|
|
124
146
|
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
def stddev_pop(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
148
|
+
"""Add STDDEV_POP aggregate function to SELECT clause.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
column: The column to calculate population standard deviation of.
|
|
152
|
+
alias: Optional alias for the result.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The current builder instance for method chaining.
|
|
127
156
|
"""
|
|
128
157
|
builder = cast("SelectBuilderProtocol", self)
|
|
129
158
|
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
130
|
-
|
|
131
|
-
select_expr = exp.alias_(
|
|
159
|
+
stddev_pop_expr = exp.StddevPop(this=col_expr)
|
|
160
|
+
select_expr = exp.alias_(stddev_pop_expr, alias) if alias else stddev_pop_expr
|
|
132
161
|
return cast("Self", builder.select(select_expr))
|
|
133
162
|
|
|
134
|
-
def
|
|
135
|
-
"""Add
|
|
163
|
+
def stddev_samp(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
164
|
+
"""Add STDDEV_SAMP aggregate function to SELECT clause.
|
|
136
165
|
|
|
137
166
|
Args:
|
|
138
|
-
column: The
|
|
167
|
+
column: The column to calculate sample standard deviation of.
|
|
168
|
+
alias: Optional alias for the result.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The current builder instance for method chaining.
|
|
172
|
+
"""
|
|
173
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
174
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
175
|
+
stddev_samp_expr = exp.StddevSamp(this=col_expr)
|
|
176
|
+
select_expr = exp.alias_(stddev_samp_expr, alias) if alias else stddev_samp_expr
|
|
177
|
+
return cast("Self", builder.select(select_expr))
|
|
178
|
+
|
|
179
|
+
def variance(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
180
|
+
"""Add VARIANCE aggregate function to SELECT clause.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
column: The column to calculate variance of.
|
|
184
|
+
alias: Optional alias for the result.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
The current builder instance for method chaining.
|
|
188
|
+
"""
|
|
189
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
190
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
191
|
+
variance_expr = exp.Variance(this=col_expr)
|
|
192
|
+
select_expr = exp.alias_(variance_expr, alias) if alias else variance_expr
|
|
193
|
+
return cast("Self", builder.select(select_expr))
|
|
194
|
+
|
|
195
|
+
def var_pop(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
196
|
+
"""Add VAR_POP aggregate function to SELECT clause.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
column: The column to calculate population variance of.
|
|
200
|
+
alias: Optional alias for the result.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
The current builder instance for method chaining.
|
|
204
|
+
"""
|
|
205
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
206
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
207
|
+
var_pop_expr = exp.VariancePop(this=col_expr)
|
|
208
|
+
select_expr = exp.alias_(var_pop_expr, alias) if alias else var_pop_expr
|
|
209
|
+
return cast("Self", builder.select(select_expr))
|
|
210
|
+
|
|
211
|
+
def string_agg(self, column: Union[str, exp.Expression], separator: str = ",", alias: Optional[str] = None) -> Self:
|
|
212
|
+
"""Add STRING_AGG aggregate function to SELECT clause.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
column: The column to aggregate into a string.
|
|
216
|
+
separator: The separator between values (default is comma).
|
|
139
217
|
alias: Optional alias for the result.
|
|
140
218
|
|
|
141
219
|
Returns:
|
|
142
220
|
The current builder instance for method chaining.
|
|
143
221
|
|
|
144
222
|
Note:
|
|
145
|
-
|
|
223
|
+
Different databases have different names for this function:
|
|
224
|
+
- PostgreSQL: STRING_AGG
|
|
225
|
+
- MySQL: GROUP_CONCAT
|
|
226
|
+
- SQLite: GROUP_CONCAT
|
|
227
|
+
SQLGlot will handle the translation.
|
|
228
|
+
"""
|
|
229
|
+
builder = cast("SelectBuilderProtocol", self)
|
|
230
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
231
|
+
# Use GroupConcat which SQLGlot can translate to STRING_AGG for Postgres
|
|
232
|
+
string_agg_expr = exp.GroupConcat(this=col_expr, separator=exp.Literal.string(separator))
|
|
233
|
+
select_expr = exp.alias_(string_agg_expr, alias) if alias else string_agg_expr
|
|
234
|
+
return cast("Self", builder.select(select_expr))
|
|
235
|
+
|
|
236
|
+
def json_agg(self, column: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
237
|
+
"""Add JSON_AGG aggregate function to SELECT clause.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
column: The column to aggregate into a JSON array.
|
|
241
|
+
alias: Optional alias for the result.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The current builder instance for method chaining.
|
|
146
245
|
"""
|
|
147
246
|
builder = cast("SelectBuilderProtocol", self)
|
|
148
247
|
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
149
|
-
|
|
150
|
-
select_expr = exp.alias_(
|
|
248
|
+
json_agg_expr = exp.JSONArrayAgg(this=col_expr)
|
|
249
|
+
select_expr = exp.alias_(json_agg_expr, alias) if alias else json_agg_expr
|
|
151
250
|
return cast("Self", builder.select(select_expr))
|
|
@@ -61,7 +61,6 @@ class CommonTableExpressionMixin:
|
|
|
61
61
|
msg = f"Could not parse CTE query: {query}"
|
|
62
62
|
raise SQLBuilderError(msg)
|
|
63
63
|
|
|
64
|
-
# Create a proper CTE with table alias
|
|
65
64
|
if columns:
|
|
66
65
|
# CTE with explicit column list: name(col1, col2, ...)
|
|
67
66
|
cte_alias_expr = exp.alias_(cte_expr, name, table=[exp.to_identifier(col) for col in columns])
|
|
@@ -26,7 +26,7 @@ class DeleteFromClauseMixin:
|
|
|
26
26
|
self._expression = exp.Delete()
|
|
27
27
|
if not isinstance(self._expression, exp.Delete):
|
|
28
28
|
current_expr_type = type(self._expression).__name__
|
|
29
|
-
msg = f"Base expression for
|
|
29
|
+
msg = f"Base expression for Delete is {current_expr_type}, expected Delete."
|
|
30
30
|
raise SQLBuilderError(msg)
|
|
31
31
|
|
|
32
32
|
setattr(self, "_table", table)
|
|
@@ -5,10 +5,10 @@ from typing_extensions import Self
|
|
|
5
5
|
|
|
6
6
|
from sqlspec.exceptions import SQLBuilderError
|
|
7
7
|
from sqlspec.statement.builder._parsing_utils import parse_table_expression
|
|
8
|
-
from sqlspec.
|
|
8
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters, is_expression
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
-
from sqlspec.
|
|
11
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
12
12
|
|
|
13
13
|
__all__ = ("FromClauseMixin",)
|
|
14
14
|
|
|
@@ -29,7 +29,7 @@ class FromClauseMixin:
|
|
|
29
29
|
Returns:
|
|
30
30
|
The current builder instance for method chaining.
|
|
31
31
|
"""
|
|
32
|
-
builder = cast("
|
|
32
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
33
33
|
if builder._expression is None:
|
|
34
34
|
builder._expression = exp.Select()
|
|
35
35
|
if not isinstance(builder._expression, exp.Select):
|
|
@@ -41,16 +41,18 @@ class FromClauseMixin:
|
|
|
41
41
|
elif is_expression(table):
|
|
42
42
|
# Direct sqlglot expression - use as is
|
|
43
43
|
from_expr = exp.alias_(table, alias) if alias else table
|
|
44
|
-
elif
|
|
44
|
+
elif has_query_builder_parameters(table):
|
|
45
45
|
# Query builder with build() method
|
|
46
|
-
subquery = table.build()
|
|
47
|
-
|
|
46
|
+
subquery = table.build()
|
|
47
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
48
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
|
|
48
49
|
from_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
49
50
|
current_params = getattr(builder, "_parameters", None)
|
|
50
51
|
merged_params = getattr(type(builder), "ParameterConverter", None)
|
|
51
|
-
if merged_params:
|
|
52
|
+
if merged_params and hasattr(subquery, "parameters"):
|
|
53
|
+
subquery_params = getattr(subquery, "parameters", {})
|
|
52
54
|
merged_params = merged_params.merge_parameters(
|
|
53
|
-
parameters=
|
|
55
|
+
parameters=subquery_params,
|
|
54
56
|
args=current_params if isinstance(current_params, list) else None,
|
|
55
57
|
kwargs=current_params if isinstance(current_params, dict) else {},
|
|
56
58
|
)
|
|
@@ -106,7 +106,6 @@ class GroupByClauseMixin:
|
|
|
106
106
|
for column_set in column_sets:
|
|
107
107
|
if isinstance(column_set, (tuple, list)):
|
|
108
108
|
if len(column_set) == 0:
|
|
109
|
-
# Empty set for grand total
|
|
110
109
|
set_expressions.append(exp.Tuple(expressions=[]))
|
|
111
110
|
else:
|
|
112
111
|
columns = [exp.column(col) for col in column_set]
|
|
@@ -38,7 +38,6 @@ class InsertFromSelectMixin:
|
|
|
38
38
|
if subquery_params:
|
|
39
39
|
for p_name, p_value in subquery_params.items():
|
|
40
40
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
41
|
-
# Set the SELECT expression as the source
|
|
42
41
|
select_expr = getattr(select_builder, "_expression", None)
|
|
43
42
|
if select_expr and isinstance(select_expr, exp.Select):
|
|
44
43
|
self._expression.set("expression", select_expr.copy())
|
|
@@ -39,7 +39,6 @@ class InsertValuesMixin:
|
|
|
39
39
|
if not isinstance(self._expression, exp.Insert):
|
|
40
40
|
msg = "Cannot add values to a non-INSERT expression."
|
|
41
41
|
raise SQLBuilderError(msg)
|
|
42
|
-
# Validate value count if _columns is present and non-empty
|
|
43
42
|
if (
|
|
44
43
|
hasattr(self, "_columns") and getattr(self, "_columns", []) and len(values) != len(self._columns) # pyright: ignore
|
|
45
44
|
):
|
|
@@ -50,7 +49,6 @@ class InsertValuesMixin:
|
|
|
50
49
|
if isinstance(v, exp.Expression):
|
|
51
50
|
row_exprs.append(v)
|
|
52
51
|
else:
|
|
53
|
-
# Add as parameter
|
|
54
52
|
_, param_name = self.add_parameter(v) # type: ignore[attr-defined]
|
|
55
53
|
row_exprs.append(exp.var(param_name))
|
|
56
54
|
values_expr = exp.Values(expressions=[row_exprs])
|
|
@@ -5,9 +5,10 @@ from typing_extensions import Self
|
|
|
5
5
|
|
|
6
6
|
from sqlspec.exceptions import SQLBuilderError
|
|
7
7
|
from sqlspec.statement.builder._parsing_utils import parse_table_expression
|
|
8
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
10
|
-
from sqlspec.
|
|
11
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
11
12
|
|
|
12
13
|
__all__ = ("JoinClauseMixin",)
|
|
13
14
|
|
|
@@ -22,7 +23,7 @@ class JoinClauseMixin:
|
|
|
22
23
|
alias: Optional[str] = None,
|
|
23
24
|
join_type: str = "INNER",
|
|
24
25
|
) -> Self:
|
|
25
|
-
builder = cast("
|
|
26
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
26
27
|
if builder._expression is None:
|
|
27
28
|
builder._expression = exp.Select()
|
|
28
29
|
if not isinstance(builder._expression, exp.Select):
|
|
@@ -31,16 +32,19 @@ class JoinClauseMixin:
|
|
|
31
32
|
table_expr: exp.Expression
|
|
32
33
|
if isinstance(table, str):
|
|
33
34
|
table_expr = parse_table_expression(table, alias)
|
|
34
|
-
elif
|
|
35
|
-
# Handle builder objects with build() method
|
|
35
|
+
elif has_query_builder_parameters(table):
|
|
36
36
|
# Work directly with AST when possible to avoid string parsing
|
|
37
37
|
if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
|
|
38
|
-
|
|
38
|
+
table_expr_value = getattr(table, "_expression", None)
|
|
39
|
+
if table_expr_value is not None:
|
|
40
|
+
subquery_exp = exp.paren(table_expr_value.copy()) # pyright: ignore
|
|
41
|
+
else:
|
|
42
|
+
subquery_exp = exp.paren(exp.Anonymous(this=""))
|
|
39
43
|
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
40
44
|
else:
|
|
41
|
-
# Fallback to string parsing
|
|
42
45
|
subquery = table.build() # pyright: ignore
|
|
43
|
-
|
|
46
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
47
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
|
|
44
48
|
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
45
49
|
# Parameter merging logic can be added here if needed
|
|
46
50
|
else:
|
|
@@ -84,7 +88,7 @@ class JoinClauseMixin:
|
|
|
84
88
|
return self.join(table, on, alias, "FULL")
|
|
85
89
|
|
|
86
90
|
def cross_join(self, table: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
|
|
87
|
-
builder = cast("
|
|
91
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
88
92
|
if builder._expression is None:
|
|
89
93
|
builder._expression = exp.Select()
|
|
90
94
|
if not isinstance(builder._expression, exp.Select):
|
|
@@ -93,15 +97,18 @@ class JoinClauseMixin:
|
|
|
93
97
|
table_expr: exp.Expression
|
|
94
98
|
if isinstance(table, str):
|
|
95
99
|
table_expr = parse_table_expression(table, alias)
|
|
96
|
-
elif
|
|
97
|
-
# Handle builder objects with build() method
|
|
100
|
+
elif has_query_builder_parameters(table):
|
|
98
101
|
if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
|
|
99
|
-
|
|
102
|
+
table_expr_value = getattr(table, "_expression", None)
|
|
103
|
+
if table_expr_value is not None:
|
|
104
|
+
subquery_exp = exp.paren(table_expr_value.copy()) # pyright: ignore
|
|
105
|
+
else:
|
|
106
|
+
subquery_exp = exp.paren(exp.Anonymous(this=""))
|
|
100
107
|
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
101
108
|
else:
|
|
102
|
-
# Fallback to string parsing
|
|
103
109
|
subquery = table.build() # pyright: ignore
|
|
104
|
-
|
|
110
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
111
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
|
|
105
112
|
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
106
113
|
else:
|
|
107
114
|
table_expr = table
|
|
@@ -4,7 +4,7 @@ from sqlglot import exp
|
|
|
4
4
|
from typing_extensions import Self
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
|
-
from sqlspec.
|
|
7
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
8
8
|
|
|
9
9
|
from sqlspec.exceptions import SQLBuilderError
|
|
10
10
|
|
|
@@ -26,7 +26,7 @@ class LimitOffsetClauseMixin:
|
|
|
26
26
|
Returns:
|
|
27
27
|
The current builder instance for method chaining.
|
|
28
28
|
"""
|
|
29
|
-
builder = cast("
|
|
29
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
30
30
|
if not isinstance(builder._expression, exp.Select):
|
|
31
31
|
msg = "LIMIT is only supported for SELECT statements."
|
|
32
32
|
raise SQLBuilderError(msg)
|
|
@@ -45,7 +45,7 @@ class LimitOffsetClauseMixin:
|
|
|
45
45
|
Returns:
|
|
46
46
|
The current builder instance for method chaining.
|
|
47
47
|
"""
|
|
48
|
-
builder = cast("
|
|
48
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
49
49
|
if not isinstance(builder._expression, exp.Select):
|
|
50
50
|
msg = "OFFSET is only supported for SELECT statements."
|
|
51
51
|
raise SQLBuilderError(msg)
|
|
@@ -4,6 +4,7 @@ from sqlglot import exp
|
|
|
4
4
|
from typing_extensions import Self
|
|
5
5
|
|
|
6
6
|
from sqlspec.exceptions import SQLBuilderError
|
|
7
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
7
8
|
|
|
8
9
|
__all__ = (
|
|
9
10
|
"MergeIntoClauseMixin",
|
|
@@ -66,9 +67,9 @@ class MergeUsingClauseMixin:
|
|
|
66
67
|
source_expr: exp.Expression
|
|
67
68
|
if isinstance(source, str):
|
|
68
69
|
source_expr = exp.to_table(source, alias=alias)
|
|
69
|
-
elif
|
|
70
|
+
elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
|
|
70
71
|
# Merge parameters from the SELECT builder or other builder
|
|
71
|
-
subquery_builder_params =
|
|
72
|
+
subquery_builder_params = source.parameters
|
|
72
73
|
if subquery_builder_params:
|
|
73
74
|
for p_name, p_value in subquery_builder_params.items():
|
|
74
75
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
@@ -145,13 +146,11 @@ class MergeMatchedClauseMixin:
|
|
|
145
146
|
if not isinstance(self._expression, exp.Merge):
|
|
146
147
|
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
147
148
|
|
|
148
|
-
# Get or create the whens object
|
|
149
149
|
whens = self._expression.args.get("whens")
|
|
150
150
|
if not whens:
|
|
151
151
|
whens = exp.Whens(expressions=[])
|
|
152
152
|
self._expression.set("whens", whens)
|
|
153
153
|
|
|
154
|
-
# Add the when clause to the whens expressions using SQLGlot's append method
|
|
155
154
|
whens.append("expressions", when_clause)
|
|
156
155
|
|
|
157
156
|
def when_matched_then_update(
|
|
@@ -7,7 +7,7 @@ from sqlspec.exceptions import SQLBuilderError
|
|
|
7
7
|
from sqlspec.statement.builder._parsing_utils import parse_order_expression
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
|
-
from sqlspec.
|
|
10
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
11
11
|
|
|
12
12
|
__all__ = ("OrderByClauseMixin",)
|
|
13
13
|
|
|
@@ -28,7 +28,7 @@ class OrderByClauseMixin:
|
|
|
28
28
|
Returns:
|
|
29
29
|
The current builder instance for method chaining.
|
|
30
30
|
"""
|
|
31
|
-
builder = cast("
|
|
31
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
32
32
|
if not isinstance(builder._expression, exp.Select):
|
|
33
33
|
msg = "ORDER BY is only supported for SELECT statements."
|
|
34
34
|
raise SQLBuilderError(msg)
|
|
@@ -5,13 +5,13 @@ from sqlglot import exp
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from sqlglot.dialects.dialect import DialectType
|
|
7
7
|
|
|
8
|
-
from sqlspec.statement.builder.select import
|
|
8
|
+
from sqlspec.statement.builder.select import Select
|
|
9
9
|
|
|
10
10
|
__all__ = ("PivotClauseMixin",)
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class PivotClauseMixin:
|
|
14
|
-
"""Mixin class to add PIVOT functionality to a
|
|
14
|
+
"""Mixin class to add PIVOT functionality to a Select."""
|
|
15
15
|
|
|
16
16
|
_expression: "Optional[exp.Expression]" = None
|
|
17
17
|
dialect: "DialectType" = None
|
|
@@ -23,7 +23,7 @@ class PivotClauseMixin:
|
|
|
23
23
|
pivot_column: Union[str, exp.Expression],
|
|
24
24
|
pivot_values: list[Union[str, int, float, exp.Expression]],
|
|
25
25
|
alias: Optional[str] = None,
|
|
26
|
-
) -> "
|
|
26
|
+
) -> "Select":
|
|
27
27
|
"""Adds a PIVOT clause to the SELECT statement.
|
|
28
28
|
|
|
29
29
|
Example:
|
|
@@ -61,7 +61,6 @@ class PivotClauseMixin:
|
|
|
61
61
|
else:
|
|
62
62
|
pivot_value_exprs.append(exp.Literal.string(str(val)))
|
|
63
63
|
|
|
64
|
-
# Create the pivot expression with proper fields structure
|
|
65
64
|
in_expr = exp.In(this=pivot_col_expr, expressions=pivot_value_exprs)
|
|
66
65
|
|
|
67
66
|
pivot_node = exp.Pivot(expressions=[pivot_agg_expr], fields=[in_expr], unpivot=False)
|
|
@@ -69,14 +68,12 @@ class PivotClauseMixin:
|
|
|
69
68
|
if alias:
|
|
70
69
|
pivot_node.set("alias", exp.TableAlias(this=exp.to_identifier(alias)))
|
|
71
70
|
|
|
72
|
-
# Add pivot to the table in the FROM clause
|
|
73
71
|
from_clause = current_expr.args.get("from")
|
|
74
72
|
if from_clause and isinstance(from_clause, exp.From):
|
|
75
73
|
table = from_clause.this
|
|
76
74
|
if isinstance(table, exp.Table):
|
|
77
|
-
# Add to pivots array
|
|
78
75
|
existing_pivots = table.args.get("pivots", [])
|
|
79
76
|
existing_pivots.append(pivot_node)
|
|
80
77
|
table.set("pivots", existing_pivots)
|
|
81
78
|
|
|
82
|
-
return cast("
|
|
79
|
+
return cast("Select", self)
|
|
@@ -7,7 +7,8 @@ from sqlspec.exceptions import SQLBuilderError
|
|
|
7
7
|
from sqlspec.statement.builder._parsing_utils import parse_column_expression
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
|
-
from sqlspec.
|
|
10
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
11
|
+
from sqlspec.statement.builder.column import Column, FunctionColumn
|
|
11
12
|
|
|
12
13
|
__all__ = ("SelectColumnsMixin",)
|
|
13
14
|
|
|
@@ -15,7 +16,7 @@ __all__ = ("SelectColumnsMixin",)
|
|
|
15
16
|
class SelectColumnsMixin:
|
|
16
17
|
"""Mixin providing SELECT column and DISTINCT clauses for SELECT builders."""
|
|
17
18
|
|
|
18
|
-
def select(self, *columns: Union[str, exp.Expression]) -> Self:
|
|
19
|
+
def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn"]) -> Self:
|
|
19
20
|
"""Add columns to SELECT clause.
|
|
20
21
|
|
|
21
22
|
Raises:
|
|
@@ -24,7 +25,7 @@ class SelectColumnsMixin:
|
|
|
24
25
|
Returns:
|
|
25
26
|
The current builder instance for method chaining.
|
|
26
27
|
"""
|
|
27
|
-
builder = cast("
|
|
28
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
28
29
|
if builder._expression is None:
|
|
29
30
|
builder._expression = exp.Select()
|
|
30
31
|
if not isinstance(builder._expression, exp.Select):
|
|
@@ -34,7 +35,7 @@ class SelectColumnsMixin:
|
|
|
34
35
|
builder._expression = builder._expression.select(parse_column_expression(column), copy=False)
|
|
35
36
|
return cast("Self", builder)
|
|
36
37
|
|
|
37
|
-
def distinct(self, *columns: Union[str, exp.Expression]) -> Self:
|
|
38
|
+
def distinct(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn"]) -> Self:
|
|
38
39
|
"""Add DISTINCT clause to SELECT.
|
|
39
40
|
|
|
40
41
|
Args:
|
|
@@ -46,7 +47,7 @@ class SelectColumnsMixin:
|
|
|
46
47
|
Returns:
|
|
47
48
|
The current builder instance for method chaining.
|
|
48
49
|
"""
|
|
49
|
-
builder = cast("
|
|
50
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
50
51
|
if builder._expression is None:
|
|
51
52
|
builder._expression = exp.Select()
|
|
52
53
|
if not isinstance(builder._expression, exp.Select):
|
|
@@ -5,13 +5,13 @@ from sqlglot import exp
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from sqlglot.dialects.dialect import DialectType
|
|
7
7
|
|
|
8
|
-
from sqlspec.statement.builder.select import
|
|
8
|
+
from sqlspec.statement.builder.select import Select
|
|
9
9
|
|
|
10
10
|
__all__ = ("UnpivotClauseMixin",)
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class UnpivotClauseMixin:
|
|
14
|
-
"""Mixin class to add UNPIVOT functionality to a
|
|
14
|
+
"""Mixin class to add UNPIVOT functionality to a Select."""
|
|
15
15
|
|
|
16
16
|
_expression: "Optional[exp.Expression]" = None
|
|
17
17
|
dialect: "DialectType" = None
|
|
@@ -22,7 +22,7 @@ class UnpivotClauseMixin:
|
|
|
22
22
|
name_column_name: str,
|
|
23
23
|
columns_to_unpivot: list[Union[str, exp.Expression]],
|
|
24
24
|
alias: Optional[str] = None,
|
|
25
|
-
) -> "
|
|
25
|
+
) -> "Select":
|
|
26
26
|
"""Adds an UNPIVOT clause to the SELECT statement.
|
|
27
27
|
|
|
28
28
|
Example:
|
|
@@ -38,12 +38,12 @@ class UnpivotClauseMixin:
|
|
|
38
38
|
TypeError: If the current expression is not a Select expression.
|
|
39
39
|
|
|
40
40
|
Returns:
|
|
41
|
-
The
|
|
41
|
+
The Select instance for chaining.
|
|
42
42
|
"""
|
|
43
43
|
current_expr = self._expression
|
|
44
44
|
if not isinstance(current_expr, exp.Select):
|
|
45
45
|
# SelectBuilder's __init__ ensures _expression is exp.Select.
|
|
46
|
-
msg = "Unpivot can only be applied to a Select expression managed by
|
|
46
|
+
msg = "Unpivot can only be applied to a Select expression managed by Select."
|
|
47
47
|
raise TypeError(msg)
|
|
48
48
|
|
|
49
49
|
value_col_ident = exp.to_identifier(value_column_name)
|
|
@@ -59,7 +59,6 @@ class UnpivotClauseMixin:
|
|
|
59
59
|
# Fallback for other types, should ideally be an error or more specific handling
|
|
60
60
|
unpivot_cols_exprs.append(exp.column(str(col_name_or_expr)))
|
|
61
61
|
|
|
62
|
-
# Create the unpivot expression (stored as Pivot with unpivot=True)
|
|
63
62
|
in_expr = exp.In(this=name_col_ident, expressions=unpivot_cols_exprs)
|
|
64
63
|
|
|
65
64
|
unpivot_node = exp.Pivot(expressions=[value_col_ident], fields=[in_expr], unpivot=True)
|
|
@@ -67,14 +66,12 @@ class UnpivotClauseMixin:
|
|
|
67
66
|
if alias:
|
|
68
67
|
unpivot_node.set("alias", exp.TableAlias(this=exp.to_identifier(alias)))
|
|
69
68
|
|
|
70
|
-
# Add unpivot to the table in the FROM clause
|
|
71
69
|
from_clause = current_expr.args.get("from")
|
|
72
70
|
if from_clause and isinstance(from_clause, exp.From):
|
|
73
71
|
table = from_clause.this
|
|
74
72
|
if isinstance(table, exp.Table):
|
|
75
|
-
# Add to pivots array
|
|
76
73
|
existing_pivots = table.args.get("pivots", [])
|
|
77
74
|
existing_pivots.append(unpivot_node)
|
|
78
75
|
table.set("pivots", existing_pivots)
|
|
79
76
|
|
|
80
|
-
return cast("
|
|
77
|
+
return cast("Select", self)
|
|
@@ -4,6 +4,7 @@ from sqlglot import exp
|
|
|
4
4
|
from typing_extensions import Self
|
|
5
5
|
|
|
6
6
|
from sqlspec.exceptions import SQLBuilderError
|
|
7
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
7
8
|
|
|
8
9
|
__all__ = ("UpdateFromClauseMixin",)
|
|
9
10
|
|
|
@@ -30,7 +31,7 @@ class UpdateFromClauseMixin:
|
|
|
30
31
|
table_expr: exp.Expression
|
|
31
32
|
if isinstance(table, str):
|
|
32
33
|
table_expr = exp.to_table(table, alias=alias)
|
|
33
|
-
elif
|
|
34
|
+
elif has_query_builder_parameters(table):
|
|
34
35
|
subquery_builder_params = getattr(table, "_parameters", None)
|
|
35
36
|
if subquery_builder_params:
|
|
36
37
|
for p_name, p_value in subquery_builder_params.items():
|