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.

Files changed (43) hide show
  1. sqlspec/_sql.py +699 -43
  2. sqlspec/builder/_base.py +77 -44
  3. sqlspec/builder/_column.py +0 -4
  4. sqlspec/builder/_ddl.py +15 -52
  5. sqlspec/builder/_ddl_utils.py +0 -1
  6. sqlspec/builder/_delete.py +4 -5
  7. sqlspec/builder/_insert.py +61 -35
  8. sqlspec/builder/_merge.py +17 -2
  9. sqlspec/builder/_parsing_utils.py +16 -12
  10. sqlspec/builder/_select.py +29 -33
  11. sqlspec/builder/_update.py +4 -2
  12. sqlspec/builder/mixins/_cte_and_set_ops.py +47 -20
  13. sqlspec/builder/mixins/_delete_operations.py +6 -1
  14. sqlspec/builder/mixins/_insert_operations.py +126 -24
  15. sqlspec/builder/mixins/_join_operations.py +11 -4
  16. sqlspec/builder/mixins/_merge_operations.py +91 -19
  17. sqlspec/builder/mixins/_order_limit_operations.py +15 -3
  18. sqlspec/builder/mixins/_pivot_operations.py +11 -2
  19. sqlspec/builder/mixins/_select_operations.py +16 -10
  20. sqlspec/builder/mixins/_update_operations.py +43 -10
  21. sqlspec/builder/mixins/_where_clause.py +177 -65
  22. sqlspec/core/cache.py +26 -28
  23. sqlspec/core/compiler.py +58 -37
  24. sqlspec/core/filters.py +12 -10
  25. sqlspec/core/parameters.py +80 -52
  26. sqlspec/core/result.py +30 -17
  27. sqlspec/core/statement.py +47 -22
  28. sqlspec/driver/_async.py +76 -46
  29. sqlspec/driver/_common.py +25 -6
  30. sqlspec/driver/_sync.py +73 -43
  31. sqlspec/driver/mixins/_result_tools.py +62 -37
  32. sqlspec/driver/mixins/_sql_translator.py +61 -11
  33. sqlspec/extensions/litestar/cli.py +1 -1
  34. sqlspec/extensions/litestar/plugin.py +2 -2
  35. sqlspec/protocols.py +7 -0
  36. sqlspec/utils/sync_tools.py +1 -1
  37. sqlspec/utils/type_guards.py +7 -3
  38. {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/METADATA +1 -1
  39. {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/RECORD +43 -43
  40. {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/WHEEL +0 -0
  41. {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/entry_points.txt +0 -0
  42. {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/licenses/LICENSE +0 -0
  43. {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: str
39
- parameters: dict[str, Any] = field(default_factory=dict)
40
- dialect: DialectType = field(default=None)
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
- dialect: DialectType = field(default=None)
52
- schema: Optional[dict[str, dict[str, str]]] = field(default=None)
53
- _expression: Optional[exp.Expression] = field(default=None, init=False, repr=False, compare=False, hash=False)
54
- _parameters: dict[str, Any] = field(default_factory=dict, init=False, repr=False, compare=False, hash=False)
55
- _parameter_counter: int = field(default=0, init=False, repr=False, compare=False, hash=False)
56
- _with_ctes: dict[str, exp.CTE] = field(default_factory=dict, init=False, repr=False, compare=False, hash=False)
57
- enable_optimization: bool = field(default=True, init=True)
58
- optimize_joins: bool = field(default=True, init=True)
59
- optimize_predicates: bool = field(default=True, init=True)
60
- simplify_expressions: bool = field(default=True, init=True)
61
-
62
- def __post_init__(self) -> None:
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=True)
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
- param_name_to_use = name
158
- else:
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._parameters[param_name_to_use] = value
163
- return self, param_name_to_use
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 = 1
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
- i += 1
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
- state_components = [
196
- f"expression:{self._expression.sql() if self._expression else 'None'}",
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:{self.dialect_name or 'default'}",
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
- config_components = [
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
- state_components.extend(config_components)
250
+ state_parts.extend(config_parts)
218
251
 
219
- state_string = "|".join(state_components)
220
- cache_key = hashlib.sha256(state_string.encode()).hexdigest()[:16]
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.copy()
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.copy()
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.copy()
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.copy() if self.enable_optimization else 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.copy(), schema=self.schema, dialect=self.dialect_name, optimizer_settings=optimizer_settings
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.copy())
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
- if hasattr(self.dialect, "__name__"):
464
+ try:
434
465
  return self.dialect.__name__.lower()
466
+ except AttributeError:
467
+ pass
435
468
  return None
436
469
 
437
470
  @property
@@ -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
- pass
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 # For computed columns
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 # 'PRIMARY KEY', 'FOREIGN KEY', 'UNIQUE', 'CHECK'
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": # Skip already handled options
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 # True: CASCADE, False: RESTRICT, None: not set
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 # "RESTART" or "CONTINUE"
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 # SQL, SelectBuilder, or str
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 # SQL, SelectBuilder, or str
844
- _with_data: Optional[bool] = None # True: WITH DATA, False: NO DATA, None: not set
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 # SQL, SelectBuilder, or str
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 # This line is unreachable but satisfies the linter
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 # 'TABLE' or 'COLUMN'
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 # This line is unreachable but satisfies the linter
1318
+ raise AssertionError
1356
1319
 
1357
1320
 
1358
1321
  @dataclass
@@ -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))
@@ -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
- _table: "Optional[str]" = field(default=None, init=False)
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