sqlspec 0.11.0__py3-none-any.whl → 0.12.0__py3-none-any.whl

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

Potentially problematic release.


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

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