sqlspec 0.13.1__py3-none-any.whl → 0.16.2__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 +71 -8
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +1 -3
- sqlspec/_serialization.py +1 -2
- sqlspec/_sql.py +930 -136
- sqlspec/_typing.py +278 -142
- sqlspec/adapters/adbc/__init__.py +4 -3
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +116 -285
- sqlspec/adapters/adbc/driver.py +462 -340
- sqlspec/adapters/aiosqlite/__init__.py +18 -3
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +202 -150
- sqlspec/adapters/aiosqlite/driver.py +226 -247
- sqlspec/adapters/asyncmy/__init__.py +18 -3
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +80 -199
- sqlspec/adapters/asyncmy/driver.py +257 -215
- sqlspec/adapters/asyncpg/__init__.py +19 -4
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +81 -214
- sqlspec/adapters/asyncpg/driver.py +284 -359
- sqlspec/adapters/bigquery/__init__.py +17 -3
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +191 -299
- sqlspec/adapters/bigquery/driver.py +474 -634
- sqlspec/adapters/duckdb/__init__.py +14 -3
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +414 -397
- sqlspec/adapters/duckdb/driver.py +342 -393
- sqlspec/adapters/oracledb/__init__.py +19 -5
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +123 -458
- sqlspec/adapters/oracledb/driver.py +505 -531
- sqlspec/adapters/psqlpy/__init__.py +13 -3
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +93 -307
- sqlspec/adapters/psqlpy/driver.py +504 -213
- sqlspec/adapters/psycopg/__init__.py +19 -5
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +143 -472
- sqlspec/adapters/psycopg/driver.py +704 -825
- sqlspec/adapters/sqlite/__init__.py +14 -3
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +208 -142
- sqlspec/adapters/sqlite/driver.py +263 -278
- sqlspec/base.py +105 -9
- sqlspec/{statement/builder → builder}/__init__.py +12 -14
- sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
- sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
- sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
- sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
- sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
- sqlspec/builder/_insert.py +421 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
- sqlspec/builder/_select.py +170 -0
- sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
- sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +604 -0
- sqlspec/builder/mixins/_update_operations.py +202 -0
- sqlspec/builder/mixins/_where_clause.py +644 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +183 -138
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.py +677 -0
- sqlspec/{statement → core}/splitter.py +321 -191
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +7 -10
- sqlspec/driver/_async.py +422 -163
- sqlspec/driver/_common.py +545 -287
- sqlspec/driver/_sync.py +426 -160
- sqlspec/driver/mixins/__init__.py +2 -13
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +65 -14
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/__init__.py +2 -1
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +21 -16
- sqlspec/extensions/litestar/providers.py +17 -52
- sqlspec/loader.py +423 -104
- 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 +51 -186
- 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 -2
- 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 +482 -235
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
- sqlspec-0.16.2.dist-info/RECORD +134 -0
- sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
- sqlspec/driver/connection.py +0 -207
- sqlspec/driver/mixins/_csv_writer.py +0 -91
- sqlspec/driver/mixins/_pipeline.py +0 -512
- sqlspec/driver/mixins/_result_utils.py +0 -140
- sqlspec/driver/mixins/_storage.py +0 -926
- sqlspec/driver/mixins/_type_coercion.py +0 -130
- sqlspec/driver/parameters.py +0 -138
- sqlspec/service/__init__.py +0 -4
- sqlspec/service/_util.py +0 -147
- sqlspec/service/base.py +0 -1131
- sqlspec/service/pagination.py +0 -26
- sqlspec/statement/__init__.py +0 -21
- sqlspec/statement/builder/insert.py +0 -288
- sqlspec/statement/builder/merge.py +0 -95
- sqlspec/statement/builder/mixins/__init__.py +0 -65
- sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
- sqlspec/statement/builder/mixins/_case_builder.py +0 -91
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
- sqlspec/statement/builder/mixins/_from.py +0 -63
- sqlspec/statement/builder/mixins/_group_by.py +0 -118
- sqlspec/statement/builder/mixins/_having.py +0 -35
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
- sqlspec/statement/builder/mixins/_insert_into.py +0 -36
- sqlspec/statement/builder/mixins/_insert_values.py +0 -67
- sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
- sqlspec/statement/builder/mixins/_order_by.py +0 -46
- sqlspec/statement/builder/mixins/_pivot.py +0 -79
- sqlspec/statement/builder/mixins/_returning.py +0 -37
- sqlspec/statement/builder/mixins/_select_columns.py +0 -61
- sqlspec/statement/builder/mixins/_set_ops.py +0 -122
- sqlspec/statement/builder/mixins/_unpivot.py +0 -77
- sqlspec/statement/builder/mixins/_update_from.py +0 -55
- sqlspec/statement/builder/mixins/_update_set.py +0 -94
- sqlspec/statement/builder/mixins/_update_table.py +0 -29
- sqlspec/statement/builder/mixins/_where.py +0 -401
- sqlspec/statement/builder/mixins/_window_functions.py +0 -86
- sqlspec/statement/builder/select.py +0 -221
- sqlspec/statement/filters.py +0 -596
- sqlspec/statement/parameter_manager.py +0 -220
- sqlspec/statement/parameters.py +0 -867
- 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 -115
- 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 -718
- sqlspec/statement/pipelines/validators/_security.py +0 -967
- sqlspec/statement/result.py +0 -435
- sqlspec/statement/sql.py +0 -1704
- sqlspec/statement/sql_compiler.py +0 -140
- sqlspec/utils/cached_property.py +0 -25
- sqlspec-0.13.1.dist-info/RECORD +0 -150
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
|
@@ -5,12 +5,16 @@ that users might pass as strings to various builder methods.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import contextlib
|
|
8
|
-
from typing import Any, Optional, Union, cast
|
|
8
|
+
from typing import Any, Final, Optional, Union, cast
|
|
9
9
|
|
|
10
10
|
from sqlglot import exp, maybe_parse, parse_one
|
|
11
11
|
|
|
12
|
+
from sqlspec.utils.type_guards import has_expression_attr, has_parameter_builder
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
def parse_column_expression(
|
|
16
|
+
column_input: Union[str, exp.Expression, Any], builder: Optional[Any] = None
|
|
17
|
+
) -> exp.Expression:
|
|
14
18
|
"""Parse a column input that might be a complex expression.
|
|
15
19
|
|
|
16
20
|
Handles cases like:
|
|
@@ -20,9 +24,11 @@ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> ex
|
|
|
20
24
|
- Function calls: "MAX(price)" -> Max(this=Column(price))
|
|
21
25
|
- Complex expressions: "CASE WHEN ... END" -> Case(...)
|
|
22
26
|
- Custom Column objects from our builder
|
|
27
|
+
- SQL objects with raw SQL expressions
|
|
23
28
|
|
|
24
29
|
Args:
|
|
25
|
-
column_input: String, SQLGlot expression, or Column object
|
|
30
|
+
column_input: String, SQLGlot expression, SQL object, or Column object
|
|
31
|
+
builder: Optional builder instance for parameter merging
|
|
26
32
|
|
|
27
33
|
Returns:
|
|
28
34
|
exp.Expression: Parsed SQLGlot expression
|
|
@@ -30,11 +36,33 @@ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> ex
|
|
|
30
36
|
if isinstance(column_input, exp.Expression):
|
|
31
37
|
return column_input
|
|
32
38
|
|
|
33
|
-
# Handle
|
|
34
|
-
if hasattr(column_input, "
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
40
|
+
if hasattr(column_input, "expression") and hasattr(column_input, "sql"):
|
|
41
|
+
# This is likely a SQL object
|
|
42
|
+
expression = getattr(column_input, "expression", None)
|
|
43
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
44
|
+
# Merge parameters from SQL object into builder if available
|
|
45
|
+
if builder and hasattr(column_input, "parameters") and hasattr(builder, "add_parameter"):
|
|
46
|
+
sql_parameters = getattr(column_input, "parameters", {})
|
|
47
|
+
for param_name, param_value in sql_parameters.items():
|
|
48
|
+
builder.add_parameter(param_value, name=param_name)
|
|
49
|
+
return cast("exp.Expression", expression)
|
|
50
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
51
|
+
sql_text = getattr(column_input, "sql", "")
|
|
52
|
+
# Merge parameters even when parsing raw SQL
|
|
53
|
+
if builder and hasattr(column_input, "parameters") and hasattr(builder, "add_parameter"):
|
|
54
|
+
sql_parameters = getattr(column_input, "parameters", {})
|
|
55
|
+
for param_name, param_value in sql_parameters.items():
|
|
56
|
+
builder.add_parameter(param_value, name=param_name)
|
|
57
|
+
return exp.maybe_parse(sql_text) or exp.column(str(sql_text))
|
|
58
|
+
|
|
59
|
+
if has_expression_attr(column_input):
|
|
60
|
+
try:
|
|
61
|
+
attr_value = column_input._expression
|
|
62
|
+
if isinstance(attr_value, exp.Expression):
|
|
63
|
+
return attr_value
|
|
64
|
+
except AttributeError:
|
|
65
|
+
pass
|
|
38
66
|
|
|
39
67
|
return exp.maybe_parse(column_input) or exp.column(str(column_input))
|
|
40
68
|
|
|
@@ -42,7 +70,6 @@ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> ex
|
|
|
42
70
|
def parse_table_expression(table_input: str, explicit_alias: Optional[str] = None) -> exp.Expression:
|
|
43
71
|
"""Parses a table string that can be a name, a name with an alias, or a subquery string."""
|
|
44
72
|
with contextlib.suppress(Exception):
|
|
45
|
-
# Wrapping in a SELECT statement is a robust way to parse various table-like syntaxes
|
|
46
73
|
parsed = parse_one(f"SELECT * FROM {table_input}")
|
|
47
74
|
if isinstance(parsed, exp.Select) and parsed.args.get("from"):
|
|
48
75
|
from_clause = cast("exp.From", parsed.args.get("from"))
|
|
@@ -102,40 +129,36 @@ def parse_condition_expression(
|
|
|
102
129
|
if isinstance(condition_input, exp.Expression):
|
|
103
130
|
return condition_input
|
|
104
131
|
|
|
105
|
-
tuple_condition_parts = 2
|
|
132
|
+
tuple_condition_parts: Final[int] = 2
|
|
106
133
|
if isinstance(condition_input, tuple) and len(condition_input) == tuple_condition_parts:
|
|
107
134
|
column, value = condition_input
|
|
108
135
|
column_expr = parse_column_expression(column)
|
|
109
136
|
if value is None:
|
|
110
137
|
return exp.Is(this=column_expr, expression=exp.null())
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
if builder and has_parameter_builder(builder):
|
|
139
|
+
from sqlspec.builder.mixins._where_clause import _extract_column_name
|
|
140
|
+
|
|
141
|
+
column_name = _extract_column_name(column)
|
|
142
|
+
param_name = builder._generate_unique_parameter_name(column_name)
|
|
143
|
+
_, param_name = builder.add_parameter(value, name=param_name)
|
|
114
144
|
return exp.EQ(this=column_expr, expression=exp.Placeholder(this=param_name))
|
|
115
145
|
if isinstance(value, str):
|
|
116
|
-
return exp.EQ(this=column_expr, expression=exp.
|
|
146
|
+
return exp.EQ(this=column_expr, expression=exp.convert(value))
|
|
117
147
|
if isinstance(value, (int, float)):
|
|
118
|
-
return exp.EQ(this=column_expr, expression=exp.
|
|
119
|
-
return exp.EQ(this=column_expr, expression=exp.
|
|
148
|
+
return exp.EQ(this=column_expr, expression=exp.convert(str(value)))
|
|
149
|
+
return exp.EQ(this=column_expr, expression=exp.convert(str(value)))
|
|
120
150
|
|
|
121
151
|
if not isinstance(condition_input, str):
|
|
122
152
|
condition_input = str(condition_input)
|
|
123
153
|
|
|
124
154
|
try:
|
|
125
|
-
# Parse as condition using SQLGlot's condition parser
|
|
126
155
|
return exp.condition(condition_input)
|
|
127
156
|
except Exception:
|
|
128
|
-
# If that fails, try parsing as a general expression
|
|
129
157
|
try:
|
|
130
158
|
parsed = exp.maybe_parse(condition_input) # type: ignore[var-annotated]
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
# SQLGlot condition parsing failed, will use raw condition
|
|
135
|
-
pass
|
|
136
|
-
|
|
137
|
-
# Ultimate fallback: treat as raw condition string
|
|
138
|
-
return exp.condition(condition_input)
|
|
159
|
+
return parsed or exp.condition(condition_input)
|
|
160
|
+
except Exception:
|
|
161
|
+
return exp.condition(condition_input)
|
|
139
162
|
|
|
140
163
|
|
|
141
164
|
__all__ = ("parse_column_expression", "parse_condition_expression", "parse_order_expression", "parse_table_expression")
|
|
@@ -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)
|
|
@@ -4,33 +4,30 @@ This module provides a fluent interface for building SQL queries safely,
|
|
|
4
4
|
with automatic parameter binding and validation.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from dataclasses import dataclass
|
|
8
7
|
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
9
8
|
|
|
10
9
|
from sqlglot import exp
|
|
11
10
|
from typing_extensions import Self
|
|
12
11
|
|
|
13
|
-
from sqlspec.
|
|
14
|
-
from sqlspec.
|
|
15
|
-
from sqlspec.statement.builder.mixins import (
|
|
12
|
+
from sqlspec.builder._base import QueryBuilder, SafeQuery
|
|
13
|
+
from sqlspec.builder.mixins import (
|
|
16
14
|
ReturningClauseMixin,
|
|
17
15
|
UpdateFromClauseMixin,
|
|
18
16
|
UpdateSetClauseMixin,
|
|
19
17
|
UpdateTableClauseMixin,
|
|
20
18
|
WhereClauseMixin,
|
|
21
19
|
)
|
|
22
|
-
from sqlspec.
|
|
23
|
-
from sqlspec.
|
|
20
|
+
from sqlspec.core.result import SQLResult
|
|
21
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
24
22
|
|
|
25
23
|
if TYPE_CHECKING:
|
|
26
|
-
from sqlspec.
|
|
24
|
+
from sqlspec.builder._select import Select
|
|
27
25
|
|
|
28
26
|
__all__ = ("Update",)
|
|
29
27
|
|
|
30
28
|
|
|
31
|
-
@dataclass(unsafe_hash=True)
|
|
32
29
|
class Update(
|
|
33
|
-
QueryBuilder
|
|
30
|
+
QueryBuilder,
|
|
34
31
|
WhereClauseMixin,
|
|
35
32
|
ReturningClauseMixin,
|
|
36
33
|
UpdateSetClauseMixin,
|
|
@@ -44,7 +41,6 @@ class Update(
|
|
|
44
41
|
|
|
45
42
|
Example:
|
|
46
43
|
```python
|
|
47
|
-
# Basic UPDATE
|
|
48
44
|
update_query = (
|
|
49
45
|
Update()
|
|
50
46
|
.table("users")
|
|
@@ -53,12 +49,10 @@ class Update(
|
|
|
53
49
|
.where("id = 1")
|
|
54
50
|
)
|
|
55
51
|
|
|
56
|
-
# Even more concise with constructor
|
|
57
52
|
update_query = (
|
|
58
53
|
Update("users").set(name="John Doe").where("id = 1")
|
|
59
54
|
)
|
|
60
55
|
|
|
61
|
-
# UPDATE with parameterized conditions
|
|
62
56
|
update_query = (
|
|
63
57
|
Update()
|
|
64
58
|
.table("users")
|
|
@@ -66,7 +60,6 @@ class Update(
|
|
|
66
60
|
.where_eq("id", 123)
|
|
67
61
|
)
|
|
68
62
|
|
|
69
|
-
# UPDATE with FROM clause (PostgreSQL style)
|
|
70
63
|
update_query = (
|
|
71
64
|
Update()
|
|
72
65
|
.table("users", "u")
|
|
@@ -77,6 +70,9 @@ class Update(
|
|
|
77
70
|
```
|
|
78
71
|
"""
|
|
79
72
|
|
|
73
|
+
__slots__ = ("_table",)
|
|
74
|
+
_expression: Optional[exp.Expression]
|
|
75
|
+
|
|
80
76
|
def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
|
|
81
77
|
"""Initialize UPDATE with optional table.
|
|
82
78
|
|
|
@@ -85,14 +81,15 @@ class Update(
|
|
|
85
81
|
**kwargs: Additional QueryBuilder arguments
|
|
86
82
|
"""
|
|
87
83
|
super().__init__(**kwargs)
|
|
84
|
+
self._initialize_expression()
|
|
88
85
|
|
|
89
86
|
if table:
|
|
90
87
|
self.table(table)
|
|
91
88
|
|
|
92
89
|
@property
|
|
93
|
-
def _expected_result_type(self) -> "type[SQLResult
|
|
90
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
94
91
|
"""Return the expected result type for this builder."""
|
|
95
|
-
return SQLResult
|
|
92
|
+
return SQLResult
|
|
96
93
|
|
|
97
94
|
def _create_base_expression(self) -> exp.Update:
|
|
98
95
|
"""Create a base UPDATE expression.
|
|
@@ -104,7 +101,7 @@ class Update(
|
|
|
104
101
|
|
|
105
102
|
def join(
|
|
106
103
|
self,
|
|
107
|
-
table: "Union[str, exp.Expression, Select
|
|
104
|
+
table: "Union[str, exp.Expression, Select]",
|
|
108
105
|
on: "Union[str, exp.Expression]",
|
|
109
106
|
alias: "Optional[str]" = None,
|
|
110
107
|
join_type: str = "INNER",
|
|
@@ -135,10 +132,9 @@ class Update(
|
|
|
135
132
|
subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=self.dialect))
|
|
136
133
|
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
137
134
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for p_name, p_value in subquery_params.items():
|
|
135
|
+
subquery_parameters = table._parameters
|
|
136
|
+
if subquery_parameters:
|
|
137
|
+
for p_name, p_value in subquery_parameters.items():
|
|
142
138
|
self.add_parameter(p_value, name=p_name)
|
|
143
139
|
else:
|
|
144
140
|
table_expr = table
|
|
@@ -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
|
+
)
|