sqlspec 0.15.0__py3-none-any.whl → 0.16.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/_sql.py +699 -43
- sqlspec/builder/_base.py +77 -44
- sqlspec/builder/_column.py +0 -4
- sqlspec/builder/_ddl.py +15 -52
- sqlspec/builder/_ddl_utils.py +0 -1
- sqlspec/builder/_delete.py +4 -5
- sqlspec/builder/_insert.py +61 -35
- sqlspec/builder/_merge.py +17 -2
- sqlspec/builder/_parsing_utils.py +16 -12
- sqlspec/builder/_select.py +29 -33
- sqlspec/builder/_update.py +4 -2
- sqlspec/builder/mixins/_cte_and_set_ops.py +47 -20
- sqlspec/builder/mixins/_delete_operations.py +6 -1
- sqlspec/builder/mixins/_insert_operations.py +126 -24
- sqlspec/builder/mixins/_join_operations.py +11 -4
- sqlspec/builder/mixins/_merge_operations.py +91 -19
- sqlspec/builder/mixins/_order_limit_operations.py +15 -3
- sqlspec/builder/mixins/_pivot_operations.py +11 -2
- sqlspec/builder/mixins/_select_operations.py +16 -10
- sqlspec/builder/mixins/_update_operations.py +43 -10
- sqlspec/builder/mixins/_where_clause.py +177 -65
- sqlspec/core/cache.py +26 -28
- sqlspec/core/compiler.py +58 -37
- sqlspec/core/filters.py +12 -10
- sqlspec/core/parameters.py +80 -52
- sqlspec/core/result.py +30 -17
- sqlspec/core/statement.py +47 -22
- sqlspec/driver/_async.py +76 -46
- sqlspec/driver/_common.py +25 -6
- sqlspec/driver/_sync.py +73 -43
- sqlspec/driver/mixins/_result_tools.py +62 -37
- sqlspec/driver/mixins/_sql_translator.py +61 -11
- sqlspec/extensions/litestar/cli.py +1 -1
- sqlspec/extensions/litestar/plugin.py +2 -2
- sqlspec/protocols.py +7 -0
- sqlspec/utils/sync_tools.py +1 -1
- sqlspec/utils/type_guards.py +7 -3
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/METADATA +1 -1
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/RECORD +43 -43
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/WHEEL +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/licenses/NOTICE +0 -0
sqlspec/builder/_base.py
CHANGED
|
@@ -5,7 +5,6 @@ with automatic parameter binding and validation.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
|
-
from dataclasses import dataclass, field
|
|
9
8
|
from typing import TYPE_CHECKING, Any, NoReturn, Optional, Union, cast
|
|
10
9
|
|
|
11
10
|
import sqlglot
|
|
@@ -31,16 +30,19 @@ __all__ = ("QueryBuilder", "SafeQuery")
|
|
|
31
30
|
logger = get_logger(__name__)
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
@dataclass(frozen=True)
|
|
35
33
|
class SafeQuery:
|
|
36
34
|
"""A safely constructed SQL query with bound parameters."""
|
|
37
35
|
|
|
38
|
-
sql
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
41
44
|
|
|
42
45
|
|
|
43
|
-
@dataclass
|
|
44
46
|
class QueryBuilder(ABC):
|
|
45
47
|
"""Abstract base class for SQL query builders with SQLGlot optimization.
|
|
46
48
|
|
|
@@ -48,18 +50,43 @@ class QueryBuilder(ABC):
|
|
|
48
50
|
query construction, and query optimization using SQLGlot.
|
|
49
51
|
"""
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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__."""
|
|
63
90
|
self._expression = self._create_base_expression()
|
|
64
91
|
if not self._expression:
|
|
65
92
|
self._raise_sql_builder_error(
|
|
@@ -135,7 +162,7 @@ class QueryBuilder(ABC):
|
|
|
135
162
|
return exp.Placeholder(this=param_name)
|
|
136
163
|
return node
|
|
137
164
|
|
|
138
|
-
return expression.transform(replacer, copy=
|
|
165
|
+
return expression.transform(replacer, copy=False)
|
|
139
166
|
|
|
140
167
|
def add_parameter(self: Self, value: Any, name: Optional[str] = None) -> tuple[Self, str]:
|
|
141
168
|
"""Explicitly adds a parameter to the query.
|
|
@@ -154,13 +181,13 @@ class QueryBuilder(ABC):
|
|
|
154
181
|
if name:
|
|
155
182
|
if name in self._parameters:
|
|
156
183
|
self._raise_sql_builder_error(f"Parameter name '{name}' already exists.")
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
self._parameter_counter += 1
|
|
160
|
-
param_name_to_use = f"param_{self._parameter_counter}"
|
|
184
|
+
self._parameters[name] = value
|
|
185
|
+
return self, name
|
|
161
186
|
|
|
162
|
-
self.
|
|
163
|
-
|
|
187
|
+
self._parameter_counter += 1
|
|
188
|
+
param_name = f"param_{self._parameter_counter}"
|
|
189
|
+
self._parameters[param_name] = value
|
|
190
|
+
return self, param_name
|
|
164
191
|
|
|
165
192
|
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
166
193
|
"""Generate unique parameter name when collision occurs.
|
|
@@ -174,12 +201,15 @@ class QueryBuilder(ABC):
|
|
|
174
201
|
if base_name not in self._parameters:
|
|
175
202
|
return base_name
|
|
176
203
|
|
|
177
|
-
i
|
|
178
|
-
while True:
|
|
204
|
+
for i in range(1, 1000): # Reasonable upper bound to prevent infinite loops
|
|
179
205
|
name = f"{base_name}_{i}"
|
|
180
206
|
if name not in self._parameters:
|
|
181
207
|
return name
|
|
182
|
-
|
|
208
|
+
|
|
209
|
+
# Fallback for edge case
|
|
210
|
+
import uuid
|
|
211
|
+
|
|
212
|
+
return f"{base_name}_{uuid.uuid4().hex[:8]}"
|
|
183
213
|
|
|
184
214
|
def _generate_builder_cache_key(self, config: "Optional[StatementConfig]" = None) -> str:
|
|
185
215
|
"""Generate cache key based on builder state and configuration.
|
|
@@ -192,11 +222,14 @@ class QueryBuilder(ABC):
|
|
|
192
222
|
"""
|
|
193
223
|
import hashlib
|
|
194
224
|
|
|
195
|
-
|
|
196
|
-
|
|
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}",
|
|
197
230
|
f"parameters:{sorted(self._parameters.items())}",
|
|
198
231
|
f"ctes:{sorted(self._with_ctes.keys())}",
|
|
199
|
-
f"dialect:{
|
|
232
|
+
f"dialect:{dialect_name}",
|
|
200
233
|
f"schema:{self.schema}",
|
|
201
234
|
f"optimization:{self.enable_optimization}",
|
|
202
235
|
f"optimize_joins:{self.optimize_joins}",
|
|
@@ -205,7 +238,7 @@ class QueryBuilder(ABC):
|
|
|
205
238
|
]
|
|
206
239
|
|
|
207
240
|
if config:
|
|
208
|
-
|
|
241
|
+
config_parts = [
|
|
209
242
|
f"config_dialect:{config.dialect or 'default'}",
|
|
210
243
|
f"enable_parsing:{config.enable_parsing}",
|
|
211
244
|
f"enable_validation:{config.enable_validation}",
|
|
@@ -214,12 +247,10 @@ class QueryBuilder(ABC):
|
|
|
214
247
|
f"enable_caching:{config.enable_caching}",
|
|
215
248
|
f"param_style:{config.parameter_config.default_parameter_style.value}",
|
|
216
249
|
]
|
|
217
|
-
|
|
250
|
+
state_parts.extend(config_parts)
|
|
218
251
|
|
|
219
|
-
state_string = "|".join(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return f"builder:{cache_key}"
|
|
252
|
+
state_string = "|".join(state_parts)
|
|
253
|
+
return f"builder:{hashlib.sha256(state_string.encode()).hexdigest()[:16]}"
|
|
223
254
|
|
|
224
255
|
def with_cte(self: Self, alias: str, query: "Union[QueryBuilder, exp.Select, str]") -> Self:
|
|
225
256
|
"""Adds a Common Table Expression (CTE) to the query.
|
|
@@ -243,7 +274,7 @@ class QueryBuilder(ABC):
|
|
|
243
274
|
if not isinstance(query._expression, exp.Select):
|
|
244
275
|
msg = f"CTE query builder expression must be a Select, got {type(query._expression).__name__}."
|
|
245
276
|
self._raise_sql_builder_error(msg)
|
|
246
|
-
cte_select_expression = query._expression
|
|
277
|
+
cte_select_expression = query._expression
|
|
247
278
|
for p_name, p_value in query.parameters.items():
|
|
248
279
|
unique_name = self._generate_unique_parameter_name(p_name)
|
|
249
280
|
self.add_parameter(p_value, unique_name)
|
|
@@ -261,7 +292,7 @@ class QueryBuilder(ABC):
|
|
|
261
292
|
msg = f"An unexpected error occurred while parsing CTE query string: {e!s}"
|
|
262
293
|
self._raise_sql_builder_error(msg, e)
|
|
263
294
|
elif isinstance(query, exp.Select):
|
|
264
|
-
cte_select_expression = query
|
|
295
|
+
cte_select_expression = query
|
|
265
296
|
else:
|
|
266
297
|
msg = f"Invalid query type for CTE: {type(query).__name__}"
|
|
267
298
|
self._raise_sql_builder_error(msg)
|
|
@@ -280,14 +311,14 @@ class QueryBuilder(ABC):
|
|
|
280
311
|
self._raise_sql_builder_error("QueryBuilder expression not initialized.")
|
|
281
312
|
|
|
282
313
|
if self._with_ctes:
|
|
283
|
-
final_expression = self._expression
|
|
314
|
+
final_expression = self._expression
|
|
284
315
|
if has_with_method(final_expression):
|
|
285
316
|
for alias, cte_node in self._with_ctes.items():
|
|
286
317
|
final_expression = cast("Any", final_expression).with_(cte_node.args["this"], as_=alias, copy=False)
|
|
287
318
|
elif isinstance(final_expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Union)):
|
|
288
319
|
final_expression = exp.With(expressions=list(self._with_ctes.values()), this=final_expression)
|
|
289
320
|
else:
|
|
290
|
-
final_expression = self._expression
|
|
321
|
+
final_expression = self._expression
|
|
291
322
|
|
|
292
323
|
if self.enable_optimization and isinstance(final_expression, exp.Expression):
|
|
293
324
|
final_expression = self._optimize_expression(final_expression)
|
|
@@ -335,10 +366,10 @@ class QueryBuilder(ABC):
|
|
|
335
366
|
|
|
336
367
|
try:
|
|
337
368
|
optimized = optimize(
|
|
338
|
-
expression
|
|
369
|
+
expression, schema=self.schema, dialect=self.dialect_name, optimizer_settings=optimizer_settings
|
|
339
370
|
)
|
|
340
371
|
|
|
341
|
-
unified_cache.put(cache_key_obj, optimized
|
|
372
|
+
unified_cache.put(cache_key_obj, optimized)
|
|
342
373
|
|
|
343
374
|
except Exception:
|
|
344
375
|
return expression
|
|
@@ -430,8 +461,10 @@ class QueryBuilder(ABC):
|
|
|
430
461
|
return self.dialect.__name__.lower()
|
|
431
462
|
if isinstance(self.dialect, Dialect):
|
|
432
463
|
return type(self.dialect).__name__.lower()
|
|
433
|
-
|
|
464
|
+
try:
|
|
434
465
|
return self.dialect.__name__.lower()
|
|
466
|
+
except AttributeError:
|
|
467
|
+
pass
|
|
435
468
|
return None
|
|
436
469
|
|
|
437
470
|
@property
|
sqlspec/builder/_column.py
CHANGED
|
@@ -67,7 +67,6 @@ class Column:
|
|
|
67
67
|
else:
|
|
68
68
|
self._expression = exp.Column(this=exp.Identifier(this=name))
|
|
69
69
|
|
|
70
|
-
# Comparison operators
|
|
71
70
|
def __eq__(self, other: object) -> ColumnExpression: # type: ignore[override]
|
|
72
71
|
"""Equal to (==)."""
|
|
73
72
|
if other is None:
|
|
@@ -100,7 +99,6 @@ class Column:
|
|
|
100
99
|
"""Apply NOT operator (~)."""
|
|
101
100
|
return ColumnExpression(exp.Not(this=self._expression))
|
|
102
101
|
|
|
103
|
-
# SQL-specific methods
|
|
104
102
|
def like(self, pattern: str, escape: Optional[str] = None) -> ColumnExpression:
|
|
105
103
|
"""SQL LIKE pattern matching."""
|
|
106
104
|
if escape:
|
|
@@ -152,7 +150,6 @@ class Column:
|
|
|
152
150
|
converted_values = [exp.convert(v) for v in values]
|
|
153
151
|
return ColumnExpression(exp.NEQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
|
|
154
152
|
|
|
155
|
-
# SQL Functions
|
|
156
153
|
def lower(self) -> "FunctionColumn":
|
|
157
154
|
"""SQL LOWER() function."""
|
|
158
155
|
return FunctionColumn(exp.Lower(this=self._expression))
|
|
@@ -203,7 +200,6 @@ class Column:
|
|
|
203
200
|
"""SQL CAST() function."""
|
|
204
201
|
return FunctionColumn(exp.Cast(this=self._expression, to=exp.DataType.build(data_type)))
|
|
205
202
|
|
|
206
|
-
# Aggregate functions
|
|
207
203
|
def count(self) -> "FunctionColumn":
|
|
208
204
|
"""SQL COUNT() function."""
|
|
209
205
|
return FunctionColumn(exp.Count(this=self._expression))
|
sqlspec/builder/_ddl.py
CHANGED
|
@@ -45,7 +45,8 @@ class DDLBuilder(QueryBuilder):
|
|
|
45
45
|
_expression: Optional[exp.Expression] = field(default=None, init=False, repr=False, compare=False, hash=False)
|
|
46
46
|
|
|
47
47
|
def __post_init__(self) -> None:
|
|
48
|
-
|
|
48
|
+
# Initialize parent class attributes since dataclass doesn't call super().__init__()
|
|
49
|
+
super().__init__(dialect=self.dialect)
|
|
49
50
|
|
|
50
51
|
def _create_base_expression(self) -> exp.Expression:
|
|
51
52
|
msg = "Subclasses must implement _create_base_expression."
|
|
@@ -64,7 +65,6 @@ class DDLBuilder(QueryBuilder):
|
|
|
64
65
|
return super().to_statement(config=config)
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
# --- Data Structures for CREATE TABLE ---
|
|
68
68
|
@dataclass
|
|
69
69
|
class ColumnDefinition:
|
|
70
70
|
"""Column definition for CREATE TABLE."""
|
|
@@ -78,7 +78,7 @@ class ColumnDefinition:
|
|
|
78
78
|
auto_increment: bool = False
|
|
79
79
|
comment: "Optional[str]" = None
|
|
80
80
|
check: "Optional[str]" = None
|
|
81
|
-
generated: "Optional[str]" = None
|
|
81
|
+
generated: "Optional[str]" = None
|
|
82
82
|
collate: "Optional[str]" = None
|
|
83
83
|
|
|
84
84
|
|
|
@@ -86,7 +86,7 @@ class ColumnDefinition:
|
|
|
86
86
|
class ConstraintDefinition:
|
|
87
87
|
"""Constraint definition for CREATE TABLE."""
|
|
88
88
|
|
|
89
|
-
constraint_type: str
|
|
89
|
+
constraint_type: str
|
|
90
90
|
name: "Optional[str]" = None
|
|
91
91
|
columns: "list[str]" = field(default_factory=list)
|
|
92
92
|
references_table: "Optional[str]" = None
|
|
@@ -98,7 +98,6 @@ class ConstraintDefinition:
|
|
|
98
98
|
initially_deferred: bool = False
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
# --- CREATE TABLE ---
|
|
102
101
|
@dataclass
|
|
103
102
|
class CreateTable(DDLBuilder):
|
|
104
103
|
"""Builder for CREATE TABLE statements with columns and constraints.
|
|
@@ -199,7 +198,6 @@ class CreateTable(DDLBuilder):
|
|
|
199
198
|
|
|
200
199
|
self._columns.append(column_def)
|
|
201
200
|
|
|
202
|
-
# If primary key is specified on column, also add a constraint
|
|
203
201
|
if primary_key and not any(c.constraint_type == "PRIMARY KEY" for c in self._constraints):
|
|
204
202
|
self.primary_key_constraint([name])
|
|
205
203
|
|
|
@@ -235,12 +233,10 @@ class CreateTable(DDLBuilder):
|
|
|
235
233
|
initially_deferred: bool = False,
|
|
236
234
|
) -> "Self":
|
|
237
235
|
"""Add a foreign key constraint."""
|
|
238
|
-
# Normalize inputs
|
|
239
236
|
col_list = [columns] if isinstance(columns, str) else list(columns)
|
|
240
237
|
|
|
241
238
|
ref_col_list = [references_columns] if isinstance(references_columns, str) else list(references_columns)
|
|
242
239
|
|
|
243
|
-
# Validation
|
|
244
240
|
if len(col_list) != len(ref_col_list):
|
|
245
241
|
self._raise_sql_builder_error("Foreign key columns and referenced columns must have same length")
|
|
246
242
|
|
|
@@ -267,7 +263,6 @@ class CreateTable(DDLBuilder):
|
|
|
267
263
|
|
|
268
264
|
def unique_constraint(self, columns: "Union[str, list[str]]", name: "Optional[str]" = None) -> "Self":
|
|
269
265
|
"""Add a unique constraint."""
|
|
270
|
-
# Normalize column list
|
|
271
266
|
col_list = [columns] if isinstance(columns, str) else list(columns)
|
|
272
267
|
|
|
273
268
|
if not col_list:
|
|
@@ -285,11 +280,9 @@ class CreateTable(DDLBuilder):
|
|
|
285
280
|
|
|
286
281
|
condition_str: str
|
|
287
282
|
if hasattr(condition, "sqlglot_expression"):
|
|
288
|
-
# This is a ColumnExpression - render as raw SQL for DDL (no parameters)
|
|
289
283
|
sqlglot_expr = getattr(condition, "sqlglot_expression", None)
|
|
290
284
|
condition_str = sqlglot_expr.sql(dialect=self.dialect) if sqlglot_expr else str(condition)
|
|
291
285
|
else:
|
|
292
|
-
# String condition - use as-is
|
|
293
286
|
condition_str = str(condition)
|
|
294
287
|
|
|
295
288
|
constraint = ConstraintDefinition(constraint_type="CHECK", name=name, condition=condition_str)
|
|
@@ -338,7 +331,6 @@ class CreateTable(DDLBuilder):
|
|
|
338
331
|
column_defs.append(col_expr)
|
|
339
332
|
|
|
340
333
|
for constraint in self._constraints:
|
|
341
|
-
# Skip PRIMARY KEY constraints that are already defined on columns
|
|
342
334
|
if constraint.constraint_type == "PRIMARY KEY" and len(constraint.columns) == 1:
|
|
343
335
|
col_name = constraint.columns[0]
|
|
344
336
|
if any(c.name == col_name and c.primary_key for c in self._columns):
|
|
@@ -361,7 +353,7 @@ class CreateTable(DDLBuilder):
|
|
|
361
353
|
props.append(exp.Property(this=exp.to_identifier("PARTITION BY"), value=exp.convert(self._partition_by)))
|
|
362
354
|
|
|
363
355
|
for key, value in self._table_options.items():
|
|
364
|
-
if key != "engine":
|
|
356
|
+
if key != "engine":
|
|
365
357
|
props.append(exp.Property(this=exp.to_identifier(key.upper()), value=exp.convert(value)))
|
|
366
358
|
|
|
367
359
|
properties_node = exp.Properties(expressions=props) if props else None
|
|
@@ -393,14 +385,13 @@ class CreateTable(DDLBuilder):
|
|
|
393
385
|
return build_constraint_expression(constraint)
|
|
394
386
|
|
|
395
387
|
|
|
396
|
-
# --- DROP TABLE ---
|
|
397
388
|
@dataclass
|
|
398
389
|
class DropTable(DDLBuilder):
|
|
399
390
|
"""Builder for DROP TABLE [IF EXISTS] ... [CASCADE|RESTRICT]."""
|
|
400
391
|
|
|
401
392
|
_table_name: Optional[str] = None
|
|
402
393
|
_if_exists: bool = False
|
|
403
|
-
_cascade: Optional[bool] = None
|
|
394
|
+
_cascade: Optional[bool] = None
|
|
404
395
|
|
|
405
396
|
def __init__(self, table_name: str, **kwargs: Any) -> None:
|
|
406
397
|
"""Initialize DROP TABLE with table name.
|
|
@@ -436,7 +427,6 @@ class DropTable(DDLBuilder):
|
|
|
436
427
|
)
|
|
437
428
|
|
|
438
429
|
|
|
439
|
-
# --- DROP INDEX ---
|
|
440
430
|
@dataclass
|
|
441
431
|
class DropIndex(DDLBuilder):
|
|
442
432
|
"""Builder for DROP INDEX [IF EXISTS] ... [ON table] [CASCADE|RESTRICT]."""
|
|
@@ -488,7 +478,6 @@ class DropIndex(DDLBuilder):
|
|
|
488
478
|
)
|
|
489
479
|
|
|
490
480
|
|
|
491
|
-
# --- DROP VIEW ---
|
|
492
481
|
@dataclass
|
|
493
482
|
class DropView(DDLBuilder):
|
|
494
483
|
"""Builder for DROP VIEW [IF EXISTS] ... [CASCADE|RESTRICT]."""
|
|
@@ -521,7 +510,6 @@ class DropView(DDLBuilder):
|
|
|
521
510
|
)
|
|
522
511
|
|
|
523
512
|
|
|
524
|
-
# --- DROP SCHEMA ---
|
|
525
513
|
@dataclass
|
|
526
514
|
class DropSchema(DDLBuilder):
|
|
527
515
|
"""Builder for DROP SCHEMA [IF EXISTS] ... [CASCADE|RESTRICT]."""
|
|
@@ -554,7 +542,6 @@ class DropSchema(DDLBuilder):
|
|
|
554
542
|
)
|
|
555
543
|
|
|
556
544
|
|
|
557
|
-
# --- CREATE INDEX ---
|
|
558
545
|
@dataclass
|
|
559
546
|
class CreateIndex(DDLBuilder):
|
|
560
547
|
"""Builder for CREATE [UNIQUE] INDEX [IF NOT EXISTS] ... ON ... (...).
|
|
@@ -579,7 +566,6 @@ class CreateIndex(DDLBuilder):
|
|
|
579
566
|
"""
|
|
580
567
|
super().__init__(**kwargs)
|
|
581
568
|
self._index_name = index_name
|
|
582
|
-
# Initialize dataclass fields that may not be set by super().__init__
|
|
583
569
|
if not hasattr(self, "_columns"):
|
|
584
570
|
self._columns = []
|
|
585
571
|
|
|
@@ -627,7 +613,6 @@ class CreateIndex(DDLBuilder):
|
|
|
627
613
|
where_expr = None
|
|
628
614
|
if self._where:
|
|
629
615
|
where_expr = exp.condition(self._where) if isinstance(self._where, str) else self._where
|
|
630
|
-
# Use exp.Create for CREATE INDEX
|
|
631
616
|
return exp.Create(
|
|
632
617
|
kind="INDEX",
|
|
633
618
|
this=exp.to_identifier(self._index_name),
|
|
@@ -640,14 +625,13 @@ class CreateIndex(DDLBuilder):
|
|
|
640
625
|
)
|
|
641
626
|
|
|
642
627
|
|
|
643
|
-
# --- TRUNCATE TABLE ---
|
|
644
628
|
@dataclass
|
|
645
629
|
class Truncate(DDLBuilder):
|
|
646
630
|
"""Builder for TRUNCATE TABLE ... [CASCADE|RESTRICT] [RESTART IDENTITY|CONTINUE IDENTITY]."""
|
|
647
631
|
|
|
648
632
|
_table_name: Optional[str] = None
|
|
649
633
|
_cascade: Optional[bool] = None
|
|
650
|
-
_identity: Optional[str] = None
|
|
634
|
+
_identity: Optional[str] = None
|
|
651
635
|
|
|
652
636
|
def table(self, name: str) -> Self:
|
|
653
637
|
self._table_name = name
|
|
@@ -676,7 +660,6 @@ class Truncate(DDLBuilder):
|
|
|
676
660
|
return exp.TruncateTable(this=exp.to_table(self._table_name), cascade=self._cascade, identity=identity_expr)
|
|
677
661
|
|
|
678
662
|
|
|
679
|
-
# --- ALTER TABLE ---
|
|
680
663
|
@dataclass
|
|
681
664
|
class AlterOperation:
|
|
682
665
|
"""Represents a single ALTER TABLE operation."""
|
|
@@ -693,7 +676,6 @@ class AlterOperation:
|
|
|
693
676
|
using_expression: "Optional[str]" = None
|
|
694
677
|
|
|
695
678
|
|
|
696
|
-
# --- CREATE SCHEMA ---
|
|
697
679
|
@dataclass
|
|
698
680
|
class CreateSchema(DDLBuilder):
|
|
699
681
|
"""Builder for CREATE SCHEMA [IF NOT EXISTS] schema_name [AUTHORIZATION user_name]."""
|
|
@@ -757,7 +739,7 @@ class CreateTableAsSelect(DDLBuilder):
|
|
|
757
739
|
_table_name: Optional[str] = None
|
|
758
740
|
_if_not_exists: bool = False
|
|
759
741
|
_columns: list[str] = field(default_factory=list)
|
|
760
|
-
_select_query: Optional[object] = None
|
|
742
|
+
_select_query: Optional[object] = None
|
|
761
743
|
|
|
762
744
|
def name(self, table_name: str) -> Self:
|
|
763
745
|
self._table_name = table_name
|
|
@@ -797,11 +779,7 @@ class CreateTableAsSelect(DDLBuilder):
|
|
|
797
779
|
if with_ctes and select_expr and isinstance(select_expr, exp.Select):
|
|
798
780
|
for alias, cte in with_ctes.items():
|
|
799
781
|
if hasattr(select_expr, "with_"):
|
|
800
|
-
select_expr = select_expr.with_(
|
|
801
|
-
cte.this, # The CTE's SELECT expression
|
|
802
|
-
as_=alias,
|
|
803
|
-
copy=False,
|
|
804
|
-
)
|
|
782
|
+
select_expr = select_expr.with_(cte.this, as_=alias, copy=False)
|
|
805
783
|
elif isinstance(self._select_query, str):
|
|
806
784
|
select_expr = exp.maybe_parse(self._select_query)
|
|
807
785
|
select_parameters = None
|
|
@@ -810,11 +788,8 @@ class CreateTableAsSelect(DDLBuilder):
|
|
|
810
788
|
if select_expr is None:
|
|
811
789
|
self._raise_sql_builder_error("SELECT query must be a valid SELECT expression.")
|
|
812
790
|
|
|
813
|
-
# Merge parameters from SELECT if present
|
|
814
791
|
if select_parameters:
|
|
815
792
|
for p_name, p_value in select_parameters.items():
|
|
816
|
-
# Always preserve the original parameter name
|
|
817
|
-
# The SELECT query already has unique parameter names
|
|
818
793
|
self._parameters[p_name] = p_value
|
|
819
794
|
|
|
820
795
|
schema_expr = None
|
|
@@ -840,8 +815,8 @@ class CreateMaterializedView(DDLBuilder):
|
|
|
840
815
|
_view_name: Optional[str] = None
|
|
841
816
|
_if_not_exists: bool = False
|
|
842
817
|
_columns: list[str] = field(default_factory=list)
|
|
843
|
-
_select_query: Optional[object] = None
|
|
844
|
-
_with_data: Optional[bool] = None
|
|
818
|
+
_select_query: Optional[object] = None
|
|
819
|
+
_with_data: Optional[bool] = None
|
|
845
820
|
_refresh_mode: Optional[str] = None
|
|
846
821
|
_storage_parameters: dict[str, Any] = field(default_factory=dict)
|
|
847
822
|
_tablespace: Optional[str] = None
|
|
@@ -917,11 +892,8 @@ class CreateMaterializedView(DDLBuilder):
|
|
|
917
892
|
if select_expr is None or not isinstance(select_expr, exp.Select):
|
|
918
893
|
self._raise_sql_builder_error("SELECT query must be a valid SELECT expression.")
|
|
919
894
|
|
|
920
|
-
# Merge parameters from SELECT if present
|
|
921
895
|
if select_parameters:
|
|
922
896
|
for p_name, p_value in select_parameters.items():
|
|
923
|
-
# Always preserve the original parameter name
|
|
924
|
-
# The SELECT query already has unique parameter names
|
|
925
897
|
self._parameters[p_name] = p_value
|
|
926
898
|
|
|
927
899
|
schema_expr = None
|
|
@@ -964,7 +936,7 @@ class CreateView(DDLBuilder):
|
|
|
964
936
|
_view_name: Optional[str] = None
|
|
965
937
|
_if_not_exists: bool = False
|
|
966
938
|
_columns: list[str] = field(default_factory=list)
|
|
967
|
-
_select_query: Optional[object] = None
|
|
939
|
+
_select_query: Optional[object] = None
|
|
968
940
|
_hints: list[str] = field(default_factory=list)
|
|
969
941
|
|
|
970
942
|
def name(self, view_name: str) -> Self:
|
|
@@ -1012,11 +984,8 @@ class CreateView(DDLBuilder):
|
|
|
1012
984
|
if select_expr is None or not isinstance(select_expr, exp.Select):
|
|
1013
985
|
self._raise_sql_builder_error("SELECT query must be a valid SELECT expression.")
|
|
1014
986
|
|
|
1015
|
-
# Merge parameters from SELECT if present
|
|
1016
987
|
if select_parameters:
|
|
1017
988
|
for p_name, p_value in select_parameters.items():
|
|
1018
|
-
# Always preserve the original parameter name
|
|
1019
|
-
# The SELECT query already has unique parameter names
|
|
1020
989
|
self._parameters[p_name] = p_value
|
|
1021
990
|
|
|
1022
991
|
schema_expr = None
|
|
@@ -1164,17 +1133,14 @@ class AlterTable(DDLBuilder):
|
|
|
1164
1133
|
if constraint_type.upper() not in valid_types:
|
|
1165
1134
|
self._raise_sql_builder_error(f"Invalid constraint type: {constraint_type}")
|
|
1166
1135
|
|
|
1167
|
-
# Normalize columns
|
|
1168
1136
|
col_list = None
|
|
1169
1137
|
if columns is not None:
|
|
1170
1138
|
col_list = [columns] if isinstance(columns, str) else list(columns)
|
|
1171
1139
|
|
|
1172
|
-
# Normalize reference columns
|
|
1173
1140
|
ref_col_list = None
|
|
1174
1141
|
if references_columns is not None:
|
|
1175
1142
|
ref_col_list = [references_columns] if isinstance(references_columns, str) else list(references_columns)
|
|
1176
1143
|
|
|
1177
|
-
# Handle ColumnExpression for CHECK constraints
|
|
1178
1144
|
condition_str: Optional[str] = None
|
|
1179
1145
|
if condition is not None:
|
|
1180
1146
|
if hasattr(condition, "sqlglot_expression"):
|
|
@@ -1246,9 +1212,6 @@ class AlterTable(DDLBuilder):
|
|
|
1246
1212
|
if op_type == "ADD COLUMN":
|
|
1247
1213
|
if not op.column_definition:
|
|
1248
1214
|
self._raise_sql_builder_error("Column definition required for ADD COLUMN")
|
|
1249
|
-
# SQLGlot expects a ColumnDef directly for ADD COLUMN actions
|
|
1250
|
-
# Note: SQLGlot doesn't support AFTER/FIRST positioning in standard ALTER TABLE ADD COLUMN
|
|
1251
|
-
# These would need to be handled at the dialect level
|
|
1252
1215
|
return build_column_expression(op.column_definition)
|
|
1253
1216
|
|
|
1254
1217
|
if op_type == "DROP COLUMN":
|
|
@@ -1311,7 +1274,7 @@ class AlterTable(DDLBuilder):
|
|
|
1311
1274
|
return exp.AlterColumn(this=exp.to_identifier(op.column_name), kind="DROP DEFAULT")
|
|
1312
1275
|
|
|
1313
1276
|
self._raise_sql_builder_error(f"Unknown operation type: {op.operation_type}")
|
|
1314
|
-
raise AssertionError
|
|
1277
|
+
raise AssertionError
|
|
1315
1278
|
|
|
1316
1279
|
|
|
1317
1280
|
@dataclass
|
|
@@ -1321,7 +1284,7 @@ class CommentOn(DDLBuilder):
|
|
|
1321
1284
|
Supports COMMENT ON TABLE and COMMENT ON COLUMN.
|
|
1322
1285
|
"""
|
|
1323
1286
|
|
|
1324
|
-
_target_type: Optional[str] = None
|
|
1287
|
+
_target_type: Optional[str] = None
|
|
1325
1288
|
_table: Optional[str] = None
|
|
1326
1289
|
_column: Optional[str] = None
|
|
1327
1290
|
_comment: Optional[str] = None
|
|
@@ -1352,7 +1315,7 @@ class CommentOn(DDLBuilder):
|
|
|
1352
1315
|
expression=exp.convert(self._comment),
|
|
1353
1316
|
)
|
|
1354
1317
|
self._raise_sql_builder_error("Must specify target and comment for COMMENT ON statement.")
|
|
1355
|
-
raise AssertionError
|
|
1318
|
+
raise AssertionError
|
|
1356
1319
|
|
|
1357
1320
|
|
|
1358
1321
|
@dataclass
|
sqlspec/builder/_ddl_utils.py
CHANGED
|
@@ -33,7 +33,6 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
|
|
|
33
33
|
else:
|
|
34
34
|
default_expr = exp.convert(col.default)
|
|
35
35
|
else:
|
|
36
|
-
# Use exp.convert for all other types (int, float, bool, None, etc.)
|
|
37
36
|
default_expr = exp.convert(col.default)
|
|
38
37
|
|
|
39
38
|
constraints.append(exp.ColumnConstraint(kind=default_expr))
|
sqlspec/builder/_delete.py
CHANGED
|
@@ -4,7 +4,6 @@ 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, field
|
|
8
7
|
from typing import Any, Optional
|
|
9
8
|
|
|
10
9
|
from sqlglot import exp
|
|
@@ -12,11 +11,11 @@ from sqlglot import exp
|
|
|
12
11
|
from sqlspec.builder._base import QueryBuilder, SafeQuery
|
|
13
12
|
from sqlspec.builder.mixins import DeleteFromClauseMixin, ReturningClauseMixin, WhereClauseMixin
|
|
14
13
|
from sqlspec.core.result import SQLResult
|
|
14
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
15
15
|
|
|
16
16
|
__all__ = ("Delete",)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
@dataclass(unsafe_hash=True)
|
|
20
19
|
class Delete(QueryBuilder, WhereClauseMixin, ReturningClauseMixin, DeleteFromClauseMixin):
|
|
21
20
|
"""Builder for DELETE statements.
|
|
22
21
|
|
|
@@ -25,7 +24,8 @@ class Delete(QueryBuilder, WhereClauseMixin, ReturningClauseMixin, DeleteFromCla
|
|
|
25
24
|
operations to maintain cross-dialect compatibility and safety.
|
|
26
25
|
"""
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
__slots__ = ("_table",)
|
|
28
|
+
_expression: Optional[exp.Expression]
|
|
29
29
|
|
|
30
30
|
def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
|
|
31
31
|
"""Initialize DELETE with optional table.
|
|
@@ -35,6 +35,7 @@ class Delete(QueryBuilder, WhereClauseMixin, ReturningClauseMixin, DeleteFromCla
|
|
|
35
35
|
**kwargs: Additional QueryBuilder arguments
|
|
36
36
|
"""
|
|
37
37
|
super().__init__(**kwargs)
|
|
38
|
+
self._initialize_expression()
|
|
38
39
|
|
|
39
40
|
self._table = None
|
|
40
41
|
|
|
@@ -69,8 +70,6 @@ class Delete(QueryBuilder, WhereClauseMixin, ReturningClauseMixin, DeleteFromCla
|
|
|
69
70
|
"""
|
|
70
71
|
|
|
71
72
|
if not self._table:
|
|
72
|
-
from sqlspec.exceptions import SQLBuilderError
|
|
73
|
-
|
|
74
73
|
msg = "DELETE requires a table to be specified. Use from() to set the table."
|
|
75
74
|
raise SQLBuilderError(msg)
|
|
76
75
|
|