sqlspec 0.14.1__py3-none-any.whl → 0.15.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/__init__.py +50 -25
- sqlspec/__main__.py +1 -1
- sqlspec/__metadata__.py +1 -3
- sqlspec/_serialization.py +1 -2
- sqlspec/_sql.py +256 -120
- sqlspec/_typing.py +278 -142
- sqlspec/adapters/adbc/__init__.py +4 -3
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +115 -260
- sqlspec/adapters/adbc/driver.py +462 -367
- sqlspec/adapters/aiosqlite/__init__.py +18 -3
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +199 -129
- sqlspec/adapters/aiosqlite/driver.py +230 -269
- sqlspec/adapters/asyncmy/__init__.py +18 -3
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +80 -168
- sqlspec/adapters/asyncmy/driver.py +260 -225
- sqlspec/adapters/asyncpg/__init__.py +19 -4
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +82 -181
- sqlspec/adapters/asyncpg/driver.py +285 -383
- sqlspec/adapters/bigquery/__init__.py +17 -3
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +191 -258
- sqlspec/adapters/bigquery/driver.py +474 -646
- sqlspec/adapters/duckdb/__init__.py +14 -3
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +415 -351
- sqlspec/adapters/duckdb/driver.py +343 -413
- sqlspec/adapters/oracledb/__init__.py +19 -5
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +123 -379
- sqlspec/adapters/oracledb/driver.py +507 -560
- sqlspec/adapters/psqlpy/__init__.py +13 -3
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +93 -254
- sqlspec/adapters/psqlpy/driver.py +505 -234
- sqlspec/adapters/psycopg/__init__.py +19 -5
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +143 -403
- sqlspec/adapters/psycopg/driver.py +706 -872
- sqlspec/adapters/sqlite/__init__.py +14 -3
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +202 -118
- sqlspec/adapters/sqlite/driver.py +264 -303
- sqlspec/base.py +105 -9
- sqlspec/{statement/builder → builder}/__init__.py +12 -14
- sqlspec/{statement/builder → builder}/_base.py +120 -55
- sqlspec/{statement/builder → builder}/_column.py +17 -6
- sqlspec/{statement/builder → builder}/_ddl.py +46 -79
- sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
- sqlspec/{statement/builder → builder}/_delete.py +6 -25
- sqlspec/{statement/builder → builder}/_insert.py +6 -64
- sqlspec/builder/_merge.py +56 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +3 -10
- sqlspec/{statement/builder → builder}/_select.py +11 -56
- sqlspec/{statement/builder → builder}/_update.py +12 -18
- sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
- sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
- sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +22 -16
- sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
- sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +3 -5
- sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
- sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
- sqlspec/{statement/builder → builder}/mixins/_select_operations.py +21 -36
- sqlspec/{statement/builder → builder}/mixins/_update_operations.py +3 -14
- sqlspec/{statement/builder → builder}/mixins/_where_clause.py +52 -79
- sqlspec/cli.py +4 -5
- sqlspec/config.py +180 -133
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.py +873 -0
- sqlspec/core/compiler.py +396 -0
- sqlspec/core/filters.py +828 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.py +1209 -0
- sqlspec/core/result.py +664 -0
- sqlspec/{statement → core}/splitter.py +321 -191
- sqlspec/core/statement.py +651 -0
- sqlspec/driver/__init__.py +7 -10
- sqlspec/driver/_async.py +387 -176
- sqlspec/driver/_common.py +527 -289
- sqlspec/driver/_sync.py +390 -172
- sqlspec/driver/mixins/__init__.py +2 -19
- sqlspec/driver/mixins/_result_tools.py +168 -0
- sqlspec/driver/mixins/_sql_translator.py +6 -3
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +16 -14
- sqlspec/extensions/litestar/providers.py +17 -52
- sqlspec/loader.py +424 -105
- sqlspec/migrations/__init__.py +12 -0
- sqlspec/migrations/base.py +92 -68
- sqlspec/migrations/commands.py +24 -106
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +49 -51
- sqlspec/migrations/tracker.py +31 -44
- sqlspec/migrations/utils.py +64 -24
- sqlspec/protocols.py +7 -183
- sqlspec/storage/__init__.py +1 -1
- sqlspec/storage/backends/base.py +37 -40
- sqlspec/storage/backends/fsspec.py +136 -112
- sqlspec/storage/backends/obstore.py +138 -160
- sqlspec/storage/capabilities.py +5 -4
- sqlspec/storage/registry.py +57 -106
- sqlspec/typing.py +136 -115
- sqlspec/utils/__init__.py +2 -3
- sqlspec/utils/correlation.py +0 -3
- sqlspec/utils/deprecation.py +6 -6
- sqlspec/utils/fixtures.py +6 -6
- sqlspec/utils/logging.py +0 -2
- sqlspec/utils/module_loader.py +7 -12
- sqlspec/utils/singleton.py +0 -1
- sqlspec/utils/sync_tools.py +16 -37
- sqlspec/utils/text.py +12 -51
- sqlspec/utils/type_guards.py +443 -232
- {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/METADATA +7 -2
- sqlspec-0.15.0.dist-info/RECORD +134 -0
- sqlspec/adapters/adbc/transformers.py +0 -108
- sqlspec/driver/connection.py +0 -207
- sqlspec/driver/mixins/_cache.py +0 -114
- sqlspec/driver/mixins/_csv_writer.py +0 -91
- sqlspec/driver/mixins/_pipeline.py +0 -508
- sqlspec/driver/mixins/_query_tools.py +0 -796
- sqlspec/driver/mixins/_result_utils.py +0 -138
- sqlspec/driver/mixins/_storage.py +0 -912
- sqlspec/driver/mixins/_type_coercion.py +0 -128
- sqlspec/driver/parameters.py +0 -138
- sqlspec/statement/__init__.py +0 -21
- sqlspec/statement/builder/_merge.py +0 -95
- sqlspec/statement/cache.py +0 -50
- sqlspec/statement/filters.py +0 -625
- sqlspec/statement/parameters.py +0 -956
- sqlspec/statement/pipelines/__init__.py +0 -210
- sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
- sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
- sqlspec/statement/pipelines/context.py +0 -109
- sqlspec/statement/pipelines/transformers/__init__.py +0 -7
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
- sqlspec/statement/pipelines/validators/__init__.py +0 -23
- sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
- sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
- sqlspec/statement/pipelines/validators/_performance.py +0 -714
- sqlspec/statement/pipelines/validators/_security.py +0 -967
- sqlspec/statement/result.py +0 -435
- sqlspec/statement/sql.py +0 -1774
- sqlspec/utils/cached_property.py +0 -25
- sqlspec/utils/statement_hashing.py +0 -203
- sqlspec-0.14.1.dist-info/RECORD +0 -145
- /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -6,13 +6,13 @@ with automatic parameter binding and validation.
|
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
|
-
from typing import Any, Optional, Union
|
|
9
|
+
from typing import Any, Optional, Union
|
|
10
10
|
|
|
11
11
|
from sqlglot import exp
|
|
12
12
|
from typing_extensions import Self
|
|
13
13
|
|
|
14
|
-
from sqlspec.
|
|
15
|
-
from sqlspec.
|
|
14
|
+
from sqlspec.builder._base import QueryBuilder, SafeQuery
|
|
15
|
+
from sqlspec.builder.mixins import (
|
|
16
16
|
CommonTableExpressionMixin,
|
|
17
17
|
HavingClauseMixin,
|
|
18
18
|
JoinClauseMixin,
|
|
@@ -24,19 +24,17 @@ from sqlspec.statement.builder.mixins import (
|
|
|
24
24
|
UnpivotClauseMixin,
|
|
25
25
|
WhereClauseMixin,
|
|
26
26
|
)
|
|
27
|
-
from sqlspec.
|
|
28
|
-
from sqlspec.typing import RowT
|
|
27
|
+
from sqlspec.core.result import SQLResult
|
|
29
28
|
|
|
30
29
|
__all__ = ("Select",)
|
|
31
30
|
|
|
32
31
|
|
|
33
|
-
# This pattern is formatted with the table name and compiled for each hint.
|
|
34
32
|
TABLE_HINT_PATTERN = r"\b{}\b(\s+AS\s+\w+)?"
|
|
35
33
|
|
|
36
34
|
|
|
37
35
|
@dataclass
|
|
38
36
|
class Select(
|
|
39
|
-
QueryBuilder
|
|
37
|
+
QueryBuilder,
|
|
40
38
|
WhereClauseMixin,
|
|
41
39
|
OrderByClauseMixin,
|
|
42
40
|
LimitOffsetClauseMixin,
|
|
@@ -51,29 +49,17 @@ class Select(
|
|
|
51
49
|
"""Type-safe builder for SELECT queries with schema/model integration.
|
|
52
50
|
|
|
53
51
|
This builder provides a fluent, safe interface for constructing SQL SELECT statements.
|
|
54
|
-
It supports type-safe result mapping via the `as_schema()` method, allowing users to
|
|
55
|
-
associate a schema/model (such as a Pydantic model, dataclass, or msgspec.Struct) with
|
|
56
|
-
the query for static type checking and IDE support.
|
|
57
52
|
|
|
58
53
|
Example:
|
|
59
54
|
>>> class User(BaseModel):
|
|
60
55
|
... id: int
|
|
61
56
|
... name: str
|
|
62
|
-
>>> builder = (
|
|
63
|
-
|
|
64
|
-
... .select("id", "name")
|
|
65
|
-
... .from_("users")
|
|
66
|
-
... .as_schema(User)
|
|
67
|
-
... )
|
|
68
|
-
>>> result: list[User] = driver.execute(builder)
|
|
69
|
-
|
|
70
|
-
Attributes:
|
|
71
|
-
_schema: The schema/model class for row typing, if set via as_schema().
|
|
57
|
+
>>> builder = Select("id", "name").from_("users")
|
|
58
|
+
>>> result = driver.execute(builder)
|
|
72
59
|
"""
|
|
73
60
|
|
|
74
61
|
_with_parts: "dict[str, Union[exp.CTE, Select]]" = field(default_factory=dict, init=False)
|
|
75
62
|
_expression: Optional[exp.Expression] = field(default=None, init=False, repr=False, compare=False, hash=False)
|
|
76
|
-
_schema: Optional[type[RowT]] = None
|
|
77
63
|
_hints: "list[dict[str, object]]" = field(default_factory=list, init=False, repr=False)
|
|
78
64
|
|
|
79
65
|
def __init__(self, *columns: str, **kwargs: Any) -> None:
|
|
@@ -85,59 +71,33 @@ class Select(
|
|
|
85
71
|
|
|
86
72
|
Examples:
|
|
87
73
|
Select("id", "name") # Shorthand for Select().select("id", "name")
|
|
88
|
-
Select() # Same as
|
|
74
|
+
Select() # Same as Select() - start empty
|
|
89
75
|
"""
|
|
90
76
|
super().__init__(**kwargs)
|
|
91
77
|
|
|
92
|
-
# Manually initialize dataclass fields here because a custom __init__ is defined.
|
|
93
|
-
# This is necessary to support the `Select("col1", "col2")` shorthand initialization.
|
|
94
78
|
self._with_parts = {}
|
|
95
79
|
self._expression = None
|
|
96
|
-
self._schema = None
|
|
97
80
|
self._hints = []
|
|
98
81
|
|
|
99
82
|
self._create_base_expression()
|
|
100
83
|
|
|
101
|
-
# Add columns if provided - just a shorthand for .select()
|
|
102
84
|
if columns:
|
|
103
85
|
self.select(*columns)
|
|
104
86
|
|
|
105
87
|
@property
|
|
106
|
-
def _expected_result_type(self) -> "type[SQLResult
|
|
88
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
107
89
|
"""Get the expected result type for SELECT operations.
|
|
108
90
|
|
|
109
91
|
Returns:
|
|
110
92
|
type: The SelectResult type.
|
|
111
93
|
"""
|
|
112
|
-
return SQLResult
|
|
94
|
+
return SQLResult
|
|
113
95
|
|
|
114
96
|
def _create_base_expression(self) -> "exp.Select":
|
|
115
97
|
if self._expression is None or not isinstance(self._expression, exp.Select):
|
|
116
98
|
self._expression = exp.Select()
|
|
117
|
-
# At this point, self._expression is exp.Select
|
|
118
99
|
return self._expression
|
|
119
100
|
|
|
120
|
-
def as_schema(self, schema: "type[RowT]") -> "Select[RowT]":
|
|
121
|
-
"""Return a new Select instance parameterized with the given schema/model type.
|
|
122
|
-
|
|
123
|
-
This enables type-safe result mapping: the returned builder will carry the schema type
|
|
124
|
-
for static analysis and IDE autocompletion. The schema should be a class such as a Pydantic
|
|
125
|
-
model, dataclass, or msgspec.Struct that describes the expected row shape.
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
schema: The schema/model class to use for row typing (e.g., a Pydantic model, dataclass, or msgspec.Struct).
|
|
129
|
-
|
|
130
|
-
Returns:
|
|
131
|
-
Select[RowT]: A new Select instance with RowT set to the provided schema/model type.
|
|
132
|
-
"""
|
|
133
|
-
new_builder = Select()
|
|
134
|
-
new_builder._expression = self._expression.copy() if self._expression is not None else None
|
|
135
|
-
new_builder._parameters = self._parameters.copy()
|
|
136
|
-
new_builder._parameter_counter = self._parameter_counter
|
|
137
|
-
new_builder.dialect = self.dialect
|
|
138
|
-
new_builder._schema = schema # type: ignore[assignment]
|
|
139
|
-
return cast("Select[RowT]", new_builder)
|
|
140
|
-
|
|
141
101
|
def with_hint(
|
|
142
102
|
self,
|
|
143
103
|
hint: "str",
|
|
@@ -176,13 +136,11 @@ class Select(
|
|
|
176
136
|
if isinstance(modified_expr, exp.Select):
|
|
177
137
|
statement_hints = [h["hint"] for h in self._hints if h.get("location") == "statement"]
|
|
178
138
|
if statement_hints:
|
|
179
|
-
# Parse each hint and create proper hint expressions
|
|
180
139
|
hint_expressions = []
|
|
181
140
|
|
|
182
141
|
def parse_hint(hint: Any) -> exp.Expression:
|
|
183
|
-
"""Parse a single hint
|
|
142
|
+
"""Parse a single hint."""
|
|
184
143
|
try:
|
|
185
|
-
# Try to parse hint as an expression (e.g., "INDEX(users idx_name)")
|
|
186
144
|
hint_str = str(hint) # Ensure hint is a string
|
|
187
145
|
hint_expr: Optional[exp.Expression] = exp.maybe_parse(hint_str, dialect=self.dialect_name)
|
|
188
146
|
if hint_expr:
|
|
@@ -197,8 +155,6 @@ class Select(
|
|
|
197
155
|
hint_node = exp.Hint(expressions=hint_expressions)
|
|
198
156
|
modified_expr.set("hint", hint_node)
|
|
199
157
|
|
|
200
|
-
# For table-level hints, we'll fall back to comment injection in SQL
|
|
201
|
-
# since SQLGlot doesn't have a standard way to attach hints to individual tables
|
|
202
158
|
modified_sql = modified_expr.sql(dialect=self.dialect_name, pretty=True)
|
|
203
159
|
|
|
204
160
|
table_hints = [h for h in self._hints if h.get("location") == "table" and h.get("table")]
|
|
@@ -206,7 +162,6 @@ class Select(
|
|
|
206
162
|
for th in table_hints:
|
|
207
163
|
table = str(th["table"])
|
|
208
164
|
hint = th["hint"]
|
|
209
|
-
# More precise regex that captures the table and optional alias
|
|
210
165
|
pattern = TABLE_HINT_PATTERN.format(re.escape(table))
|
|
211
166
|
compiled_pattern = re.compile(pattern, re.IGNORECASE)
|
|
212
167
|
|
|
@@ -10,27 +10,26 @@ from typing import TYPE_CHECKING, Any, Optional, Union
|
|
|
10
10
|
from sqlglot import exp
|
|
11
11
|
from typing_extensions import Self
|
|
12
12
|
|
|
13
|
-
from sqlspec.
|
|
14
|
-
from sqlspec.
|
|
15
|
-
from sqlspec.statement.builder.mixins import (
|
|
13
|
+
from sqlspec.builder._base import QueryBuilder, SafeQuery
|
|
14
|
+
from sqlspec.builder.mixins import (
|
|
16
15
|
ReturningClauseMixin,
|
|
17
16
|
UpdateFromClauseMixin,
|
|
18
17
|
UpdateSetClauseMixin,
|
|
19
18
|
UpdateTableClauseMixin,
|
|
20
19
|
WhereClauseMixin,
|
|
21
20
|
)
|
|
22
|
-
from sqlspec.
|
|
23
|
-
from sqlspec.
|
|
21
|
+
from sqlspec.core.result import SQLResult
|
|
22
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
24
23
|
|
|
25
24
|
if TYPE_CHECKING:
|
|
26
|
-
from sqlspec.
|
|
25
|
+
from sqlspec.builder._select import Select
|
|
27
26
|
|
|
28
27
|
__all__ = ("Update",)
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
@dataclass(unsafe_hash=True)
|
|
32
31
|
class Update(
|
|
33
|
-
QueryBuilder
|
|
32
|
+
QueryBuilder,
|
|
34
33
|
WhereClauseMixin,
|
|
35
34
|
ReturningClauseMixin,
|
|
36
35
|
UpdateSetClauseMixin,
|
|
@@ -44,7 +43,6 @@ class Update(
|
|
|
44
43
|
|
|
45
44
|
Example:
|
|
46
45
|
```python
|
|
47
|
-
# Basic UPDATE
|
|
48
46
|
update_query = (
|
|
49
47
|
Update()
|
|
50
48
|
.table("users")
|
|
@@ -53,12 +51,10 @@ class Update(
|
|
|
53
51
|
.where("id = 1")
|
|
54
52
|
)
|
|
55
53
|
|
|
56
|
-
# Even more concise with constructor
|
|
57
54
|
update_query = (
|
|
58
55
|
Update("users").set(name="John Doe").where("id = 1")
|
|
59
56
|
)
|
|
60
57
|
|
|
61
|
-
# UPDATE with parameterized conditions
|
|
62
58
|
update_query = (
|
|
63
59
|
Update()
|
|
64
60
|
.table("users")
|
|
@@ -66,7 +62,6 @@ class Update(
|
|
|
66
62
|
.where_eq("id", 123)
|
|
67
63
|
)
|
|
68
64
|
|
|
69
|
-
# UPDATE with FROM clause (PostgreSQL style)
|
|
70
65
|
update_query = (
|
|
71
66
|
Update()
|
|
72
67
|
.table("users", "u")
|
|
@@ -90,9 +85,9 @@ class Update(
|
|
|
90
85
|
self.table(table)
|
|
91
86
|
|
|
92
87
|
@property
|
|
93
|
-
def _expected_result_type(self) -> "type[SQLResult
|
|
88
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
94
89
|
"""Return the expected result type for this builder."""
|
|
95
|
-
return SQLResult
|
|
90
|
+
return SQLResult
|
|
96
91
|
|
|
97
92
|
def _create_base_expression(self) -> exp.Update:
|
|
98
93
|
"""Create a base UPDATE expression.
|
|
@@ -104,7 +99,7 @@ class Update(
|
|
|
104
99
|
|
|
105
100
|
def join(
|
|
106
101
|
self,
|
|
107
|
-
table: "Union[str, exp.Expression, Select
|
|
102
|
+
table: "Union[str, exp.Expression, Select]",
|
|
108
103
|
on: "Union[str, exp.Expression]",
|
|
109
104
|
alias: "Optional[str]" = None,
|
|
110
105
|
join_type: str = "INNER",
|
|
@@ -135,10 +130,9 @@ class Update(
|
|
|
135
130
|
subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=self.dialect))
|
|
136
131
|
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
137
132
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for p_name, p_value in subquery_params.items():
|
|
133
|
+
subquery_parameters = table._parameters
|
|
134
|
+
if subquery_parameters:
|
|
135
|
+
for p_name, p_value in subquery_parameters.items():
|
|
142
136
|
self.add_parameter(p_value, name=p_name)
|
|
143
137
|
else:
|
|
144
138
|
table_expr = table
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
"""SQL statement builder mixins."""
|
|
2
2
|
|
|
3
|
-
from sqlspec.
|
|
4
|
-
from sqlspec.
|
|
5
|
-
from sqlspec.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
InsertValuesMixin,
|
|
9
|
-
)
|
|
10
|
-
from sqlspec.statement.builder.mixins._join_operations import JoinClauseMixin
|
|
11
|
-
from sqlspec.statement.builder.mixins._merge_operations import (
|
|
3
|
+
from sqlspec.builder.mixins._cte_and_set_ops import CommonTableExpressionMixin, SetOperationMixin
|
|
4
|
+
from sqlspec.builder.mixins._delete_operations import DeleteFromClauseMixin
|
|
5
|
+
from sqlspec.builder.mixins._insert_operations import InsertFromSelectMixin, InsertIntoClauseMixin, InsertValuesMixin
|
|
6
|
+
from sqlspec.builder.mixins._join_operations import JoinClauseMixin
|
|
7
|
+
from sqlspec.builder.mixins._merge_operations import (
|
|
12
8
|
MergeIntoClauseMixin,
|
|
13
9
|
MergeMatchedClauseMixin,
|
|
14
10
|
MergeNotMatchedBySourceClauseMixin,
|
|
@@ -16,19 +12,19 @@ from sqlspec.statement.builder.mixins._merge_operations import (
|
|
|
16
12
|
MergeOnClauseMixin,
|
|
17
13
|
MergeUsingClauseMixin,
|
|
18
14
|
)
|
|
19
|
-
from sqlspec.
|
|
15
|
+
from sqlspec.builder.mixins._order_limit_operations import (
|
|
20
16
|
LimitOffsetClauseMixin,
|
|
21
17
|
OrderByClauseMixin,
|
|
22
18
|
ReturningClauseMixin,
|
|
23
19
|
)
|
|
24
|
-
from sqlspec.
|
|
25
|
-
from sqlspec.
|
|
26
|
-
from sqlspec.
|
|
20
|
+
from sqlspec.builder.mixins._pivot_operations import PivotClauseMixin, UnpivotClauseMixin
|
|
21
|
+
from sqlspec.builder.mixins._select_operations import CaseBuilder, SelectClauseMixin
|
|
22
|
+
from sqlspec.builder.mixins._update_operations import (
|
|
27
23
|
UpdateFromClauseMixin,
|
|
28
24
|
UpdateSetClauseMixin,
|
|
29
25
|
UpdateTableClauseMixin,
|
|
30
26
|
)
|
|
31
|
-
from sqlspec.
|
|
27
|
+
from sqlspec.builder.mixins._where_clause import HavingClauseMixin, WhereClauseMixin
|
|
32
28
|
|
|
33
29
|
__all__ = (
|
|
34
30
|
"CaseBuilder",
|
|
@@ -36,57 +36,49 @@ class CommonTableExpressionMixin:
|
|
|
36
36
|
msg = "Cannot add WITH clause: expression not initialized."
|
|
37
37
|
raise SQLBuilderError(msg)
|
|
38
38
|
|
|
39
|
-
if not
|
|
40
|
-
self._expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)
|
|
41
|
-
):
|
|
39
|
+
if not isinstance(self._expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)):
|
|
42
40
|
msg = f"Cannot add WITH clause to {type(self._expression).__name__} expression."
|
|
43
41
|
raise SQLBuilderError(msg)
|
|
44
42
|
|
|
45
43
|
cte_expr: Optional[exp.Expression] = None
|
|
46
|
-
if
|
|
47
|
-
|
|
48
|
-
built_query = query.to_statement() # pyright: ignore
|
|
49
|
-
cte_sql = built_query.to_sql()
|
|
50
|
-
cte_expr = exp.maybe_parse(cte_sql, dialect=getattr(self, "dialect", None))
|
|
51
|
-
|
|
52
|
-
# Merge parameters
|
|
53
|
-
if hasattr(self, "add_parameter"):
|
|
54
|
-
parameters = getattr(built_query, "parameters", None) or {}
|
|
55
|
-
for param_name, param_value in parameters.items():
|
|
56
|
-
self.add_parameter(param_value, name=param_name) # pyright: ignore
|
|
57
|
-
elif isinstance(query, str):
|
|
58
|
-
cte_expr = exp.maybe_parse(query, dialect=getattr(self, "dialect", None))
|
|
44
|
+
if isinstance(query, str):
|
|
45
|
+
cte_expr = exp.maybe_parse(query, dialect=self.dialect) # type: ignore[attr-defined]
|
|
59
46
|
elif isinstance(query, exp.Expression):
|
|
60
47
|
cte_expr = query
|
|
48
|
+
else:
|
|
49
|
+
built_query = query.to_statement() # pyright: ignore
|
|
50
|
+
cte_sql = built_query.sql
|
|
51
|
+
cte_expr = exp.maybe_parse(cte_sql, dialect=self.dialect) # type: ignore[attr-defined]
|
|
52
|
+
|
|
53
|
+
parameters = built_query.parameters
|
|
54
|
+
if parameters:
|
|
55
|
+
if isinstance(parameters, dict):
|
|
56
|
+
for param_name, param_value in parameters.items():
|
|
57
|
+
self.add_parameter(param_value, name=param_name) # type: ignore[attr-defined]
|
|
58
|
+
elif isinstance(parameters, (list, tuple)):
|
|
59
|
+
for param_value in parameters:
|
|
60
|
+
self.add_parameter(param_value) # type: ignore[attr-defined]
|
|
61
61
|
|
|
62
62
|
if not cte_expr:
|
|
63
63
|
msg = f"Could not parse CTE query: {query}"
|
|
64
64
|
raise SQLBuilderError(msg)
|
|
65
65
|
|
|
66
66
|
if columns:
|
|
67
|
-
# CTE with explicit column list: name(col1, col2, ...)
|
|
68
67
|
cte_alias_expr = exp.alias_(cte_expr, name, table=[exp.to_identifier(col) for col in columns])
|
|
69
68
|
else:
|
|
70
|
-
# Simple CTE alias: name
|
|
71
69
|
cte_alias_expr = exp.alias_(cte_expr, name)
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
if
|
|
75
|
-
existing_with
|
|
76
|
-
if
|
|
77
|
-
existing_with.
|
|
78
|
-
if recursive:
|
|
79
|
-
existing_with.set("recursive", recursive)
|
|
80
|
-
else:
|
|
81
|
-
self._expression = self._expression.with_(cte_alias_expr, as_=name, copy=False) # pyright: ignore
|
|
82
|
-
if recursive:
|
|
83
|
-
with_clause = self._expression.find(exp.With)
|
|
84
|
-
if with_clause:
|
|
85
|
-
with_clause.set("recursive", recursive)
|
|
71
|
+
existing_with = self._expression.args.get("with") # pyright: ignore
|
|
72
|
+
if existing_with:
|
|
73
|
+
existing_with.expressions.append(cte_alias_expr)
|
|
74
|
+
if recursive:
|
|
75
|
+
existing_with.set("recursive", recursive)
|
|
86
76
|
else:
|
|
87
|
-
|
|
88
|
-
if
|
|
89
|
-
|
|
77
|
+
self._expression = self._expression.with_(cte_alias_expr, as_=name, copy=False) # type: ignore[union-attr]
|
|
78
|
+
if recursive:
|
|
79
|
+
with_clause = self._expression.find(exp.With)
|
|
80
|
+
if with_clause:
|
|
81
|
+
with_clause.set("recursive", recursive)
|
|
90
82
|
self._with_ctes[name] = exp.CTE(this=cte_expr, alias=exp.to_table(name)) # type: ignore[attr-defined]
|
|
91
83
|
|
|
92
84
|
return self
|
|
@@ -96,7 +88,7 @@ class SetOperationMixin:
|
|
|
96
88
|
"""Mixin providing set operations (UNION, INTERSECT, EXCEPT) for SELECT builders."""
|
|
97
89
|
|
|
98
90
|
_expression: Any = None
|
|
99
|
-
_parameters: dict[str, Any]
|
|
91
|
+
_parameters: dict[str, Any]
|
|
100
92
|
dialect: Any = None
|
|
101
93
|
|
|
102
94
|
def union(self, other: Any, all_: bool = False) -> Self:
|
|
@@ -114,25 +106,24 @@ class SetOperationMixin:
|
|
|
114
106
|
"""
|
|
115
107
|
left_query = self.build() # type: ignore[attr-defined]
|
|
116
108
|
right_query = other.build()
|
|
117
|
-
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=
|
|
118
|
-
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=
|
|
109
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
|
|
110
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
|
|
119
111
|
if not left_expr or not right_expr:
|
|
120
112
|
msg = "Could not parse queries for UNION operation"
|
|
121
113
|
raise SQLBuilderError(msg)
|
|
122
114
|
union_expr = exp.union(left_expr, right_expr, distinct=not all_)
|
|
123
115
|
new_builder = type(self)()
|
|
124
|
-
new_builder.dialect =
|
|
116
|
+
new_builder.dialect = self.dialect
|
|
125
117
|
new_builder._expression = union_expr
|
|
126
|
-
|
|
118
|
+
merged_parameters = dict(left_query.parameters)
|
|
127
119
|
for param_name, param_value in right_query.parameters.items():
|
|
128
|
-
if param_name in
|
|
120
|
+
if param_name in merged_parameters:
|
|
129
121
|
counter = 1
|
|
130
122
|
new_param_name = f"{param_name}_right_{counter}"
|
|
131
|
-
while new_param_name in
|
|
123
|
+
while new_param_name in merged_parameters:
|
|
132
124
|
counter += 1
|
|
133
125
|
new_param_name = f"{param_name}_right_{counter}"
|
|
134
126
|
|
|
135
|
-
# Use AST transformation instead of string manipulation
|
|
136
127
|
def rename_parameter(node: exp.Expression) -> exp.Expression:
|
|
137
128
|
if isinstance(node, exp.Placeholder) and node.name == param_name: # noqa: B023
|
|
138
129
|
return exp.Placeholder(this=new_param_name) # noqa: B023
|
|
@@ -141,10 +132,10 @@ class SetOperationMixin:
|
|
|
141
132
|
right_expr = right_expr.transform(rename_parameter)
|
|
142
133
|
union_expr = exp.union(left_expr, right_expr, distinct=not all_)
|
|
143
134
|
new_builder._expression = union_expr
|
|
144
|
-
|
|
135
|
+
merged_parameters[new_param_name] = param_value
|
|
145
136
|
else:
|
|
146
|
-
|
|
147
|
-
new_builder._parameters =
|
|
137
|
+
merged_parameters[param_name] = param_value
|
|
138
|
+
new_builder._parameters = merged_parameters
|
|
148
139
|
return new_builder
|
|
149
140
|
|
|
150
141
|
def intersect(self, other: Any) -> Self:
|
|
@@ -161,19 +152,18 @@ class SetOperationMixin:
|
|
|
161
152
|
"""
|
|
162
153
|
left_query = self.build() # type: ignore[attr-defined]
|
|
163
154
|
right_query = other.build()
|
|
164
|
-
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=
|
|
165
|
-
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=
|
|
155
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
|
|
156
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
|
|
166
157
|
if not left_expr or not right_expr:
|
|
167
158
|
msg = "Could not parse queries for INTERSECT operation"
|
|
168
159
|
raise SQLBuilderError(msg)
|
|
169
160
|
intersect_expr = exp.intersect(left_expr, right_expr, distinct=True)
|
|
170
161
|
new_builder = type(self)()
|
|
171
|
-
new_builder.dialect =
|
|
162
|
+
new_builder.dialect = self.dialect
|
|
172
163
|
new_builder._expression = intersect_expr
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
new_builder._parameters = merged_params
|
|
164
|
+
merged_parameters = dict(left_query.parameters)
|
|
165
|
+
merged_parameters.update(right_query.parameters)
|
|
166
|
+
new_builder._parameters = merged_parameters
|
|
177
167
|
return new_builder
|
|
178
168
|
|
|
179
169
|
def except_(self, other: Any) -> Self:
|
|
@@ -190,17 +180,16 @@ class SetOperationMixin:
|
|
|
190
180
|
"""
|
|
191
181
|
left_query = self.build() # type: ignore[attr-defined]
|
|
192
182
|
right_query = other.build()
|
|
193
|
-
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=
|
|
194
|
-
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=
|
|
183
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
|
|
184
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
|
|
195
185
|
if not left_expr or not right_expr:
|
|
196
186
|
msg = "Could not parse queries for EXCEPT operation"
|
|
197
187
|
raise SQLBuilderError(msg)
|
|
198
188
|
except_expr = exp.except_(left_expr, right_expr)
|
|
199
189
|
new_builder = type(self)()
|
|
200
|
-
new_builder.dialect =
|
|
190
|
+
new_builder.dialect = self.dialect
|
|
201
191
|
new_builder._expression = except_expr
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
new_builder._parameters = merged_params
|
|
192
|
+
merged_parameters = dict(left_query.parameters)
|
|
193
|
+
merged_parameters.update(right_query.parameters)
|
|
194
|
+
new_builder._parameters = merged_parameters
|
|
206
195
|
return new_builder
|
|
@@ -53,13 +53,14 @@ class InsertValuesMixin:
|
|
|
53
53
|
raise SQLBuilderError(msg)
|
|
54
54
|
column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
|
|
55
55
|
self._expression.set("columns", column_exprs)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# If no columns, clear the list
|
|
56
|
+
try:
|
|
57
|
+
cols = self._columns # type: ignore[attr-defined]
|
|
59
58
|
if not columns:
|
|
60
|
-
|
|
59
|
+
cols.clear()
|
|
61
60
|
else:
|
|
62
|
-
|
|
61
|
+
cols[:] = [col.name if isinstance(col, exp.Column) else str(col) for col in columns]
|
|
62
|
+
except AttributeError:
|
|
63
|
+
pass
|
|
63
64
|
return self
|
|
64
65
|
|
|
65
66
|
def values(self, *values: Any) -> Self:
|
|
@@ -69,11 +70,13 @@ class InsertValuesMixin:
|
|
|
69
70
|
if not isinstance(self._expression, exp.Insert):
|
|
70
71
|
msg = "Cannot add values to a non-INSERT expression."
|
|
71
72
|
raise SQLBuilderError(msg)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
try:
|
|
74
|
+
_columns = self._columns # type: ignore[attr-defined]
|
|
75
|
+
if _columns and len(values) != len(_columns):
|
|
76
|
+
msg = f"Number of values ({len(values)}) does not match the number of specified columns ({len(_columns)})."
|
|
77
|
+
raise SQLBuilderError(msg)
|
|
78
|
+
except AttributeError:
|
|
79
|
+
pass
|
|
77
80
|
row_exprs = []
|
|
78
81
|
for v in values:
|
|
79
82
|
if isinstance(v, exp.Expression):
|
|
@@ -114,7 +117,11 @@ class InsertFromSelectMixin:
|
|
|
114
117
|
Raises:
|
|
115
118
|
SQLBuilderError: If the table is not set or the select_builder is invalid.
|
|
116
119
|
"""
|
|
117
|
-
|
|
120
|
+
try:
|
|
121
|
+
if not self._table: # type: ignore[attr-defined]
|
|
122
|
+
msg = "The target table must be set using .into() before adding values."
|
|
123
|
+
raise SQLBuilderError(msg)
|
|
124
|
+
except AttributeError:
|
|
118
125
|
msg = "The target table must be set using .into() before adding values."
|
|
119
126
|
raise SQLBuilderError(msg)
|
|
120
127
|
if self._expression is None:
|
|
@@ -122,12 +129,11 @@ class InsertFromSelectMixin:
|
|
|
122
129
|
if not isinstance(self._expression, exp.Insert):
|
|
123
130
|
msg = "Cannot set INSERT source on a non-INSERT expression."
|
|
124
131
|
raise SQLBuilderError(msg)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
for p_name, p_value in subquery_params.items():
|
|
132
|
+
subquery_parameters = select_builder._parameters # pyright: ignore[attr-defined]
|
|
133
|
+
if subquery_parameters:
|
|
134
|
+
for p_name, p_value in subquery_parameters.items():
|
|
129
135
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
130
|
-
select_expr =
|
|
136
|
+
select_expr = select_builder._expression # pyright: ignore[attr-defined]
|
|
131
137
|
if select_expr and isinstance(select_expr, exp.Select):
|
|
132
138
|
self._expression.set("expression", select_expr.copy())
|
|
133
139
|
else:
|
|
@@ -3,8 +3,8 @@ from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
|
3
3
|
from sqlglot import exp
|
|
4
4
|
from typing_extensions import Self
|
|
5
5
|
|
|
6
|
+
from sqlspec.builder._parsing_utils import parse_table_expression
|
|
6
7
|
from sqlspec.exceptions import SQLBuilderError
|
|
7
|
-
from sqlspec.statement.builder._parsing_utils import parse_table_expression
|
|
8
8
|
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
@@ -33,7 +33,6 @@ class JoinClauseMixin:
|
|
|
33
33
|
if isinstance(table, str):
|
|
34
34
|
table_expr = parse_table_expression(table, alias)
|
|
35
35
|
elif has_query_builder_parameters(table):
|
|
36
|
-
# Work directly with AST when possible to avoid string parsing
|
|
37
36
|
if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
|
|
38
37
|
table_expr_value = getattr(table, "_expression", None)
|
|
39
38
|
if table_expr_value is not None:
|
|
@@ -46,7 +45,6 @@ class JoinClauseMixin:
|
|
|
46
45
|
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
47
46
|
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
|
|
48
47
|
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
49
|
-
# Parameter merging logic can be added here if needed
|
|
50
48
|
else:
|
|
51
49
|
table_expr = table
|
|
52
50
|
on_expr: Optional[exp.Expression] = None
|
|
@@ -70,10 +70,9 @@ class MergeUsingClauseMixin:
|
|
|
70
70
|
if isinstance(source, str):
|
|
71
71
|
source_expr = exp.to_table(source, alias=alias)
|
|
72
72
|
elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
for p_name, p_value in subquery_builder_params.items():
|
|
73
|
+
subquery_builder_parameters = source.parameters
|
|
74
|
+
if subquery_builder_parameters:
|
|
75
|
+
for p_name, p_value in subquery_builder_parameters.items():
|
|
77
76
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
78
77
|
|
|
79
78
|
subquery_exp = exp.paren(getattr(source, "_expression", exp.select()))
|
|
@@ -281,7 +280,6 @@ class MergeNotMatchedClauseMixin:
|
|
|
281
280
|
msg = "Specifying columns without values for INSERT action is complex and not fully supported yet. Consider providing full expressions."
|
|
282
281
|
raise SQLBuilderError(msg)
|
|
283
282
|
elif not columns and not values:
|
|
284
|
-
# INSERT DEFAULT VALUES case
|
|
285
283
|
pass
|
|
286
284
|
else:
|
|
287
285
|
msg = "Cannot specify values without columns for INSERT action."
|
|
@@ -5,8 +5,8 @@ from typing import TYPE_CHECKING, Optional, Union, cast
|
|
|
5
5
|
from sqlglot import exp
|
|
6
6
|
from typing_extensions import Self
|
|
7
7
|
|
|
8
|
+
from sqlspec.builder._parsing_utils import parse_order_expression
|
|
8
9
|
from sqlspec.exceptions import SQLBuilderError
|
|
9
|
-
from sqlspec.statement.builder._parsing_utils import parse_order_expression
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from sqlspec.protocols import SQLBuilderProtocol
|
|
@@ -71,7 +71,7 @@ class LimitOffsetClauseMixin:
|
|
|
71
71
|
if not isinstance(builder._expression, exp.Select):
|
|
72
72
|
msg = "LIMIT is only supported for SELECT statements."
|
|
73
73
|
raise SQLBuilderError(msg)
|
|
74
|
-
builder._expression = builder._expression.limit(exp.
|
|
74
|
+
builder._expression = builder._expression.limit(exp.convert(value), copy=False)
|
|
75
75
|
return cast("Self", builder)
|
|
76
76
|
|
|
77
77
|
def offset(self, value: int) -> Self:
|
|
@@ -90,7 +90,7 @@ class LimitOffsetClauseMixin:
|
|
|
90
90
|
if not isinstance(builder._expression, exp.Select):
|
|
91
91
|
msg = "OFFSET is only supported for SELECT statements."
|
|
92
92
|
raise SQLBuilderError(msg)
|
|
93
|
-
builder._expression = builder._expression.offset(exp.
|
|
93
|
+
builder._expression = builder._expression.offset(exp.convert(value), copy=False)
|
|
94
94
|
return cast("Self", builder)
|
|
95
95
|
|
|
96
96
|
|