sqlspec 0.16.1__cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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.
- 51ff5a9eadfdefd49f98__mypyc.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/__init__.py +92 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +77 -0
- sqlspec/_sql.py +1780 -0
- sqlspec/_typing.py +680 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +361 -0
- sqlspec/adapters/adbc/driver.py +512 -0
- sqlspec/adapters/aiosqlite/__init__.py +19 -0
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +253 -0
- sqlspec/adapters/aiosqlite/driver.py +248 -0
- sqlspec/adapters/asyncmy/__init__.py +19 -0
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +180 -0
- sqlspec/adapters/asyncmy/driver.py +274 -0
- sqlspec/adapters/asyncpg/__init__.py +21 -0
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +229 -0
- sqlspec/adapters/asyncpg/driver.py +344 -0
- sqlspec/adapters/bigquery/__init__.py +18 -0
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +298 -0
- sqlspec/adapters/bigquery/driver.py +558 -0
- sqlspec/adapters/duckdb/__init__.py +22 -0
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +504 -0
- sqlspec/adapters/duckdb/driver.py +368 -0
- sqlspec/adapters/oracledb/__init__.py +32 -0
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +317 -0
- sqlspec/adapters/oracledb/driver.py +538 -0
- sqlspec/adapters/psqlpy/__init__.py +16 -0
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +214 -0
- sqlspec/adapters/psqlpy/driver.py +530 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +426 -0
- sqlspec/adapters/psycopg/driver.py +796 -0
- sqlspec/adapters/sqlite/__init__.py +15 -0
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +240 -0
- sqlspec/adapters/sqlite/driver.py +294 -0
- sqlspec/base.py +571 -0
- sqlspec/builder/__init__.py +62 -0
- sqlspec/builder/_base.py +473 -0
- sqlspec/builder/_column.py +320 -0
- sqlspec/builder/_ddl.py +1346 -0
- sqlspec/builder/_ddl_utils.py +103 -0
- sqlspec/builder/_delete.py +76 -0
- sqlspec/builder/_insert.py +256 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/builder/_parsing_utils.py +140 -0
- sqlspec/builder/_select.py +170 -0
- sqlspec/builder/_update.py +188 -0
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/builder/mixins/_delete_operations.py +41 -0
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/builder/mixins/_join_operations.py +122 -0
- sqlspec/builder/mixins/_merge_operations.py +476 -0
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +603 -0
- sqlspec/builder/mixins/_update_operations.py +187 -0
- sqlspec/builder/mixins/_where_clause.py +621 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +395 -0
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result.py +677 -0
- sqlspec/core/splitter.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +819 -0
- sqlspec/core/statement.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +19 -0
- sqlspec/driver/_async.py +502 -0
- sqlspec/driver/_common.py +631 -0
- sqlspec/driver/_sync.py +503 -0
- sqlspec/driver/mixins/__init__.py +6 -0
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +86 -0
- sqlspec/exceptions.py +193 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +461 -0
- sqlspec/extensions/litestar/__init__.py +6 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +92 -0
- sqlspec/extensions/litestar/handlers.py +260 -0
- sqlspec/extensions/litestar/plugin.py +145 -0
- sqlspec/extensions/litestar/providers.py +454 -0
- sqlspec/loader.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/loader.py +760 -0
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +407 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +23 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +163 -0
- sqlspec/storage/backends/fsspec.py +386 -0
- sqlspec/storage/backends/obstore.py +459 -0
- sqlspec/storage/capabilities.py +102 -0
- sqlspec/storage/registry.py +239 -0
- sqlspec/typing.py +299 -0
- sqlspec/utils/__init__.py +3 -0
- sqlspec/utils/correlation.py +150 -0
- sqlspec/utils/deprecation.py +106 -0
- sqlspec/utils/fixtures.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/fixtures.py +58 -0
- sqlspec/utils/logging.py +127 -0
- sqlspec/utils/module_loader.py +89 -0
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +32 -0
- sqlspec/utils/sync_tools.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +237 -0
- sqlspec/utils/text.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +96 -0
- sqlspec/utils/type_guards.cpython-39-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_guards.py +1139 -0
- sqlspec-0.16.1.dist-info/METADATA +365 -0
- sqlspec-0.16.1.dist-info/RECORD +148 -0
- sqlspec-0.16.1.dist-info/WHEEL +7 -0
- sqlspec-0.16.1.dist-info/entry_points.txt +2 -0
- sqlspec-0.16.1.dist-info/licenses/LICENSE +21 -0
- sqlspec-0.16.1.dist-info/licenses/NOTICE +29 -0
|
@@ -0,0 +1,170 @@
|
|
|
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 typing import Any, Callable, Final, Optional, Union
|
|
9
|
+
|
|
10
|
+
from sqlglot import exp
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from sqlspec.builder._base import QueryBuilder, SafeQuery
|
|
14
|
+
from sqlspec.builder.mixins import (
|
|
15
|
+
CommonTableExpressionMixin,
|
|
16
|
+
HavingClauseMixin,
|
|
17
|
+
JoinClauseMixin,
|
|
18
|
+
LimitOffsetClauseMixin,
|
|
19
|
+
OrderByClauseMixin,
|
|
20
|
+
PivotClauseMixin,
|
|
21
|
+
SelectClauseMixin,
|
|
22
|
+
SetOperationMixin,
|
|
23
|
+
UnpivotClauseMixin,
|
|
24
|
+
WhereClauseMixin,
|
|
25
|
+
)
|
|
26
|
+
from sqlspec.core.result import SQLResult
|
|
27
|
+
|
|
28
|
+
__all__ = ("Select",)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
TABLE_HINT_PATTERN: Final[str] = r"\b{}\b(\s+AS\s+\w+)?"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Select(
|
|
35
|
+
QueryBuilder,
|
|
36
|
+
WhereClauseMixin,
|
|
37
|
+
OrderByClauseMixin,
|
|
38
|
+
LimitOffsetClauseMixin,
|
|
39
|
+
SelectClauseMixin,
|
|
40
|
+
JoinClauseMixin,
|
|
41
|
+
HavingClauseMixin,
|
|
42
|
+
SetOperationMixin,
|
|
43
|
+
CommonTableExpressionMixin,
|
|
44
|
+
PivotClauseMixin,
|
|
45
|
+
UnpivotClauseMixin,
|
|
46
|
+
):
|
|
47
|
+
"""Type-safe builder for SELECT queries with schema/model integration.
|
|
48
|
+
|
|
49
|
+
This builder provides a fluent, safe interface for constructing SQL SELECT statements.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> class User(BaseModel):
|
|
53
|
+
... id: int
|
|
54
|
+
... name: str
|
|
55
|
+
>>> builder = Select("id", "name").from_("users")
|
|
56
|
+
>>> result = driver.execute(builder)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
__slots__ = ("_hints", "_with_parts")
|
|
60
|
+
_expression: Optional[exp.Expression]
|
|
61
|
+
|
|
62
|
+
def __init__(self, *columns: str, **kwargs: Any) -> None:
|
|
63
|
+
"""Initialize SELECT with optional columns.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
*columns: Column names to select (e.g., "id", "name", "u.email")
|
|
67
|
+
**kwargs: Additional QueryBuilder arguments (dialect, schema, etc.)
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
Select("id", "name") # Shorthand for Select().select("id", "name")
|
|
71
|
+
Select() # Same as Select() - start empty
|
|
72
|
+
"""
|
|
73
|
+
super().__init__(**kwargs)
|
|
74
|
+
|
|
75
|
+
# Initialize Select-specific attributes
|
|
76
|
+
self._with_parts: dict[str, Union[exp.CTE, Select]] = {}
|
|
77
|
+
self._hints: list[dict[str, object]] = []
|
|
78
|
+
|
|
79
|
+
self._initialize_expression()
|
|
80
|
+
|
|
81
|
+
if columns:
|
|
82
|
+
self.select(*columns)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
86
|
+
"""Get the expected result type for SELECT operations.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
type: The SelectResult type.
|
|
90
|
+
"""
|
|
91
|
+
return SQLResult
|
|
92
|
+
|
|
93
|
+
def _create_base_expression(self) -> exp.Select:
|
|
94
|
+
"""Create base SELECT expression."""
|
|
95
|
+
if self._expression is None or not isinstance(self._expression, exp.Select):
|
|
96
|
+
self._expression = exp.Select()
|
|
97
|
+
return self._expression
|
|
98
|
+
|
|
99
|
+
def with_hint(
|
|
100
|
+
self,
|
|
101
|
+
hint: "str",
|
|
102
|
+
*,
|
|
103
|
+
location: "str" = "statement",
|
|
104
|
+
table: "Optional[str]" = None,
|
|
105
|
+
dialect: "Optional[str]" = None,
|
|
106
|
+
) -> "Self":
|
|
107
|
+
"""Attach an optimizer or dialect-specific hint to the query.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
hint: The raw hint string (e.g., 'INDEX(users idx_users_name)').
|
|
111
|
+
location: Where to apply the hint ('statement', 'table').
|
|
112
|
+
table: Table name if the hint is for a specific table.
|
|
113
|
+
dialect: Restrict the hint to a specific dialect (optional).
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The current builder instance for method chaining.
|
|
117
|
+
"""
|
|
118
|
+
self._hints.append({"hint": hint, "location": location, "table": table, "dialect": dialect})
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def build(self) -> "SafeQuery":
|
|
122
|
+
"""Builds the SQL query string and parameters with hint injection.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
SafeQuery: A dataclass containing the SQL string and parameters.
|
|
126
|
+
"""
|
|
127
|
+
safe_query = super().build()
|
|
128
|
+
|
|
129
|
+
if not self._hints:
|
|
130
|
+
return safe_query
|
|
131
|
+
|
|
132
|
+
modified_expr = self._expression or self._create_base_expression()
|
|
133
|
+
|
|
134
|
+
if isinstance(modified_expr, exp.Select):
|
|
135
|
+
statement_hints = [h["hint"] for h in self._hints if h.get("location") == "statement"]
|
|
136
|
+
if statement_hints:
|
|
137
|
+
|
|
138
|
+
def parse_hint_safely(hint: Any) -> exp.Expression:
|
|
139
|
+
try:
|
|
140
|
+
hint_str = str(hint)
|
|
141
|
+
hint_expr: Optional[exp.Expression] = exp.maybe_parse(hint_str, dialect=self.dialect_name)
|
|
142
|
+
return hint_expr or exp.Anonymous(this=hint_str)
|
|
143
|
+
except Exception:
|
|
144
|
+
return exp.Anonymous(this=str(hint))
|
|
145
|
+
|
|
146
|
+
hint_expressions: list[exp.Expression] = [parse_hint_safely(hint) for hint in statement_hints]
|
|
147
|
+
|
|
148
|
+
if hint_expressions:
|
|
149
|
+
modified_expr.set("hint", exp.Hint(expressions=hint_expressions))
|
|
150
|
+
|
|
151
|
+
modified_sql = modified_expr.sql(dialect=self.dialect_name, pretty=True)
|
|
152
|
+
|
|
153
|
+
for hint_dict in self._hints:
|
|
154
|
+
if hint_dict.get("location") == "table" and hint_dict.get("table"):
|
|
155
|
+
table = str(hint_dict["table"])
|
|
156
|
+
hint = str(hint_dict["hint"])
|
|
157
|
+
pattern = TABLE_HINT_PATTERN.format(re.escape(table))
|
|
158
|
+
|
|
159
|
+
def make_replacement(hint_val: str, table_val: str) -> "Callable[[re.Match[str]], str]":
|
|
160
|
+
def replacement_func(match: re.Match[str]) -> str:
|
|
161
|
+
alias_part = match.group(1) or ""
|
|
162
|
+
return f"/*+ {hint_val} */ {table_val}{alias_part}"
|
|
163
|
+
|
|
164
|
+
return replacement_func
|
|
165
|
+
|
|
166
|
+
modified_sql = re.sub(
|
|
167
|
+
pattern, make_replacement(hint, table), modified_sql, count=1, flags=re.IGNORECASE
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return SafeQuery(sql=modified_sql, parameters=safe_query.parameters, dialect=safe_query.dialect)
|
|
@@ -0,0 +1,188 @@
|
|
|
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 typing import TYPE_CHECKING, Any, Optional, Union
|
|
8
|
+
|
|
9
|
+
from sqlglot import exp
|
|
10
|
+
from typing_extensions import Self
|
|
11
|
+
|
|
12
|
+
from sqlspec.builder._base import QueryBuilder, SafeQuery
|
|
13
|
+
from sqlspec.builder.mixins import (
|
|
14
|
+
ReturningClauseMixin,
|
|
15
|
+
UpdateFromClauseMixin,
|
|
16
|
+
UpdateSetClauseMixin,
|
|
17
|
+
UpdateTableClauseMixin,
|
|
18
|
+
WhereClauseMixin,
|
|
19
|
+
)
|
|
20
|
+
from sqlspec.core.result import SQLResult
|
|
21
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from sqlspec.builder._select import Select
|
|
25
|
+
|
|
26
|
+
__all__ = ("Update",)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Update(
|
|
30
|
+
QueryBuilder,
|
|
31
|
+
WhereClauseMixin,
|
|
32
|
+
ReturningClauseMixin,
|
|
33
|
+
UpdateSetClauseMixin,
|
|
34
|
+
UpdateFromClauseMixin,
|
|
35
|
+
UpdateTableClauseMixin,
|
|
36
|
+
):
|
|
37
|
+
"""Builder for UPDATE statements.
|
|
38
|
+
|
|
39
|
+
This builder provides a fluent interface for constructing SQL UPDATE statements
|
|
40
|
+
with automatic parameter binding and validation.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
```python
|
|
44
|
+
update_query = (
|
|
45
|
+
Update()
|
|
46
|
+
.table("users")
|
|
47
|
+
.set(name="John Doe")
|
|
48
|
+
.set(email="john@example.com")
|
|
49
|
+
.where("id = 1")
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
update_query = (
|
|
53
|
+
Update("users").set(name="John Doe").where("id = 1")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
update_query = (
|
|
57
|
+
Update()
|
|
58
|
+
.table("users")
|
|
59
|
+
.set(status="active")
|
|
60
|
+
.where_eq("id", 123)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
update_query = (
|
|
64
|
+
Update()
|
|
65
|
+
.table("users", "u")
|
|
66
|
+
.set(name="Updated Name")
|
|
67
|
+
.from_("profiles", "p")
|
|
68
|
+
.where("u.id = p.user_id AND p.is_verified = true")
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
__slots__ = ("_table",)
|
|
74
|
+
_expression: Optional[exp.Expression]
|
|
75
|
+
|
|
76
|
+
def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
|
|
77
|
+
"""Initialize UPDATE with optional table.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
table: Target table name
|
|
81
|
+
**kwargs: Additional QueryBuilder arguments
|
|
82
|
+
"""
|
|
83
|
+
super().__init__(**kwargs)
|
|
84
|
+
self._initialize_expression()
|
|
85
|
+
|
|
86
|
+
if table:
|
|
87
|
+
self.table(table)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
91
|
+
"""Return the expected result type for this builder."""
|
|
92
|
+
return SQLResult
|
|
93
|
+
|
|
94
|
+
def _create_base_expression(self) -> exp.Update:
|
|
95
|
+
"""Create a base UPDATE expression.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
A new sqlglot Update expression with empty clauses.
|
|
99
|
+
"""
|
|
100
|
+
return exp.Update(this=None, expressions=[], joins=[])
|
|
101
|
+
|
|
102
|
+
def join(
|
|
103
|
+
self,
|
|
104
|
+
table: "Union[str, exp.Expression, Select]",
|
|
105
|
+
on: "Union[str, exp.Expression]",
|
|
106
|
+
alias: "Optional[str]" = None,
|
|
107
|
+
join_type: str = "INNER",
|
|
108
|
+
) -> "Self":
|
|
109
|
+
"""Add JOIN clause to the UPDATE statement.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
table: The table name, expression, or subquery to join.
|
|
113
|
+
on: The JOIN condition.
|
|
114
|
+
alias: Optional alias for the joined table.
|
|
115
|
+
join_type: Type of join (INNER, LEFT, RIGHT, FULL).
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
The current builder instance for method chaining.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
SQLBuilderError: If the current expression is not an UPDATE statement.
|
|
122
|
+
"""
|
|
123
|
+
if self._expression is None or not isinstance(self._expression, exp.Update):
|
|
124
|
+
msg = "Cannot add JOIN clause to non-UPDATE expression."
|
|
125
|
+
raise SQLBuilderError(msg)
|
|
126
|
+
|
|
127
|
+
table_expr: exp.Expression
|
|
128
|
+
if isinstance(table, str):
|
|
129
|
+
table_expr = exp.table_(table, alias=alias)
|
|
130
|
+
elif isinstance(table, QueryBuilder):
|
|
131
|
+
subquery = table.build()
|
|
132
|
+
subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=self.dialect))
|
|
133
|
+
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
134
|
+
|
|
135
|
+
subquery_parameters = table._parameters
|
|
136
|
+
if subquery_parameters:
|
|
137
|
+
for p_name, p_value in subquery_parameters.items():
|
|
138
|
+
self.add_parameter(p_value, name=p_name)
|
|
139
|
+
else:
|
|
140
|
+
table_expr = table
|
|
141
|
+
|
|
142
|
+
on_expr: exp.Expression = exp.condition(on) if isinstance(on, str) else on
|
|
143
|
+
|
|
144
|
+
join_type_upper = join_type.upper()
|
|
145
|
+
if join_type_upper == "INNER":
|
|
146
|
+
join_expr = exp.Join(this=table_expr, on=on_expr)
|
|
147
|
+
elif join_type_upper == "LEFT":
|
|
148
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="LEFT")
|
|
149
|
+
elif join_type_upper == "RIGHT":
|
|
150
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="RIGHT")
|
|
151
|
+
elif join_type_upper == "FULL":
|
|
152
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="FULL", kind="OUTER")
|
|
153
|
+
else:
|
|
154
|
+
msg = f"Unsupported join type: {join_type}"
|
|
155
|
+
raise SQLBuilderError(msg)
|
|
156
|
+
|
|
157
|
+
if not self._expression.args.get("joins"):
|
|
158
|
+
self._expression.set("joins", [])
|
|
159
|
+
self._expression.args["joins"].append(join_expr)
|
|
160
|
+
|
|
161
|
+
return self
|
|
162
|
+
|
|
163
|
+
def build(self) -> "SafeQuery":
|
|
164
|
+
"""Build the UPDATE query with validation.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
SafeQuery: The built query with SQL and parameters.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
SQLBuilderError: If no table is set or expression is not an UPDATE.
|
|
171
|
+
"""
|
|
172
|
+
if self._expression is None:
|
|
173
|
+
msg = "UPDATE expression not initialized."
|
|
174
|
+
raise SQLBuilderError(msg)
|
|
175
|
+
|
|
176
|
+
if not isinstance(self._expression, exp.Update):
|
|
177
|
+
msg = "No UPDATE expression to build or expression is of the wrong type."
|
|
178
|
+
raise SQLBuilderError(msg)
|
|
179
|
+
|
|
180
|
+
if getattr(self._expression, "this", None) is None:
|
|
181
|
+
msg = "No table specified for UPDATE statement."
|
|
182
|
+
raise SQLBuilderError(msg)
|
|
183
|
+
|
|
184
|
+
if not self._expression.args.get("expressions"):
|
|
185
|
+
msg = "At least one SET clause must be specified for UPDATE statement."
|
|
186
|
+
raise SQLBuilderError(msg)
|
|
187
|
+
|
|
188
|
+
return super().build()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""SQL statement builder mixins."""
|
|
2
|
+
|
|
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 (
|
|
8
|
+
MergeIntoClauseMixin,
|
|
9
|
+
MergeMatchedClauseMixin,
|
|
10
|
+
MergeNotMatchedBySourceClauseMixin,
|
|
11
|
+
MergeNotMatchedClauseMixin,
|
|
12
|
+
MergeOnClauseMixin,
|
|
13
|
+
MergeUsingClauseMixin,
|
|
14
|
+
)
|
|
15
|
+
from sqlspec.builder.mixins._order_limit_operations import (
|
|
16
|
+
LimitOffsetClauseMixin,
|
|
17
|
+
OrderByClauseMixin,
|
|
18
|
+
ReturningClauseMixin,
|
|
19
|
+
)
|
|
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 (
|
|
23
|
+
UpdateFromClauseMixin,
|
|
24
|
+
UpdateSetClauseMixin,
|
|
25
|
+
UpdateTableClauseMixin,
|
|
26
|
+
)
|
|
27
|
+
from sqlspec.builder.mixins._where_clause import HavingClauseMixin, WhereClauseMixin
|
|
28
|
+
|
|
29
|
+
__all__ = (
|
|
30
|
+
"CaseBuilder",
|
|
31
|
+
"CommonTableExpressionMixin",
|
|
32
|
+
"DeleteFromClauseMixin",
|
|
33
|
+
"HavingClauseMixin",
|
|
34
|
+
"InsertFromSelectMixin",
|
|
35
|
+
"InsertIntoClauseMixin",
|
|
36
|
+
"InsertValuesMixin",
|
|
37
|
+
"JoinClauseMixin",
|
|
38
|
+
"LimitOffsetClauseMixin",
|
|
39
|
+
"MergeIntoClauseMixin",
|
|
40
|
+
"MergeMatchedClauseMixin",
|
|
41
|
+
"MergeNotMatchedBySourceClauseMixin",
|
|
42
|
+
"MergeNotMatchedClauseMixin",
|
|
43
|
+
"MergeOnClauseMixin",
|
|
44
|
+
"MergeUsingClauseMixin",
|
|
45
|
+
"OrderByClauseMixin",
|
|
46
|
+
"PivotClauseMixin",
|
|
47
|
+
"ReturningClauseMixin",
|
|
48
|
+
"SelectClauseMixin",
|
|
49
|
+
"SetOperationMixin",
|
|
50
|
+
"UnpivotClauseMixin",
|
|
51
|
+
"UpdateFromClauseMixin",
|
|
52
|
+
"UpdateSetClauseMixin",
|
|
53
|
+
"UpdateTableClauseMixin",
|
|
54
|
+
"WhereClauseMixin",
|
|
55
|
+
)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""CTE (Common Table Expression) and Set Operations mixins for SQL builders."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
|
+
|
|
5
|
+
from mypy_extensions import trait
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
10
|
+
|
|
11
|
+
__all__ = ("CommonTableExpressionMixin", "SetOperationMixin")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@trait
|
|
15
|
+
class CommonTableExpressionMixin:
|
|
16
|
+
"""Mixin providing WITH clause (Common Table Expressions) support for SQL builders."""
|
|
17
|
+
|
|
18
|
+
__slots__ = ()
|
|
19
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
20
|
+
_expression: Optional[exp.Expression]
|
|
21
|
+
|
|
22
|
+
_with_ctes: Any # Provided by QueryBuilder
|
|
23
|
+
dialect: Any # Provided by QueryBuilder
|
|
24
|
+
|
|
25
|
+
def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
|
|
26
|
+
"""Add parameter - provided by QueryBuilder."""
|
|
27
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
28
|
+
raise NotImplementedError(msg)
|
|
29
|
+
|
|
30
|
+
def with_(
|
|
31
|
+
self, name: str, query: Union[Any, str], recursive: bool = False, columns: Optional[list[str]] = None
|
|
32
|
+
) -> Self:
|
|
33
|
+
"""Add WITH clause (Common Table Expression).
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
name: The name of the CTE.
|
|
37
|
+
query: The query for the CTE (builder instance or SQL string).
|
|
38
|
+
recursive: Whether this is a recursive CTE.
|
|
39
|
+
columns: Optional column names for the CTE.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
SQLBuilderError: If the query type is unsupported.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The current builder instance for method chaining.
|
|
46
|
+
"""
|
|
47
|
+
if self._expression is None:
|
|
48
|
+
msg = "Cannot add WITH clause: expression not initialized."
|
|
49
|
+
raise SQLBuilderError(msg)
|
|
50
|
+
|
|
51
|
+
if not isinstance(self._expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)):
|
|
52
|
+
msg = f"Cannot add WITH clause to {type(self._expression).__name__} expression."
|
|
53
|
+
raise SQLBuilderError(msg)
|
|
54
|
+
|
|
55
|
+
cte_expr: Optional[exp.Expression] = None
|
|
56
|
+
if isinstance(query, str):
|
|
57
|
+
cte_expr = exp.maybe_parse(query, dialect=self.dialect)
|
|
58
|
+
elif isinstance(query, exp.Expression):
|
|
59
|
+
cte_expr = query
|
|
60
|
+
else:
|
|
61
|
+
built_query = query.to_statement()
|
|
62
|
+
cte_sql = built_query.sql
|
|
63
|
+
cte_expr = exp.maybe_parse(cte_sql, dialect=self.dialect)
|
|
64
|
+
|
|
65
|
+
parameters = built_query.parameters
|
|
66
|
+
if parameters:
|
|
67
|
+
if isinstance(parameters, dict):
|
|
68
|
+
for param_name, param_value in parameters.items():
|
|
69
|
+
self.add_parameter(param_value, name=param_name)
|
|
70
|
+
elif isinstance(parameters, (list, tuple)):
|
|
71
|
+
for param_value in parameters:
|
|
72
|
+
self.add_parameter(param_value)
|
|
73
|
+
|
|
74
|
+
if not cte_expr:
|
|
75
|
+
msg = f"Could not parse CTE query: {query}"
|
|
76
|
+
raise SQLBuilderError(msg)
|
|
77
|
+
|
|
78
|
+
if columns:
|
|
79
|
+
cte_alias_expr = exp.alias_(cte_expr, name, table=[exp.to_identifier(col) for col in columns])
|
|
80
|
+
else:
|
|
81
|
+
cte_alias_expr = exp.alias_(cte_expr, name)
|
|
82
|
+
|
|
83
|
+
existing_with = self._expression.args.get("with")
|
|
84
|
+
if existing_with:
|
|
85
|
+
existing_with.expressions.append(cte_alias_expr)
|
|
86
|
+
if recursive:
|
|
87
|
+
existing_with.set("recursive", recursive)
|
|
88
|
+
else:
|
|
89
|
+
# Only SELECT, INSERT, UPDATE support WITH clauses
|
|
90
|
+
if hasattr(self._expression, "with_") and isinstance(
|
|
91
|
+
self._expression, (exp.Select, exp.Insert, exp.Update)
|
|
92
|
+
):
|
|
93
|
+
self._expression = self._expression.with_(cte_alias_expr, as_=name, copy=False)
|
|
94
|
+
if recursive:
|
|
95
|
+
with_clause = self._expression.find(exp.With)
|
|
96
|
+
if with_clause:
|
|
97
|
+
with_clause.set("recursive", recursive)
|
|
98
|
+
self._with_ctes[name] = exp.CTE(this=cte_expr, alias=exp.to_table(name))
|
|
99
|
+
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@trait
|
|
104
|
+
class SetOperationMixin:
|
|
105
|
+
"""Mixin providing set operations (UNION, INTERSECT, EXCEPT) for SELECT builders."""
|
|
106
|
+
|
|
107
|
+
__slots__ = ()
|
|
108
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
109
|
+
_expression: Optional[exp.Expression]
|
|
110
|
+
|
|
111
|
+
_parameters: dict[str, Any]
|
|
112
|
+
dialect: Any = None
|
|
113
|
+
|
|
114
|
+
def build(self) -> Any:
|
|
115
|
+
"""Build the query - provided by QueryBuilder."""
|
|
116
|
+
msg = "Method must be provided by QueryBuilder subclass"
|
|
117
|
+
raise NotImplementedError(msg)
|
|
118
|
+
|
|
119
|
+
def union(self, other: Any, all_: bool = False) -> Self:
|
|
120
|
+
"""Combine this query with another using UNION.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
other: Another SelectBuilder or compatible builder to union with.
|
|
124
|
+
all_: If True, use UNION ALL instead of UNION.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The new builder instance for the union query.
|
|
131
|
+
"""
|
|
132
|
+
left_query = self.build()
|
|
133
|
+
right_query = other.build()
|
|
134
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
|
|
135
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
|
|
136
|
+
if not left_expr or not right_expr:
|
|
137
|
+
msg = "Could not parse queries for UNION operation"
|
|
138
|
+
raise SQLBuilderError(msg)
|
|
139
|
+
union_expr = exp.union(left_expr, right_expr, distinct=not all_)
|
|
140
|
+
new_builder = type(self)()
|
|
141
|
+
new_builder.dialect = self.dialect
|
|
142
|
+
new_builder._expression = union_expr
|
|
143
|
+
merged_parameters = dict(left_query.parameters)
|
|
144
|
+
for param_name, param_value in right_query.parameters.items():
|
|
145
|
+
if param_name in merged_parameters:
|
|
146
|
+
counter = 1
|
|
147
|
+
new_param_name = f"{param_name}_right_{counter}"
|
|
148
|
+
while new_param_name in merged_parameters:
|
|
149
|
+
counter += 1
|
|
150
|
+
new_param_name = f"{param_name}_right_{counter}"
|
|
151
|
+
|
|
152
|
+
def rename_parameter(
|
|
153
|
+
node: exp.Expression, old_name: str = param_name, new_name: str = new_param_name
|
|
154
|
+
) -> exp.Expression:
|
|
155
|
+
if isinstance(node, exp.Placeholder) and node.name == old_name:
|
|
156
|
+
return exp.Placeholder(this=new_name)
|
|
157
|
+
return node
|
|
158
|
+
|
|
159
|
+
right_expr = right_expr.transform(rename_parameter)
|
|
160
|
+
union_expr = exp.union(left_expr, right_expr, distinct=not all_)
|
|
161
|
+
new_builder._expression = union_expr
|
|
162
|
+
merged_parameters[new_param_name] = param_value
|
|
163
|
+
else:
|
|
164
|
+
merged_parameters[param_name] = param_value
|
|
165
|
+
new_builder._parameters = merged_parameters
|
|
166
|
+
return new_builder
|
|
167
|
+
|
|
168
|
+
def intersect(self, other: Any) -> Self:
|
|
169
|
+
"""Add INTERSECT clause.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
other: Another SelectBuilder or compatible builder to intersect with.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
The new builder instance for the intersect query.
|
|
179
|
+
"""
|
|
180
|
+
left_query = self.build()
|
|
181
|
+
right_query = other.build()
|
|
182
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
|
|
183
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
|
|
184
|
+
if not left_expr or not right_expr:
|
|
185
|
+
msg = "Could not parse queries for INTERSECT operation"
|
|
186
|
+
raise SQLBuilderError(msg)
|
|
187
|
+
intersect_expr = exp.intersect(left_expr, right_expr, distinct=True)
|
|
188
|
+
new_builder = type(self)()
|
|
189
|
+
new_builder.dialect = self.dialect
|
|
190
|
+
new_builder._expression = intersect_expr
|
|
191
|
+
merged_parameters = dict(left_query.parameters)
|
|
192
|
+
merged_parameters.update(right_query.parameters)
|
|
193
|
+
new_builder._parameters = merged_parameters
|
|
194
|
+
return new_builder
|
|
195
|
+
|
|
196
|
+
def except_(self, other: Any) -> Self:
|
|
197
|
+
"""Combine this query with another using EXCEPT.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
other: Another SelectBuilder or compatible builder to except with.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
SQLBuilderError: If the current expression is not a SELECT statement.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The new builder instance for the except query.
|
|
207
|
+
"""
|
|
208
|
+
left_query = self.build()
|
|
209
|
+
right_query = other.build()
|
|
210
|
+
left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
|
|
211
|
+
right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
|
|
212
|
+
if not left_expr or not right_expr:
|
|
213
|
+
msg = "Could not parse queries for EXCEPT operation"
|
|
214
|
+
raise SQLBuilderError(msg)
|
|
215
|
+
except_expr = exp.except_(left_expr, right_expr)
|
|
216
|
+
new_builder = type(self)()
|
|
217
|
+
new_builder.dialect = self.dialect
|
|
218
|
+
new_builder._expression = except_expr
|
|
219
|
+
merged_parameters = dict(left_query.parameters)
|
|
220
|
+
merged_parameters.update(right_query.parameters)
|
|
221
|
+
new_builder._parameters = merged_parameters
|
|
222
|
+
return new_builder
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Delete operation mixins for SQL builders."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mypy_extensions import trait
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
10
|
+
|
|
11
|
+
__all__ = ("DeleteFromClauseMixin",)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@trait
|
|
15
|
+
class DeleteFromClauseMixin:
|
|
16
|
+
"""Mixin providing FROM clause for DELETE builders."""
|
|
17
|
+
|
|
18
|
+
__slots__ = ()
|
|
19
|
+
|
|
20
|
+
# Type annotation for PyRight - this will be provided by the base class
|
|
21
|
+
_expression: Optional[exp.Expression]
|
|
22
|
+
|
|
23
|
+
def from_(self, table: str) -> Self:
|
|
24
|
+
"""Set the target table for the DELETE statement.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
table: The table name to delete from.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The current builder instance for method chaining.
|
|
31
|
+
"""
|
|
32
|
+
if self._expression is None:
|
|
33
|
+
self._expression = exp.Delete()
|
|
34
|
+
if not isinstance(self._expression, exp.Delete):
|
|
35
|
+
current_expr_type = type(self._expression).__name__
|
|
36
|
+
msg = f"Base expression for Delete is {current_expr_type}, expected Delete."
|
|
37
|
+
raise SQLBuilderError(msg)
|
|
38
|
+
|
|
39
|
+
setattr(self, "_table", table)
|
|
40
|
+
self._expression.set("this", exp.to_table(table))
|
|
41
|
+
return self
|