sqlspec 0.11.1__py3-none-any.whl → 0.12.1__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 +16 -3
- sqlspec/_serialization.py +3 -10
- sqlspec/_sql.py +1147 -0
- sqlspec/_typing.py +343 -41
- sqlspec/adapters/adbc/__init__.py +2 -6
- sqlspec/adapters/adbc/config.py +474 -149
- sqlspec/adapters/adbc/driver.py +330 -621
- sqlspec/adapters/aiosqlite/__init__.py +2 -6
- sqlspec/adapters/aiosqlite/config.py +143 -57
- sqlspec/adapters/aiosqlite/driver.py +269 -431
- sqlspec/adapters/asyncmy/__init__.py +3 -8
- sqlspec/adapters/asyncmy/config.py +247 -202
- sqlspec/adapters/asyncmy/driver.py +218 -436
- sqlspec/adapters/asyncpg/__init__.py +4 -7
- sqlspec/adapters/asyncpg/config.py +329 -176
- sqlspec/adapters/asyncpg/driver.py +417 -487
- sqlspec/adapters/bigquery/__init__.py +2 -2
- sqlspec/adapters/bigquery/config.py +407 -0
- sqlspec/adapters/bigquery/driver.py +600 -553
- sqlspec/adapters/duckdb/__init__.py +4 -1
- sqlspec/adapters/duckdb/config.py +432 -321
- sqlspec/adapters/duckdb/driver.py +392 -406
- sqlspec/adapters/oracledb/__init__.py +3 -8
- sqlspec/adapters/oracledb/config.py +625 -0
- sqlspec/adapters/oracledb/driver.py +548 -921
- sqlspec/adapters/psqlpy/__init__.py +4 -7
- sqlspec/adapters/psqlpy/config.py +372 -203
- sqlspec/adapters/psqlpy/driver.py +197 -533
- sqlspec/adapters/psycopg/__init__.py +3 -8
- sqlspec/adapters/psycopg/config.py +725 -0
- sqlspec/adapters/psycopg/driver.py +734 -694
- sqlspec/adapters/sqlite/__init__.py +2 -6
- sqlspec/adapters/sqlite/config.py +146 -81
- sqlspec/adapters/sqlite/driver.py +242 -405
- sqlspec/base.py +220 -784
- sqlspec/config.py +354 -0
- sqlspec/driver/__init__.py +22 -0
- sqlspec/driver/_async.py +252 -0
- sqlspec/driver/_common.py +338 -0
- sqlspec/driver/_sync.py +261 -0
- sqlspec/driver/mixins/__init__.py +17 -0
- sqlspec/driver/mixins/_pipeline.py +523 -0
- sqlspec/driver/mixins/_result_utils.py +122 -0
- sqlspec/driver/mixins/_sql_translator.py +35 -0
- sqlspec/driver/mixins/_storage.py +993 -0
- sqlspec/driver/mixins/_type_coercion.py +131 -0
- sqlspec/exceptions.py +299 -7
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +474 -0
- sqlspec/extensions/litestar/__init__.py +1 -6
- sqlspec/extensions/litestar/_utils.py +1 -5
- sqlspec/extensions/litestar/config.py +5 -6
- sqlspec/extensions/litestar/handlers.py +13 -12
- sqlspec/extensions/litestar/plugin.py +22 -24
- sqlspec/extensions/litestar/providers.py +37 -55
- sqlspec/loader.py +528 -0
- sqlspec/service/__init__.py +3 -0
- sqlspec/service/base.py +24 -0
- sqlspec/service/pagination.py +26 -0
- sqlspec/statement/__init__.py +21 -0
- sqlspec/statement/builder/__init__.py +54 -0
- sqlspec/statement/builder/_ddl_utils.py +119 -0
- sqlspec/statement/builder/_parsing_utils.py +135 -0
- sqlspec/statement/builder/base.py +328 -0
- sqlspec/statement/builder/ddl.py +1379 -0
- sqlspec/statement/builder/delete.py +80 -0
- sqlspec/statement/builder/insert.py +274 -0
- sqlspec/statement/builder/merge.py +95 -0
- sqlspec/statement/builder/mixins/__init__.py +65 -0
- sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
- sqlspec/statement/builder/mixins/_case_builder.py +91 -0
- sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
- sqlspec/statement/builder/mixins/_delete_from.py +34 -0
- sqlspec/statement/builder/mixins/_from.py +61 -0
- sqlspec/statement/builder/mixins/_group_by.py +119 -0
- sqlspec/statement/builder/mixins/_having.py +35 -0
- sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
- sqlspec/statement/builder/mixins/_insert_into.py +36 -0
- sqlspec/statement/builder/mixins/_insert_values.py +69 -0
- sqlspec/statement/builder/mixins/_join.py +110 -0
- sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
- sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
- sqlspec/statement/builder/mixins/_order_by.py +46 -0
- sqlspec/statement/builder/mixins/_pivot.py +82 -0
- sqlspec/statement/builder/mixins/_returning.py +37 -0
- sqlspec/statement/builder/mixins/_select_columns.py +60 -0
- sqlspec/statement/builder/mixins/_set_ops.py +122 -0
- sqlspec/statement/builder/mixins/_unpivot.py +80 -0
- sqlspec/statement/builder/mixins/_update_from.py +54 -0
- sqlspec/statement/builder/mixins/_update_set.py +91 -0
- sqlspec/statement/builder/mixins/_update_table.py +29 -0
- sqlspec/statement/builder/mixins/_where.py +374 -0
- sqlspec/statement/builder/mixins/_window_functions.py +86 -0
- sqlspec/statement/builder/protocols.py +20 -0
- sqlspec/statement/builder/select.py +206 -0
- sqlspec/statement/builder/update.py +178 -0
- sqlspec/statement/filters.py +571 -0
- sqlspec/statement/parameters.py +736 -0
- sqlspec/statement/pipelines/__init__.py +67 -0
- sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
- sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
- sqlspec/statement/pipelines/base.py +315 -0
- sqlspec/statement/pipelines/context.py +119 -0
- sqlspec/statement/pipelines/result_types.py +41 -0
- sqlspec/statement/pipelines/transformers/__init__.py +8 -0
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
- sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
- sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
- sqlspec/statement/pipelines/validators/__init__.py +23 -0
- sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
- sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
- sqlspec/statement/pipelines/validators/_performance.py +703 -0
- sqlspec/statement/pipelines/validators/_security.py +990 -0
- sqlspec/statement/pipelines/validators/base.py +67 -0
- sqlspec/statement/result.py +527 -0
- sqlspec/statement/splitter.py +701 -0
- sqlspec/statement/sql.py +1198 -0
- sqlspec/storage/__init__.py +15 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +166 -0
- sqlspec/storage/backends/fsspec.py +315 -0
- sqlspec/storage/backends/obstore.py +464 -0
- sqlspec/storage/protocol.py +170 -0
- sqlspec/storage/registry.py +315 -0
- sqlspec/typing.py +157 -36
- sqlspec/utils/correlation.py +155 -0
- sqlspec/utils/deprecation.py +3 -6
- sqlspec/utils/fixtures.py +6 -11
- sqlspec/utils/logging.py +135 -0
- sqlspec/utils/module_loader.py +45 -43
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +6 -8
- sqlspec/utils/sync_tools.py +15 -27
- sqlspec/utils/text.py +58 -26
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/METADATA +97 -26
- sqlspec-0.12.1.dist-info/RECORD +145 -0
- sqlspec/adapters/bigquery/config/__init__.py +0 -3
- sqlspec/adapters/bigquery/config/_common.py +0 -40
- sqlspec/adapters/bigquery/config/_sync.py +0 -87
- sqlspec/adapters/oracledb/config/__init__.py +0 -9
- sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
- sqlspec/adapters/oracledb/config/_common.py +0 -131
- sqlspec/adapters/oracledb/config/_sync.py +0 -186
- sqlspec/adapters/psycopg/config/__init__.py +0 -19
- sqlspec/adapters/psycopg/config/_async.py +0 -169
- sqlspec/adapters/psycopg/config/_common.py +0 -56
- sqlspec/adapters/psycopg/config/_sync.py +0 -168
- sqlspec/filters.py +0 -331
- sqlspec/mixins.py +0 -305
- sqlspec/statement.py +0 -378
- sqlspec-0.11.1.dist-info/RECORD +0 -69
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/WHEEL +0 -0
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Optional, Union, cast
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from sqlglot.dialects.dialect import DialectType
|
|
7
|
+
|
|
8
|
+
from sqlspec.statement.builder.select import SelectBuilder
|
|
9
|
+
|
|
10
|
+
__all__ = ("PivotClauseMixin",)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PivotClauseMixin:
|
|
14
|
+
"""Mixin class to add PIVOT functionality to a SelectBuilder."""
|
|
15
|
+
|
|
16
|
+
_expression: "Optional[exp.Expression]" = None
|
|
17
|
+
dialect: "DialectType" = None
|
|
18
|
+
|
|
19
|
+
def pivot(
|
|
20
|
+
self: "PivotClauseMixin",
|
|
21
|
+
aggregate_function: Union[str, exp.Expression],
|
|
22
|
+
aggregate_column: Union[str, exp.Expression],
|
|
23
|
+
pivot_column: Union[str, exp.Expression],
|
|
24
|
+
pivot_values: list[Union[str, int, float, exp.Expression]],
|
|
25
|
+
alias: Optional[str] = None,
|
|
26
|
+
) -> "SelectBuilder":
|
|
27
|
+
"""Adds a PIVOT clause to the SELECT statement.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
`query.pivot(aggregate_function="SUM", aggregate_column="Sales", pivot_column="Quarter", pivot_values=["Q1", "Q2", "Q3", "Q4"], alias="PivotTable")`
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
aggregate_function: The aggregate function to use (e.g., "SUM", "AVG").
|
|
34
|
+
aggregate_column: The column to be aggregated.
|
|
35
|
+
pivot_column: The column whose unique values will become new column headers.
|
|
36
|
+
pivot_values: A list of specific values from the pivot_column to be turned into columns.
|
|
37
|
+
alias: Optional alias for the pivoted table/subquery.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The SelectBuilder instance for chaining.
|
|
41
|
+
"""
|
|
42
|
+
current_expr = self._expression
|
|
43
|
+
if not isinstance(current_expr, exp.Select):
|
|
44
|
+
msg = "Pivot can only be applied to a Select expression managed by SelectBuilder."
|
|
45
|
+
raise TypeError(msg)
|
|
46
|
+
|
|
47
|
+
agg_func_name = aggregate_function if isinstance(aggregate_function, str) else aggregate_function.name
|
|
48
|
+
agg_col_expr = exp.column(aggregate_column) if isinstance(aggregate_column, str) else aggregate_column
|
|
49
|
+
pivot_col_expr = exp.column(pivot_column) if isinstance(pivot_column, str) else pivot_column
|
|
50
|
+
|
|
51
|
+
pivot_agg_expr = exp.func(agg_func_name, agg_col_expr)
|
|
52
|
+
|
|
53
|
+
pivot_value_exprs: list[exp.Expression] = []
|
|
54
|
+
for val in pivot_values:
|
|
55
|
+
if isinstance(val, exp.Expression):
|
|
56
|
+
pivot_value_exprs.append(val)
|
|
57
|
+
elif isinstance(val, str):
|
|
58
|
+
pivot_value_exprs.append(exp.Literal.string(val))
|
|
59
|
+
elif isinstance(val, (int, float)):
|
|
60
|
+
pivot_value_exprs.append(exp.Literal.number(val))
|
|
61
|
+
else:
|
|
62
|
+
pivot_value_exprs.append(exp.Literal.string(str(val)))
|
|
63
|
+
|
|
64
|
+
# Create the pivot expression with proper fields structure
|
|
65
|
+
in_expr = exp.In(this=pivot_col_expr, expressions=pivot_value_exprs)
|
|
66
|
+
|
|
67
|
+
pivot_node = exp.Pivot(expressions=[pivot_agg_expr], fields=[in_expr], unpivot=False)
|
|
68
|
+
|
|
69
|
+
if alias:
|
|
70
|
+
pivot_node.set("alias", exp.TableAlias(this=exp.to_identifier(alias)))
|
|
71
|
+
|
|
72
|
+
# Add pivot to the table in the FROM clause
|
|
73
|
+
from_clause = current_expr.args.get("from")
|
|
74
|
+
if from_clause and isinstance(from_clause, exp.From):
|
|
75
|
+
table = from_clause.this
|
|
76
|
+
if isinstance(table, exp.Table):
|
|
77
|
+
# Add to pivots array
|
|
78
|
+
existing_pivots = table.args.get("pivots", [])
|
|
79
|
+
existing_pivots.append(pivot_node)
|
|
80
|
+
table.set("pivots", existing_pivots)
|
|
81
|
+
|
|
82
|
+
return cast("SelectBuilder", self)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Optional, Union
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
7
|
+
|
|
8
|
+
__all__ = ("ReturningClauseMixin",)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReturningClauseMixin:
|
|
12
|
+
"""Mixin providing RETURNING clause for INSERT, UPDATE, and DELETE builders."""
|
|
13
|
+
|
|
14
|
+
_expression: Optional[exp.Expression] = None
|
|
15
|
+
|
|
16
|
+
def returning(self, *columns: Union[str, exp.Expression]) -> Self:
|
|
17
|
+
"""Add RETURNING clause to the statement.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
*columns: Columns to return. Can be strings or sqlglot expressions.
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
SQLBuilderError: If the current expression is not INSERT, UPDATE, or DELETE.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The current builder instance for method chaining.
|
|
27
|
+
"""
|
|
28
|
+
if self._expression is None:
|
|
29
|
+
msg = "Cannot add RETURNING: expression is not initialized."
|
|
30
|
+
raise SQLBuilderError(msg)
|
|
31
|
+
valid_types = (exp.Insert, exp.Update, exp.Delete)
|
|
32
|
+
if not isinstance(self._expression, valid_types):
|
|
33
|
+
msg = "RETURNING is only supported for INSERT, UPDATE, and DELETE statements."
|
|
34
|
+
raise SQLBuilderError(msg)
|
|
35
|
+
returning_exprs = [exp.column(c) if isinstance(c, str) else c for c in columns]
|
|
36
|
+
self._expression.set("returning", exp.Returning(expressions=returning_exprs))
|
|
37
|
+
return self
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Union, cast
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
7
|
+
from sqlspec.statement.builder._parsing_utils import parse_column_expression
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from sqlspec.statement.builder.protocols import BuilderProtocol
|
|
11
|
+
|
|
12
|
+
__all__ = ("SelectColumnsMixin",)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SelectColumnsMixin:
|
|
16
|
+
"""Mixin providing SELECT column and DISTINCT clauses for SELECT builders."""
|
|
17
|
+
|
|
18
|
+
def select(self, *columns: Union[str, exp.Expression]) -> Self:
|
|
19
|
+
"""Add columns to SELECT clause.
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The current builder instance for method chaining.
|
|
26
|
+
"""
|
|
27
|
+
builder = cast("BuilderProtocol", self)
|
|
28
|
+
if builder._expression is None:
|
|
29
|
+
builder._expression = exp.Select()
|
|
30
|
+
if not isinstance(builder._expression, exp.Select):
|
|
31
|
+
msg = "Cannot add select columns to a non-SELECT expression."
|
|
32
|
+
raise SQLBuilderError(msg)
|
|
33
|
+
for column in columns:
|
|
34
|
+
builder._expression = builder._expression.select(parse_column_expression(column), copy=False)
|
|
35
|
+
return cast("Self", builder)
|
|
36
|
+
|
|
37
|
+
def distinct(self, *columns: Union[str, exp.Expression]) -> Self:
|
|
38
|
+
"""Add DISTINCT clause to SELECT.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
*columns: Optional columns to make distinct. If none provided, applies DISTINCT to all selected columns.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The current builder instance for method chaining.
|
|
48
|
+
"""
|
|
49
|
+
builder = cast("BuilderProtocol", self)
|
|
50
|
+
if builder._expression is None:
|
|
51
|
+
builder._expression = exp.Select()
|
|
52
|
+
if not isinstance(builder._expression, exp.Select):
|
|
53
|
+
msg = "Cannot add DISTINCT to a non-SELECT expression."
|
|
54
|
+
raise SQLBuilderError(msg)
|
|
55
|
+
if not columns:
|
|
56
|
+
builder._expression.set("distinct", exp.Distinct())
|
|
57
|
+
else:
|
|
58
|
+
distinct_columns = [parse_column_expression(column) for column in columns]
|
|
59
|
+
builder._expression.set("distinct", exp.Distinct(expressions=distinct_columns))
|
|
60
|
+
return cast("Self", builder)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
7
|
+
|
|
8
|
+
__all__ = ("SetOperationMixin",)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SetOperationMixin:
|
|
12
|
+
"""Mixin providing set operations (UNION, INTERSECT, EXCEPT) for SELECT builders."""
|
|
13
|
+
|
|
14
|
+
_expression: Any = None
|
|
15
|
+
_parameters: dict[str, Any] = {}
|
|
16
|
+
dialect: Any = None
|
|
17
|
+
|
|
18
|
+
def union(self, other: Any, all_: bool = False) -> Self:
|
|
19
|
+
"""Combine this query with another using UNION.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
other: Another SelectBuilder or compatible builder to union with.
|
|
23
|
+
all_: If True, use UNION ALL instead of UNION.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The new builder instance for the union query.
|
|
30
|
+
"""
|
|
31
|
+
left_query = self.build() # type: ignore[attr-defined]
|
|
32
|
+
right_query = other.build()
|
|
33
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=getattr(self, "dialect", None))
|
|
34
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=getattr(self, "dialect", None))
|
|
35
|
+
if not left_expr or not right_expr:
|
|
36
|
+
msg = "Could not parse queries for UNION operation"
|
|
37
|
+
raise SQLBuilderError(msg)
|
|
38
|
+
union_expr = exp.union(left_expr, right_expr, distinct=not all_)
|
|
39
|
+
new_builder = type(self)()
|
|
40
|
+
new_builder.dialect = getattr(self, "dialect", None)
|
|
41
|
+
new_builder._expression = union_expr
|
|
42
|
+
merged_params = dict(left_query.parameters)
|
|
43
|
+
for param_name, param_value in right_query.parameters.items():
|
|
44
|
+
if param_name in merged_params:
|
|
45
|
+
counter = 1
|
|
46
|
+
new_param_name = f"{param_name}_right_{counter}"
|
|
47
|
+
while new_param_name in merged_params:
|
|
48
|
+
counter += 1
|
|
49
|
+
new_param_name = f"{param_name}_right_{counter}"
|
|
50
|
+
|
|
51
|
+
# Use AST transformation instead of string manipulation
|
|
52
|
+
def rename_parameter(node: exp.Expression) -> exp.Expression:
|
|
53
|
+
if isinstance(node, exp.Placeholder) and node.name == param_name: # noqa: B023
|
|
54
|
+
return exp.Placeholder(this=new_param_name) # noqa: B023
|
|
55
|
+
return node
|
|
56
|
+
|
|
57
|
+
right_expr = right_expr.transform(rename_parameter)
|
|
58
|
+
union_expr = exp.union(left_expr, right_expr, distinct=not all_)
|
|
59
|
+
new_builder._expression = union_expr
|
|
60
|
+
merged_params[new_param_name] = param_value
|
|
61
|
+
else:
|
|
62
|
+
merged_params[param_name] = param_value
|
|
63
|
+
new_builder._parameters = merged_params
|
|
64
|
+
return new_builder
|
|
65
|
+
|
|
66
|
+
def intersect(self, other: Any) -> Self:
|
|
67
|
+
"""Add INTERSECT clause.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
other: Another SelectBuilder or compatible builder to intersect with.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The new builder instance for the intersect query.
|
|
77
|
+
"""
|
|
78
|
+
left_query = self.build() # type: ignore[attr-defined]
|
|
79
|
+
right_query = other.build()
|
|
80
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=getattr(self, "dialect", None))
|
|
81
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=getattr(self, "dialect", None))
|
|
82
|
+
if not left_expr or not right_expr:
|
|
83
|
+
msg = "Could not parse queries for INTERSECT operation"
|
|
84
|
+
raise SQLBuilderError(msg)
|
|
85
|
+
intersect_expr = exp.intersect(left_expr, right_expr, distinct=True)
|
|
86
|
+
new_builder = type(self)()
|
|
87
|
+
new_builder.dialect = getattr(self, "dialect", None)
|
|
88
|
+
new_builder._expression = intersect_expr
|
|
89
|
+
# Merge parameters
|
|
90
|
+
merged_params = dict(left_query.parameters)
|
|
91
|
+
merged_params.update(right_query.parameters)
|
|
92
|
+
new_builder._parameters = merged_params
|
|
93
|
+
return new_builder
|
|
94
|
+
|
|
95
|
+
def except_(self, other: Any) -> Self:
|
|
96
|
+
"""Combine this query with another using EXCEPT.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
other: Another SelectBuilder or compatible builder to except with.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The new builder instance for the except query.
|
|
106
|
+
"""
|
|
107
|
+
left_query = self.build() # type: ignore[attr-defined]
|
|
108
|
+
right_query = other.build()
|
|
109
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=getattr(self, "dialect", None))
|
|
110
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=getattr(self, "dialect", None))
|
|
111
|
+
if not left_expr or not right_expr:
|
|
112
|
+
msg = "Could not parse queries for EXCEPT operation"
|
|
113
|
+
raise SQLBuilderError(msg)
|
|
114
|
+
except_expr = exp.except_(left_expr, right_expr)
|
|
115
|
+
new_builder = type(self)()
|
|
116
|
+
new_builder.dialect = getattr(self, "dialect", None)
|
|
117
|
+
new_builder._expression = except_expr
|
|
118
|
+
# Merge parameters
|
|
119
|
+
merged_params = dict(left_query.parameters)
|
|
120
|
+
merged_params.update(right_query.parameters)
|
|
121
|
+
new_builder._parameters = merged_params
|
|
122
|
+
return new_builder
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Optional, Union, cast
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from sqlglot.dialects.dialect import DialectType
|
|
7
|
+
|
|
8
|
+
from sqlspec.statement.builder.select import SelectBuilder
|
|
9
|
+
|
|
10
|
+
__all__ = ("UnpivotClauseMixin",)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnpivotClauseMixin:
|
|
14
|
+
"""Mixin class to add UNPIVOT functionality to a SelectBuilder."""
|
|
15
|
+
|
|
16
|
+
_expression: "Optional[exp.Expression]" = None
|
|
17
|
+
dialect: "DialectType" = None
|
|
18
|
+
|
|
19
|
+
def unpivot(
|
|
20
|
+
self: "UnpivotClauseMixin",
|
|
21
|
+
value_column_name: str,
|
|
22
|
+
name_column_name: str,
|
|
23
|
+
columns_to_unpivot: list[Union[str, exp.Expression]],
|
|
24
|
+
alias: Optional[str] = None,
|
|
25
|
+
) -> "SelectBuilder":
|
|
26
|
+
"""Adds an UNPIVOT clause to the SELECT statement.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
`query.unpivot(value_column_name="Sales", name_column_name="Quarter", columns_to_unpivot=["Q1Sales", "Q2Sales"], alias="UnpivotTable")`
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
value_column_name: The name for the new column that will hold the values from the unpivoted columns.
|
|
33
|
+
name_column_name: The name for the new column that will hold the names of the original unpivoted columns.
|
|
34
|
+
columns_to_unpivot: A list of columns to be unpivoted into rows.
|
|
35
|
+
alias: Optional alias for the unpivoted table/subquery.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
TypeError: If the current expression is not a Select expression.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The SelectBuilder instance for chaining.
|
|
42
|
+
"""
|
|
43
|
+
current_expr = self._expression
|
|
44
|
+
if not isinstance(current_expr, exp.Select):
|
|
45
|
+
# SelectBuilder's __init__ ensures _expression is exp.Select.
|
|
46
|
+
msg = "Unpivot can only be applied to a Select expression managed by SelectBuilder."
|
|
47
|
+
raise TypeError(msg)
|
|
48
|
+
|
|
49
|
+
value_col_ident = exp.to_identifier(value_column_name)
|
|
50
|
+
name_col_ident = exp.to_identifier(name_column_name)
|
|
51
|
+
|
|
52
|
+
unpivot_cols_exprs: list[exp.Expression] = []
|
|
53
|
+
for col_name_or_expr in columns_to_unpivot:
|
|
54
|
+
if isinstance(col_name_or_expr, exp.Expression):
|
|
55
|
+
unpivot_cols_exprs.append(col_name_or_expr)
|
|
56
|
+
elif isinstance(col_name_or_expr, str):
|
|
57
|
+
unpivot_cols_exprs.append(exp.column(col_name_or_expr))
|
|
58
|
+
else:
|
|
59
|
+
# Fallback for other types, should ideally be an error or more specific handling
|
|
60
|
+
unpivot_cols_exprs.append(exp.column(str(col_name_or_expr)))
|
|
61
|
+
|
|
62
|
+
# Create the unpivot expression (stored as Pivot with unpivot=True)
|
|
63
|
+
in_expr = exp.In(this=name_col_ident, expressions=unpivot_cols_exprs)
|
|
64
|
+
|
|
65
|
+
unpivot_node = exp.Pivot(expressions=[value_col_ident], fields=[in_expr], unpivot=True)
|
|
66
|
+
|
|
67
|
+
if alias:
|
|
68
|
+
unpivot_node.set("alias", exp.TableAlias(this=exp.to_identifier(alias)))
|
|
69
|
+
|
|
70
|
+
# Add unpivot to the table in the FROM clause
|
|
71
|
+
from_clause = current_expr.args.get("from")
|
|
72
|
+
if from_clause and isinstance(from_clause, exp.From):
|
|
73
|
+
table = from_clause.this
|
|
74
|
+
if isinstance(table, exp.Table):
|
|
75
|
+
# Add to pivots array
|
|
76
|
+
existing_pivots = table.args.get("pivots", [])
|
|
77
|
+
existing_pivots.append(unpivot_node)
|
|
78
|
+
table.set("pivots", existing_pivots)
|
|
79
|
+
|
|
80
|
+
return cast("SelectBuilder", self)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Any, Optional, Union
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
7
|
+
|
|
8
|
+
__all__ = ("UpdateFromClauseMixin",)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UpdateFromClauseMixin:
|
|
12
|
+
"""Mixin providing FROM clause for UPDATE builders (e.g., PostgreSQL style)."""
|
|
13
|
+
|
|
14
|
+
def from_(self, table: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
|
|
15
|
+
"""Add a FROM clause to the UPDATE statement.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
table: The table name, expression, or subquery to add to the FROM clause.
|
|
19
|
+
alias: Optional alias for the table in the FROM clause.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The current builder instance for method chaining.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
SQLBuilderError: If the current expression is not an UPDATE statement.
|
|
26
|
+
"""
|
|
27
|
+
if self._expression is None or not isinstance(self._expression, exp.Update): # type: ignore[attr-defined]
|
|
28
|
+
msg = "Cannot add FROM clause to non-UPDATE expression. Set the main table first."
|
|
29
|
+
raise SQLBuilderError(msg)
|
|
30
|
+
table_expr: exp.Expression
|
|
31
|
+
if isinstance(table, str):
|
|
32
|
+
table_expr = exp.to_table(table, alias=alias)
|
|
33
|
+
elif hasattr(table, "build"):
|
|
34
|
+
subquery_builder_params = getattr(table, "_parameters", None)
|
|
35
|
+
if subquery_builder_params:
|
|
36
|
+
for p_name, p_value in subquery_builder_params.items():
|
|
37
|
+
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
38
|
+
subquery_exp = exp.paren(getattr(table, "_expression", exp.select()))
|
|
39
|
+
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
40
|
+
elif isinstance(table, exp.Expression):
|
|
41
|
+
table_expr = exp.alias_(table, alias) if alias else table
|
|
42
|
+
else:
|
|
43
|
+
msg = f"Unsupported table type for FROM clause: {type(table)}"
|
|
44
|
+
raise SQLBuilderError(msg)
|
|
45
|
+
if self._expression.args.get("from") is None: # type: ignore[attr-defined]
|
|
46
|
+
self._expression.set("from", exp.From(expressions=[])) # type: ignore[attr-defined]
|
|
47
|
+
from_clause = self._expression.args["from"] # type: ignore[attr-defined]
|
|
48
|
+
if hasattr(from_clause, "append"):
|
|
49
|
+
from_clause.append("expressions", table_expr)
|
|
50
|
+
else:
|
|
51
|
+
if not from_clause.expressions:
|
|
52
|
+
from_clause.expressions = []
|
|
53
|
+
from_clause.expressions.append(table_expr)
|
|
54
|
+
return self
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from sqlglot import exp
|
|
5
|
+
from typing_extensions import Self
|
|
6
|
+
|
|
7
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
8
|
+
|
|
9
|
+
__all__ = ("UpdateSetClauseMixin",)
|
|
10
|
+
|
|
11
|
+
MIN_SET_ARGS = 2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UpdateSetClauseMixin:
|
|
15
|
+
"""Mixin providing SET clause for UPDATE builders."""
|
|
16
|
+
|
|
17
|
+
_expression: Optional[exp.Expression] = None
|
|
18
|
+
|
|
19
|
+
def set(self, *args: Any, **kwargs: Any) -> Self:
|
|
20
|
+
"""Set columns and values for the UPDATE statement.
|
|
21
|
+
|
|
22
|
+
Supports:
|
|
23
|
+
- set(column, value)
|
|
24
|
+
- set(mapping)
|
|
25
|
+
- set(**kwargs)
|
|
26
|
+
- set(mapping, **kwargs)
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
*args: Either (column, value) or a mapping.
|
|
30
|
+
**kwargs: Column-value pairs to set.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
SQLBuilderError: If the current expression is not an UPDATE statement or usage is invalid.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The current builder instance for method chaining.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
if self._expression is None:
|
|
40
|
+
self._expression = exp.Update()
|
|
41
|
+
if not isinstance(self._expression, exp.Update):
|
|
42
|
+
msg = "Cannot add SET clause to non-UPDATE expression."
|
|
43
|
+
raise SQLBuilderError(msg)
|
|
44
|
+
assignments = []
|
|
45
|
+
# (column, value) signature
|
|
46
|
+
if len(args) == MIN_SET_ARGS and not kwargs:
|
|
47
|
+
col, val = args
|
|
48
|
+
col_expr = col if isinstance(col, exp.Column) else exp.column(col)
|
|
49
|
+
# If value is an expression, use it directly
|
|
50
|
+
if isinstance(val, exp.Expression):
|
|
51
|
+
value_expr = val
|
|
52
|
+
elif hasattr(val, "_expression") and hasattr(val, "build"):
|
|
53
|
+
# It's a builder (like SelectBuilder), convert to subquery
|
|
54
|
+
subquery = val.build()
|
|
55
|
+
# Parse the SQL and use as expression
|
|
56
|
+
value_expr = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect", None)))
|
|
57
|
+
# Merge parameters from subquery
|
|
58
|
+
if hasattr(val, "_parameters"):
|
|
59
|
+
for p_name, p_value in val._parameters.items():
|
|
60
|
+
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
61
|
+
else:
|
|
62
|
+
param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
|
|
63
|
+
value_expr = exp.Placeholder(this=param_name)
|
|
64
|
+
assignments.append(exp.EQ(this=col_expr, expression=value_expr))
|
|
65
|
+
# mapping and/or kwargs
|
|
66
|
+
elif (len(args) == 1 and isinstance(args[0], Mapping)) or kwargs:
|
|
67
|
+
all_values = dict(args[0] if args else {}, **kwargs)
|
|
68
|
+
for col, val in all_values.items():
|
|
69
|
+
# If value is an expression, use it directly
|
|
70
|
+
if isinstance(val, exp.Expression):
|
|
71
|
+
value_expr = val
|
|
72
|
+
elif hasattr(val, "_expression") and hasattr(val, "build"):
|
|
73
|
+
# It's a builder (like SelectBuilder), convert to subquery
|
|
74
|
+
subquery = val.build()
|
|
75
|
+
# Parse the SQL and use as expression
|
|
76
|
+
value_expr = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect", None)))
|
|
77
|
+
# Merge parameters from subquery
|
|
78
|
+
if hasattr(val, "_parameters"):
|
|
79
|
+
for p_name, p_value in val._parameters.items():
|
|
80
|
+
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
81
|
+
else:
|
|
82
|
+
param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
|
|
83
|
+
value_expr = exp.Placeholder(this=param_name)
|
|
84
|
+
assignments.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
85
|
+
else:
|
|
86
|
+
msg = "Invalid arguments for set(): use (column, value), mapping, or kwargs."
|
|
87
|
+
raise SQLBuilderError(msg)
|
|
88
|
+
# Append to existing expressions instead of replacing
|
|
89
|
+
existing = self._expression.args.get("expressions", [])
|
|
90
|
+
self._expression.set("expressions", existing + assignments)
|
|
91
|
+
return self
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
__all__ = ("UpdateTableClauseMixin",)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UpdateTableClauseMixin:
|
|
10
|
+
"""Mixin providing TABLE clause for UPDATE builders."""
|
|
11
|
+
|
|
12
|
+
_expression: Optional[exp.Expression] = None
|
|
13
|
+
|
|
14
|
+
def table(self, table_name: str, alias: Optional[str] = None) -> Self:
|
|
15
|
+
"""Set the table to update.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
table_name: The name of the table.
|
|
19
|
+
alias: Optional alias for the table.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The current builder instance for method chaining.
|
|
23
|
+
"""
|
|
24
|
+
if self._expression is None or not isinstance(self._expression, exp.Update):
|
|
25
|
+
self._expression = exp.Update(this=None, expressions=[], joins=[])
|
|
26
|
+
table_expr: exp.Expression = exp.to_table(table_name, alias=alias)
|
|
27
|
+
self._expression.set("this", table_expr)
|
|
28
|
+
setattr(self, "_table", table_name)
|
|
29
|
+
return self
|