sqlspec 0.16.1__cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

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