sqlspec 0.14.1__py3-none-any.whl → 0.16.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 +480 -121
- 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 +18 -65
- sqlspec/builder/_merge.py +56 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +8 -11
- 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 +34 -18
- sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
- sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +19 -9
- 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 +25 -38
- sqlspec/{statement/builder → builder}/mixins/_update_operations.py +15 -16
- sqlspec/{statement/builder → builder}/mixins/_where_clause.py +210 -137
- 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 +830 -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 +666 -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 +164 -0
- sqlspec/driver/mixins/_sql_translator.py +6 -3
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/cli.py +1 -1
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +18 -16
- 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 +17 -38
- sqlspec/utils/text.py +12 -51
- sqlspec/utils/type_guards.py +443 -232
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/METADATA +7 -2
- sqlspec-0.16.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.16.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -32,7 +32,6 @@ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> ex
|
|
|
32
32
|
if isinstance(column_input, exp.Expression):
|
|
33
33
|
return column_input
|
|
34
34
|
|
|
35
|
-
# Handle our custom Column objects
|
|
36
35
|
if has_expression_attr(column_input):
|
|
37
36
|
attr_value = getattr(column_input, "_expression", None)
|
|
38
37
|
if isinstance(attr_value, exp.Expression):
|
|
@@ -44,7 +43,6 @@ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> ex
|
|
|
44
43
|
def parse_table_expression(table_input: str, explicit_alias: Optional[str] = None) -> exp.Expression:
|
|
45
44
|
"""Parses a table string that can be a name, a name with an alias, or a subquery string."""
|
|
46
45
|
with contextlib.suppress(Exception):
|
|
47
|
-
# Wrapping in a SELECT statement is a robust way to parse various table-like syntaxes
|
|
48
46
|
parsed = parse_one(f"SELECT * FROM {table_input}")
|
|
49
47
|
if isinstance(parsed, exp.Select) and parsed.args.get("from"):
|
|
50
48
|
from_clause = cast("exp.From", parsed.args.get("from"))
|
|
@@ -110,33 +108,32 @@ def parse_condition_expression(
|
|
|
110
108
|
column_expr = parse_column_expression(column)
|
|
111
109
|
if value is None:
|
|
112
110
|
return exp.Is(this=column_expr, expression=exp.null())
|
|
113
|
-
# Use builder's parameter system if available
|
|
114
111
|
if builder and has_parameter_builder(builder):
|
|
115
|
-
|
|
112
|
+
from sqlspec.builder.mixins._where_clause import _extract_column_name
|
|
113
|
+
|
|
114
|
+
column_name = _extract_column_name(column)
|
|
115
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
116
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
116
117
|
return exp.EQ(this=column_expr, expression=exp.Placeholder(this=param_name))
|
|
117
118
|
if isinstance(value, str):
|
|
118
|
-
return exp.EQ(this=column_expr, expression=exp.
|
|
119
|
+
return exp.EQ(this=column_expr, expression=exp.convert(value))
|
|
119
120
|
if isinstance(value, (int, float)):
|
|
120
|
-
return exp.EQ(this=column_expr, expression=exp.
|
|
121
|
-
return exp.EQ(this=column_expr, expression=exp.
|
|
121
|
+
return exp.EQ(this=column_expr, expression=exp.convert(str(value)))
|
|
122
|
+
return exp.EQ(this=column_expr, expression=exp.convert(str(value)))
|
|
122
123
|
|
|
123
124
|
if not isinstance(condition_input, str):
|
|
124
125
|
condition_input = str(condition_input)
|
|
125
126
|
|
|
126
127
|
try:
|
|
127
|
-
# Parse as condition using SQLGlot's condition parser
|
|
128
128
|
return exp.condition(condition_input)
|
|
129
129
|
except Exception:
|
|
130
|
-
# If that fails, try parsing as a general expression
|
|
131
130
|
try:
|
|
132
131
|
parsed = exp.maybe_parse(condition_input) # type: ignore[var-annotated]
|
|
133
132
|
if parsed:
|
|
134
133
|
return parsed # type:ignore[no-any-return]
|
|
135
134
|
except Exception: # noqa: S110
|
|
136
|
-
# SQLGlot condition parsing failed, will use raw condition
|
|
137
135
|
pass
|
|
138
136
|
|
|
139
|
-
# Ultimate fallback: treat as raw condition string
|
|
140
137
|
return exp.condition(condition_input)
|
|
141
138
|
|
|
142
139
|
|
|
@@ -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,17 +70,29 @@ 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
|
-
for v in values:
|
|
81
|
+
for i, v in enumerate(values):
|
|
79
82
|
if isinstance(v, exp.Expression):
|
|
80
83
|
row_exprs.append(v)
|
|
81
84
|
else:
|
|
82
|
-
|
|
85
|
+
# Try to use column name if available, otherwise use position-based name
|
|
86
|
+
try:
|
|
87
|
+
_columns = self._columns # type: ignore[attr-defined]
|
|
88
|
+
if _columns and i < len(_columns):
|
|
89
|
+
column_name = str(_columns[i]).split(".")[-1] if "." in str(_columns[i]) else str(_columns[i])
|
|
90
|
+
param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
|
|
91
|
+
else:
|
|
92
|
+
param_name = self._generate_unique_parameter_name(f"value_{i + 1}") # type: ignore[attr-defined]
|
|
93
|
+
except AttributeError:
|
|
94
|
+
param_name = self._generate_unique_parameter_name(f"value_{i + 1}") # type: ignore[attr-defined]
|
|
95
|
+
_, param_name = self.add_parameter(v, name=param_name) # type: ignore[attr-defined]
|
|
83
96
|
row_exprs.append(exp.var(param_name))
|
|
84
97
|
values_expr = exp.Values(expressions=[row_exprs])
|
|
85
98
|
self._expression.set("expression", values_expr)
|
|
@@ -114,7 +127,11 @@ class InsertFromSelectMixin:
|
|
|
114
127
|
Raises:
|
|
115
128
|
SQLBuilderError: If the table is not set or the select_builder is invalid.
|
|
116
129
|
"""
|
|
117
|
-
|
|
130
|
+
try:
|
|
131
|
+
if not self._table: # type: ignore[attr-defined]
|
|
132
|
+
msg = "The target table must be set using .into() before adding values."
|
|
133
|
+
raise SQLBuilderError(msg)
|
|
134
|
+
except AttributeError:
|
|
118
135
|
msg = "The target table must be set using .into() before adding values."
|
|
119
136
|
raise SQLBuilderError(msg)
|
|
120
137
|
if self._expression is None:
|
|
@@ -122,12 +139,11 @@ class InsertFromSelectMixin:
|
|
|
122
139
|
if not isinstance(self._expression, exp.Insert):
|
|
123
140
|
msg = "Cannot set INSERT source on a non-INSERT expression."
|
|
124
141
|
raise SQLBuilderError(msg)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
for p_name, p_value in subquery_params.items():
|
|
142
|
+
subquery_parameters = select_builder._parameters # pyright: ignore[attr-defined]
|
|
143
|
+
if subquery_parameters:
|
|
144
|
+
for p_name, p_value in subquery_parameters.items():
|
|
129
145
|
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
130
|
-
select_expr =
|
|
146
|
+
select_expr = select_builder._expression # pyright: ignore[attr-defined]
|
|
131
147
|
if select_expr and isinstance(select_expr, exp.Select):
|
|
132
148
|
self._expression.set("expression", select_expr.copy())
|
|
133
149
|
else:
|