sqlspec 0.16.0__cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.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 (148) hide show
  1. 51ff5a9eadfdefd49f98__mypyc.cpython-312-x86_64-linux-gnu.so +0 -0
  2. sqlspec/__init__.py +92 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +77 -0
  6. sqlspec/_sql.py +1347 -0
  7. sqlspec/_typing.py +680 -0
  8. sqlspec/adapters/__init__.py +0 -0
  9. sqlspec/adapters/adbc/__init__.py +5 -0
  10. sqlspec/adapters/adbc/_types.py +12 -0
  11. sqlspec/adapters/adbc/config.py +361 -0
  12. sqlspec/adapters/adbc/driver.py +512 -0
  13. sqlspec/adapters/aiosqlite/__init__.py +19 -0
  14. sqlspec/adapters/aiosqlite/_types.py +13 -0
  15. sqlspec/adapters/aiosqlite/config.py +253 -0
  16. sqlspec/adapters/aiosqlite/driver.py +248 -0
  17. sqlspec/adapters/asyncmy/__init__.py +19 -0
  18. sqlspec/adapters/asyncmy/_types.py +12 -0
  19. sqlspec/adapters/asyncmy/config.py +180 -0
  20. sqlspec/adapters/asyncmy/driver.py +274 -0
  21. sqlspec/adapters/asyncpg/__init__.py +21 -0
  22. sqlspec/adapters/asyncpg/_types.py +17 -0
  23. sqlspec/adapters/asyncpg/config.py +229 -0
  24. sqlspec/adapters/asyncpg/driver.py +344 -0
  25. sqlspec/adapters/bigquery/__init__.py +18 -0
  26. sqlspec/adapters/bigquery/_types.py +12 -0
  27. sqlspec/adapters/bigquery/config.py +298 -0
  28. sqlspec/adapters/bigquery/driver.py +558 -0
  29. sqlspec/adapters/duckdb/__init__.py +22 -0
  30. sqlspec/adapters/duckdb/_types.py +12 -0
  31. sqlspec/adapters/duckdb/config.py +504 -0
  32. sqlspec/adapters/duckdb/driver.py +368 -0
  33. sqlspec/adapters/oracledb/__init__.py +32 -0
  34. sqlspec/adapters/oracledb/_types.py +14 -0
  35. sqlspec/adapters/oracledb/config.py +317 -0
  36. sqlspec/adapters/oracledb/driver.py +538 -0
  37. sqlspec/adapters/psqlpy/__init__.py +16 -0
  38. sqlspec/adapters/psqlpy/_types.py +11 -0
  39. sqlspec/adapters/psqlpy/config.py +214 -0
  40. sqlspec/adapters/psqlpy/driver.py +530 -0
  41. sqlspec/adapters/psycopg/__init__.py +32 -0
  42. sqlspec/adapters/psycopg/_types.py +17 -0
  43. sqlspec/adapters/psycopg/config.py +426 -0
  44. sqlspec/adapters/psycopg/driver.py +796 -0
  45. sqlspec/adapters/sqlite/__init__.py +15 -0
  46. sqlspec/adapters/sqlite/_types.py +11 -0
  47. sqlspec/adapters/sqlite/config.py +240 -0
  48. sqlspec/adapters/sqlite/driver.py +294 -0
  49. sqlspec/base.py +571 -0
  50. sqlspec/builder/__init__.py +62 -0
  51. sqlspec/builder/_base.py +440 -0
  52. sqlspec/builder/_column.py +324 -0
  53. sqlspec/builder/_ddl.py +1383 -0
  54. sqlspec/builder/_ddl_utils.py +104 -0
  55. sqlspec/builder/_delete.py +77 -0
  56. sqlspec/builder/_insert.py +241 -0
  57. sqlspec/builder/_merge.py +56 -0
  58. sqlspec/builder/_parsing_utils.py +140 -0
  59. sqlspec/builder/_select.py +174 -0
  60. sqlspec/builder/_update.py +186 -0
  61. sqlspec/builder/mixins/__init__.py +55 -0
  62. sqlspec/builder/mixins/_cte_and_set_ops.py +195 -0
  63. sqlspec/builder/mixins/_delete_operations.py +36 -0
  64. sqlspec/builder/mixins/_insert_operations.py +152 -0
  65. sqlspec/builder/mixins/_join_operations.py +115 -0
  66. sqlspec/builder/mixins/_merge_operations.py +416 -0
  67. sqlspec/builder/mixins/_order_limit_operations.py +123 -0
  68. sqlspec/builder/mixins/_pivot_operations.py +144 -0
  69. sqlspec/builder/mixins/_select_operations.py +599 -0
  70. sqlspec/builder/mixins/_update_operations.py +164 -0
  71. sqlspec/builder/mixins/_where_clause.py +609 -0
  72. sqlspec/cli.py +247 -0
  73. sqlspec/config.py +395 -0
  74. sqlspec/core/__init__.py +63 -0
  75. sqlspec/core/cache.cpython-312-x86_64-linux-gnu.so +0 -0
  76. sqlspec/core/cache.py +873 -0
  77. sqlspec/core/compiler.cpython-312-x86_64-linux-gnu.so +0 -0
  78. sqlspec/core/compiler.py +396 -0
  79. sqlspec/core/filters.cpython-312-x86_64-linux-gnu.so +0 -0
  80. sqlspec/core/filters.py +830 -0
  81. sqlspec/core/hashing.cpython-312-x86_64-linux-gnu.so +0 -0
  82. sqlspec/core/hashing.py +310 -0
  83. sqlspec/core/parameters.cpython-312-x86_64-linux-gnu.so +0 -0
  84. sqlspec/core/parameters.py +1209 -0
  85. sqlspec/core/result.cpython-312-x86_64-linux-gnu.so +0 -0
  86. sqlspec/core/result.py +664 -0
  87. sqlspec/core/splitter.cpython-312-x86_64-linux-gnu.so +0 -0
  88. sqlspec/core/splitter.py +819 -0
  89. sqlspec/core/statement.cpython-312-x86_64-linux-gnu.so +0 -0
  90. sqlspec/core/statement.py +666 -0
  91. sqlspec/driver/__init__.py +19 -0
  92. sqlspec/driver/_async.py +472 -0
  93. sqlspec/driver/_common.py +612 -0
  94. sqlspec/driver/_sync.py +473 -0
  95. sqlspec/driver/mixins/__init__.py +6 -0
  96. sqlspec/driver/mixins/_result_tools.py +164 -0
  97. sqlspec/driver/mixins/_sql_translator.py +36 -0
  98. sqlspec/exceptions.py +193 -0
  99. sqlspec/extensions/__init__.py +0 -0
  100. sqlspec/extensions/aiosql/__init__.py +10 -0
  101. sqlspec/extensions/aiosql/adapter.py +461 -0
  102. sqlspec/extensions/litestar/__init__.py +6 -0
  103. sqlspec/extensions/litestar/_utils.py +52 -0
  104. sqlspec/extensions/litestar/cli.py +48 -0
  105. sqlspec/extensions/litestar/config.py +92 -0
  106. sqlspec/extensions/litestar/handlers.py +260 -0
  107. sqlspec/extensions/litestar/plugin.py +145 -0
  108. sqlspec/extensions/litestar/providers.py +454 -0
  109. sqlspec/loader.cpython-312-x86_64-linux-gnu.so +0 -0
  110. sqlspec/loader.py +760 -0
  111. sqlspec/migrations/__init__.py +35 -0
  112. sqlspec/migrations/base.py +414 -0
  113. sqlspec/migrations/commands.py +443 -0
  114. sqlspec/migrations/loaders.py +402 -0
  115. sqlspec/migrations/runner.py +213 -0
  116. sqlspec/migrations/tracker.py +140 -0
  117. sqlspec/migrations/utils.py +129 -0
  118. sqlspec/protocols.py +400 -0
  119. sqlspec/py.typed +0 -0
  120. sqlspec/storage/__init__.py +23 -0
  121. sqlspec/storage/backends/__init__.py +0 -0
  122. sqlspec/storage/backends/base.py +163 -0
  123. sqlspec/storage/backends/fsspec.py +386 -0
  124. sqlspec/storage/backends/obstore.py +459 -0
  125. sqlspec/storage/capabilities.py +102 -0
  126. sqlspec/storage/registry.py +239 -0
  127. sqlspec/typing.py +299 -0
  128. sqlspec/utils/__init__.py +3 -0
  129. sqlspec/utils/correlation.py +150 -0
  130. sqlspec/utils/deprecation.py +106 -0
  131. sqlspec/utils/fixtures.cpython-312-x86_64-linux-gnu.so +0 -0
  132. sqlspec/utils/fixtures.py +58 -0
  133. sqlspec/utils/logging.py +127 -0
  134. sqlspec/utils/module_loader.py +89 -0
  135. sqlspec/utils/serializers.py +4 -0
  136. sqlspec/utils/singleton.py +32 -0
  137. sqlspec/utils/sync_tools.cpython-312-x86_64-linux-gnu.so +0 -0
  138. sqlspec/utils/sync_tools.py +237 -0
  139. sqlspec/utils/text.cpython-312-x86_64-linux-gnu.so +0 -0
  140. sqlspec/utils/text.py +96 -0
  141. sqlspec/utils/type_guards.cpython-312-x86_64-linux-gnu.so +0 -0
  142. sqlspec/utils/type_guards.py +1135 -0
  143. sqlspec-0.16.0.dist-info/METADATA +365 -0
  144. sqlspec-0.16.0.dist-info/RECORD +148 -0
  145. sqlspec-0.16.0.dist-info/WHEEL +4 -0
  146. sqlspec-0.16.0.dist-info/entry_points.txt +2 -0
  147. sqlspec-0.16.0.dist-info/licenses/LICENSE +21 -0
  148. sqlspec-0.16.0.dist-info/licenses/NOTICE +29 -0
@@ -0,0 +1,1383 @@
1
+ """DDL builders for SQLSpec: DROP, CREATE INDEX, TRUNCATE, etc."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Any, Optional, Union
5
+
6
+ from sqlglot import exp
7
+ from sqlglot.dialects.dialect import DialectType
8
+ from typing_extensions import Self
9
+
10
+ from sqlspec.builder._base import QueryBuilder, SafeQuery
11
+ from sqlspec.builder._ddl_utils import build_column_expression, build_constraint_expression
12
+ from sqlspec.core.result import SQLResult
13
+
14
+ if TYPE_CHECKING:
15
+ from sqlspec.builder._column import ColumnExpression
16
+ from sqlspec.core.statement import SQL, StatementConfig
17
+
18
+ __all__ = (
19
+ "AlterOperation",
20
+ "AlterTable",
21
+ "ColumnDefinition",
22
+ "CommentOn",
23
+ "ConstraintDefinition",
24
+ "CreateIndex",
25
+ "CreateMaterializedView",
26
+ "CreateSchema",
27
+ "CreateTable",
28
+ "CreateTableAsSelect",
29
+ "CreateView",
30
+ "DDLBuilder",
31
+ "DropIndex",
32
+ "DropSchema",
33
+ "DropTable",
34
+ "DropView",
35
+ "RenameTable",
36
+ "Truncate",
37
+ )
38
+
39
+
40
+ @dataclass
41
+ class DDLBuilder(QueryBuilder):
42
+ """Base class for DDL builders (CREATE, DROP, ALTER, etc)."""
43
+
44
+ dialect: DialectType = None
45
+ _expression: Optional[exp.Expression] = field(default=None, init=False, repr=False, compare=False, hash=False)
46
+
47
+ def __post_init__(self) -> None:
48
+ pass
49
+
50
+ def _create_base_expression(self) -> exp.Expression:
51
+ msg = "Subclasses must implement _create_base_expression."
52
+ raise NotImplementedError(msg)
53
+
54
+ @property
55
+ def _expected_result_type(self) -> "type[SQLResult]":
56
+ return SQLResult
57
+
58
+ def build(self) -> "SafeQuery":
59
+ if self._expression is None:
60
+ self._expression = self._create_base_expression()
61
+ return super().build()
62
+
63
+ def to_statement(self, config: "Optional[StatementConfig]" = None) -> "SQL":
64
+ return super().to_statement(config=config)
65
+
66
+
67
+ # --- Data Structures for CREATE TABLE ---
68
+ @dataclass
69
+ class ColumnDefinition:
70
+ """Column definition for CREATE TABLE."""
71
+
72
+ name: str
73
+ dtype: str
74
+ default: "Optional[Any]" = None
75
+ not_null: bool = False
76
+ primary_key: bool = False
77
+ unique: bool = False
78
+ auto_increment: bool = False
79
+ comment: "Optional[str]" = None
80
+ check: "Optional[str]" = None
81
+ generated: "Optional[str]" = None # For computed columns
82
+ collate: "Optional[str]" = None
83
+
84
+
85
+ @dataclass
86
+ class ConstraintDefinition:
87
+ """Constraint definition for CREATE TABLE."""
88
+
89
+ constraint_type: str # 'PRIMARY KEY', 'FOREIGN KEY', 'UNIQUE', 'CHECK'
90
+ name: "Optional[str]" = None
91
+ columns: "list[str]" = field(default_factory=list)
92
+ references_table: "Optional[str]" = None
93
+ references_columns: "list[str]" = field(default_factory=list)
94
+ condition: "Optional[str]" = None
95
+ on_delete: "Optional[str]" = None
96
+ on_update: "Optional[str]" = None
97
+ deferrable: bool = False
98
+ initially_deferred: bool = False
99
+
100
+
101
+ # --- CREATE TABLE ---
102
+ @dataclass
103
+ class CreateTable(DDLBuilder):
104
+ """Builder for CREATE TABLE statements with columns and constraints.
105
+
106
+ Example:
107
+ builder = (
108
+ CreateTable("users")
109
+ .column("id", "SERIAL", primary_key=True)
110
+ .column("email", "VARCHAR(255)", not_null=True, unique=True)
111
+ .column("created_at", "TIMESTAMP", default="CURRENT_TIMESTAMP")
112
+ .foreign_key_constraint("org_id", "organizations", "id")
113
+ )
114
+ sql = builder.build().sql
115
+ """
116
+
117
+ _table_name: str = field(default="", init=False)
118
+ _if_not_exists: bool = False
119
+ _temporary: bool = False
120
+ _columns: "list[ColumnDefinition]" = field(default_factory=list)
121
+ _constraints: "list[ConstraintDefinition]" = field(default_factory=list)
122
+ _table_options: "dict[str, Any]" = field(default_factory=dict)
123
+ _schema: "Optional[str]" = None
124
+ _tablespace: "Optional[str]" = None
125
+ _like_table: "Optional[str]" = None
126
+ _partition_by: "Optional[str]" = None
127
+
128
+ def __init__(self, table_name: str) -> None:
129
+ super().__init__()
130
+ self._table_name = table_name
131
+
132
+ def in_schema(self, schema_name: str) -> "Self":
133
+ """Set the schema for the table."""
134
+ self._schema = schema_name
135
+ return self
136
+
137
+ def if_not_exists(self) -> "Self":
138
+ """Add IF NOT EXISTS clause."""
139
+ self._if_not_exists = True
140
+ return self
141
+
142
+ def temporary(self) -> "Self":
143
+ """Create a temporary table."""
144
+ self._temporary = True
145
+ return self
146
+
147
+ def like(self, source_table: str) -> "Self":
148
+ """Create table LIKE another table."""
149
+ self._like_table = source_table
150
+ return self
151
+
152
+ def tablespace(self, name: str) -> "Self":
153
+ """Set tablespace for the table."""
154
+ self._tablespace = name
155
+ return self
156
+
157
+ def partition_by(self, partition_spec: str) -> "Self":
158
+ """Set partitioning specification."""
159
+ self._partition_by = partition_spec
160
+ return self
161
+
162
+ def column(
163
+ self,
164
+ name: str,
165
+ dtype: str,
166
+ default: "Optional[Any]" = None,
167
+ not_null: bool = False,
168
+ primary_key: bool = False,
169
+ unique: bool = False,
170
+ auto_increment: bool = False,
171
+ comment: "Optional[str]" = None,
172
+ check: "Optional[str]" = None,
173
+ generated: "Optional[str]" = None,
174
+ collate: "Optional[str]" = None,
175
+ ) -> "Self":
176
+ """Add a column definition to the table."""
177
+ if not name:
178
+ self._raise_sql_builder_error("Column name must be a non-empty string")
179
+
180
+ if not dtype:
181
+ self._raise_sql_builder_error("Column type must be a non-empty string")
182
+
183
+ if any(col.name == name for col in self._columns):
184
+ self._raise_sql_builder_error(f"Column '{name}' already defined")
185
+
186
+ column_def = ColumnDefinition(
187
+ name=name,
188
+ dtype=dtype,
189
+ default=default,
190
+ not_null=not_null,
191
+ primary_key=primary_key,
192
+ unique=unique,
193
+ auto_increment=auto_increment,
194
+ comment=comment,
195
+ check=check,
196
+ generated=generated,
197
+ collate=collate,
198
+ )
199
+
200
+ self._columns.append(column_def)
201
+
202
+ # If primary key is specified on column, also add a constraint
203
+ if primary_key and not any(c.constraint_type == "PRIMARY KEY" for c in self._constraints):
204
+ self.primary_key_constraint([name])
205
+
206
+ return self
207
+
208
+ def primary_key_constraint(self, columns: "Union[str, list[str]]", name: "Optional[str]" = None) -> "Self":
209
+ """Add a primary key constraint."""
210
+ col_list = [columns] if isinstance(columns, str) else list(columns)
211
+
212
+ if not col_list:
213
+ self._raise_sql_builder_error("Primary key must include at least one column")
214
+
215
+ existing_pk = next((c for c in self._constraints if c.constraint_type == "PRIMARY KEY"), None)
216
+ if existing_pk:
217
+ for col in col_list:
218
+ if col not in existing_pk.columns:
219
+ existing_pk.columns.append(col)
220
+ else:
221
+ constraint = ConstraintDefinition(constraint_type="PRIMARY KEY", name=name, columns=col_list)
222
+ self._constraints.append(constraint)
223
+
224
+ return self
225
+
226
+ def foreign_key_constraint(
227
+ self,
228
+ columns: "Union[str, list[str]]",
229
+ references_table: str,
230
+ references_columns: "Union[str, list[str]]",
231
+ name: "Optional[str]" = None,
232
+ on_delete: "Optional[str]" = None,
233
+ on_update: "Optional[str]" = None,
234
+ deferrable: bool = False,
235
+ initially_deferred: bool = False,
236
+ ) -> "Self":
237
+ """Add a foreign key constraint."""
238
+ # Normalize inputs
239
+ col_list = [columns] if isinstance(columns, str) else list(columns)
240
+
241
+ ref_col_list = [references_columns] if isinstance(references_columns, str) else list(references_columns)
242
+
243
+ # Validation
244
+ if len(col_list) != len(ref_col_list):
245
+ self._raise_sql_builder_error("Foreign key columns and referenced columns must have same length")
246
+
247
+ valid_actions = {"CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION", None}
248
+ if on_delete and on_delete.upper() not in valid_actions:
249
+ self._raise_sql_builder_error(f"Invalid ON DELETE action: {on_delete}")
250
+ if on_update and on_update.upper() not in valid_actions:
251
+ self._raise_sql_builder_error(f"Invalid ON UPDATE action: {on_update}")
252
+
253
+ constraint = ConstraintDefinition(
254
+ constraint_type="FOREIGN KEY",
255
+ name=name,
256
+ columns=col_list,
257
+ references_table=references_table,
258
+ references_columns=ref_col_list,
259
+ on_delete=on_delete.upper() if on_delete else None,
260
+ on_update=on_update.upper() if on_update else None,
261
+ deferrable=deferrable,
262
+ initially_deferred=initially_deferred,
263
+ )
264
+
265
+ self._constraints.append(constraint)
266
+ return self
267
+
268
+ def unique_constraint(self, columns: "Union[str, list[str]]", name: "Optional[str]" = None) -> "Self":
269
+ """Add a unique constraint."""
270
+ # Normalize column list
271
+ col_list = [columns] if isinstance(columns, str) else list(columns)
272
+
273
+ if not col_list:
274
+ self._raise_sql_builder_error("Unique constraint must include at least one column")
275
+
276
+ constraint = ConstraintDefinition(constraint_type="UNIQUE", name=name, columns=col_list)
277
+
278
+ self._constraints.append(constraint)
279
+ return self
280
+
281
+ def check_constraint(self, condition: Union[str, "ColumnExpression"], name: "Optional[str]" = None) -> "Self":
282
+ """Add a check constraint."""
283
+ if not condition:
284
+ self._raise_sql_builder_error("Check constraint must have a condition")
285
+
286
+ condition_str: str
287
+ if hasattr(condition, "sqlglot_expression"):
288
+ # This is a ColumnExpression - render as raw SQL for DDL (no parameters)
289
+ sqlglot_expr = getattr(condition, "sqlglot_expression", None)
290
+ condition_str = sqlglot_expr.sql(dialect=self.dialect) if sqlglot_expr else str(condition)
291
+ else:
292
+ # String condition - use as-is
293
+ condition_str = str(condition)
294
+
295
+ constraint = ConstraintDefinition(constraint_type="CHECK", name=name, condition=condition_str)
296
+
297
+ self._constraints.append(constraint)
298
+ return self
299
+
300
+ def engine(self, engine_name: str) -> "Self":
301
+ """Set storage engine (MySQL/MariaDB)."""
302
+ self._table_options["engine"] = engine_name
303
+ return self
304
+
305
+ def charset(self, charset_name: str) -> "Self":
306
+ """Set character set."""
307
+ self._table_options["charset"] = charset_name
308
+ return self
309
+
310
+ def collate(self, collation: str) -> "Self":
311
+ """Set table collation."""
312
+ self._table_options["collate"] = collation
313
+ return self
314
+
315
+ def comment(self, comment_text: str) -> "Self":
316
+ """Set table comment."""
317
+ self._table_options["comment"] = comment_text
318
+ return self
319
+
320
+ def with_option(self, key: str, value: "Any") -> "Self":
321
+ """Add custom table option."""
322
+ self._table_options[key] = value
323
+ return self
324
+
325
+ def _create_base_expression(self) -> "exp.Expression":
326
+ """Create the SQLGlot expression for CREATE TABLE."""
327
+ if not self._columns and not self._like_table:
328
+ self._raise_sql_builder_error("Table must have at least one column or use LIKE clause")
329
+
330
+ if self._schema:
331
+ table = exp.Table(this=exp.to_identifier(self._table_name), db=exp.to_identifier(self._schema))
332
+ else:
333
+ table = exp.to_table(self._table_name)
334
+
335
+ column_defs: list[exp.Expression] = []
336
+ for col in self._columns:
337
+ col_expr = build_column_expression(col)
338
+ column_defs.append(col_expr)
339
+
340
+ for constraint in self._constraints:
341
+ # Skip PRIMARY KEY constraints that are already defined on columns
342
+ if constraint.constraint_type == "PRIMARY KEY" and len(constraint.columns) == 1:
343
+ col_name = constraint.columns[0]
344
+ if any(c.name == col_name and c.primary_key for c in self._columns):
345
+ continue
346
+
347
+ constraint_expr = build_constraint_expression(constraint)
348
+ if constraint_expr:
349
+ column_defs.append(constraint_expr)
350
+
351
+ props: list[exp.Property] = []
352
+ if self._table_options.get("engine"):
353
+ props.append(
354
+ exp.Property(
355
+ this=exp.to_identifier("ENGINE"), value=exp.to_identifier(self._table_options.get("engine"))
356
+ )
357
+ )
358
+ if self._tablespace:
359
+ props.append(exp.Property(this=exp.to_identifier("TABLESPACE"), value=exp.to_identifier(self._tablespace)))
360
+ if self._partition_by:
361
+ props.append(exp.Property(this=exp.to_identifier("PARTITION BY"), value=exp.convert(self._partition_by)))
362
+
363
+ for key, value in self._table_options.items():
364
+ if key != "engine": # Skip already handled options
365
+ props.append(exp.Property(this=exp.to_identifier(key.upper()), value=exp.convert(value)))
366
+
367
+ properties_node = exp.Properties(expressions=props) if props else None
368
+
369
+ schema_expr = exp.Schema(expressions=column_defs) if column_defs else None
370
+
371
+ like_expr = None
372
+ if self._like_table:
373
+ like_expr = exp.to_table(self._like_table)
374
+
375
+ return exp.Create(
376
+ kind="TABLE",
377
+ this=table,
378
+ exists=self._if_not_exists,
379
+ temporary=self._temporary,
380
+ expression=schema_expr,
381
+ properties=properties_node,
382
+ like=like_expr,
383
+ )
384
+
385
+ @staticmethod
386
+ def _build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
387
+ """Build SQLGlot expression for a column definition."""
388
+ return build_column_expression(col)
389
+
390
+ @staticmethod
391
+ def _build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional[exp.Expression]":
392
+ """Build SQLGlot expression for a table constraint."""
393
+ return build_constraint_expression(constraint)
394
+
395
+
396
+ # --- DROP TABLE ---
397
+ @dataclass
398
+ class DropTable(DDLBuilder):
399
+ """Builder for DROP TABLE [IF EXISTS] ... [CASCADE|RESTRICT]."""
400
+
401
+ _table_name: Optional[str] = None
402
+ _if_exists: bool = False
403
+ _cascade: Optional[bool] = None # True: CASCADE, False: RESTRICT, None: not set
404
+
405
+ def __init__(self, table_name: str, **kwargs: Any) -> None:
406
+ """Initialize DROP TABLE with table name.
407
+
408
+ Args:
409
+ table_name: Name of the table to drop
410
+ **kwargs: Additional DDLBuilder arguments
411
+ """
412
+ super().__init__(**kwargs)
413
+ self._table_name = table_name
414
+
415
+ def table(self, name: str) -> Self:
416
+ self._table_name = name
417
+ return self
418
+
419
+ def if_exists(self) -> Self:
420
+ self._if_exists = True
421
+ return self
422
+
423
+ def cascade(self) -> Self:
424
+ self._cascade = True
425
+ return self
426
+
427
+ def restrict(self) -> Self:
428
+ self._cascade = False
429
+ return self
430
+
431
+ def _create_base_expression(self) -> exp.Expression:
432
+ if not self._table_name:
433
+ self._raise_sql_builder_error("Table name must be set for DROP TABLE.")
434
+ return exp.Drop(
435
+ kind="TABLE", this=exp.to_table(self._table_name), exists=self._if_exists, cascade=self._cascade
436
+ )
437
+
438
+
439
+ # --- DROP INDEX ---
440
+ @dataclass
441
+ class DropIndex(DDLBuilder):
442
+ """Builder for DROP INDEX [IF EXISTS] ... [ON table] [CASCADE|RESTRICT]."""
443
+
444
+ _index_name: Optional[str] = None
445
+ _table_name: Optional[str] = None
446
+ _if_exists: bool = False
447
+ _cascade: Optional[bool] = None
448
+
449
+ def __init__(self, index_name: str, **kwargs: Any) -> None:
450
+ """Initialize DROP INDEX with index name.
451
+
452
+ Args:
453
+ index_name: Name of the index to drop
454
+ **kwargs: Additional DDLBuilder arguments
455
+ """
456
+ super().__init__(**kwargs)
457
+ self._index_name = index_name
458
+
459
+ def name(self, index_name: str) -> Self:
460
+ self._index_name = index_name
461
+ return self
462
+
463
+ def on_table(self, table_name: str) -> Self:
464
+ self._table_name = table_name
465
+ return self
466
+
467
+ def if_exists(self) -> Self:
468
+ self._if_exists = True
469
+ return self
470
+
471
+ def cascade(self) -> Self:
472
+ self._cascade = True
473
+ return self
474
+
475
+ def restrict(self) -> Self:
476
+ self._cascade = False
477
+ return self
478
+
479
+ def _create_base_expression(self) -> exp.Expression:
480
+ if not self._index_name:
481
+ self._raise_sql_builder_error("Index name must be set for DROP INDEX.")
482
+ return exp.Drop(
483
+ kind="INDEX",
484
+ this=exp.to_identifier(self._index_name),
485
+ table=exp.to_table(self._table_name) if self._table_name else None,
486
+ exists=self._if_exists,
487
+ cascade=self._cascade,
488
+ )
489
+
490
+
491
+ # --- DROP VIEW ---
492
+ @dataclass
493
+ class DropView(DDLBuilder):
494
+ """Builder for DROP VIEW [IF EXISTS] ... [CASCADE|RESTRICT]."""
495
+
496
+ _view_name: Optional[str] = None
497
+ _if_exists: bool = False
498
+ _cascade: Optional[bool] = None
499
+
500
+ def name(self, view_name: str) -> Self:
501
+ self._view_name = view_name
502
+ return self
503
+
504
+ def if_exists(self) -> Self:
505
+ self._if_exists = True
506
+ return self
507
+
508
+ def cascade(self) -> Self:
509
+ self._cascade = True
510
+ return self
511
+
512
+ def restrict(self) -> Self:
513
+ self._cascade = False
514
+ return self
515
+
516
+ def _create_base_expression(self) -> exp.Expression:
517
+ if not self._view_name:
518
+ self._raise_sql_builder_error("View name must be set for DROP VIEW.")
519
+ return exp.Drop(
520
+ kind="VIEW", this=exp.to_identifier(self._view_name), exists=self._if_exists, cascade=self._cascade
521
+ )
522
+
523
+
524
+ # --- DROP SCHEMA ---
525
+ @dataclass
526
+ class DropSchema(DDLBuilder):
527
+ """Builder for DROP SCHEMA [IF EXISTS] ... [CASCADE|RESTRICT]."""
528
+
529
+ _schema_name: Optional[str] = None
530
+ _if_exists: bool = False
531
+ _cascade: Optional[bool] = None
532
+
533
+ def name(self, schema_name: str) -> Self:
534
+ self._schema_name = schema_name
535
+ return self
536
+
537
+ def if_exists(self) -> Self:
538
+ self._if_exists = True
539
+ return self
540
+
541
+ def cascade(self) -> Self:
542
+ self._cascade = True
543
+ return self
544
+
545
+ def restrict(self) -> Self:
546
+ self._cascade = False
547
+ return self
548
+
549
+ def _create_base_expression(self) -> exp.Expression:
550
+ if not self._schema_name:
551
+ self._raise_sql_builder_error("Schema name must be set for DROP SCHEMA.")
552
+ return exp.Drop(
553
+ kind="SCHEMA", this=exp.to_identifier(self._schema_name), exists=self._if_exists, cascade=self._cascade
554
+ )
555
+
556
+
557
+ # --- CREATE INDEX ---
558
+ @dataclass
559
+ class CreateIndex(DDLBuilder):
560
+ """Builder for CREATE [UNIQUE] INDEX [IF NOT EXISTS] ... ON ... (...).
561
+
562
+ Supports columns, expressions, ordering, using, and where.
563
+ """
564
+
565
+ _index_name: Optional[str] = None
566
+ _table_name: Optional[str] = None
567
+ _columns: list[Union[str, exp.Ordered, exp.Expression]] = field(default_factory=list)
568
+ _unique: bool = False
569
+ _if_not_exists: bool = False
570
+ _using: Optional[str] = None
571
+ _where: Optional[Union[str, exp.Expression]] = None
572
+
573
+ def __init__(self, index_name: str, **kwargs: Any) -> None:
574
+ """Initialize CREATE INDEX with index name.
575
+
576
+ Args:
577
+ index_name: Name of the index to create
578
+ **kwargs: Additional DDLBuilder arguments
579
+ """
580
+ super().__init__(**kwargs)
581
+ self._index_name = index_name
582
+ # Initialize dataclass fields that may not be set by super().__init__
583
+ if not hasattr(self, "_columns"):
584
+ self._columns = []
585
+
586
+ def name(self, index_name: str) -> Self:
587
+ self._index_name = index_name
588
+ return self
589
+
590
+ def on_table(self, table_name: str) -> Self:
591
+ self._table_name = table_name
592
+ return self
593
+
594
+ def columns(self, *cols: Union[str, exp.Ordered, exp.Expression]) -> Self:
595
+ self._columns.extend(cols)
596
+ return self
597
+
598
+ def expressions(self, *exprs: Union[str, exp.Expression]) -> Self:
599
+ self._columns.extend(exprs)
600
+ return self
601
+
602
+ def unique(self) -> Self:
603
+ self._unique = True
604
+ return self
605
+
606
+ def if_not_exists(self) -> Self:
607
+ self._if_not_exists = True
608
+ return self
609
+
610
+ def using(self, method: str) -> Self:
611
+ self._using = method
612
+ return self
613
+
614
+ def where(self, condition: Union[str, exp.Expression]) -> Self:
615
+ self._where = condition
616
+ return self
617
+
618
+ def _create_base_expression(self) -> exp.Expression:
619
+ if not self._index_name or not self._table_name:
620
+ self._raise_sql_builder_error("Index name and table name must be set for CREATE INDEX.")
621
+ exprs: list[exp.Expression] = []
622
+ for col in self._columns:
623
+ if isinstance(col, str):
624
+ exprs.append(exp.column(col))
625
+ else:
626
+ exprs.append(col)
627
+ where_expr = None
628
+ if self._where:
629
+ where_expr = exp.condition(self._where) if isinstance(self._where, str) else self._where
630
+ # Use exp.Create for CREATE INDEX
631
+ return exp.Create(
632
+ kind="INDEX",
633
+ this=exp.to_identifier(self._index_name),
634
+ table=exp.to_table(self._table_name),
635
+ expressions=exprs,
636
+ unique=self._unique,
637
+ exists=self._if_not_exists,
638
+ using=exp.to_identifier(self._using) if self._using else None,
639
+ where=where_expr,
640
+ )
641
+
642
+
643
+ # --- TRUNCATE TABLE ---
644
+ @dataclass
645
+ class Truncate(DDLBuilder):
646
+ """Builder for TRUNCATE TABLE ... [CASCADE|RESTRICT] [RESTART IDENTITY|CONTINUE IDENTITY]."""
647
+
648
+ _table_name: Optional[str] = None
649
+ _cascade: Optional[bool] = None
650
+ _identity: Optional[str] = None # "RESTART" or "CONTINUE"
651
+
652
+ def table(self, name: str) -> Self:
653
+ self._table_name = name
654
+ return self
655
+
656
+ def cascade(self) -> Self:
657
+ self._cascade = True
658
+ return self
659
+
660
+ def restrict(self) -> Self:
661
+ self._cascade = False
662
+ return self
663
+
664
+ def restart_identity(self) -> Self:
665
+ self._identity = "RESTART"
666
+ return self
667
+
668
+ def continue_identity(self) -> Self:
669
+ self._identity = "CONTINUE"
670
+ return self
671
+
672
+ def _create_base_expression(self) -> exp.Expression:
673
+ if not self._table_name:
674
+ self._raise_sql_builder_error("Table name must be set for TRUNCATE TABLE.")
675
+ identity_expr = exp.Var(this=self._identity) if self._identity else None
676
+ return exp.TruncateTable(this=exp.to_table(self._table_name), cascade=self._cascade, identity=identity_expr)
677
+
678
+
679
+ # --- ALTER TABLE ---
680
+ @dataclass
681
+ class AlterOperation:
682
+ """Represents a single ALTER TABLE operation."""
683
+
684
+ operation_type: str
685
+ column_name: "Optional[str]" = None
686
+ column_definition: "Optional[ColumnDefinition]" = None
687
+ constraint_name: "Optional[str]" = None
688
+ constraint_definition: "Optional[ConstraintDefinition]" = None
689
+ new_type: "Optional[str]" = None
690
+ new_name: "Optional[str]" = None
691
+ after_column: "Optional[str]" = None
692
+ first: bool = False
693
+ using_expression: "Optional[str]" = None
694
+
695
+
696
+ # --- CREATE SCHEMA ---
697
+ @dataclass
698
+ class CreateSchema(DDLBuilder):
699
+ """Builder for CREATE SCHEMA [IF NOT EXISTS] schema_name [AUTHORIZATION user_name]."""
700
+
701
+ _schema_name: Optional[str] = None
702
+ _if_not_exists: bool = False
703
+ _authorization: Optional[str] = None
704
+
705
+ def name(self, schema_name: str) -> Self:
706
+ self._schema_name = schema_name
707
+ return self
708
+
709
+ def if_not_exists(self) -> Self:
710
+ self._if_not_exists = True
711
+ return self
712
+
713
+ def authorization(self, user_name: str) -> Self:
714
+ self._authorization = user_name
715
+ return self
716
+
717
+ def _create_base_expression(self) -> exp.Expression:
718
+ if not self._schema_name:
719
+ self._raise_sql_builder_error("Schema name must be set for CREATE SCHEMA.")
720
+ props: list[exp.Property] = []
721
+ if self._authorization:
722
+ props.append(
723
+ exp.Property(this=exp.to_identifier("AUTHORIZATION"), value=exp.to_identifier(self._authorization))
724
+ )
725
+ properties_node = exp.Properties(expressions=props) if props else None
726
+ return exp.Create(
727
+ kind="SCHEMA",
728
+ this=exp.to_identifier(self._schema_name),
729
+ exists=self._if_not_exists,
730
+ properties=properties_node,
731
+ )
732
+
733
+
734
+ @dataclass
735
+ class CreateTableAsSelect(DDLBuilder):
736
+ """Builder for CREATE TABLE [IF NOT EXISTS] ... AS SELECT ... (CTAS).
737
+
738
+ Supports optional column list and parameterized SELECT sources.
739
+
740
+ Example:
741
+ builder = (
742
+ CreateTableAsSelectBuilder()
743
+ .name("my_table")
744
+ .if_not_exists()
745
+ .columns("id", "name")
746
+ .as_select(select_builder)
747
+ )
748
+ sql = builder.build().sql
749
+
750
+ Methods:
751
+ - name(table_name: str): Set the table name.
752
+ - if_not_exists(): Add IF NOT EXISTS.
753
+ - columns(*cols: str): Set explicit column list (optional).
754
+ - as_select(select_query): Set the SELECT source (SQL, SelectBuilder, or str).
755
+ """
756
+
757
+ _table_name: Optional[str] = None
758
+ _if_not_exists: bool = False
759
+ _columns: list[str] = field(default_factory=list)
760
+ _select_query: Optional[object] = None # SQL, SelectBuilder, or str
761
+
762
+ def name(self, table_name: str) -> Self:
763
+ self._table_name = table_name
764
+ return self
765
+
766
+ def if_not_exists(self) -> Self:
767
+ self._if_not_exists = True
768
+ return self
769
+
770
+ def columns(self, *cols: str) -> Self:
771
+ self._columns = list(cols)
772
+ return self
773
+
774
+ def as_select(self, select_query: object) -> Self:
775
+ self._select_query = select_query
776
+ return self
777
+
778
+ def _create_base_expression(self) -> exp.Expression:
779
+ if not self._table_name:
780
+ self._raise_sql_builder_error("Table name must be set for CREATE TABLE AS SELECT.")
781
+ if self._select_query is None:
782
+ self._raise_sql_builder_error("SELECT query must be set for CREATE TABLE AS SELECT.")
783
+
784
+ select_expr = None
785
+ select_parameters = None
786
+ from sqlspec.builder._select import Select
787
+ from sqlspec.core.statement import SQL
788
+
789
+ if isinstance(self._select_query, SQL):
790
+ select_expr = self._select_query.expression
791
+ select_parameters = getattr(self._select_query, "parameters", None)
792
+ elif isinstance(self._select_query, Select):
793
+ select_expr = getattr(self._select_query, "_expression", None)
794
+ select_parameters = getattr(self._select_query, "_parameters", None)
795
+
796
+ with_ctes = getattr(self._select_query, "_with_ctes", {})
797
+ if with_ctes and select_expr and isinstance(select_expr, exp.Select):
798
+ for alias, cte in with_ctes.items():
799
+ 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
+ )
805
+ elif isinstance(self._select_query, str):
806
+ select_expr = exp.maybe_parse(self._select_query)
807
+ select_parameters = None
808
+ else:
809
+ self._raise_sql_builder_error("Unsupported type for SELECT query in CTAS.")
810
+ if select_expr is None:
811
+ self._raise_sql_builder_error("SELECT query must be a valid SELECT expression.")
812
+
813
+ # Merge parameters from SELECT if present
814
+ if select_parameters:
815
+ 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
+ self._parameters[p_name] = p_value
819
+
820
+ schema_expr = None
821
+ if self._columns:
822
+ schema_expr = exp.Schema(expressions=[exp.column(c) for c in self._columns])
823
+
824
+ return exp.Create(
825
+ kind="TABLE",
826
+ this=exp.to_table(self._table_name),
827
+ exists=self._if_not_exists,
828
+ expression=select_expr,
829
+ schema=schema_expr,
830
+ )
831
+
832
+
833
+ @dataclass
834
+ class CreateMaterializedView(DDLBuilder):
835
+ """Builder for CREATE MATERIALIZED VIEW [IF NOT EXISTS] ... AS SELECT ...
836
+
837
+ Supports optional column list, parameterized SELECT sources, and dialect-specific options.
838
+ """
839
+
840
+ _view_name: Optional[str] = None
841
+ _if_not_exists: bool = False
842
+ _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
845
+ _refresh_mode: Optional[str] = None
846
+ _storage_parameters: dict[str, Any] = field(default_factory=dict)
847
+ _tablespace: Optional[str] = None
848
+ _using_index: Optional[str] = None
849
+ _hints: list[str] = field(default_factory=list)
850
+
851
+ def name(self, view_name: str) -> Self:
852
+ self._view_name = view_name
853
+ return self
854
+
855
+ def if_not_exists(self) -> Self:
856
+ self._if_not_exists = True
857
+ return self
858
+
859
+ def columns(self, *cols: str) -> Self:
860
+ self._columns = list(cols)
861
+ return self
862
+
863
+ def as_select(self, select_query: object) -> Self:
864
+ self._select_query = select_query
865
+ return self
866
+
867
+ def with_data(self) -> Self:
868
+ self._with_data = True
869
+ return self
870
+
871
+ def no_data(self) -> Self:
872
+ self._with_data = False
873
+ return self
874
+
875
+ def refresh_mode(self, mode: str) -> Self:
876
+ self._refresh_mode = mode
877
+ return self
878
+
879
+ def storage_parameter(self, key: str, value: Any) -> Self:
880
+ self._storage_parameters[key] = value
881
+ return self
882
+
883
+ def tablespace(self, name: str) -> Self:
884
+ self._tablespace = name
885
+ return self
886
+
887
+ def using_index(self, index_name: str) -> Self:
888
+ self._using_index = index_name
889
+ return self
890
+
891
+ def with_hint(self, hint: str) -> Self:
892
+ self._hints.append(hint)
893
+ return self
894
+
895
+ def _create_base_expression(self) -> exp.Expression:
896
+ if not self._view_name:
897
+ self._raise_sql_builder_error("View name must be set for CREATE MATERIALIZED VIEW.")
898
+ if self._select_query is None:
899
+ self._raise_sql_builder_error("SELECT query must be set for CREATE MATERIALIZED VIEW.")
900
+
901
+ select_expr = None
902
+ select_parameters = None
903
+ from sqlspec.builder._select import Select
904
+ from sqlspec.core.statement import SQL
905
+
906
+ if isinstance(self._select_query, SQL):
907
+ select_expr = self._select_query.expression
908
+ select_parameters = getattr(self._select_query, "parameters", None)
909
+ elif isinstance(self._select_query, Select):
910
+ select_expr = getattr(self._select_query, "_expression", None)
911
+ select_parameters = getattr(self._select_query, "_parameters", None)
912
+ elif isinstance(self._select_query, str):
913
+ select_expr = exp.maybe_parse(self._select_query)
914
+ select_parameters = None
915
+ else:
916
+ self._raise_sql_builder_error("Unsupported type for SELECT query in materialized view.")
917
+ if select_expr is None or not isinstance(select_expr, exp.Select):
918
+ self._raise_sql_builder_error("SELECT query must be a valid SELECT expression.")
919
+
920
+ # Merge parameters from SELECT if present
921
+ if select_parameters:
922
+ 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
+ self._parameters[p_name] = p_value
926
+
927
+ schema_expr = None
928
+ if self._columns:
929
+ schema_expr = exp.Schema(expressions=[exp.column(c) for c in self._columns])
930
+
931
+ props: list[exp.Property] = []
932
+ if self._refresh_mode:
933
+ props.append(exp.Property(this=exp.to_identifier("REFRESH_MODE"), value=exp.convert(self._refresh_mode)))
934
+ if self._tablespace:
935
+ props.append(exp.Property(this=exp.to_identifier("TABLESPACE"), value=exp.to_identifier(self._tablespace)))
936
+ if self._using_index:
937
+ props.append(
938
+ exp.Property(this=exp.to_identifier("USING_INDEX"), value=exp.to_identifier(self._using_index))
939
+ )
940
+ for k, v in self._storage_parameters.items():
941
+ props.append(exp.Property(this=exp.to_identifier(k), value=exp.convert(str(v))))
942
+ if self._with_data is not None:
943
+ props.append(exp.Property(this=exp.to_identifier("WITH_DATA" if self._with_data else "NO_DATA")))
944
+ props.extend(exp.Property(this=exp.to_identifier("HINT"), value=exp.convert(hint)) for hint in self._hints)
945
+ properties_node = exp.Properties(expressions=props) if props else None
946
+
947
+ return exp.Create(
948
+ kind="MATERIALIZED_VIEW",
949
+ this=exp.to_identifier(self._view_name),
950
+ exists=self._if_not_exists,
951
+ expression=select_expr,
952
+ schema=schema_expr,
953
+ properties=properties_node,
954
+ )
955
+
956
+
957
+ @dataclass
958
+ class CreateView(DDLBuilder):
959
+ """Builder for CREATE VIEW [IF NOT EXISTS] ... AS SELECT ...
960
+
961
+ Supports optional column list, parameterized SELECT sources, and hints.
962
+ """
963
+
964
+ _view_name: Optional[str] = None
965
+ _if_not_exists: bool = False
966
+ _columns: list[str] = field(default_factory=list)
967
+ _select_query: Optional[object] = None # SQL, SelectBuilder, or str
968
+ _hints: list[str] = field(default_factory=list)
969
+
970
+ def name(self, view_name: str) -> Self:
971
+ self._view_name = view_name
972
+ return self
973
+
974
+ def if_not_exists(self) -> Self:
975
+ self._if_not_exists = True
976
+ return self
977
+
978
+ def columns(self, *cols: str) -> Self:
979
+ self._columns = list(cols)
980
+ return self
981
+
982
+ def as_select(self, select_query: object) -> Self:
983
+ self._select_query = select_query
984
+ return self
985
+
986
+ def with_hint(self, hint: str) -> Self:
987
+ self._hints.append(hint)
988
+ return self
989
+
990
+ def _create_base_expression(self) -> exp.Expression:
991
+ if not self._view_name:
992
+ self._raise_sql_builder_error("View name must be set for CREATE VIEW.")
993
+ if self._select_query is None:
994
+ self._raise_sql_builder_error("SELECT query must be set for CREATE VIEW.")
995
+
996
+ select_expr = None
997
+ select_parameters = None
998
+ from sqlspec.builder._select import Select
999
+ from sqlspec.core.statement import SQL
1000
+
1001
+ if isinstance(self._select_query, SQL):
1002
+ select_expr = self._select_query.expression
1003
+ select_parameters = getattr(self._select_query, "parameters", None)
1004
+ elif isinstance(self._select_query, Select):
1005
+ select_expr = getattr(self._select_query, "_expression", None)
1006
+ select_parameters = getattr(self._select_query, "_parameters", None)
1007
+ elif isinstance(self._select_query, str):
1008
+ select_expr = exp.maybe_parse(self._select_query)
1009
+ select_parameters = None
1010
+ else:
1011
+ self._raise_sql_builder_error("Unsupported type for SELECT query in view.")
1012
+ if select_expr is None or not isinstance(select_expr, exp.Select):
1013
+ self._raise_sql_builder_error("SELECT query must be a valid SELECT expression.")
1014
+
1015
+ # Merge parameters from SELECT if present
1016
+ if select_parameters:
1017
+ 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
+ self._parameters[p_name] = p_value
1021
+
1022
+ schema_expr = None
1023
+ if self._columns:
1024
+ schema_expr = exp.Schema(expressions=[exp.column(c) for c in self._columns])
1025
+
1026
+ props: list[exp.Property] = [
1027
+ exp.Property(this=exp.to_identifier("HINT"), value=exp.convert(h)) for h in self._hints
1028
+ ]
1029
+ properties_node = exp.Properties(expressions=props) if props else None
1030
+
1031
+ return exp.Create(
1032
+ kind="VIEW",
1033
+ this=exp.to_identifier(self._view_name),
1034
+ exists=self._if_not_exists,
1035
+ expression=select_expr,
1036
+ schema=schema_expr,
1037
+ properties=properties_node,
1038
+ )
1039
+
1040
+
1041
+ @dataclass
1042
+ class AlterTable(DDLBuilder):
1043
+ """Builder for ALTER TABLE with granular operations.
1044
+
1045
+ Supports column operations (add, drop, alter type, rename) and constraint operations.
1046
+
1047
+ Example:
1048
+ builder = (
1049
+ AlterTableBuilder("users")
1050
+ .add_column("email", "VARCHAR(255)", not_null=True)
1051
+ .drop_column("old_field")
1052
+ .add_constraint("check_age", "CHECK (age >= 18)")
1053
+ )
1054
+ """
1055
+
1056
+ _table_name: str = field(default="", init=False)
1057
+ _operations: "list[AlterOperation]" = field(default_factory=list)
1058
+ _schema: "Optional[str]" = None
1059
+ _if_exists: bool = False
1060
+
1061
+ def __init__(self, table_name: str) -> None:
1062
+ super().__init__()
1063
+ self._table_name = table_name
1064
+ self._operations = []
1065
+ self._schema = None
1066
+ self._if_exists = False
1067
+
1068
+ def if_exists(self) -> "Self":
1069
+ """Add IF EXISTS clause."""
1070
+ self._if_exists = True
1071
+ return self
1072
+
1073
+ def add_column(
1074
+ self,
1075
+ name: str,
1076
+ dtype: str,
1077
+ default: "Optional[Any]" = None,
1078
+ not_null: bool = False,
1079
+ unique: bool = False,
1080
+ comment: "Optional[str]" = None,
1081
+ after: "Optional[str]" = None,
1082
+ first: bool = False,
1083
+ ) -> "Self":
1084
+ """Add a new column to the table."""
1085
+ if not name:
1086
+ self._raise_sql_builder_error("Column name must be a non-empty string")
1087
+
1088
+ if not dtype:
1089
+ self._raise_sql_builder_error("Column type must be a non-empty string")
1090
+
1091
+ column_def = ColumnDefinition(
1092
+ name=name, dtype=dtype, default=default, not_null=not_null, unique=unique, comment=comment
1093
+ )
1094
+
1095
+ operation = AlterOperation(
1096
+ operation_type="ADD COLUMN", column_definition=column_def, after_column=after, first=first
1097
+ )
1098
+
1099
+ self._operations.append(operation)
1100
+ return self
1101
+
1102
+ def drop_column(self, name: str, cascade: bool = False) -> "Self":
1103
+ """Drop a column from the table."""
1104
+ if not name:
1105
+ self._raise_sql_builder_error("Column name must be a non-empty string")
1106
+
1107
+ operation = AlterOperation(operation_type="DROP COLUMN CASCADE" if cascade else "DROP COLUMN", column_name=name)
1108
+
1109
+ self._operations.append(operation)
1110
+ return self
1111
+
1112
+ def alter_column_type(self, name: str, new_type: str, using: "Optional[str]" = None) -> "Self":
1113
+ """Change the type of an existing column."""
1114
+ if not name:
1115
+ self._raise_sql_builder_error("Column name must be a non-empty string")
1116
+
1117
+ if not new_type:
1118
+ self._raise_sql_builder_error("New type must be a non-empty string")
1119
+
1120
+ operation = AlterOperation(
1121
+ operation_type="ALTER COLUMN TYPE", column_name=name, new_type=new_type, using_expression=using
1122
+ )
1123
+
1124
+ self._operations.append(operation)
1125
+ return self
1126
+
1127
+ def rename_column(self, old_name: str, new_name: str) -> "Self":
1128
+ """Rename a column."""
1129
+ if not old_name:
1130
+ self._raise_sql_builder_error("Old column name must be a non-empty string")
1131
+
1132
+ if not new_name:
1133
+ self._raise_sql_builder_error("New column name must be a non-empty string")
1134
+
1135
+ operation = AlterOperation(operation_type="RENAME COLUMN", column_name=old_name, new_name=new_name)
1136
+
1137
+ self._operations.append(operation)
1138
+ return self
1139
+
1140
+ def add_constraint(
1141
+ self,
1142
+ constraint_type: str,
1143
+ columns: "Optional[Union[str, list[str]]]" = None,
1144
+ name: "Optional[str]" = None,
1145
+ references_table: "Optional[str]" = None,
1146
+ references_columns: "Optional[Union[str, list[str]]]" = None,
1147
+ condition: "Optional[Union[str, ColumnExpression]]" = None,
1148
+ on_delete: "Optional[str]" = None,
1149
+ on_update: "Optional[str]" = None,
1150
+ ) -> "Self":
1151
+ """Add a constraint to the table.
1152
+
1153
+ Args:
1154
+ constraint_type: Type of constraint ('PRIMARY KEY', 'FOREIGN KEY', 'UNIQUE', 'CHECK')
1155
+ columns: Column(s) for the constraint (not needed for CHECK)
1156
+ name: Optional constraint name
1157
+ references_table: Table referenced by foreign key
1158
+ references_columns: Columns referenced by foreign key
1159
+ condition: CHECK constraint condition
1160
+ on_delete: Foreign key ON DELETE action
1161
+ on_update: Foreign key ON UPDATE action
1162
+ """
1163
+ valid_types = {"PRIMARY KEY", "FOREIGN KEY", "UNIQUE", "CHECK"}
1164
+ if constraint_type.upper() not in valid_types:
1165
+ self._raise_sql_builder_error(f"Invalid constraint type: {constraint_type}")
1166
+
1167
+ # Normalize columns
1168
+ col_list = None
1169
+ if columns is not None:
1170
+ col_list = [columns] if isinstance(columns, str) else list(columns)
1171
+
1172
+ # Normalize reference columns
1173
+ ref_col_list = None
1174
+ if references_columns is not None:
1175
+ ref_col_list = [references_columns] if isinstance(references_columns, str) else list(references_columns)
1176
+
1177
+ # Handle ColumnExpression for CHECK constraints
1178
+ condition_str: Optional[str] = None
1179
+ if condition is not None:
1180
+ if hasattr(condition, "sqlglot_expression"):
1181
+ sqlglot_expr = getattr(condition, "sqlglot_expression", None)
1182
+ condition_str = sqlglot_expr.sql(dialect=self.dialect) if sqlglot_expr else str(condition)
1183
+ else:
1184
+ condition_str = str(condition)
1185
+
1186
+ constraint_def = ConstraintDefinition(
1187
+ constraint_type=constraint_type.upper(),
1188
+ name=name,
1189
+ columns=col_list or [],
1190
+ references_table=references_table,
1191
+ references_columns=ref_col_list or [],
1192
+ condition=condition_str,
1193
+ on_delete=on_delete,
1194
+ on_update=on_update,
1195
+ )
1196
+
1197
+ operation = AlterOperation(operation_type="ADD CONSTRAINT", constraint_definition=constraint_def)
1198
+
1199
+ self._operations.append(operation)
1200
+ return self
1201
+
1202
+ def drop_constraint(self, name: str, cascade: bool = False) -> "Self":
1203
+ """Drop a constraint from the table."""
1204
+ if not name:
1205
+ self._raise_sql_builder_error("Constraint name must be a non-empty string")
1206
+
1207
+ operation = AlterOperation(
1208
+ operation_type="DROP CONSTRAINT CASCADE" if cascade else "DROP CONSTRAINT", constraint_name=name
1209
+ )
1210
+
1211
+ self._operations.append(operation)
1212
+ return self
1213
+
1214
+ def set_not_null(self, column: str) -> "Self":
1215
+ """Set a column to NOT NULL."""
1216
+ operation = AlterOperation(operation_type="ALTER COLUMN SET NOT NULL", column_name=column)
1217
+
1218
+ self._operations.append(operation)
1219
+ return self
1220
+
1221
+ def drop_not_null(self, column: str) -> "Self":
1222
+ """Remove NOT NULL constraint from a column."""
1223
+ operation = AlterOperation(operation_type="ALTER COLUMN DROP NOT NULL", column_name=column)
1224
+
1225
+ self._operations.append(operation)
1226
+ return self
1227
+
1228
+ def _create_base_expression(self) -> "exp.Expression":
1229
+ """Create the SQLGlot expression for ALTER TABLE."""
1230
+ if not self._operations:
1231
+ self._raise_sql_builder_error("At least one operation must be specified for ALTER TABLE")
1232
+
1233
+ if self._schema:
1234
+ table = exp.Table(this=exp.to_identifier(self._table_name), db=exp.to_identifier(self._schema))
1235
+ else:
1236
+ table = exp.to_table(self._table_name)
1237
+
1238
+ actions: list[exp.Expression] = [self._build_operation_expression(op) for op in self._operations]
1239
+
1240
+ return exp.Alter(this=table, kind="TABLE", actions=actions, exists=self._if_exists)
1241
+
1242
+ def _build_operation_expression(self, op: "AlterOperation") -> exp.Expression:
1243
+ """Build a structured SQLGlot expression for a single alter operation."""
1244
+ op_type = op.operation_type.upper()
1245
+
1246
+ if op_type == "ADD COLUMN":
1247
+ if not op.column_definition:
1248
+ 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
+ return build_column_expression(op.column_definition)
1253
+
1254
+ if op_type == "DROP COLUMN":
1255
+ return exp.Drop(this=exp.to_identifier(op.column_name), kind="COLUMN", exists=True)
1256
+
1257
+ if op_type == "DROP COLUMN CASCADE":
1258
+ return exp.Drop(this=exp.to_identifier(op.column_name), kind="COLUMN", cascade=True, exists=True)
1259
+
1260
+ if op_type == "ALTER COLUMN TYPE":
1261
+ if not op.new_type:
1262
+ self._raise_sql_builder_error("New type required for ALTER COLUMN TYPE")
1263
+ return exp.AlterColumn(
1264
+ this=exp.to_identifier(op.column_name),
1265
+ dtype=exp.DataType.build(op.new_type),
1266
+ using=exp.maybe_parse(op.using_expression) if op.using_expression else None,
1267
+ )
1268
+
1269
+ if op_type == "RENAME COLUMN":
1270
+ return exp.RenameColumn(this=exp.to_identifier(op.column_name), to=exp.to_identifier(op.new_name))
1271
+
1272
+ if op_type == "ADD CONSTRAINT":
1273
+ if not op.constraint_definition:
1274
+ self._raise_sql_builder_error("Constraint definition required for ADD CONSTRAINT")
1275
+ constraint_expr = build_constraint_expression(op.constraint_definition)
1276
+ return exp.AddConstraint(this=constraint_expr)
1277
+
1278
+ if op_type == "DROP CONSTRAINT":
1279
+ return exp.Drop(this=exp.to_identifier(op.constraint_name), kind="CONSTRAINT", exists=True)
1280
+
1281
+ if op_type == "DROP CONSTRAINT CASCADE":
1282
+ return exp.Drop(this=exp.to_identifier(op.constraint_name), kind="CONSTRAINT", cascade=True, exists=True)
1283
+
1284
+ if op_type == "ALTER COLUMN SET NOT NULL":
1285
+ return exp.AlterColumn(this=exp.to_identifier(op.column_name), allow_null=False)
1286
+
1287
+ if op_type == "ALTER COLUMN DROP NOT NULL":
1288
+ return exp.AlterColumn(this=exp.to_identifier(op.column_name), drop=True, allow_null=True)
1289
+
1290
+ if op_type == "ALTER COLUMN SET DEFAULT":
1291
+ if not op.column_definition or op.column_definition.default is None:
1292
+ self._raise_sql_builder_error("Default value required for SET DEFAULT")
1293
+ default_val = op.column_definition.default
1294
+ default_expr: Optional[exp.Expression]
1295
+ if isinstance(default_val, str):
1296
+ if default_val.upper() in {"CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME"} or "(" in default_val:
1297
+ default_expr = exp.maybe_parse(default_val)
1298
+ else:
1299
+ default_expr = exp.convert(default_val)
1300
+ elif isinstance(default_val, (int, float)):
1301
+ default_expr = exp.convert(default_val)
1302
+ elif default_val is True:
1303
+ default_expr = exp.true()
1304
+ elif default_val is False:
1305
+ default_expr = exp.false()
1306
+ else:
1307
+ default_expr = exp.convert(str(default_val))
1308
+ return exp.AlterColumn(this=exp.to_identifier(op.column_name), default=default_expr)
1309
+
1310
+ if op_type == "ALTER COLUMN DROP DEFAULT":
1311
+ return exp.AlterColumn(this=exp.to_identifier(op.column_name), kind="DROP DEFAULT")
1312
+
1313
+ self._raise_sql_builder_error(f"Unknown operation type: {op.operation_type}")
1314
+ raise AssertionError # This line is unreachable but satisfies the linter
1315
+
1316
+
1317
+ @dataclass
1318
+ class CommentOn(DDLBuilder):
1319
+ """Builder for COMMENT ON ... IS ... statements.
1320
+
1321
+ Supports COMMENT ON TABLE and COMMENT ON COLUMN.
1322
+ """
1323
+
1324
+ _target_type: Optional[str] = None # 'TABLE' or 'COLUMN'
1325
+ _table: Optional[str] = None
1326
+ _column: Optional[str] = None
1327
+ _comment: Optional[str] = None
1328
+
1329
+ def on_table(self, table: str) -> Self:
1330
+ self._target_type = "TABLE"
1331
+ self._table = table
1332
+ self._column = None
1333
+ return self
1334
+
1335
+ def on_column(self, table: str, column: str) -> Self:
1336
+ self._target_type = "COLUMN"
1337
+ self._table = table
1338
+ self._column = column
1339
+ return self
1340
+
1341
+ def is_(self, comment: str) -> Self:
1342
+ self._comment = comment
1343
+ return self
1344
+
1345
+ def _create_base_expression(self) -> exp.Expression:
1346
+ if self._target_type == "TABLE" and self._table and self._comment is not None:
1347
+ return exp.Comment(this=exp.to_table(self._table), kind="TABLE", expression=exp.convert(self._comment))
1348
+ if self._target_type == "COLUMN" and self._table and self._column and self._comment is not None:
1349
+ return exp.Comment(
1350
+ this=exp.Column(table=self._table, this=self._column),
1351
+ kind="COLUMN",
1352
+ expression=exp.convert(self._comment),
1353
+ )
1354
+ 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
1356
+
1357
+
1358
+ @dataclass
1359
+ class RenameTable(DDLBuilder):
1360
+ """Builder for ALTER TABLE ... RENAME TO ... statements.
1361
+
1362
+ Supports renaming a table.
1363
+ """
1364
+
1365
+ _old_name: Optional[str] = None
1366
+ _new_name: Optional[str] = None
1367
+
1368
+ def table(self, old_name: str) -> Self:
1369
+ self._old_name = old_name
1370
+ return self
1371
+
1372
+ def to(self, new_name: str) -> Self:
1373
+ self._new_name = new_name
1374
+ return self
1375
+
1376
+ def _create_base_expression(self) -> exp.Expression:
1377
+ if not self._old_name or not self._new_name:
1378
+ self._raise_sql_builder_error("Both old and new table names must be set for RENAME TABLE.")
1379
+ return exp.Alter(
1380
+ this=exp.to_table(self._old_name),
1381
+ kind="TABLE",
1382
+ actions=[exp.AlterRename(this=exp.to_identifier(self._new_name))],
1383
+ )