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,206 @@
|
|
|
1
|
+
"""Safe SQL query builder with validation and parameter binding.
|
|
2
|
+
|
|
3
|
+
This module provides a fluent interface for building SQL queries safely,
|
|
4
|
+
with automatic parameter binding and validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Optional, Union, cast
|
|
10
|
+
|
|
11
|
+
from sqlglot import exp
|
|
12
|
+
from typing_extensions import Self
|
|
13
|
+
|
|
14
|
+
from sqlspec.statement.builder.base import QueryBuilder, SafeQuery
|
|
15
|
+
from sqlspec.statement.builder.mixins import (
|
|
16
|
+
AggregateFunctionsMixin,
|
|
17
|
+
CaseBuilderMixin,
|
|
18
|
+
CommonTableExpressionMixin,
|
|
19
|
+
FromClauseMixin,
|
|
20
|
+
GroupByClauseMixin,
|
|
21
|
+
HavingClauseMixin,
|
|
22
|
+
JoinClauseMixin,
|
|
23
|
+
LimitOffsetClauseMixin,
|
|
24
|
+
OrderByClauseMixin,
|
|
25
|
+
PivotClauseMixin,
|
|
26
|
+
SelectColumnsMixin,
|
|
27
|
+
SetOperationMixin,
|
|
28
|
+
UnpivotClauseMixin,
|
|
29
|
+
WhereClauseMixin,
|
|
30
|
+
WindowFunctionsMixin,
|
|
31
|
+
)
|
|
32
|
+
from sqlspec.statement.result import SQLResult
|
|
33
|
+
from sqlspec.typing import RowT
|
|
34
|
+
|
|
35
|
+
__all__ = ("SelectBuilder",)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class SelectBuilder(
|
|
40
|
+
QueryBuilder[RowT],
|
|
41
|
+
WhereClauseMixin,
|
|
42
|
+
OrderByClauseMixin,
|
|
43
|
+
LimitOffsetClauseMixin,
|
|
44
|
+
SelectColumnsMixin,
|
|
45
|
+
JoinClauseMixin,
|
|
46
|
+
FromClauseMixin,
|
|
47
|
+
GroupByClauseMixin,
|
|
48
|
+
HavingClauseMixin,
|
|
49
|
+
SetOperationMixin,
|
|
50
|
+
CommonTableExpressionMixin,
|
|
51
|
+
AggregateFunctionsMixin,
|
|
52
|
+
WindowFunctionsMixin,
|
|
53
|
+
CaseBuilderMixin,
|
|
54
|
+
PivotClauseMixin,
|
|
55
|
+
UnpivotClauseMixin,
|
|
56
|
+
):
|
|
57
|
+
"""Type-safe builder for SELECT queries with schema/model integration.
|
|
58
|
+
|
|
59
|
+
This builder provides a fluent, safe interface for constructing SQL SELECT statements.
|
|
60
|
+
It supports type-safe result mapping via the `as_schema()` method, allowing users to
|
|
61
|
+
associate a schema/model (such as a Pydantic model, dataclass, or msgspec.Struct) with
|
|
62
|
+
the query for static type checking and IDE support.
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
>>> class User(BaseModel):
|
|
66
|
+
... id: int
|
|
67
|
+
... name: str
|
|
68
|
+
>>> builder = (
|
|
69
|
+
... SelectBuilder()
|
|
70
|
+
... .select("id", "name")
|
|
71
|
+
... .from_("users")
|
|
72
|
+
... .as_schema(User)
|
|
73
|
+
... )
|
|
74
|
+
>>> result: list[User] = driver.execute(builder)
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
_schema: The schema/model class for row typing, if set via as_schema().
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
_with_parts: "dict[str, Union[exp.CTE, SelectBuilder]]" = field(default_factory=dict, init=False)
|
|
81
|
+
_expression: Optional[exp.Expression] = field(default=None, init=False, repr=False, compare=False, hash=False)
|
|
82
|
+
_schema: Optional[type[RowT]] = None
|
|
83
|
+
_hints: "list[dict[str, object]]" = field(default_factory=list, init=False, repr=False)
|
|
84
|
+
|
|
85
|
+
def __post_init__(self) -> "None":
|
|
86
|
+
super().__post_init__()
|
|
87
|
+
if self._expression is None:
|
|
88
|
+
self._create_base_expression()
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def _expected_result_type(self) -> "type[SQLResult[RowT]]":
|
|
92
|
+
"""Get the expected result type for SELECT operations.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
type: The SelectResult type.
|
|
96
|
+
"""
|
|
97
|
+
return SQLResult[RowT]
|
|
98
|
+
|
|
99
|
+
def _create_base_expression(self) -> "exp.Select":
|
|
100
|
+
if self._expression is None or not isinstance(self._expression, exp.Select):
|
|
101
|
+
self._expression = exp.Select()
|
|
102
|
+
# At this point, self._expression is exp.Select
|
|
103
|
+
return self._expression
|
|
104
|
+
|
|
105
|
+
def as_schema(self, schema: "type[RowT]") -> "SelectBuilder[RowT]":
|
|
106
|
+
"""Return a new SelectBuilder instance parameterized with the given schema/model type.
|
|
107
|
+
|
|
108
|
+
This enables type-safe result mapping: the returned builder will carry the schema type
|
|
109
|
+
for static analysis and IDE autocompletion. The schema should be a class such as a Pydantic
|
|
110
|
+
model, dataclass, or msgspec.Struct that describes the expected row shape.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
schema: The schema/model class to use for row typing (e.g., a Pydantic model, dataclass, or msgspec.Struct).
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
SelectBuilder[RowT]: A new SelectBuilder instance with RowT set to the provided schema/model type.
|
|
117
|
+
"""
|
|
118
|
+
new_builder = SelectBuilder()
|
|
119
|
+
new_builder._expression = self._expression.copy() if self._expression is not None else None
|
|
120
|
+
new_builder._parameters = self._parameters.copy()
|
|
121
|
+
new_builder._parameter_counter = self._parameter_counter
|
|
122
|
+
new_builder.dialect = self.dialect
|
|
123
|
+
new_builder._schema = schema # type: ignore[assignment]
|
|
124
|
+
return cast("SelectBuilder[RowT]", new_builder)
|
|
125
|
+
|
|
126
|
+
def with_hint(
|
|
127
|
+
self,
|
|
128
|
+
hint: "str",
|
|
129
|
+
*,
|
|
130
|
+
location: "str" = "statement",
|
|
131
|
+
table: "Optional[str]" = None,
|
|
132
|
+
dialect: "Optional[str]" = None,
|
|
133
|
+
) -> "Self":
|
|
134
|
+
"""Attach an optimizer or dialect-specific hint to the query.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
hint: The raw hint string (e.g., 'INDEX(users idx_users_name)').
|
|
138
|
+
location: Where to apply the hint ('statement', 'table').
|
|
139
|
+
table: Table name if the hint is for a specific table.
|
|
140
|
+
dialect: Restrict the hint to a specific dialect (optional).
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The current builder instance for method chaining.
|
|
144
|
+
"""
|
|
145
|
+
self._hints.append({"hint": hint, "location": location, "table": table, "dialect": dialect})
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def build(self) -> "SafeQuery":
|
|
149
|
+
"""Builds the SQL query string and parameters with hint injection.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
SafeQuery: A dataclass containing the SQL string and parameters.
|
|
153
|
+
"""
|
|
154
|
+
# Call parent build method which handles CTEs and optimization
|
|
155
|
+
safe_query = super().build()
|
|
156
|
+
|
|
157
|
+
# Apply hints using SQLGlot's proper hint support (more robust than regex)
|
|
158
|
+
if hasattr(self, "_hints") and self._hints:
|
|
159
|
+
modified_expr = self._expression.copy() if self._expression else None
|
|
160
|
+
|
|
161
|
+
if modified_expr and isinstance(modified_expr, exp.Select):
|
|
162
|
+
# Apply statement-level hints using SQLGlot's Hint expression
|
|
163
|
+
statement_hints = [h["hint"] for h in self._hints if h.get("location") == "statement"]
|
|
164
|
+
if statement_hints:
|
|
165
|
+
# Parse each hint and create proper hint expressions
|
|
166
|
+
hint_expressions = []
|
|
167
|
+
for hint in statement_hints:
|
|
168
|
+
try:
|
|
169
|
+
# Try to parse hint as an expression (e.g., "INDEX(users idx_name)")
|
|
170
|
+
hint_str = str(hint) # Ensure hint is a string
|
|
171
|
+
hint_expr: Optional[exp.Expression] = exp.maybe_parse(hint_str, dialect=self.dialect_name)
|
|
172
|
+
if hint_expr:
|
|
173
|
+
hint_expressions.append(hint_expr)
|
|
174
|
+
else:
|
|
175
|
+
# Create a raw identifier for unparsable hints
|
|
176
|
+
hint_expressions.append(exp.Anonymous(this=hint_str))
|
|
177
|
+
except Exception: # noqa: PERF203
|
|
178
|
+
hint_expressions.append(exp.Anonymous(this=str(hint)))
|
|
179
|
+
|
|
180
|
+
# Create a Hint node and attach to SELECT
|
|
181
|
+
if hint_expressions:
|
|
182
|
+
hint_node = exp.Hint(expressions=hint_expressions)
|
|
183
|
+
modified_expr.set("hint", hint_node)
|
|
184
|
+
|
|
185
|
+
# For table-level hints, we'll fall back to comment injection in SQL
|
|
186
|
+
# since SQLGlot doesn't have a standard way to attach hints to individual tables
|
|
187
|
+
modified_sql = modified_expr.sql(dialect=self.dialect_name, pretty=True)
|
|
188
|
+
|
|
189
|
+
# Apply table-level hints via string manipulation (as fallback)
|
|
190
|
+
table_hints = [h for h in self._hints if h.get("location") == "table" and h.get("table")]
|
|
191
|
+
if table_hints:
|
|
192
|
+
for th in table_hints:
|
|
193
|
+
table = str(th["table"])
|
|
194
|
+
hint = th["hint"]
|
|
195
|
+
# More precise regex that captures the table and optional alias
|
|
196
|
+
pattern = rf"\b{re.escape(table)}\b(\s+AS\s+\w+)?"
|
|
197
|
+
|
|
198
|
+
def replacement_func(match: re.Match[str]) -> str:
|
|
199
|
+
alias_part = match.group(1) or ""
|
|
200
|
+
return f"/*+ {hint} */ {table}{alias_part}" # noqa: B023
|
|
201
|
+
|
|
202
|
+
modified_sql = re.sub(pattern, replacement_func, modified_sql, flags=re.IGNORECASE, count=1)
|
|
203
|
+
|
|
204
|
+
return SafeQuery(sql=modified_sql, parameters=safe_query.parameters, dialect=safe_query.dialect)
|
|
205
|
+
|
|
206
|
+
return safe_query
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Safe SQL query builder with validation and parameter binding.
|
|
2
|
+
|
|
3
|
+
This module provides a fluent interface for building SQL queries safely,
|
|
4
|
+
with automatic parameter binding and validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
9
|
+
|
|
10
|
+
from sqlglot import exp
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
14
|
+
from sqlspec.statement.builder.base import QueryBuilder, SafeQuery
|
|
15
|
+
from sqlspec.statement.builder.mixins import (
|
|
16
|
+
ReturningClauseMixin,
|
|
17
|
+
UpdateFromClauseMixin,
|
|
18
|
+
UpdateSetClauseMixin,
|
|
19
|
+
UpdateTableClauseMixin,
|
|
20
|
+
WhereClauseMixin,
|
|
21
|
+
)
|
|
22
|
+
from sqlspec.statement.result import SQLResult
|
|
23
|
+
from sqlspec.typing import RowT
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from sqlspec.statement.builder.select import SelectBuilder
|
|
27
|
+
|
|
28
|
+
__all__ = ("UpdateBuilder",)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(unsafe_hash=True)
|
|
32
|
+
class UpdateBuilder(
|
|
33
|
+
QueryBuilder[RowT],
|
|
34
|
+
WhereClauseMixin,
|
|
35
|
+
ReturningClauseMixin,
|
|
36
|
+
UpdateSetClauseMixin,
|
|
37
|
+
UpdateFromClauseMixin,
|
|
38
|
+
UpdateTableClauseMixin,
|
|
39
|
+
):
|
|
40
|
+
"""Builder for UPDATE statements.
|
|
41
|
+
|
|
42
|
+
This builder provides a fluent interface for constructing SQL UPDATE statements
|
|
43
|
+
with automatic parameter binding and validation.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
```python
|
|
47
|
+
# Basic UPDATE
|
|
48
|
+
update_query = (
|
|
49
|
+
UpdateBuilder()
|
|
50
|
+
.table("users")
|
|
51
|
+
.set(name="John Doe")
|
|
52
|
+
.set(email="john@example.com")
|
|
53
|
+
.where("id = 1")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# UPDATE with parameterized conditions
|
|
57
|
+
update_query = (
|
|
58
|
+
UpdateBuilder()
|
|
59
|
+
.table("users")
|
|
60
|
+
.set(status="active")
|
|
61
|
+
.where_eq("id", 123)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# UPDATE with FROM clause (PostgreSQL style)
|
|
65
|
+
update_query = (
|
|
66
|
+
UpdateBuilder()
|
|
67
|
+
.table("users", "u")
|
|
68
|
+
.set(name="Updated Name")
|
|
69
|
+
.from_("profiles", "p")
|
|
70
|
+
.where("u.id = p.user_id AND p.is_verified = true")
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def _expected_result_type(self) -> "type[SQLResult[RowT]]":
|
|
77
|
+
"""Return the expected result type for this builder."""
|
|
78
|
+
return SQLResult[RowT]
|
|
79
|
+
|
|
80
|
+
def _create_base_expression(self) -> exp.Update:
|
|
81
|
+
"""Create a base UPDATE expression.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
A new sqlglot Update expression with empty clauses.
|
|
85
|
+
"""
|
|
86
|
+
return exp.Update(this=None, expressions=[], joins=[])
|
|
87
|
+
|
|
88
|
+
def join(
|
|
89
|
+
self,
|
|
90
|
+
table: "Union[str, exp.Expression, SelectBuilder[RowT]]",
|
|
91
|
+
on: "Union[str, exp.Expression]",
|
|
92
|
+
alias: "Optional[str]" = None,
|
|
93
|
+
join_type: str = "INNER",
|
|
94
|
+
) -> "Self":
|
|
95
|
+
"""Add JOIN clause to the UPDATE statement.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
table: The table name, expression, or subquery to join.
|
|
99
|
+
on: The JOIN condition.
|
|
100
|
+
alias: Optional alias for the joined table.
|
|
101
|
+
join_type: Type of join (INNER, LEFT, RIGHT, FULL).
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The current builder instance for method chaining.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
SQLBuilderError: If the current expression is not an UPDATE statement.
|
|
108
|
+
"""
|
|
109
|
+
if self._expression is None or not isinstance(self._expression, exp.Update):
|
|
110
|
+
msg = "Cannot add JOIN clause to non-UPDATE expression."
|
|
111
|
+
raise SQLBuilderError(msg)
|
|
112
|
+
|
|
113
|
+
table_expr: exp.Expression
|
|
114
|
+
if isinstance(table, str):
|
|
115
|
+
table_expr = exp.table_(table, alias=alias)
|
|
116
|
+
elif isinstance(table, QueryBuilder):
|
|
117
|
+
subquery = table.build()
|
|
118
|
+
subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=self.dialect))
|
|
119
|
+
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
120
|
+
|
|
121
|
+
# Merge parameters
|
|
122
|
+
subquery_params = table._parameters
|
|
123
|
+
if subquery_params:
|
|
124
|
+
for p_name, p_value in subquery_params.items():
|
|
125
|
+
self.add_parameter(p_value, name=p_name)
|
|
126
|
+
else:
|
|
127
|
+
table_expr = table
|
|
128
|
+
|
|
129
|
+
on_expr: exp.Expression = exp.condition(on) if isinstance(on, str) else on
|
|
130
|
+
|
|
131
|
+
join_type_upper = join_type.upper()
|
|
132
|
+
if join_type_upper == "INNER":
|
|
133
|
+
join_expr = exp.Join(this=table_expr, on=on_expr)
|
|
134
|
+
elif join_type_upper == "LEFT":
|
|
135
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="LEFT")
|
|
136
|
+
elif join_type_upper == "RIGHT":
|
|
137
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="RIGHT")
|
|
138
|
+
elif join_type_upper == "FULL":
|
|
139
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="FULL", kind="OUTER")
|
|
140
|
+
else:
|
|
141
|
+
msg = f"Unsupported join type: {join_type}"
|
|
142
|
+
raise SQLBuilderError(msg)
|
|
143
|
+
|
|
144
|
+
# Add join to the UPDATE expression
|
|
145
|
+
if not self._expression.args.get("joins"):
|
|
146
|
+
self._expression.set("joins", [])
|
|
147
|
+
self._expression.args["joins"].append(join_expr)
|
|
148
|
+
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
def build(self) -> "SafeQuery":
|
|
152
|
+
"""Build the UPDATE query with validation.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
SafeQuery: The built query with SQL and parameters.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
SQLBuilderError: If no table is set or expression is not an UPDATE.
|
|
159
|
+
"""
|
|
160
|
+
if self._expression is None:
|
|
161
|
+
msg = "UPDATE expression not initialized."
|
|
162
|
+
raise SQLBuilderError(msg)
|
|
163
|
+
|
|
164
|
+
if not isinstance(self._expression, exp.Update):
|
|
165
|
+
msg = "No UPDATE expression to build or expression is of the wrong type."
|
|
166
|
+
raise SQLBuilderError(msg)
|
|
167
|
+
|
|
168
|
+
# Check that the table is set
|
|
169
|
+
if getattr(self._expression, "this", None) is None:
|
|
170
|
+
msg = "No table specified for UPDATE statement."
|
|
171
|
+
raise SQLBuilderError(msg)
|
|
172
|
+
|
|
173
|
+
# Check that at least one SET expression exists
|
|
174
|
+
if not self._expression.args.get("expressions"):
|
|
175
|
+
msg = "At least one SET clause must be specified for UPDATE statement."
|
|
176
|
+
raise SQLBuilderError(msg)
|
|
177
|
+
|
|
178
|
+
return super().build()
|