sqlspec 0.24.1__py3-none-any.whl → 0.26.0__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 (95) hide show
  1. sqlspec/_serialization.py +223 -21
  2. sqlspec/_sql.py +20 -62
  3. sqlspec/_typing.py +11 -0
  4. sqlspec/adapters/adbc/config.py +8 -1
  5. sqlspec/adapters/adbc/data_dictionary.py +290 -0
  6. sqlspec/adapters/adbc/driver.py +129 -20
  7. sqlspec/adapters/adbc/type_converter.py +159 -0
  8. sqlspec/adapters/aiosqlite/config.py +3 -0
  9. sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
  10. sqlspec/adapters/aiosqlite/driver.py +17 -3
  11. sqlspec/adapters/asyncmy/_types.py +1 -1
  12. sqlspec/adapters/asyncmy/config.py +11 -8
  13. sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
  14. sqlspec/adapters/asyncmy/driver.py +31 -7
  15. sqlspec/adapters/asyncpg/config.py +3 -0
  16. sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
  17. sqlspec/adapters/asyncpg/driver.py +19 -4
  18. sqlspec/adapters/bigquery/config.py +3 -0
  19. sqlspec/adapters/bigquery/data_dictionary.py +109 -0
  20. sqlspec/adapters/bigquery/driver.py +21 -3
  21. sqlspec/adapters/bigquery/type_converter.py +93 -0
  22. sqlspec/adapters/duckdb/_types.py +1 -1
  23. sqlspec/adapters/duckdb/config.py +2 -0
  24. sqlspec/adapters/duckdb/data_dictionary.py +124 -0
  25. sqlspec/adapters/duckdb/driver.py +32 -5
  26. sqlspec/adapters/duckdb/pool.py +1 -1
  27. sqlspec/adapters/duckdb/type_converter.py +103 -0
  28. sqlspec/adapters/oracledb/config.py +6 -0
  29. sqlspec/adapters/oracledb/data_dictionary.py +442 -0
  30. sqlspec/adapters/oracledb/driver.py +68 -9
  31. sqlspec/adapters/oracledb/migrations.py +51 -67
  32. sqlspec/adapters/oracledb/type_converter.py +132 -0
  33. sqlspec/adapters/psqlpy/config.py +3 -0
  34. sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
  35. sqlspec/adapters/psqlpy/driver.py +23 -179
  36. sqlspec/adapters/psqlpy/type_converter.py +73 -0
  37. sqlspec/adapters/psycopg/config.py +8 -4
  38. sqlspec/adapters/psycopg/data_dictionary.py +257 -0
  39. sqlspec/adapters/psycopg/driver.py +40 -5
  40. sqlspec/adapters/sqlite/config.py +3 -0
  41. sqlspec/adapters/sqlite/data_dictionary.py +117 -0
  42. sqlspec/adapters/sqlite/driver.py +18 -3
  43. sqlspec/adapters/sqlite/pool.py +13 -4
  44. sqlspec/base.py +3 -4
  45. sqlspec/builder/_base.py +130 -48
  46. sqlspec/builder/_column.py +66 -24
  47. sqlspec/builder/_ddl.py +91 -41
  48. sqlspec/builder/_insert.py +40 -58
  49. sqlspec/builder/_parsing_utils.py +127 -12
  50. sqlspec/builder/_select.py +147 -2
  51. sqlspec/builder/_update.py +1 -1
  52. sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
  53. sqlspec/builder/mixins/_delete_operations.py +12 -7
  54. sqlspec/builder/mixins/_insert_operations.py +50 -36
  55. sqlspec/builder/mixins/_join_operations.py +15 -30
  56. sqlspec/builder/mixins/_merge_operations.py +210 -78
  57. sqlspec/builder/mixins/_order_limit_operations.py +4 -10
  58. sqlspec/builder/mixins/_pivot_operations.py +1 -0
  59. sqlspec/builder/mixins/_select_operations.py +44 -22
  60. sqlspec/builder/mixins/_update_operations.py +30 -37
  61. sqlspec/builder/mixins/_where_clause.py +52 -70
  62. sqlspec/cli.py +246 -140
  63. sqlspec/config.py +33 -19
  64. sqlspec/core/__init__.py +3 -2
  65. sqlspec/core/cache.py +298 -352
  66. sqlspec/core/compiler.py +61 -4
  67. sqlspec/core/filters.py +246 -213
  68. sqlspec/core/hashing.py +9 -11
  69. sqlspec/core/parameters.py +27 -10
  70. sqlspec/core/statement.py +72 -12
  71. sqlspec/core/type_conversion.py +234 -0
  72. sqlspec/driver/__init__.py +6 -3
  73. sqlspec/driver/_async.py +108 -5
  74. sqlspec/driver/_common.py +186 -17
  75. sqlspec/driver/_sync.py +108 -5
  76. sqlspec/driver/mixins/_result_tools.py +60 -7
  77. sqlspec/exceptions.py +5 -0
  78. sqlspec/loader.py +8 -9
  79. sqlspec/migrations/__init__.py +4 -3
  80. sqlspec/migrations/base.py +153 -14
  81. sqlspec/migrations/commands.py +34 -96
  82. sqlspec/migrations/context.py +145 -0
  83. sqlspec/migrations/loaders.py +25 -8
  84. sqlspec/migrations/runner.py +352 -82
  85. sqlspec/storage/backends/fsspec.py +1 -0
  86. sqlspec/typing.py +4 -0
  87. sqlspec/utils/config_resolver.py +153 -0
  88. sqlspec/utils/serializers.py +50 -2
  89. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
  90. sqlspec-0.26.0.dist-info/RECORD +157 -0
  91. sqlspec-0.24.1.dist-info/RECORD +0 -139
  92. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
  93. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
  94. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
  95. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
@@ -5,6 +5,7 @@ SQL conditions with parameter binding.
5
5
  """
6
6
 
7
7
  from collections.abc import Iterable
8
+ from datetime import date, datetime
8
9
  from typing import Any, Optional, cast
9
10
 
10
11
  from sqlglot import exp
@@ -14,6 +15,24 @@ from sqlspec.utils.type_guards import has_sql_method
14
15
  __all__ = ("Column", "ColumnExpression", "FunctionColumn")
15
16
 
16
17
 
18
+ def _convert_value(value: Any) -> exp.Expression:
19
+ """Convert a Python value to a SQLGlot expression.
20
+
21
+ Special handling for datetime objects to prevent SQLGlot from
22
+ converting them to TIME_STR_TO_TIME function calls. Datetime
23
+ objects should be passed as parameters, not converted to SQL functions.
24
+
25
+ Args:
26
+ value: The value to convert
27
+
28
+ Returns:
29
+ A SQLGlot expression representing the value
30
+ """
31
+ if isinstance(value, (datetime, date)):
32
+ return exp.Literal(this=value, is_string=False)
33
+ return exp.convert(value)
34
+
35
+
17
36
  class ColumnExpression:
18
37
  """Base class for column expressions that can be combined with operators."""
19
38
 
@@ -67,33 +86,37 @@ class Column:
67
86
  else:
68
87
  self._expression = exp.Column(this=exp.Identifier(this=name))
69
88
 
89
+ def _convert_value(self, value: Any) -> exp.Expression:
90
+ """Convert a Python value to a SQLGlot expression."""
91
+ return _convert_value(value)
92
+
70
93
  def __eq__(self, other: object) -> ColumnExpression: # type: ignore[override]
71
94
  """Equal to (==)."""
72
95
  if other is None:
73
96
  return ColumnExpression(exp.Is(this=self._expression, expression=exp.Null()))
74
- return ColumnExpression(exp.EQ(this=self._expression, expression=exp.convert(other)))
97
+ return ColumnExpression(exp.EQ(this=self._expression, expression=self._convert_value(other)))
75
98
 
76
99
  def __ne__(self, other: object) -> ColumnExpression: # type: ignore[override]
77
100
  """Not equal to (!=)."""
78
101
  if other is None:
79
102
  return ColumnExpression(exp.Not(this=exp.Is(this=self._expression, expression=exp.Null())))
80
- return ColumnExpression(exp.NEQ(this=self._expression, expression=exp.convert(other)))
103
+ return ColumnExpression(exp.NEQ(this=self._expression, expression=self._convert_value(other)))
81
104
 
82
105
  def __gt__(self, other: Any) -> ColumnExpression:
83
106
  """Greater than (>)."""
84
- return ColumnExpression(exp.GT(this=self._expression, expression=exp.convert(other)))
107
+ return ColumnExpression(exp.GT(this=self._expression, expression=self._convert_value(other)))
85
108
 
86
109
  def __ge__(self, other: Any) -> ColumnExpression:
87
110
  """Greater than or equal (>=)."""
88
- return ColumnExpression(exp.GTE(this=self._expression, expression=exp.convert(other)))
111
+ return ColumnExpression(exp.GTE(this=self._expression, expression=self._convert_value(other)))
89
112
 
90
113
  def __lt__(self, other: Any) -> ColumnExpression:
91
114
  """Less than (<)."""
92
- return ColumnExpression(exp.LT(this=self._expression, expression=exp.convert(other)))
115
+ return ColumnExpression(exp.LT(this=self._expression, expression=self._convert_value(other)))
93
116
 
94
117
  def __le__(self, other: Any) -> ColumnExpression:
95
118
  """Less than or equal (<=)."""
96
- return ColumnExpression(exp.LTE(this=self._expression, expression=exp.convert(other)))
119
+ return ColumnExpression(exp.LTE(this=self._expression, expression=self._convert_value(other)))
97
120
 
98
121
  def __invert__(self) -> ColumnExpression:
99
122
  """Apply NOT operator (~)."""
@@ -102,18 +125,20 @@ class Column:
102
125
  def like(self, pattern: str, escape: Optional[str] = None) -> ColumnExpression:
103
126
  """SQL LIKE pattern matching."""
104
127
  if escape:
105
- like_expr = exp.Like(this=self._expression, expression=exp.convert(pattern), escape=exp.convert(escape))
128
+ like_expr = exp.Like(
129
+ this=self._expression, expression=self._convert_value(pattern), escape=self._convert_value(escape)
130
+ )
106
131
  else:
107
- like_expr = exp.Like(this=self._expression, expression=exp.convert(pattern))
132
+ like_expr = exp.Like(this=self._expression, expression=self._convert_value(pattern))
108
133
  return ColumnExpression(like_expr)
109
134
 
110
135
  def ilike(self, pattern: str) -> ColumnExpression:
111
136
  """Case-insensitive LIKE."""
112
- return ColumnExpression(exp.ILike(this=self._expression, expression=exp.convert(pattern)))
137
+ return ColumnExpression(exp.ILike(this=self._expression, expression=self._convert_value(pattern)))
113
138
 
114
139
  def in_(self, values: Iterable[Any]) -> ColumnExpression:
115
140
  """SQL IN clause."""
116
- converted_values = [exp.convert(v) for v in values]
141
+ converted_values = [self._convert_value(v) for v in values]
117
142
  return ColumnExpression(exp.In(this=self._expression, expressions=converted_values))
118
143
 
119
144
  def not_in(self, values: Iterable[Any]) -> ColumnExpression:
@@ -122,7 +147,9 @@ class Column:
122
147
 
123
148
  def between(self, start: Any, end: Any) -> ColumnExpression:
124
149
  """SQL BETWEEN clause."""
125
- return ColumnExpression(exp.Between(this=self._expression, low=exp.convert(start), high=exp.convert(end)))
150
+ return ColumnExpression(
151
+ exp.Between(this=self._expression, low=self._convert_value(start), high=self._convert_value(end))
152
+ )
126
153
 
127
154
  def is_null(self) -> ColumnExpression:
128
155
  """SQL IS NULL."""
@@ -142,12 +169,12 @@ class Column:
142
169
 
143
170
  def any_(self, values: Iterable[Any]) -> ColumnExpression:
144
171
  """SQL = ANY(...) clause."""
145
- converted_values = [exp.convert(v) for v in values]
172
+ converted_values = [self._convert_value(v) for v in values]
146
173
  return ColumnExpression(exp.EQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
147
174
 
148
175
  def not_any_(self, values: Iterable[Any]) -> ColumnExpression:
149
176
  """SQL <> ANY(...) clause."""
150
- converted_values = [exp.convert(v) for v in values]
177
+ converted_values = [self._convert_value(v) for v in values]
151
178
  return ColumnExpression(exp.NEQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
152
179
 
153
180
  def lower(self) -> "FunctionColumn":
@@ -186,14 +213,14 @@ class Column:
186
213
 
187
214
  def substring(self, start: int, length: Optional[int] = None) -> "FunctionColumn":
188
215
  """SQL SUBSTRING() function."""
189
- args = [exp.convert(start)]
216
+ args = [self._convert_value(start)]
190
217
  if length is not None:
191
- args.append(exp.convert(length))
218
+ args.append(self._convert_value(length))
192
219
  return FunctionColumn(exp.Substring(this=self._expression, expressions=args))
193
220
 
194
221
  def coalesce(self, *values: Any) -> "FunctionColumn":
195
222
  """SQL COALESCE() function."""
196
- expressions = [self._expression] + [exp.convert(v) for v in values]
223
+ expressions = [self._expression] + [self._convert_value(v) for v in values]
197
224
  return FunctionColumn(exp.Coalesce(expressions=expressions))
198
225
 
199
226
  def cast(self, data_type: str) -> "FunctionColumn":
@@ -254,6 +281,15 @@ class Column:
254
281
  """Hash based on table and column name."""
255
282
  return hash((self.table, self.name))
256
283
 
284
+ @property
285
+ def sqlglot_expression(self) -> exp.Expression:
286
+ """Get the underlying SQLGlot expression (public API).
287
+
288
+ Returns:
289
+ The SQLGlot expression for this column
290
+ """
291
+ return self._expression
292
+
257
293
 
258
294
  class FunctionColumn:
259
295
  """Represents the result of a SQL function call on a column."""
@@ -263,22 +299,26 @@ class FunctionColumn:
263
299
  def __init__(self, expression: exp.Expression) -> None:
264
300
  self._expression = expression
265
301
 
302
+ def _convert_value(self, value: Any) -> exp.Expression:
303
+ """Convert a Python value to a SQLGlot expression."""
304
+ return _convert_value(value)
305
+
266
306
  def __eq__(self, other: object) -> ColumnExpression: # type: ignore[override]
267
- return ColumnExpression(exp.EQ(this=self._expression, expression=exp.convert(other)))
307
+ return ColumnExpression(exp.EQ(this=self._expression, expression=self._convert_value(other)))
268
308
 
269
309
  def __ne__(self, other: object) -> ColumnExpression: # type: ignore[override]
270
- return ColumnExpression(exp.NEQ(this=self._expression, expression=exp.convert(other)))
310
+ return ColumnExpression(exp.NEQ(this=self._expression, expression=self._convert_value(other)))
271
311
 
272
312
  def like(self, pattern: str) -> ColumnExpression:
273
- return ColumnExpression(exp.Like(this=self._expression, expression=exp.convert(pattern)))
313
+ return ColumnExpression(exp.Like(this=self._expression, expression=self._convert_value(pattern)))
274
314
 
275
315
  def ilike(self, pattern: str) -> ColumnExpression:
276
316
  """Case-insensitive LIKE."""
277
- return ColumnExpression(exp.ILike(this=self._expression, expression=exp.convert(pattern)))
317
+ return ColumnExpression(exp.ILike(this=self._expression, expression=self._convert_value(pattern)))
278
318
 
279
319
  def in_(self, values: Iterable[Any]) -> ColumnExpression:
280
320
  """SQL IN clause."""
281
- converted_values = [exp.convert(v) for v in values]
321
+ converted_values = [self._convert_value(v) for v in values]
282
322
  return ColumnExpression(exp.In(this=self._expression, expressions=converted_values))
283
323
 
284
324
  def not_in_(self, values: Iterable[Any]) -> ColumnExpression:
@@ -295,7 +335,9 @@ class FunctionColumn:
295
335
 
296
336
  def between(self, start: Any, end: Any) -> ColumnExpression:
297
337
  """SQL BETWEEN clause."""
298
- return ColumnExpression(exp.Between(this=self._expression, low=exp.convert(start), high=exp.convert(end)))
338
+ return ColumnExpression(
339
+ exp.Between(this=self._expression, low=self._convert_value(start), high=self._convert_value(end))
340
+ )
299
341
 
300
342
  def is_null(self) -> ColumnExpression:
301
343
  """SQL IS NULL."""
@@ -307,12 +349,12 @@ class FunctionColumn:
307
349
 
308
350
  def any_(self, values: Iterable[Any]) -> ColumnExpression:
309
351
  """SQL = ANY(...) clause."""
310
- converted_values = [exp.convert(v) for v in values]
352
+ converted_values = [self._convert_value(v) for v in values]
311
353
  return ColumnExpression(exp.EQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
312
354
 
313
355
  def not_any_(self, values: Iterable[Any]) -> ColumnExpression:
314
356
  """SQL <> ANY(...) clause."""
315
- converted_values = [exp.convert(v) for v in values]
357
+ converted_values = [self._convert_value(v) for v in values]
316
358
  return ColumnExpression(exp.NEQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
317
359
 
318
360
  def alias(self, alias_name: str) -> exp.Expression:
sqlspec/builder/_ddl.py CHANGED
@@ -11,12 +11,14 @@ from sqlglot.dialects.dialect import DialectType
11
11
  from typing_extensions import Self
12
12
 
13
13
  from sqlspec.builder._base import QueryBuilder, SafeQuery
14
+ from sqlspec.builder._select import Select
14
15
  from sqlspec.core.result import SQLResult
16
+ from sqlspec.core.statement import SQL
15
17
  from sqlspec.utils.type_guards import has_sqlglot_expression, has_with_method
16
18
 
17
19
  if TYPE_CHECKING:
18
20
  from sqlspec.builder._column import ColumnExpression
19
- from sqlspec.core.statement import SQL, StatementConfig
21
+ from sqlspec.core.statement import StatementConfig
20
22
 
21
23
  __all__ = (
22
24
  "AlterOperation",
@@ -39,6 +41,37 @@ __all__ = (
39
41
  "Truncate",
40
42
  )
41
43
 
44
+ CONSTRAINT_TYPE_PRIMARY_KEY = "PRIMARY KEY"
45
+ CONSTRAINT_TYPE_FOREIGN_KEY = "FOREIGN KEY"
46
+ CONSTRAINT_TYPE_UNIQUE = "UNIQUE"
47
+ CONSTRAINT_TYPE_CHECK = "CHECK"
48
+
49
+ FOREIGN_KEY_ACTION_CASCADE = "CASCADE"
50
+ FOREIGN_KEY_ACTION_SET_NULL = "SET NULL"
51
+ FOREIGN_KEY_ACTION_SET_DEFAULT = "SET DEFAULT"
52
+ FOREIGN_KEY_ACTION_RESTRICT = "RESTRICT"
53
+ FOREIGN_KEY_ACTION_NO_ACTION = "NO ACTION"
54
+
55
+ VALID_FOREIGN_KEY_ACTIONS = {
56
+ FOREIGN_KEY_ACTION_CASCADE,
57
+ FOREIGN_KEY_ACTION_SET_NULL,
58
+ FOREIGN_KEY_ACTION_SET_DEFAULT,
59
+ FOREIGN_KEY_ACTION_RESTRICT,
60
+ FOREIGN_KEY_ACTION_NO_ACTION,
61
+ None,
62
+ }
63
+
64
+ VALID_CONSTRAINT_TYPES = {
65
+ CONSTRAINT_TYPE_PRIMARY_KEY,
66
+ CONSTRAINT_TYPE_FOREIGN_KEY,
67
+ CONSTRAINT_TYPE_UNIQUE,
68
+ CONSTRAINT_TYPE_CHECK,
69
+ }
70
+
71
+ CURRENT_TIMESTAMP_KEYWORD = "CURRENT_TIMESTAMP"
72
+ CURRENT_DATE_KEYWORD = "CURRENT_DATE"
73
+ CURRENT_TIME_KEYWORD = "CURRENT_TIME"
74
+
42
75
 
43
76
  def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
44
77
  """Build SQLGlot expression for a column definition."""
@@ -59,11 +92,11 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
59
92
  default_expr: Optional[exp.Expression] = None
60
93
  if isinstance(col.default, str):
61
94
  default_upper = col.default.upper()
62
- if default_upper == "CURRENT_TIMESTAMP":
95
+ if default_upper == CURRENT_TIMESTAMP_KEYWORD:
63
96
  default_expr = exp.CurrentTimestamp()
64
- elif default_upper == "CURRENT_DATE":
97
+ elif default_upper == CURRENT_DATE_KEYWORD:
65
98
  default_expr = exp.CurrentDate()
66
- elif default_upper == "CURRENT_TIME":
99
+ elif default_upper == CURRENT_TIME_KEYWORD:
67
100
  default_expr = exp.CurrentTime()
68
101
  elif "(" in col.default:
69
102
  default_expr = exp.maybe_parse(col.default)
@@ -96,14 +129,14 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
96
129
 
97
130
  def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional[exp.Expression]":
98
131
  """Build SQLGlot expression for a table constraint."""
99
- if constraint.constraint_type == "PRIMARY KEY":
132
+ if constraint.constraint_type == CONSTRAINT_TYPE_PRIMARY_KEY:
100
133
  pk_constraint = exp.PrimaryKey(expressions=[exp.to_identifier(col) for col in constraint.columns])
101
134
 
102
135
  if constraint.name:
103
136
  return exp.Constraint(this=exp.to_identifier(constraint.name), expression=pk_constraint)
104
137
  return pk_constraint
105
138
 
106
- if constraint.constraint_type == "FOREIGN KEY":
139
+ if constraint.constraint_type == CONSTRAINT_TYPE_FOREIGN_KEY:
107
140
  fk_constraint = exp.ForeignKey(
108
141
  expressions=[exp.to_identifier(col) for col in constraint.columns],
109
142
  reference=exp.Reference(
@@ -118,14 +151,14 @@ def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional
118
151
  return exp.Constraint(this=exp.to_identifier(constraint.name), expression=fk_constraint)
119
152
  return fk_constraint
120
153
 
121
- if constraint.constraint_type == "UNIQUE":
154
+ if constraint.constraint_type == CONSTRAINT_TYPE_UNIQUE:
122
155
  unique_constraint = exp.UniqueKeyProperty(expressions=[exp.to_identifier(col) for col in constraint.columns])
123
156
 
124
157
  if constraint.name:
125
158
  return exp.Constraint(this=exp.to_identifier(constraint.name), expression=unique_constraint)
126
159
  return unique_constraint
127
160
 
128
- if constraint.constraint_type == "CHECK":
161
+ if constraint.constraint_type == CONSTRAINT_TYPE_CHECK:
129
162
  check_expr = exp.Check(this=exp.maybe_parse(constraint.condition) if constraint.condition else None)
130
163
 
131
164
  if constraint.name:
@@ -356,7 +389,7 @@ class CreateTable(DDLBuilder):
356
389
 
357
390
  self._columns.append(column_def)
358
391
 
359
- if primary_key and not any(c.constraint_type == "PRIMARY KEY" for c in self._constraints):
392
+ if primary_key and not self._has_primary_key_constraint():
360
393
  self.primary_key_constraint([name])
361
394
 
362
395
  return self
@@ -368,13 +401,13 @@ class CreateTable(DDLBuilder):
368
401
  if not col_list:
369
402
  self._raise_sql_builder_error("Primary key must include at least one column")
370
403
 
371
- existing_pk = next((c for c in self._constraints if c.constraint_type == "PRIMARY KEY"), None)
404
+ existing_pk = self._find_primary_key_constraint()
372
405
  if existing_pk:
373
406
  for col in col_list:
374
407
  if col not in existing_pk.columns:
375
408
  existing_pk.columns.append(col)
376
409
  else:
377
- constraint = ConstraintDefinition(constraint_type="PRIMARY KEY", name=name, columns=col_list)
410
+ constraint = ConstraintDefinition(constraint_type=CONSTRAINT_TYPE_PRIMARY_KEY, name=name, columns=col_list)
378
411
  self._constraints.append(constraint)
379
412
 
380
413
  return self
@@ -398,14 +431,11 @@ class CreateTable(DDLBuilder):
398
431
  if len(col_list) != len(ref_col_list):
399
432
  self._raise_sql_builder_error("Foreign key columns and referenced columns must have same length")
400
433
 
401
- valid_actions = {"CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION", None}
402
- if on_delete and on_delete.upper() not in valid_actions:
403
- self._raise_sql_builder_error(f"Invalid ON DELETE action: {on_delete}")
404
- if on_update and on_update.upper() not in valid_actions:
405
- self._raise_sql_builder_error(f"Invalid ON UPDATE action: {on_update}")
434
+ self._validate_foreign_key_action(on_delete, "ON DELETE")
435
+ self._validate_foreign_key_action(on_update, "ON UPDATE")
406
436
 
407
437
  constraint = ConstraintDefinition(
408
- constraint_type="FOREIGN KEY",
438
+ constraint_type=CONSTRAINT_TYPE_FOREIGN_KEY,
409
439
  name=name,
410
440
  columns=col_list,
411
441
  references_table=references_table,
@@ -426,7 +456,7 @@ class CreateTable(DDLBuilder):
426
456
  if not col_list:
427
457
  self._raise_sql_builder_error("Unique constraint must include at least one column")
428
458
 
429
- constraint = ConstraintDefinition(constraint_type="UNIQUE", name=name, columns=col_list)
459
+ constraint = ConstraintDefinition(constraint_type=CONSTRAINT_TYPE_UNIQUE, name=name, columns=col_list)
430
460
 
431
461
  self._constraints.append(constraint)
432
462
  return self
@@ -443,7 +473,7 @@ class CreateTable(DDLBuilder):
443
473
  else:
444
474
  condition_str = str(condition)
445
475
 
446
- constraint = ConstraintDefinition(constraint_type="CHECK", name=name, condition=condition_str)
476
+ constraint = ConstraintDefinition(constraint_type=CONSTRAINT_TYPE_CHECK, name=name, condition=condition_str)
447
477
 
448
478
  self._constraints.append(constraint)
449
479
  return self
@@ -484,10 +514,8 @@ class CreateTable(DDLBuilder):
484
514
  column_defs.append(col_expr)
485
515
 
486
516
  for constraint in self._constraints:
487
- if constraint.constraint_type == "PRIMARY KEY" and len(constraint.columns) == 1:
488
- col_name = constraint.columns[0]
489
- if any(c.name == col_name and c.primary_key for c in self._columns):
490
- continue
517
+ if self._is_redundant_single_column_primary_key(constraint):
518
+ continue
491
519
 
492
520
  constraint_expr = build_constraint_expression(constraint)
493
521
  if constraint_expr:
@@ -531,6 +559,27 @@ class CreateTable(DDLBuilder):
531
559
  like=like_expr,
532
560
  )
533
561
 
562
+ def _has_primary_key_constraint(self) -> bool:
563
+ """Check if table already has a primary key constraint."""
564
+ return any(c.constraint_type == CONSTRAINT_TYPE_PRIMARY_KEY for c in self._constraints)
565
+
566
+ def _find_primary_key_constraint(self) -> "Optional[ConstraintDefinition]":
567
+ """Find existing primary key constraint."""
568
+ return next((c for c in self._constraints if c.constraint_type == CONSTRAINT_TYPE_PRIMARY_KEY), None)
569
+
570
+ def _validate_foreign_key_action(self, action: "Optional[str]", action_type: str) -> None:
571
+ """Validate foreign key action (ON DELETE or ON UPDATE)."""
572
+ if action and action.upper() not in VALID_FOREIGN_KEY_ACTIONS:
573
+ self._raise_sql_builder_error(f"Invalid {action_type} action: {action}")
574
+
575
+ def _is_redundant_single_column_primary_key(self, constraint: "ConstraintDefinition") -> bool:
576
+ """Check if constraint is a redundant single-column primary key."""
577
+ if constraint.constraint_type != CONSTRAINT_TYPE_PRIMARY_KEY or len(constraint.columns) != 1:
578
+ return False
579
+
580
+ col_name = constraint.columns[0]
581
+ return any(c.name == col_name and c.primary_key for c in self._columns)
582
+
534
583
 
535
584
  class DropTable(DDLBuilder):
536
585
  """Builder for DROP TABLE [IF EXISTS] ... [CASCADE|RESTRICT]."""
@@ -966,17 +1015,15 @@ class CreateTableAsSelect(DDLBuilder):
966
1015
 
967
1016
  select_expr = None
968
1017
  select_parameters = None
969
- from sqlspec.builder._select import Select
970
- from sqlspec.core.statement import SQL
971
1018
 
972
1019
  if isinstance(self._select_query, SQL):
973
1020
  select_expr = self._select_query.expression
974
1021
  select_parameters = self._select_query.parameters
975
1022
  elif isinstance(self._select_query, Select):
976
- select_expr = self._select_query._expression
977
- select_parameters = self._select_query._parameters
1023
+ select_expr = self._select_query.get_expression()
1024
+ select_parameters = self._select_query.parameters
978
1025
 
979
- with_ctes = self._select_query._with_ctes
1026
+ with_ctes = self._select_query.with_ctes
980
1027
  if with_ctes and select_expr and isinstance(select_expr, exp.Select):
981
1028
  for alias, cte in with_ctes.items():
982
1029
  if has_with_method(select_expr):
@@ -1093,15 +1140,13 @@ class CreateMaterializedView(DDLBuilder):
1093
1140
 
1094
1141
  select_expr = None
1095
1142
  select_parameters = None
1096
- from sqlspec.builder._select import Select
1097
- from sqlspec.core.statement import SQL
1098
1143
 
1099
1144
  if isinstance(self._select_query, SQL):
1100
1145
  select_expr = self._select_query.expression
1101
1146
  select_parameters = self._select_query.parameters
1102
1147
  elif isinstance(self._select_query, Select):
1103
- select_expr = self._select_query._expression
1104
- select_parameters = self._select_query._parameters
1148
+ select_expr = self._select_query.get_expression()
1149
+ select_parameters = self._select_query.parameters
1105
1150
  elif isinstance(self._select_query, str):
1106
1151
  select_expr = exp.maybe_parse(self._select_query)
1107
1152
  select_parameters = None
@@ -1191,15 +1236,13 @@ class CreateView(DDLBuilder):
1191
1236
 
1192
1237
  select_expr = None
1193
1238
  select_parameters = None
1194
- from sqlspec.builder._select import Select
1195
- from sqlspec.core.statement import SQL
1196
1239
 
1197
1240
  if isinstance(self._select_query, SQL):
1198
1241
  select_expr = self._select_query.expression
1199
1242
  select_parameters = self._select_query.parameters
1200
1243
  elif isinstance(self._select_query, Select):
1201
- select_expr = self._select_query._expression
1202
- select_parameters = self._select_query._parameters
1244
+ select_expr = self._select_query.get_expression()
1245
+ select_parameters = self._select_query.parameters
1203
1246
  elif isinstance(self._select_query, str):
1204
1247
  select_expr = exp.maybe_parse(self._select_query)
1205
1248
  select_parameters = None
@@ -1347,8 +1390,7 @@ class AlterTable(DDLBuilder):
1347
1390
  on_delete: Foreign key ON DELETE action
1348
1391
  on_update: Foreign key ON UPDATE action
1349
1392
  """
1350
- valid_types = {"PRIMARY KEY", "FOREIGN KEY", "UNIQUE", "CHECK"}
1351
- if constraint_type.upper() not in valid_types:
1393
+ if constraint_type.upper() not in VALID_CONSTRAINT_TYPES:
1352
1394
  self._raise_sql_builder_error(f"Invalid constraint type: {constraint_type}")
1353
1395
 
1354
1396
  col_list = None
@@ -1361,8 +1403,8 @@ class AlterTable(DDLBuilder):
1361
1403
 
1362
1404
  condition_str: Optional[str] = None
1363
1405
  if condition is not None:
1364
- if hasattr(condition, "sqlglot_expression"):
1365
- sqlglot_expr = getattr(condition, "sqlglot_expression", None)
1406
+ if has_sqlglot_expression(condition):
1407
+ sqlglot_expr = condition.sqlglot_expression
1366
1408
  condition_str = sqlglot_expr.sql(dialect=self.dialect) if sqlglot_expr else str(condition)
1367
1409
  else:
1368
1410
  condition_str = str(condition)
@@ -1474,7 +1516,7 @@ class AlterTable(DDLBuilder):
1474
1516
  default_val = op.column_definition.default
1475
1517
  default_expr: Optional[exp.Expression]
1476
1518
  if isinstance(default_val, str):
1477
- if default_val.upper() in {"CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME"} or "(" in default_val:
1519
+ if self._is_sql_function_default(default_val):
1478
1520
  default_expr = exp.maybe_parse(default_val)
1479
1521
  else:
1480
1522
  default_expr = exp.convert(default_val)
@@ -1494,6 +1536,14 @@ class AlterTable(DDLBuilder):
1494
1536
  self._raise_sql_builder_error(f"Unknown operation type: {op.operation_type}")
1495
1537
  raise AssertionError
1496
1538
 
1539
+ def _is_sql_function_default(self, default_val: str) -> bool:
1540
+ """Check if default value is a SQL function or expression."""
1541
+ default_upper = default_val.upper()
1542
+ return (
1543
+ default_upper in {CURRENT_TIMESTAMP_KEYWORD, CURRENT_DATE_KEYWORD, CURRENT_TIME_KEYWORD}
1544
+ or "(" in default_val
1545
+ )
1546
+
1497
1547
 
1498
1548
  class CommentOn(DDLBuilder):
1499
1549
  """Builder for COMMENT ON ... IS ... statements."""