sqlspec 0.16.1__cp312-cp312-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-312-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-312-aarch64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.cpython-312-aarch64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.cpython-312-aarch64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.cpython-312-aarch64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.cpython-312-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.cpython-312-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result.py +677 -0
- sqlspec/core/splitter.cpython-312-aarch64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +819 -0
- sqlspec/core/statement.cpython-312-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-312-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-312-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-312-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +237 -0
- sqlspec/utils/text.cpython-312-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +96 -0
- sqlspec/utils/type_guards.cpython-312-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
sqlspec/builder/_base.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
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 abc import ABC, abstractmethod
|
|
8
|
+
from typing import TYPE_CHECKING, Any, NoReturn, Optional, Union, cast
|
|
9
|
+
|
|
10
|
+
import sqlglot
|
|
11
|
+
from sqlglot import Dialect, exp
|
|
12
|
+
from sqlglot.dialects.dialect import DialectType
|
|
13
|
+
from sqlglot.errors import ParseError as SQLGlotParseError
|
|
14
|
+
from sqlglot.optimizer import optimize
|
|
15
|
+
from typing_extensions import Self
|
|
16
|
+
|
|
17
|
+
from sqlspec.core.cache import CacheKey, get_cache_config, get_default_cache
|
|
18
|
+
from sqlspec.core.hashing import hash_optimized_expression
|
|
19
|
+
from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
|
|
20
|
+
from sqlspec.core.statement import SQL, StatementConfig
|
|
21
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
22
|
+
from sqlspec.utils.logging import get_logger
|
|
23
|
+
from sqlspec.utils.type_guards import has_sql_method, has_with_method
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from sqlspec.core.result import SQLResult
|
|
27
|
+
|
|
28
|
+
__all__ = ("QueryBuilder", "SafeQuery")
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SafeQuery:
|
|
34
|
+
"""A safely constructed SQL query with bound parameters."""
|
|
35
|
+
|
|
36
|
+
__slots__ = ("dialect", "parameters", "sql")
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self, sql: str, parameters: Optional[dict[str, Any]] = None, dialect: Optional[DialectType] = None
|
|
40
|
+
) -> None:
|
|
41
|
+
self.sql = sql
|
|
42
|
+
self.parameters = parameters if parameters is not None else {}
|
|
43
|
+
self.dialect = dialect
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class QueryBuilder(ABC):
|
|
47
|
+
"""Abstract base class for SQL query builders with SQLGlot optimization.
|
|
48
|
+
|
|
49
|
+
Provides common functionality for dialect handling, parameter management,
|
|
50
|
+
query construction, and query optimization using SQLGlot.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
__slots__ = (
|
|
54
|
+
"_expression",
|
|
55
|
+
"_parameter_counter",
|
|
56
|
+
"_parameters",
|
|
57
|
+
"_with_ctes",
|
|
58
|
+
"dialect",
|
|
59
|
+
"enable_optimization",
|
|
60
|
+
"optimize_joins",
|
|
61
|
+
"optimize_predicates",
|
|
62
|
+
"schema",
|
|
63
|
+
"simplify_expressions",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
dialect: Optional[DialectType] = None,
|
|
69
|
+
schema: Optional[dict[str, dict[str, str]]] = None,
|
|
70
|
+
enable_optimization: bool = True,
|
|
71
|
+
optimize_joins: bool = True,
|
|
72
|
+
optimize_predicates: bool = True,
|
|
73
|
+
simplify_expressions: bool = True,
|
|
74
|
+
) -> None:
|
|
75
|
+
self.dialect = dialect
|
|
76
|
+
self.schema = schema
|
|
77
|
+
self.enable_optimization = enable_optimization
|
|
78
|
+
self.optimize_joins = optimize_joins
|
|
79
|
+
self.optimize_predicates = optimize_predicates
|
|
80
|
+
self.simplify_expressions = simplify_expressions
|
|
81
|
+
|
|
82
|
+
# Initialize mutable attributes
|
|
83
|
+
self._expression: Optional[exp.Expression] = None
|
|
84
|
+
self._parameters: dict[str, Any] = {}
|
|
85
|
+
self._parameter_counter: int = 0
|
|
86
|
+
self._with_ctes: dict[str, exp.CTE] = {}
|
|
87
|
+
|
|
88
|
+
def _initialize_expression(self) -> None:
|
|
89
|
+
"""Initialize the base expression. Called after __init__."""
|
|
90
|
+
self._expression = self._create_base_expression()
|
|
91
|
+
if not self._expression:
|
|
92
|
+
self._raise_sql_builder_error(
|
|
93
|
+
"QueryBuilder._create_base_expression must return a valid sqlglot expression."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def _create_base_expression(self) -> exp.Expression:
|
|
98
|
+
"""Create the base sqlglot expression for the specific query type.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
A new sqlglot expression appropriate for the query type.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
107
|
+
"""The expected result type for the query being built.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
type[ResultT]: The type of the result.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _raise_sql_builder_error(message: str, cause: Optional[BaseException] = None) -> NoReturn:
|
|
115
|
+
"""Helper to raise SQLBuilderError, potentially with a cause.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
message: The error message.
|
|
119
|
+
cause: The optional original exception to chain.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
SQLBuilderError: Always raises this exception.
|
|
123
|
+
"""
|
|
124
|
+
raise SQLBuilderError(message) from cause
|
|
125
|
+
|
|
126
|
+
def _add_parameter(self, value: Any, context: Optional[str] = None) -> str:
|
|
127
|
+
"""Adds a parameter to the query and returns its placeholder name.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
value: The value of the parameter.
|
|
131
|
+
context: Optional context hint for parameter naming (e.g., "where", "join")
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
str: The placeholder name for the parameter (e.g., :param_1 or :where_param_1).
|
|
135
|
+
"""
|
|
136
|
+
self._parameter_counter += 1
|
|
137
|
+
|
|
138
|
+
param_name = f"{context}_param_{self._parameter_counter}" if context else f"param_{self._parameter_counter}"
|
|
139
|
+
|
|
140
|
+
self._parameters[param_name] = value
|
|
141
|
+
return param_name
|
|
142
|
+
|
|
143
|
+
def _parameterize_expression(self, expression: exp.Expression) -> exp.Expression:
|
|
144
|
+
"""Replace literal values in an expression with bound parameters.
|
|
145
|
+
|
|
146
|
+
This method traverses a SQLGlot expression tree and replaces literal
|
|
147
|
+
values with parameter placeholders, adding the values to the builder's
|
|
148
|
+
parameter collection.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
expression: The SQLGlot expression to parameterize
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
A new expression with literals replaced by parameter placeholders
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def replacer(node: exp.Expression) -> exp.Expression:
|
|
158
|
+
if isinstance(node, exp.Literal):
|
|
159
|
+
if node.this in {True, False, None}:
|
|
160
|
+
return node
|
|
161
|
+
param_name = self._add_parameter(node.this, context="where")
|
|
162
|
+
return exp.Placeholder(this=param_name)
|
|
163
|
+
return node
|
|
164
|
+
|
|
165
|
+
return expression.transform(replacer, copy=False)
|
|
166
|
+
|
|
167
|
+
def add_parameter(self: Self, value: Any, name: Optional[str] = None) -> tuple[Self, str]:
|
|
168
|
+
"""Explicitly adds a parameter to the query.
|
|
169
|
+
|
|
170
|
+
This is useful for parameters that are not directly tied to a
|
|
171
|
+
builder method like `where` or `values`.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
value: The value of the parameter.
|
|
175
|
+
name: Optional explicit name for the parameter. If None, a name
|
|
176
|
+
will be generated.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
tuple[Self, str]: The builder instance and the parameter name.
|
|
180
|
+
"""
|
|
181
|
+
if name:
|
|
182
|
+
if name in self._parameters:
|
|
183
|
+
self._raise_sql_builder_error(f"Parameter name '{name}' already exists.")
|
|
184
|
+
self._parameters[name] = value
|
|
185
|
+
return self, name
|
|
186
|
+
|
|
187
|
+
self._parameter_counter += 1
|
|
188
|
+
param_name = f"param_{self._parameter_counter}"
|
|
189
|
+
self._parameters[param_name] = value
|
|
190
|
+
return self, param_name
|
|
191
|
+
|
|
192
|
+
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
193
|
+
"""Generate unique parameter name when collision occurs.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
base_name: The desired base name for the parameter
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
A unique parameter name that doesn't exist in current parameters
|
|
200
|
+
"""
|
|
201
|
+
if base_name not in self._parameters:
|
|
202
|
+
return base_name
|
|
203
|
+
|
|
204
|
+
for i in range(1, 1000): # Reasonable upper bound to prevent infinite loops
|
|
205
|
+
name = f"{base_name}_{i}"
|
|
206
|
+
if name not in self._parameters:
|
|
207
|
+
return name
|
|
208
|
+
|
|
209
|
+
# Fallback for edge case
|
|
210
|
+
import uuid
|
|
211
|
+
|
|
212
|
+
return f"{base_name}_{uuid.uuid4().hex[:8]}"
|
|
213
|
+
|
|
214
|
+
def _generate_builder_cache_key(self, config: "Optional[StatementConfig]" = None) -> str:
|
|
215
|
+
"""Generate cache key based on builder state and configuration.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
config: Optional SQL configuration that affects the generated SQL
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
A unique cache key representing the builder state and configuration
|
|
222
|
+
"""
|
|
223
|
+
import hashlib
|
|
224
|
+
|
|
225
|
+
dialect_name: str = self.dialect_name or "default"
|
|
226
|
+
expr_sql: str = self._expression.sql() if self._expression else "None"
|
|
227
|
+
|
|
228
|
+
state_parts = [
|
|
229
|
+
f"expression:{expr_sql}",
|
|
230
|
+
f"parameters:{sorted(self._parameters.items())}",
|
|
231
|
+
f"ctes:{sorted(self._with_ctes.keys())}",
|
|
232
|
+
f"dialect:{dialect_name}",
|
|
233
|
+
f"schema:{self.schema}",
|
|
234
|
+
f"optimization:{self.enable_optimization}",
|
|
235
|
+
f"optimize_joins:{self.optimize_joins}",
|
|
236
|
+
f"optimize_predicates:{self.optimize_predicates}",
|
|
237
|
+
f"simplify_expressions:{self.simplify_expressions}",
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
if config:
|
|
241
|
+
config_parts = [
|
|
242
|
+
f"config_dialect:{config.dialect or 'default'}",
|
|
243
|
+
f"enable_parsing:{config.enable_parsing}",
|
|
244
|
+
f"enable_validation:{config.enable_validation}",
|
|
245
|
+
f"enable_transformations:{config.enable_transformations}",
|
|
246
|
+
f"enable_analysis:{config.enable_analysis}",
|
|
247
|
+
f"enable_caching:{config.enable_caching}",
|
|
248
|
+
f"param_style:{config.parameter_config.default_parameter_style.value}",
|
|
249
|
+
]
|
|
250
|
+
state_parts.extend(config_parts)
|
|
251
|
+
|
|
252
|
+
state_string = "|".join(state_parts)
|
|
253
|
+
return f"builder:{hashlib.sha256(state_string.encode()).hexdigest()[:16]}"
|
|
254
|
+
|
|
255
|
+
def with_cte(self: Self, alias: str, query: "Union[QueryBuilder, exp.Select, str]") -> Self:
|
|
256
|
+
"""Adds a Common Table Expression (CTE) to the query.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
alias: The alias for the CTE.
|
|
260
|
+
query: The CTE query, which can be another QueryBuilder instance,
|
|
261
|
+
a raw SQL string, or a sqlglot Select expression.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Self: The current builder instance for method chaining.
|
|
265
|
+
"""
|
|
266
|
+
if alias in self._with_ctes:
|
|
267
|
+
self._raise_sql_builder_error(f"CTE with alias '{alias}' already exists.")
|
|
268
|
+
|
|
269
|
+
cte_select_expression: exp.Select
|
|
270
|
+
|
|
271
|
+
if isinstance(query, QueryBuilder):
|
|
272
|
+
if query._expression is None:
|
|
273
|
+
self._raise_sql_builder_error("CTE query builder has no expression.")
|
|
274
|
+
if not isinstance(query._expression, exp.Select):
|
|
275
|
+
msg = f"CTE query builder expression must be a Select, got {type(query._expression).__name__}."
|
|
276
|
+
self._raise_sql_builder_error(msg)
|
|
277
|
+
cte_select_expression = query._expression
|
|
278
|
+
for p_name, p_value in query.parameters.items():
|
|
279
|
+
unique_name = self._generate_unique_parameter_name(p_name)
|
|
280
|
+
self.add_parameter(p_value, unique_name)
|
|
281
|
+
|
|
282
|
+
elif isinstance(query, str):
|
|
283
|
+
try:
|
|
284
|
+
parsed_expression = sqlglot.parse_one(query, read=self.dialect_name)
|
|
285
|
+
if not isinstance(parsed_expression, exp.Select):
|
|
286
|
+
msg = f"CTE query string must parse to a SELECT statement, got {type(parsed_expression).__name__}."
|
|
287
|
+
self._raise_sql_builder_error(msg)
|
|
288
|
+
cte_select_expression = parsed_expression
|
|
289
|
+
except SQLGlotParseError as e:
|
|
290
|
+
self._raise_sql_builder_error(f"Failed to parse CTE query string: {e!s}", e)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
msg = f"An unexpected error occurred while parsing CTE query string: {e!s}"
|
|
293
|
+
self._raise_sql_builder_error(msg, e)
|
|
294
|
+
elif isinstance(query, exp.Select):
|
|
295
|
+
cte_select_expression = query
|
|
296
|
+
else:
|
|
297
|
+
msg = f"Invalid query type for CTE: {type(query).__name__}"
|
|
298
|
+
self._raise_sql_builder_error(msg)
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
self._with_ctes[alias] = exp.CTE(this=cte_select_expression, alias=exp.to_table(alias))
|
|
302
|
+
return self
|
|
303
|
+
|
|
304
|
+
def build(self) -> "SafeQuery":
|
|
305
|
+
"""Builds the SQL query string and parameters.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
SafeQuery: A dataclass containing the SQL string and parameters.
|
|
309
|
+
"""
|
|
310
|
+
if self._expression is None:
|
|
311
|
+
self._raise_sql_builder_error("QueryBuilder expression not initialized.")
|
|
312
|
+
|
|
313
|
+
if self._with_ctes:
|
|
314
|
+
final_expression = self._expression
|
|
315
|
+
if has_with_method(final_expression):
|
|
316
|
+
for alias, cte_node in self._with_ctes.items():
|
|
317
|
+
final_expression = cast("Any", final_expression).with_(cte_node.args["this"], as_=alias, copy=False)
|
|
318
|
+
elif isinstance(final_expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Union)):
|
|
319
|
+
final_expression = exp.With(expressions=list(self._with_ctes.values()), this=final_expression)
|
|
320
|
+
else:
|
|
321
|
+
final_expression = self._expression
|
|
322
|
+
|
|
323
|
+
if self.enable_optimization and isinstance(final_expression, exp.Expression):
|
|
324
|
+
final_expression = self._optimize_expression(final_expression)
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
if has_sql_method(final_expression):
|
|
328
|
+
sql_string = final_expression.sql(dialect=self.dialect_name, pretty=True)
|
|
329
|
+
else:
|
|
330
|
+
sql_string = str(final_expression)
|
|
331
|
+
except Exception as e:
|
|
332
|
+
err_msg = f"Error generating SQL from expression: {e!s}"
|
|
333
|
+
logger.exception("SQL generation failed")
|
|
334
|
+
self._raise_sql_builder_error(err_msg, e)
|
|
335
|
+
|
|
336
|
+
return SafeQuery(sql=sql_string, parameters=self._parameters.copy(), dialect=self.dialect)
|
|
337
|
+
|
|
338
|
+
def _optimize_expression(self, expression: exp.Expression) -> exp.Expression:
|
|
339
|
+
"""Apply SQLGlot optimizations to the expression with caching.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
expression: The expression to optimize
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
The optimized expression
|
|
346
|
+
"""
|
|
347
|
+
if not self.enable_optimization:
|
|
348
|
+
return expression
|
|
349
|
+
|
|
350
|
+
optimizer_settings = {
|
|
351
|
+
"optimize_joins": self.optimize_joins,
|
|
352
|
+
"pushdown_predicates": self.optimize_predicates,
|
|
353
|
+
"simplify_expressions": self.simplify_expressions,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
dialect_name = self.dialect_name or "default"
|
|
357
|
+
cache_key = hash_optimized_expression(
|
|
358
|
+
expression, dialect=dialect_name, schema=self.schema, optimizer_settings=optimizer_settings
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
cache_key_obj = CacheKey((cache_key,))
|
|
362
|
+
unified_cache = get_default_cache()
|
|
363
|
+
cached_optimized = unified_cache.get(cache_key_obj)
|
|
364
|
+
if cached_optimized:
|
|
365
|
+
return cast("exp.Expression", cached_optimized)
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
optimized = optimize(
|
|
369
|
+
expression, schema=self.schema, dialect=self.dialect_name, optimizer_settings=optimizer_settings
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
unified_cache.put(cache_key_obj, optimized)
|
|
373
|
+
|
|
374
|
+
except Exception:
|
|
375
|
+
return expression
|
|
376
|
+
else:
|
|
377
|
+
return optimized
|
|
378
|
+
|
|
379
|
+
def to_statement(self, config: "Optional[StatementConfig]" = None) -> "SQL":
|
|
380
|
+
"""Converts the built query into a SQL statement object with caching.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
config: Optional SQL configuration.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
SQL: A SQL statement object.
|
|
387
|
+
"""
|
|
388
|
+
cache_config = get_cache_config()
|
|
389
|
+
if not cache_config.compiled_cache_enabled:
|
|
390
|
+
return self._to_statement_without_cache(config)
|
|
391
|
+
|
|
392
|
+
cache_key_str = self._generate_builder_cache_key(config)
|
|
393
|
+
cache_key = CacheKey((cache_key_str,))
|
|
394
|
+
|
|
395
|
+
unified_cache = get_default_cache()
|
|
396
|
+
cached_sql = unified_cache.get(cache_key)
|
|
397
|
+
if cached_sql is not None:
|
|
398
|
+
return cast("SQL", cached_sql)
|
|
399
|
+
|
|
400
|
+
sql_statement = self._to_statement_without_cache(config)
|
|
401
|
+
unified_cache.put(cache_key, sql_statement)
|
|
402
|
+
|
|
403
|
+
return sql_statement
|
|
404
|
+
|
|
405
|
+
def _to_statement_without_cache(self, config: "Optional[StatementConfig]" = None) -> "SQL":
|
|
406
|
+
"""Internal method to create SQL statement without caching.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
config: Optional SQL configuration.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
SQL: A SQL statement object.
|
|
413
|
+
"""
|
|
414
|
+
safe_query = self.build()
|
|
415
|
+
|
|
416
|
+
if isinstance(safe_query.parameters, dict):
|
|
417
|
+
kwargs = safe_query.parameters
|
|
418
|
+
parameters: Optional[tuple] = None
|
|
419
|
+
else:
|
|
420
|
+
kwargs = None
|
|
421
|
+
parameters = (
|
|
422
|
+
safe_query.parameters
|
|
423
|
+
if isinstance(safe_query.parameters, tuple)
|
|
424
|
+
else tuple(safe_query.parameters)
|
|
425
|
+
if safe_query.parameters
|
|
426
|
+
else None
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if config is None:
|
|
430
|
+
from sqlspec.core.statement import StatementConfig
|
|
431
|
+
|
|
432
|
+
parameter_config = ParameterStyleConfig(
|
|
433
|
+
default_parameter_style=ParameterStyle.QMARK, supported_parameter_styles={ParameterStyle.QMARK}
|
|
434
|
+
)
|
|
435
|
+
config = StatementConfig(parameter_config=parameter_config, dialect=safe_query.dialect)
|
|
436
|
+
|
|
437
|
+
if kwargs:
|
|
438
|
+
return SQL(safe_query.sql, statement_config=config, **kwargs)
|
|
439
|
+
if parameters:
|
|
440
|
+
return SQL(safe_query.sql, *parameters, statement_config=config)
|
|
441
|
+
return SQL(safe_query.sql, statement_config=config)
|
|
442
|
+
|
|
443
|
+
def __str__(self) -> str:
|
|
444
|
+
"""Return the SQL string representation of the query.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
str: The SQL string for this query.
|
|
448
|
+
"""
|
|
449
|
+
try:
|
|
450
|
+
return self.build().sql
|
|
451
|
+
except Exception:
|
|
452
|
+
return super().__str__()
|
|
453
|
+
|
|
454
|
+
@property
|
|
455
|
+
def dialect_name(self) -> "Optional[str]":
|
|
456
|
+
"""Returns the name of the dialect, if set."""
|
|
457
|
+
if isinstance(self.dialect, str):
|
|
458
|
+
return self.dialect
|
|
459
|
+
if self.dialect is not None:
|
|
460
|
+
if isinstance(self.dialect, type) and issubclass(self.dialect, Dialect):
|
|
461
|
+
return self.dialect.__name__.lower()
|
|
462
|
+
if isinstance(self.dialect, Dialect):
|
|
463
|
+
return type(self.dialect).__name__.lower()
|
|
464
|
+
try:
|
|
465
|
+
return self.dialect.__name__.lower()
|
|
466
|
+
except AttributeError:
|
|
467
|
+
pass
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def parameters(self) -> dict[str, Any]:
|
|
472
|
+
"""Public access to query parameters."""
|
|
473
|
+
return self._parameters
|