sqlspec 0.16.0__cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

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

Potentially problematic release.


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

Files changed (148) hide show
  1. 51ff5a9eadfdefd49f98__mypyc.cpython-312-x86_64-linux-gnu.so +0 -0
  2. sqlspec/__init__.py +92 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +77 -0
  6. sqlspec/_sql.py +1347 -0
  7. sqlspec/_typing.py +680 -0
  8. sqlspec/adapters/__init__.py +0 -0
  9. sqlspec/adapters/adbc/__init__.py +5 -0
  10. sqlspec/adapters/adbc/_types.py +12 -0
  11. sqlspec/adapters/adbc/config.py +361 -0
  12. sqlspec/adapters/adbc/driver.py +512 -0
  13. sqlspec/adapters/aiosqlite/__init__.py +19 -0
  14. sqlspec/adapters/aiosqlite/_types.py +13 -0
  15. sqlspec/adapters/aiosqlite/config.py +253 -0
  16. sqlspec/adapters/aiosqlite/driver.py +248 -0
  17. sqlspec/adapters/asyncmy/__init__.py +19 -0
  18. sqlspec/adapters/asyncmy/_types.py +12 -0
  19. sqlspec/adapters/asyncmy/config.py +180 -0
  20. sqlspec/adapters/asyncmy/driver.py +274 -0
  21. sqlspec/adapters/asyncpg/__init__.py +21 -0
  22. sqlspec/adapters/asyncpg/_types.py +17 -0
  23. sqlspec/adapters/asyncpg/config.py +229 -0
  24. sqlspec/adapters/asyncpg/driver.py +344 -0
  25. sqlspec/adapters/bigquery/__init__.py +18 -0
  26. sqlspec/adapters/bigquery/_types.py +12 -0
  27. sqlspec/adapters/bigquery/config.py +298 -0
  28. sqlspec/adapters/bigquery/driver.py +558 -0
  29. sqlspec/adapters/duckdb/__init__.py +22 -0
  30. sqlspec/adapters/duckdb/_types.py +12 -0
  31. sqlspec/adapters/duckdb/config.py +504 -0
  32. sqlspec/adapters/duckdb/driver.py +368 -0
  33. sqlspec/adapters/oracledb/__init__.py +32 -0
  34. sqlspec/adapters/oracledb/_types.py +14 -0
  35. sqlspec/adapters/oracledb/config.py +317 -0
  36. sqlspec/adapters/oracledb/driver.py +538 -0
  37. sqlspec/adapters/psqlpy/__init__.py +16 -0
  38. sqlspec/adapters/psqlpy/_types.py +11 -0
  39. sqlspec/adapters/psqlpy/config.py +214 -0
  40. sqlspec/adapters/psqlpy/driver.py +530 -0
  41. sqlspec/adapters/psycopg/__init__.py +32 -0
  42. sqlspec/adapters/psycopg/_types.py +17 -0
  43. sqlspec/adapters/psycopg/config.py +426 -0
  44. sqlspec/adapters/psycopg/driver.py +796 -0
  45. sqlspec/adapters/sqlite/__init__.py +15 -0
  46. sqlspec/adapters/sqlite/_types.py +11 -0
  47. sqlspec/adapters/sqlite/config.py +240 -0
  48. sqlspec/adapters/sqlite/driver.py +294 -0
  49. sqlspec/base.py +571 -0
  50. sqlspec/builder/__init__.py +62 -0
  51. sqlspec/builder/_base.py +440 -0
  52. sqlspec/builder/_column.py +324 -0
  53. sqlspec/builder/_ddl.py +1383 -0
  54. sqlspec/builder/_ddl_utils.py +104 -0
  55. sqlspec/builder/_delete.py +77 -0
  56. sqlspec/builder/_insert.py +241 -0
  57. sqlspec/builder/_merge.py +56 -0
  58. sqlspec/builder/_parsing_utils.py +140 -0
  59. sqlspec/builder/_select.py +174 -0
  60. sqlspec/builder/_update.py +186 -0
  61. sqlspec/builder/mixins/__init__.py +55 -0
  62. sqlspec/builder/mixins/_cte_and_set_ops.py +195 -0
  63. sqlspec/builder/mixins/_delete_operations.py +36 -0
  64. sqlspec/builder/mixins/_insert_operations.py +152 -0
  65. sqlspec/builder/mixins/_join_operations.py +115 -0
  66. sqlspec/builder/mixins/_merge_operations.py +416 -0
  67. sqlspec/builder/mixins/_order_limit_operations.py +123 -0
  68. sqlspec/builder/mixins/_pivot_operations.py +144 -0
  69. sqlspec/builder/mixins/_select_operations.py +599 -0
  70. sqlspec/builder/mixins/_update_operations.py +164 -0
  71. sqlspec/builder/mixins/_where_clause.py +609 -0
  72. sqlspec/cli.py +247 -0
  73. sqlspec/config.py +395 -0
  74. sqlspec/core/__init__.py +63 -0
  75. sqlspec/core/cache.cpython-312-x86_64-linux-gnu.so +0 -0
  76. sqlspec/core/cache.py +873 -0
  77. sqlspec/core/compiler.cpython-312-x86_64-linux-gnu.so +0 -0
  78. sqlspec/core/compiler.py +396 -0
  79. sqlspec/core/filters.cpython-312-x86_64-linux-gnu.so +0 -0
  80. sqlspec/core/filters.py +830 -0
  81. sqlspec/core/hashing.cpython-312-x86_64-linux-gnu.so +0 -0
  82. sqlspec/core/hashing.py +310 -0
  83. sqlspec/core/parameters.cpython-312-x86_64-linux-gnu.so +0 -0
  84. sqlspec/core/parameters.py +1209 -0
  85. sqlspec/core/result.cpython-312-x86_64-linux-gnu.so +0 -0
  86. sqlspec/core/result.py +664 -0
  87. sqlspec/core/splitter.cpython-312-x86_64-linux-gnu.so +0 -0
  88. sqlspec/core/splitter.py +819 -0
  89. sqlspec/core/statement.cpython-312-x86_64-linux-gnu.so +0 -0
  90. sqlspec/core/statement.py +666 -0
  91. sqlspec/driver/__init__.py +19 -0
  92. sqlspec/driver/_async.py +472 -0
  93. sqlspec/driver/_common.py +612 -0
  94. sqlspec/driver/_sync.py +473 -0
  95. sqlspec/driver/mixins/__init__.py +6 -0
  96. sqlspec/driver/mixins/_result_tools.py +164 -0
  97. sqlspec/driver/mixins/_sql_translator.py +36 -0
  98. sqlspec/exceptions.py +193 -0
  99. sqlspec/extensions/__init__.py +0 -0
  100. sqlspec/extensions/aiosql/__init__.py +10 -0
  101. sqlspec/extensions/aiosql/adapter.py +461 -0
  102. sqlspec/extensions/litestar/__init__.py +6 -0
  103. sqlspec/extensions/litestar/_utils.py +52 -0
  104. sqlspec/extensions/litestar/cli.py +48 -0
  105. sqlspec/extensions/litestar/config.py +92 -0
  106. sqlspec/extensions/litestar/handlers.py +260 -0
  107. sqlspec/extensions/litestar/plugin.py +145 -0
  108. sqlspec/extensions/litestar/providers.py +454 -0
  109. sqlspec/loader.cpython-312-x86_64-linux-gnu.so +0 -0
  110. sqlspec/loader.py +760 -0
  111. sqlspec/migrations/__init__.py +35 -0
  112. sqlspec/migrations/base.py +414 -0
  113. sqlspec/migrations/commands.py +443 -0
  114. sqlspec/migrations/loaders.py +402 -0
  115. sqlspec/migrations/runner.py +213 -0
  116. sqlspec/migrations/tracker.py +140 -0
  117. sqlspec/migrations/utils.py +129 -0
  118. sqlspec/protocols.py +400 -0
  119. sqlspec/py.typed +0 -0
  120. sqlspec/storage/__init__.py +23 -0
  121. sqlspec/storage/backends/__init__.py +0 -0
  122. sqlspec/storage/backends/base.py +163 -0
  123. sqlspec/storage/backends/fsspec.py +386 -0
  124. sqlspec/storage/backends/obstore.py +459 -0
  125. sqlspec/storage/capabilities.py +102 -0
  126. sqlspec/storage/registry.py +239 -0
  127. sqlspec/typing.py +299 -0
  128. sqlspec/utils/__init__.py +3 -0
  129. sqlspec/utils/correlation.py +150 -0
  130. sqlspec/utils/deprecation.py +106 -0
  131. sqlspec/utils/fixtures.cpython-312-x86_64-linux-gnu.so +0 -0
  132. sqlspec/utils/fixtures.py +58 -0
  133. sqlspec/utils/logging.py +127 -0
  134. sqlspec/utils/module_loader.py +89 -0
  135. sqlspec/utils/serializers.py +4 -0
  136. sqlspec/utils/singleton.py +32 -0
  137. sqlspec/utils/sync_tools.cpython-312-x86_64-linux-gnu.so +0 -0
  138. sqlspec/utils/sync_tools.py +237 -0
  139. sqlspec/utils/text.cpython-312-x86_64-linux-gnu.so +0 -0
  140. sqlspec/utils/text.py +96 -0
  141. sqlspec/utils/type_guards.cpython-312-x86_64-linux-gnu.so +0 -0
  142. sqlspec/utils/type_guards.py +1135 -0
  143. sqlspec-0.16.0.dist-info/METADATA +365 -0
  144. sqlspec-0.16.0.dist-info/RECORD +148 -0
  145. sqlspec-0.16.0.dist-info/WHEEL +4 -0
  146. sqlspec-0.16.0.dist-info/entry_points.txt +2 -0
  147. sqlspec-0.16.0.dist-info/licenses/LICENSE +21 -0
  148. sqlspec-0.16.0.dist-info/licenses/NOTICE +29 -0
sqlspec/_sql.py ADDED
@@ -0,0 +1,1347 @@
1
+ """Unified SQL factory for creating SQL builders and column expressions with a clean API.
2
+
3
+ Provides both statement builders (select, insert, update, etc.) and column expressions.
4
+ """
5
+
6
+ import logging
7
+ from typing import TYPE_CHECKING, Any, Optional, Union
8
+
9
+ import sqlglot
10
+ from sqlglot import exp
11
+ from sqlglot.dialects.dialect import DialectType
12
+ from sqlglot.errors import ParseError as SQLGlotParseError
13
+
14
+ from sqlspec.builder import (
15
+ AlterTable,
16
+ Column,
17
+ CommentOn,
18
+ CreateIndex,
19
+ CreateMaterializedView,
20
+ CreateSchema,
21
+ CreateTable,
22
+ CreateTableAsSelect,
23
+ CreateView,
24
+ Delete,
25
+ DropIndex,
26
+ DropSchema,
27
+ DropTable,
28
+ DropView,
29
+ Insert,
30
+ Merge,
31
+ RenameTable,
32
+ Select,
33
+ Truncate,
34
+ Update,
35
+ )
36
+ from sqlspec.exceptions import SQLBuilderError
37
+
38
+ if TYPE_CHECKING:
39
+ from sqlspec.core.statement import SQL
40
+
41
+ __all__ = (
42
+ "AlterTable",
43
+ "Case",
44
+ "Column",
45
+ "CommentOn",
46
+ "CreateIndex",
47
+ "CreateMaterializedView",
48
+ "CreateSchema",
49
+ "CreateTable",
50
+ "CreateTableAsSelect",
51
+ "CreateView",
52
+ "Delete",
53
+ "DropIndex",
54
+ "DropSchema",
55
+ "DropTable",
56
+ "DropView",
57
+ "Insert",
58
+ "Merge",
59
+ "RenameTable",
60
+ "SQLFactory",
61
+ "Select",
62
+ "Truncate",
63
+ "Update",
64
+ "sql",
65
+ )
66
+
67
+ logger = logging.getLogger("sqlspec")
68
+
69
+ MIN_SQL_LIKE_STRING_LENGTH = 6
70
+ MIN_DECODE_ARGS = 2
71
+ SQL_STARTERS = {
72
+ "SELECT",
73
+ "INSERT",
74
+ "UPDATE",
75
+ "DELETE",
76
+ "MERGE",
77
+ "WITH",
78
+ "CALL",
79
+ "DECLARE",
80
+ "BEGIN",
81
+ "END",
82
+ "CREATE",
83
+ "DROP",
84
+ "ALTER",
85
+ "TRUNCATE",
86
+ "RENAME",
87
+ "GRANT",
88
+ "REVOKE",
89
+ "SET",
90
+ "SHOW",
91
+ "USE",
92
+ "EXPLAIN",
93
+ "OPTIMIZE",
94
+ "VACUUM",
95
+ "COPY",
96
+ }
97
+
98
+
99
+ class SQLFactory:
100
+ """Unified factory for creating SQL builders and column expressions with a fluent API."""
101
+
102
+ @classmethod
103
+ def detect_sql_type(cls, sql: str, dialect: DialectType = None) -> str:
104
+ try:
105
+ parsed_expr = sqlglot.parse_one(sql, read=dialect)
106
+ if parsed_expr and parsed_expr.key:
107
+ return parsed_expr.key.upper()
108
+ if parsed_expr:
109
+ command_type = type(parsed_expr).__name__.upper()
110
+ if command_type == "COMMAND" and parsed_expr.this:
111
+ return str(parsed_expr.this).upper()
112
+ return command_type
113
+ except SQLGlotParseError:
114
+ logger.debug("Failed to parse SQL for type detection: %s", sql[:100])
115
+ except (ValueError, TypeError, AttributeError) as e:
116
+ logger.warning("Unexpected error during SQL type detection for '%s...': %s", sql[:50], e)
117
+ return "UNKNOWN"
118
+
119
+ def __init__(self, dialect: DialectType = None) -> None:
120
+ """Initialize the SQL factory.
121
+
122
+ Args:
123
+ dialect: Default SQL dialect to use for all builders.
124
+ """
125
+ self.dialect = dialect
126
+
127
+ # ===================
128
+ # Callable Interface
129
+ # ===================
130
+ def __call__(self, statement: str, dialect: DialectType = None) -> "Any":
131
+ """Create a SelectBuilder from a SQL string, only allowing SELECT/CTE queries.
132
+
133
+ Args:
134
+ statement: The SQL statement string.
135
+ parameters: Optional parameters for the query.
136
+ *filters: Optional filters.
137
+ config: Optional config.
138
+ dialect: Optional SQL dialect.
139
+ **kwargs: Additional parameters.
140
+
141
+ Returns:
142
+ SelectBuilder instance.
143
+
144
+ Raises:
145
+ SQLBuilderError: If the SQL is not a SELECT/CTE statement.
146
+ """
147
+
148
+ try:
149
+ parsed_expr = sqlglot.parse_one(statement, read=dialect or self.dialect)
150
+ except Exception as e:
151
+ msg = f"Failed to parse SQL: {e}"
152
+ raise SQLBuilderError(msg) from e
153
+ actual_type = type(parsed_expr).__name__.upper()
154
+ expr_type_map = {
155
+ "SELECT": "SELECT",
156
+ "INSERT": "INSERT",
157
+ "UPDATE": "UPDATE",
158
+ "DELETE": "DELETE",
159
+ "MERGE": "MERGE",
160
+ "WITH": "WITH",
161
+ }
162
+ actual_type_str = expr_type_map.get(actual_type, actual_type)
163
+ if actual_type_str == "SELECT" or (
164
+ actual_type_str == "WITH" and parsed_expr.this and isinstance(parsed_expr.this, exp.Select)
165
+ ):
166
+ builder = Select(dialect=dialect or self.dialect)
167
+ builder._expression = parsed_expr
168
+ return builder
169
+ msg = (
170
+ f"sql(...) only supports SELECT statements. Detected type: {actual_type_str}. "
171
+ f"Use sql.{actual_type_str.lower()}() instead."
172
+ )
173
+ raise SQLBuilderError(msg)
174
+
175
+ # ===================
176
+ # Statement Builders
177
+ # ===================
178
+ def select(self, *columns_or_sql: Union[str, exp.Expression, Column], dialect: DialectType = None) -> "Select":
179
+ builder_dialect = dialect or self.dialect
180
+ if len(columns_or_sql) == 1 and isinstance(columns_or_sql[0], str):
181
+ sql_candidate = columns_or_sql[0].strip()
182
+ if self._looks_like_sql(sql_candidate):
183
+ detected = self.detect_sql_type(sql_candidate, dialect=builder_dialect)
184
+ if detected not in {"SELECT", "WITH"}:
185
+ msg = (
186
+ f"sql.select() expects a SELECT or WITH statement, got {detected}. "
187
+ f"Use sql.{detected.lower()}() if a dedicated builder exists, or ensure the SQL is SELECT/WITH."
188
+ )
189
+ raise SQLBuilderError(msg)
190
+ select_builder = Select(dialect=builder_dialect)
191
+ if select_builder._expression is None:
192
+ select_builder.__post_init__()
193
+ return self._populate_select_from_sql(select_builder, sql_candidate)
194
+ select_builder = Select(dialect=builder_dialect)
195
+ if select_builder._expression is None:
196
+ select_builder.__post_init__()
197
+ if columns_or_sql:
198
+ select_builder.select(*columns_or_sql)
199
+ return select_builder
200
+
201
+ def insert(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Insert":
202
+ builder_dialect = dialect or self.dialect
203
+ builder = Insert(dialect=builder_dialect)
204
+ if builder._expression is None:
205
+ builder.__post_init__()
206
+ if table_or_sql:
207
+ if self._looks_like_sql(table_or_sql):
208
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
209
+ if detected not in {"INSERT", "SELECT"}:
210
+ msg = (
211
+ f"sql.insert() expects INSERT or SELECT (for insert-from-select), got {detected}. "
212
+ f"Use sql.{detected.lower()}() if a dedicated builder exists, "
213
+ f"or ensure the SQL is INSERT/SELECT."
214
+ )
215
+ raise SQLBuilderError(msg)
216
+ return self._populate_insert_from_sql(builder, table_or_sql)
217
+ return builder.into(table_or_sql)
218
+ return builder
219
+
220
+ def update(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Update":
221
+ builder_dialect = dialect or self.dialect
222
+ builder = Update(dialect=builder_dialect)
223
+ if builder._expression is None:
224
+ builder.__post_init__()
225
+ if table_or_sql:
226
+ if self._looks_like_sql(table_or_sql):
227
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
228
+ if detected != "UPDATE":
229
+ msg = f"sql.update() expects UPDATE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
230
+ raise SQLBuilderError(msg)
231
+ return self._populate_update_from_sql(builder, table_or_sql)
232
+ return builder.table(table_or_sql)
233
+ return builder
234
+
235
+ def delete(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Delete":
236
+ builder_dialect = dialect or self.dialect
237
+ builder = Delete(dialect=builder_dialect)
238
+ if builder._expression is None:
239
+ builder.__post_init__()
240
+ if table_or_sql and self._looks_like_sql(table_or_sql):
241
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
242
+ if detected != "DELETE":
243
+ msg = f"sql.delete() expects DELETE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
244
+ raise SQLBuilderError(msg)
245
+ return self._populate_delete_from_sql(builder, table_or_sql)
246
+ return builder
247
+
248
+ def merge(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Merge":
249
+ builder_dialect = dialect or self.dialect
250
+ builder = Merge(dialect=builder_dialect)
251
+ if builder._expression is None:
252
+ builder.__post_init__()
253
+ if table_or_sql:
254
+ if self._looks_like_sql(table_or_sql):
255
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
256
+ if detected != "MERGE":
257
+ msg = f"sql.merge() expects MERGE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
258
+ raise SQLBuilderError(msg)
259
+ return self._populate_merge_from_sql(builder, table_or_sql)
260
+ return builder.into(table_or_sql)
261
+ return builder
262
+
263
+ # ===================
264
+ # DDL Statement Builders
265
+ # ===================
266
+
267
+ def create_table(self, table_name: str, dialect: DialectType = None) -> "CreateTable":
268
+ """Create a CREATE TABLE builder.
269
+
270
+ Args:
271
+ table_name: Name of the table to create
272
+ dialect: Optional SQL dialect
273
+
274
+ Returns:
275
+ CreateTable builder instance
276
+ """
277
+ builder = CreateTable(table_name)
278
+ builder.dialect = dialect or self.dialect
279
+ return builder
280
+
281
+ def create_table_as_select(self, dialect: DialectType = None) -> "CreateTableAsSelect":
282
+ """Create a CREATE TABLE AS SELECT builder.
283
+
284
+ Args:
285
+ dialect: Optional SQL dialect
286
+
287
+ Returns:
288
+ CreateTableAsSelect builder instance
289
+ """
290
+ builder = CreateTableAsSelect()
291
+ builder.dialect = dialect or self.dialect
292
+ return builder
293
+
294
+ def create_view(self, dialect: DialectType = None) -> "CreateView":
295
+ """Create a CREATE VIEW builder.
296
+
297
+ Args:
298
+ dialect: Optional SQL dialect
299
+
300
+ Returns:
301
+ CreateView builder instance
302
+ """
303
+ builder = CreateView()
304
+ builder.dialect = dialect or self.dialect
305
+ return builder
306
+
307
+ def create_materialized_view(self, dialect: DialectType = None) -> "CreateMaterializedView":
308
+ """Create a CREATE MATERIALIZED VIEW builder.
309
+
310
+ Args:
311
+ dialect: Optional SQL dialect
312
+
313
+ Returns:
314
+ CreateMaterializedView builder instance
315
+ """
316
+ builder = CreateMaterializedView()
317
+ builder.dialect = dialect or self.dialect
318
+ return builder
319
+
320
+ def create_index(self, index_name: str, dialect: DialectType = None) -> "CreateIndex":
321
+ """Create a CREATE INDEX builder.
322
+
323
+ Args:
324
+ index_name: Name of the index to create
325
+ dialect: Optional SQL dialect
326
+
327
+ Returns:
328
+ CreateIndex builder instance
329
+ """
330
+ return CreateIndex(index_name, dialect=dialect or self.dialect)
331
+
332
+ def create_schema(self, dialect: DialectType = None) -> "CreateSchema":
333
+ """Create a CREATE SCHEMA builder.
334
+
335
+ Args:
336
+ dialect: Optional SQL dialect
337
+
338
+ Returns:
339
+ CreateSchema builder instance
340
+ """
341
+ builder = CreateSchema()
342
+ builder.dialect = dialect or self.dialect
343
+ return builder
344
+
345
+ def drop_table(self, table_name: str, dialect: DialectType = None) -> "DropTable":
346
+ """Create a DROP TABLE builder.
347
+
348
+ Args:
349
+ table_name: Name of the table to drop
350
+ dialect: Optional SQL dialect
351
+
352
+ Returns:
353
+ DropTable builder instance
354
+ """
355
+ return DropTable(table_name, dialect=dialect or self.dialect)
356
+
357
+ def drop_view(self, dialect: DialectType = None) -> "DropView":
358
+ """Create a DROP VIEW builder.
359
+
360
+ Args:
361
+ dialect: Optional SQL dialect
362
+
363
+ Returns:
364
+ DropView builder instance
365
+ """
366
+ return DropView(dialect=dialect or self.dialect)
367
+
368
+ def drop_index(self, index_name: str, dialect: DialectType = None) -> "DropIndex":
369
+ """Create a DROP INDEX builder.
370
+
371
+ Args:
372
+ index_name: Name of the index to drop
373
+ dialect: Optional SQL dialect
374
+
375
+ Returns:
376
+ DropIndex builder instance
377
+ """
378
+ return DropIndex(index_name, dialect=dialect or self.dialect)
379
+
380
+ def drop_schema(self, dialect: DialectType = None) -> "DropSchema":
381
+ """Create a DROP SCHEMA builder.
382
+
383
+ Args:
384
+ dialect: Optional SQL dialect
385
+
386
+ Returns:
387
+ DropSchema builder instance
388
+ """
389
+ return DropSchema(dialect=dialect or self.dialect)
390
+
391
+ def alter_table(self, table_name: str, dialect: DialectType = None) -> "AlterTable":
392
+ """Create an ALTER TABLE builder.
393
+
394
+ Args:
395
+ table_name: Name of the table to alter
396
+ dialect: Optional SQL dialect
397
+
398
+ Returns:
399
+ AlterTable builder instance
400
+ """
401
+ builder = AlterTable(table_name)
402
+ builder.dialect = dialect or self.dialect
403
+ return builder
404
+
405
+ def rename_table(self, dialect: DialectType = None) -> "RenameTable":
406
+ """Create a RENAME TABLE builder.
407
+
408
+ Args:
409
+ dialect: Optional SQL dialect
410
+
411
+ Returns:
412
+ RenameTable builder instance
413
+ """
414
+ builder = RenameTable()
415
+ builder.dialect = dialect or self.dialect
416
+ return builder
417
+
418
+ def comment_on(self, dialect: DialectType = None) -> "CommentOn":
419
+ """Create a COMMENT ON builder.
420
+
421
+ Args:
422
+ dialect: Optional SQL dialect
423
+
424
+ Returns:
425
+ CommentOn builder instance
426
+ """
427
+ builder = CommentOn()
428
+ builder.dialect = dialect or self.dialect
429
+ return builder
430
+
431
+ # ===================
432
+ # SQL Analysis Helpers
433
+ # ===================
434
+
435
+ @staticmethod
436
+ def _looks_like_sql(candidate: str, expected_type: Optional[str] = None) -> bool:
437
+ """Efficiently determine if a string looks like SQL.
438
+
439
+ Args:
440
+ candidate: String to check
441
+ expected_type: Expected SQL statement type (SELECT, INSERT, etc.)
442
+
443
+ Returns:
444
+ True if the string appears to be SQL
445
+ """
446
+ if not candidate or len(candidate.strip()) < MIN_SQL_LIKE_STRING_LENGTH:
447
+ return False
448
+
449
+ candidate_upper = candidate.strip().upper()
450
+
451
+ if expected_type:
452
+ return candidate_upper.startswith(expected_type.upper())
453
+
454
+ # More sophisticated check for SQL vs column names
455
+ # Column names that start with SQL keywords are common (user_id, insert_date, etc.)
456
+ if any(candidate_upper.startswith(starter) for starter in SQL_STARTERS):
457
+ # Additional checks to distinguish real SQL from column names:
458
+ # 1. Real SQL typically has spaces (SELECT ... FROM, INSERT INTO, etc.)
459
+ # 2. Check for common SQL syntax patterns
460
+ return " " in candidate
461
+
462
+ return False
463
+
464
+ def _populate_insert_from_sql(self, builder: "Insert", sql_string: str) -> "Insert":
465
+ """Parse SQL string and populate INSERT builder using SQLGlot directly."""
466
+ try:
467
+ # Use SQLGlot directly for parsing - no validation here
468
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
469
+
470
+ if isinstance(parsed_expr, exp.Insert):
471
+ builder._expression = parsed_expr
472
+ return builder
473
+
474
+ if isinstance(parsed_expr, exp.Select):
475
+ # The actual conversion logic can be handled by the builder itself
476
+ logger.info("Detected SELECT statement for INSERT - may need target table specification")
477
+ return builder
478
+
479
+ # For other statement types, just return the builder as-is
480
+ logger.warning("Cannot create INSERT from %s statement", type(parsed_expr).__name__)
481
+
482
+ except Exception as e:
483
+ logger.warning("Failed to parse INSERT SQL, falling back to traditional mode: %s", e)
484
+ return builder
485
+
486
+ def _populate_select_from_sql(self, builder: "Select", sql_string: str) -> "Select":
487
+ """Parse SQL string and populate SELECT builder using SQLGlot directly."""
488
+ try:
489
+ # Use SQLGlot directly for parsing - no validation here
490
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
491
+
492
+ if isinstance(parsed_expr, exp.Select):
493
+ builder._expression = parsed_expr
494
+ return builder
495
+
496
+ logger.warning("Cannot create SELECT from %s statement", type(parsed_expr).__name__)
497
+
498
+ except Exception as e:
499
+ logger.warning("Failed to parse SELECT SQL, falling back to traditional mode: %s", e)
500
+ return builder
501
+
502
+ def _populate_update_from_sql(self, builder: "Update", sql_string: str) -> "Update":
503
+ """Parse SQL string and populate UPDATE builder using SQLGlot directly."""
504
+ try:
505
+ # Use SQLGlot directly for parsing - no validation here
506
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
507
+
508
+ if isinstance(parsed_expr, exp.Update):
509
+ builder._expression = parsed_expr
510
+ return builder
511
+
512
+ logger.warning("Cannot create UPDATE from %s statement", type(parsed_expr).__name__)
513
+
514
+ except Exception as e:
515
+ logger.warning("Failed to parse UPDATE SQL, falling back to traditional mode: %s", e)
516
+ return builder
517
+
518
+ def _populate_delete_from_sql(self, builder: "Delete", sql_string: str) -> "Delete":
519
+ """Parse SQL string and populate DELETE builder using SQLGlot directly."""
520
+ try:
521
+ # Use SQLGlot directly for parsing - no validation here
522
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
523
+
524
+ if isinstance(parsed_expr, exp.Delete):
525
+ builder._expression = parsed_expr
526
+ return builder
527
+
528
+ logger.warning("Cannot create DELETE from %s statement", type(parsed_expr).__name__)
529
+
530
+ except Exception as e:
531
+ logger.warning("Failed to parse DELETE SQL, falling back to traditional mode: %s", e)
532
+ return builder
533
+
534
+ def _populate_merge_from_sql(self, builder: "Merge", sql_string: str) -> "Merge":
535
+ """Parse SQL string and populate MERGE builder using SQLGlot directly."""
536
+ try:
537
+ # Use SQLGlot directly for parsing - no validation here
538
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
539
+
540
+ if isinstance(parsed_expr, exp.Merge):
541
+ builder._expression = parsed_expr
542
+ return builder
543
+
544
+ logger.warning("Cannot create MERGE from %s statement", type(parsed_expr).__name__)
545
+
546
+ except Exception as e:
547
+ logger.warning("Failed to parse MERGE SQL, falling back to traditional mode: %s", e)
548
+ return builder
549
+
550
+ # ===================
551
+ # Column References
552
+ # ===================
553
+
554
+ def column(self, name: str, table: Optional[str] = None) -> Column:
555
+ """Create a column reference.
556
+
557
+ Args:
558
+ name: Column name.
559
+ table: Optional table name.
560
+
561
+ Returns:
562
+ Column object that supports method chaining and operator overloading.
563
+ """
564
+ return Column(name, table)
565
+
566
+ def __getattr__(self, name: str) -> Column:
567
+ """Dynamically create column references.
568
+
569
+ Args:
570
+ name: Column name.
571
+
572
+ Returns:
573
+ Column object that supports method chaining and operator overloading.
574
+ """
575
+ return Column(name)
576
+
577
+ # ===================
578
+ # Raw SQL Expressions
579
+ # ===================
580
+
581
+ @staticmethod
582
+ def raw(sql_fragment: str, **parameters: Any) -> "Union[exp.Expression, SQL]":
583
+ """Create a raw SQL expression from a string fragment with optional parameters.
584
+
585
+ This method makes it explicit that you are passing raw SQL that should
586
+ be parsed and included directly in the query. Useful for complex expressions,
587
+ database-specific functions, or when you need precise control over the SQL.
588
+
589
+ Args:
590
+ sql_fragment: Raw SQL string to parse into an expression.
591
+ **parameters: Named parameters for parameter binding.
592
+
593
+ Returns:
594
+ SQLGlot expression from the parsed SQL fragment (if no parameters).
595
+ SQL statement object (if parameters provided).
596
+
597
+ Raises:
598
+ SQLBuilderError: If the SQL fragment cannot be parsed.
599
+
600
+ Example:
601
+ ```python
602
+ # Raw expression without parameters (current behavior)
603
+ expr = sql.raw("COALESCE(name, 'Unknown')")
604
+
605
+ # Raw SQL with named parameters (new functionality)
606
+ stmt = sql.raw(
607
+ "LOWER(name) LIKE LOWER(:pattern)", pattern=f"%{query}%"
608
+ )
609
+
610
+ # Raw complex expression with parameters
611
+ expr = sql.raw(
612
+ "price BETWEEN :min_price AND :max_price",
613
+ min_price=100,
614
+ max_price=500,
615
+ )
616
+
617
+ # Raw window function
618
+ query = sql.select(
619
+ "name",
620
+ sql.raw(
621
+ "ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC)"
622
+ ),
623
+ ).from_("employees")
624
+ ```
625
+ """
626
+ if not parameters:
627
+ # Original behavior - return pure expression
628
+ try:
629
+ parsed: Optional[exp.Expression] = exp.maybe_parse(sql_fragment)
630
+ if parsed is not None:
631
+ return parsed
632
+ if sql_fragment.strip().replace("_", "").replace(".", "").isalnum():
633
+ return exp.to_identifier(sql_fragment)
634
+ return exp.Literal.string(sql_fragment)
635
+ except Exception as e:
636
+ msg = f"Failed to parse raw SQL fragment '{sql_fragment}': {e}"
637
+ raise SQLBuilderError(msg) from e
638
+
639
+ # New behavior - return SQL statement with parameters
640
+ from sqlspec.core.statement import SQL
641
+
642
+ return SQL(sql_fragment, parameters)
643
+
644
+ # ===================
645
+ # Aggregate Functions
646
+ # ===================
647
+
648
+ @staticmethod
649
+ def count(column: Union[str, exp.Expression] = "*", distinct: bool = False) -> exp.Expression:
650
+ """Create a COUNT expression.
651
+
652
+ Args:
653
+ column: Column to count (default "*").
654
+ distinct: Whether to use COUNT DISTINCT.
655
+
656
+ Returns:
657
+ COUNT expression.
658
+ """
659
+ if column == "*":
660
+ return exp.Count(this=exp.Star(), distinct=distinct)
661
+ col_expr = exp.column(column) if isinstance(column, str) else column
662
+ return exp.Count(this=col_expr, distinct=distinct)
663
+
664
+ def count_distinct(self, column: Union[str, exp.Expression]) -> exp.Expression:
665
+ """Create a COUNT(DISTINCT column) expression.
666
+
667
+ Args:
668
+ column: Column to count distinct values.
669
+
670
+ Returns:
671
+ COUNT DISTINCT expression.
672
+ """
673
+ return self.count(column, distinct=True)
674
+
675
+ @staticmethod
676
+ def sum(column: Union[str, exp.Expression], distinct: bool = False) -> exp.Expression:
677
+ """Create a SUM expression.
678
+
679
+ Args:
680
+ column: Column to sum.
681
+ distinct: Whether to use SUM DISTINCT.
682
+
683
+ Returns:
684
+ SUM expression.
685
+ """
686
+ col_expr = exp.column(column) if isinstance(column, str) else column
687
+ return exp.Sum(this=col_expr, distinct=distinct)
688
+
689
+ @staticmethod
690
+ def avg(column: Union[str, exp.Expression]) -> exp.Expression:
691
+ """Create an AVG expression.
692
+
693
+ Args:
694
+ column: Column to average.
695
+
696
+ Returns:
697
+ AVG expression.
698
+ """
699
+ col_expr = exp.column(column) if isinstance(column, str) else column
700
+ return exp.Avg(this=col_expr)
701
+
702
+ @staticmethod
703
+ def max(column: Union[str, exp.Expression]) -> exp.Expression:
704
+ """Create a MAX expression.
705
+
706
+ Args:
707
+ column: Column to find maximum.
708
+
709
+ Returns:
710
+ MAX expression.
711
+ """
712
+ col_expr = exp.column(column) if isinstance(column, str) else column
713
+ return exp.Max(this=col_expr)
714
+
715
+ @staticmethod
716
+ def min(column: Union[str, exp.Expression]) -> exp.Expression:
717
+ """Create a MIN expression.
718
+
719
+ Args:
720
+ column: Column to find minimum.
721
+
722
+ Returns:
723
+ MIN expression.
724
+ """
725
+ col_expr = exp.column(column) if isinstance(column, str) else column
726
+ return exp.Min(this=col_expr)
727
+
728
+ # ===================
729
+ # Advanced SQL Operations
730
+ # ===================
731
+
732
+ @staticmethod
733
+ def rollup(*columns: Union[str, exp.Expression]) -> exp.Expression:
734
+ """Create a ROLLUP expression for GROUP BY clauses.
735
+
736
+ Args:
737
+ *columns: Columns to include in the rollup.
738
+
739
+ Returns:
740
+ ROLLUP expression.
741
+
742
+ Example:
743
+ ```python
744
+ # GROUP BY ROLLUP(product, region)
745
+ query = (
746
+ sql.select("product", "region", sql.sum("sales"))
747
+ .from_("sales_data")
748
+ .group_by(sql.rollup("product", "region"))
749
+ )
750
+ ```
751
+ """
752
+ column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
753
+ return exp.Rollup(expressions=column_exprs)
754
+
755
+ @staticmethod
756
+ def cube(*columns: Union[str, exp.Expression]) -> exp.Expression:
757
+ """Create a CUBE expression for GROUP BY clauses.
758
+
759
+ Args:
760
+ *columns: Columns to include in the cube.
761
+
762
+ Returns:
763
+ CUBE expression.
764
+
765
+ Example:
766
+ ```python
767
+ # GROUP BY CUBE(product, region)
768
+ query = (
769
+ sql.select("product", "region", sql.sum("sales"))
770
+ .from_("sales_data")
771
+ .group_by(sql.cube("product", "region"))
772
+ )
773
+ ```
774
+ """
775
+ column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
776
+ return exp.Cube(expressions=column_exprs)
777
+
778
+ @staticmethod
779
+ def grouping_sets(*column_sets: Union[tuple[str, ...], list[str]]) -> exp.Expression:
780
+ """Create a GROUPING SETS expression for GROUP BY clauses.
781
+
782
+ Args:
783
+ *column_sets: Sets of columns to group by.
784
+
785
+ Returns:
786
+ GROUPING SETS expression.
787
+
788
+ Example:
789
+ ```python
790
+ # GROUP BY GROUPING SETS ((product), (region), ())
791
+ query = (
792
+ sql.select("product", "region", sql.sum("sales"))
793
+ .from_("sales_data")
794
+ .group_by(
795
+ sql.grouping_sets(("product",), ("region",), ())
796
+ )
797
+ )
798
+ ```
799
+ """
800
+ set_expressions = []
801
+ for column_set in column_sets:
802
+ if isinstance(column_set, (tuple, list)):
803
+ if len(column_set) == 0:
804
+ set_expressions.append(exp.Tuple(expressions=[]))
805
+ else:
806
+ columns = [exp.column(col) for col in column_set]
807
+ set_expressions.append(exp.Tuple(expressions=columns))
808
+ else:
809
+ set_expressions.append(exp.column(column_set))
810
+
811
+ return exp.GroupingSets(expressions=set_expressions)
812
+
813
+ @staticmethod
814
+ def any(values: Union[list[Any], exp.Expression, str]) -> exp.Expression:
815
+ """Create an ANY expression for use with comparison operators.
816
+
817
+ Args:
818
+ values: Values, expression, or subquery for the ANY clause.
819
+
820
+ Returns:
821
+ ANY expression.
822
+
823
+ Example:
824
+ ```python
825
+ # WHERE id = ANY(subquery)
826
+ subquery = sql.select("user_id").from_("active_users")
827
+ query = (
828
+ sql.select("*")
829
+ .from_("users")
830
+ .where(sql.id.eq(sql.any(subquery)))
831
+ )
832
+ ```
833
+ """
834
+ if isinstance(values, list):
835
+ literals = [SQLFactory._to_literal(v) for v in values]
836
+ return exp.Any(this=exp.Array(expressions=literals))
837
+ if isinstance(values, str):
838
+ # Parse as SQL
839
+ parsed = exp.maybe_parse(values) # type: ignore[var-annotated]
840
+ if parsed:
841
+ return exp.Any(this=parsed)
842
+ return exp.Any(this=exp.Literal.string(values))
843
+ return exp.Any(this=values)
844
+
845
+ @staticmethod
846
+ def not_any_(values: Union[list[Any], exp.Expression, str]) -> exp.Expression:
847
+ """Create a NOT ANY expression for use with comparison operators.
848
+
849
+ Args:
850
+ values: Values, expression, or subquery for the NOT ANY clause.
851
+
852
+ Returns:
853
+ NOT ANY expression.
854
+
855
+ Example:
856
+ ```python
857
+ # WHERE id <> ANY(subquery)
858
+ subquery = sql.select("user_id").from_("blocked_users")
859
+ query = (
860
+ sql.select("*")
861
+ .from_("users")
862
+ .where(sql.id.neq(sql.not_any(subquery)))
863
+ )
864
+ ```
865
+ """
866
+ return SQLFactory.any(values) # NOT ANY is handled by the comparison operator
867
+
868
+ # ===================
869
+ # String Functions
870
+ # ===================
871
+
872
+ @staticmethod
873
+ def concat(*expressions: Union[str, exp.Expression]) -> exp.Expression:
874
+ """Create a CONCAT expression.
875
+
876
+ Args:
877
+ *expressions: Expressions to concatenate.
878
+
879
+ Returns:
880
+ CONCAT expression.
881
+ """
882
+ exprs = [exp.column(expr) if isinstance(expr, str) else expr for expr in expressions]
883
+ return exp.Concat(expressions=exprs)
884
+
885
+ @staticmethod
886
+ def upper(column: Union[str, exp.Expression]) -> exp.Expression:
887
+ """Create an UPPER expression.
888
+
889
+ Args:
890
+ column: Column to convert to uppercase.
891
+
892
+ Returns:
893
+ UPPER expression.
894
+ """
895
+ col_expr = exp.column(column) if isinstance(column, str) else column
896
+ return exp.Upper(this=col_expr)
897
+
898
+ @staticmethod
899
+ def lower(column: Union[str, exp.Expression]) -> exp.Expression:
900
+ """Create a LOWER expression.
901
+
902
+ Args:
903
+ column: Column to convert to lowercase.
904
+
905
+ Returns:
906
+ LOWER expression.
907
+ """
908
+ col_expr = exp.column(column) if isinstance(column, str) else column
909
+ return exp.Lower(this=col_expr)
910
+
911
+ @staticmethod
912
+ def length(column: Union[str, exp.Expression]) -> exp.Expression:
913
+ """Create a LENGTH expression.
914
+
915
+ Args:
916
+ column: Column to get length of.
917
+
918
+ Returns:
919
+ LENGTH expression.
920
+ """
921
+ col_expr = exp.column(column) if isinstance(column, str) else column
922
+ return exp.Length(this=col_expr)
923
+
924
+ # ===================
925
+ # Math Functions
926
+ # ===================
927
+
928
+ @staticmethod
929
+ def round(column: Union[str, exp.Expression], decimals: int = 0) -> exp.Expression:
930
+ """Create a ROUND expression.
931
+
932
+ Args:
933
+ column: Column to round.
934
+ decimals: Number of decimal places.
935
+
936
+ Returns:
937
+ ROUND expression.
938
+ """
939
+ col_expr = exp.column(column) if isinstance(column, str) else column
940
+ if decimals == 0:
941
+ return exp.Round(this=col_expr)
942
+ return exp.Round(this=col_expr, expression=exp.Literal.number(decimals))
943
+
944
+ # ===================
945
+ # Conversion Functions
946
+ # ===================
947
+
948
+ @staticmethod
949
+ def _to_literal(value: Any) -> exp.Expression:
950
+ """Convert a Python value to a SQLGlot literal expression.
951
+
952
+ Uses SQLGlot's built-in exp.convert() function for optimal dialect-agnostic
953
+ literal creation. Handles all Python primitive types correctly:
954
+ - None -> exp.Null (renders as NULL)
955
+ - bool -> exp.Boolean (renders as TRUE/FALSE or 1/0 based on dialect)
956
+ - int/float -> exp.Literal with is_number=True
957
+ - str -> exp.Literal with is_string=True
958
+ - exp.Expression -> returned as-is (passthrough)
959
+
960
+ Args:
961
+ value: Python value or SQLGlot expression to convert.
962
+
963
+ Returns:
964
+ SQLGlot expression representing the literal value.
965
+ """
966
+ if isinstance(value, exp.Expression):
967
+ return value
968
+ return exp.convert(value)
969
+
970
+ @staticmethod
971
+ def decode(column: Union[str, exp.Expression], *args: Union[str, exp.Expression, Any]) -> exp.Expression:
972
+ """Create a DECODE expression (Oracle-style conditional logic).
973
+
974
+ DECODE compares column to each search value and returns the corresponding result.
975
+ If no match is found, returns the default value (if provided) or NULL.
976
+
977
+ Args:
978
+ column: Column to compare.
979
+ *args: Alternating search values and results, with optional default at the end.
980
+ Format: search1, result1, search2, result2, ..., [default]
981
+
982
+ Raises:
983
+ ValueError: If fewer than two search/result pairs are provided.
984
+
985
+ Returns:
986
+ CASE expression equivalent to DECODE.
987
+
988
+ Example:
989
+ ```python
990
+ # DECODE(status, 'A', 'Active', 'I', 'Inactive', 'Unknown')
991
+ sql.decode(
992
+ "status", "A", "Active", "I", "Inactive", "Unknown"
993
+ )
994
+ ```
995
+ """
996
+ col_expr = exp.column(column) if isinstance(column, str) else column
997
+
998
+ if len(args) < MIN_DECODE_ARGS:
999
+ msg = "DECODE requires at least one search/result pair"
1000
+ raise ValueError(msg)
1001
+
1002
+ conditions = []
1003
+ default = None
1004
+
1005
+ for i in range(0, len(args) - 1, 2):
1006
+ if i + 1 >= len(args):
1007
+ # Odd number of args means last one is default
1008
+ default = SQLFactory._to_literal(args[i])
1009
+ break
1010
+
1011
+ search_val = args[i]
1012
+ result_val = args[i + 1]
1013
+
1014
+ search_expr = SQLFactory._to_literal(search_val)
1015
+ result_expr = SQLFactory._to_literal(result_val)
1016
+
1017
+ condition = exp.EQ(this=col_expr, expression=search_expr)
1018
+ conditions.append(exp.When(this=condition, then=result_expr))
1019
+
1020
+ return exp.Case(ifs=conditions, default=default)
1021
+
1022
+ @staticmethod
1023
+ def cast(column: Union[str, exp.Expression], data_type: str) -> exp.Expression:
1024
+ """Create a CAST expression for type conversion.
1025
+
1026
+ Args:
1027
+ column: Column or expression to cast.
1028
+ data_type: Target data type (e.g., 'INT', 'VARCHAR(100)', 'DECIMAL(10,2)').
1029
+
1030
+ Returns:
1031
+ CAST expression.
1032
+ """
1033
+ col_expr = exp.column(column) if isinstance(column, str) else column
1034
+ return exp.Cast(this=col_expr, to=exp.DataType.build(data_type))
1035
+
1036
+ @staticmethod
1037
+ def coalesce(*expressions: Union[str, exp.Expression]) -> exp.Expression:
1038
+ """Create a COALESCE expression.
1039
+
1040
+ Args:
1041
+ *expressions: Expressions to coalesce.
1042
+
1043
+ Returns:
1044
+ COALESCE expression.
1045
+ """
1046
+ exprs = [exp.column(expr) if isinstance(expr, str) else expr for expr in expressions]
1047
+ return exp.Coalesce(expressions=exprs)
1048
+
1049
+ @staticmethod
1050
+ def nvl(column: Union[str, exp.Expression], substitute_value: Union[str, exp.Expression, Any]) -> exp.Expression:
1051
+ """Create an NVL (Oracle-style) expression using COALESCE.
1052
+
1053
+ Args:
1054
+ column: Column to check for NULL.
1055
+ substitute_value: Value to use if column is NULL.
1056
+
1057
+ Returns:
1058
+ COALESCE expression equivalent to NVL.
1059
+ """
1060
+ col_expr = exp.column(column) if isinstance(column, str) else column
1061
+ sub_expr = SQLFactory._to_literal(substitute_value)
1062
+ return exp.Coalesce(expressions=[col_expr, sub_expr])
1063
+
1064
+ @staticmethod
1065
+ def nvl2(
1066
+ column: Union[str, exp.Expression],
1067
+ value_if_not_null: Union[str, exp.Expression, Any],
1068
+ value_if_null: Union[str, exp.Expression, Any],
1069
+ ) -> exp.Expression:
1070
+ """Create an NVL2 (Oracle-style) expression using CASE.
1071
+
1072
+ NVL2 returns value_if_not_null if column is not NULL,
1073
+ otherwise returns value_if_null.
1074
+
1075
+ Args:
1076
+ column: Column to check for NULL.
1077
+ value_if_not_null: Value to use if column is NOT NULL.
1078
+ value_if_null: Value to use if column is NULL.
1079
+
1080
+ Returns:
1081
+ CASE expression equivalent to NVL2.
1082
+
1083
+ Example:
1084
+ ```python
1085
+ # NVL2(salary, 'Has Salary', 'No Salary')
1086
+ sql.nvl2("salary", "Has Salary", "No Salary")
1087
+ ```
1088
+ """
1089
+ col_expr = exp.column(column) if isinstance(column, str) else column
1090
+ not_null_expr = SQLFactory._to_literal(value_if_not_null)
1091
+ null_expr = SQLFactory._to_literal(value_if_null)
1092
+
1093
+ # Create CASE WHEN column IS NOT NULL THEN value_if_not_null ELSE value_if_null END
1094
+ is_null = exp.Is(this=col_expr, expression=exp.Null())
1095
+ condition = exp.Not(this=is_null)
1096
+ when_clause = exp.If(this=condition, true=not_null_expr)
1097
+
1098
+ return exp.Case(ifs=[when_clause], default=null_expr)
1099
+
1100
+ # ===================
1101
+ # Bulk Operations
1102
+ # ===================
1103
+
1104
+ @staticmethod
1105
+ def bulk_insert(table_name: str, column_count: int, placeholder_style: str = "?") -> exp.Expression:
1106
+ """Create bulk INSERT expression for executemany operations.
1107
+
1108
+ This is specifically for bulk loading operations like CSV ingestion where
1109
+ we need an INSERT expression with placeholders for executemany().
1110
+
1111
+ Args:
1112
+ table_name: Name of the table to insert into
1113
+ column_count: Number of columns (for placeholder generation)
1114
+ placeholder_style: Placeholder style ("?" for SQLite/PostgreSQL, "%s" for MySQL, ":1" for Oracle)
1115
+
1116
+ Returns:
1117
+ INSERT expression with proper placeholders for bulk operations
1118
+
1119
+ Example:
1120
+ ```python
1121
+ from sqlspec import sql
1122
+
1123
+ # SQLite/PostgreSQL style
1124
+ insert_expr = sql.bulk_insert("my_table", 3)
1125
+ # Creates: INSERT INTO "my_table" VALUES (?, ?, ?)
1126
+
1127
+ # MySQL style
1128
+ insert_expr = sql.bulk_insert(
1129
+ "my_table", 3, placeholder_style="%s"
1130
+ )
1131
+ # Creates: INSERT INTO "my_table" VALUES (%s, %s, %s)
1132
+
1133
+ # Oracle style
1134
+ insert_expr = sql.bulk_insert(
1135
+ "my_table", 3, placeholder_style=":1"
1136
+ )
1137
+ # Creates: INSERT INTO "my_table" VALUES (:1, :2, :3)
1138
+ ```
1139
+ """
1140
+ return exp.Insert(
1141
+ this=exp.Table(this=exp.to_identifier(table_name)),
1142
+ expression=exp.Values(
1143
+ expressions=[
1144
+ exp.Tuple(expressions=[exp.Placeholder(this=placeholder_style) for _ in range(column_count)])
1145
+ ]
1146
+ ),
1147
+ )
1148
+
1149
+ def truncate(self, table_name: str) -> "Truncate":
1150
+ """Create a TRUNCATE TABLE builder.
1151
+
1152
+ Args:
1153
+ table_name: Name of the table to truncate
1154
+
1155
+ Returns:
1156
+ TruncateTable builder instance
1157
+
1158
+ Example:
1159
+ ```python
1160
+ from sqlspec import sql
1161
+
1162
+ # Simple truncate
1163
+ truncate_sql = sql.truncate_table("my_table").build().sql
1164
+
1165
+ # Truncate with options
1166
+ truncate_sql = (
1167
+ sql.truncate_table("my_table")
1168
+ .cascade()
1169
+ .restart_identity()
1170
+ .build()
1171
+ .sql
1172
+ )
1173
+ ```
1174
+ """
1175
+ builder = Truncate(dialect=self.dialect)
1176
+ builder._table_name = table_name
1177
+ return builder
1178
+
1179
+ # ===================
1180
+ # Case Expressions
1181
+ # ===================
1182
+
1183
+ @staticmethod
1184
+ def case() -> "Case":
1185
+ """Create a CASE expression builder.
1186
+
1187
+ Returns:
1188
+ CaseExpressionBuilder for building CASE expressions.
1189
+ """
1190
+ return Case()
1191
+
1192
+ # ===================
1193
+ # Window Functions
1194
+ # ===================
1195
+
1196
+ def row_number(
1197
+ self,
1198
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
1199
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
1200
+ ) -> exp.Expression:
1201
+ """Create a ROW_NUMBER() window function.
1202
+
1203
+ Args:
1204
+ partition_by: Columns to partition by.
1205
+ order_by: Columns to order by.
1206
+
1207
+ Returns:
1208
+ ROW_NUMBER window function expression.
1209
+ """
1210
+ return self._create_window_function("ROW_NUMBER", [], partition_by, order_by)
1211
+
1212
+ def rank(
1213
+ self,
1214
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
1215
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
1216
+ ) -> exp.Expression:
1217
+ """Create a RANK() window function.
1218
+
1219
+ Args:
1220
+ partition_by: Columns to partition by.
1221
+ order_by: Columns to order by.
1222
+
1223
+ Returns:
1224
+ RANK window function expression.
1225
+ """
1226
+ return self._create_window_function("RANK", [], partition_by, order_by)
1227
+
1228
+ def dense_rank(
1229
+ self,
1230
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
1231
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
1232
+ ) -> exp.Expression:
1233
+ """Create a DENSE_RANK() window function.
1234
+
1235
+ Args:
1236
+ partition_by: Columns to partition by.
1237
+ order_by: Columns to order by.
1238
+
1239
+ Returns:
1240
+ DENSE_RANK window function expression.
1241
+ """
1242
+ return self._create_window_function("DENSE_RANK", [], partition_by, order_by)
1243
+
1244
+ @staticmethod
1245
+ def _create_window_function(
1246
+ func_name: str,
1247
+ func_args: list[exp.Expression],
1248
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
1249
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
1250
+ ) -> exp.Expression:
1251
+ """Helper to create window function expressions.
1252
+
1253
+ Args:
1254
+ func_name: Name of the window function.
1255
+ func_args: Arguments to the function.
1256
+ partition_by: Columns to partition by.
1257
+ order_by: Columns to order by.
1258
+
1259
+ Returns:
1260
+ Window function expression.
1261
+ """
1262
+ func_expr = exp.Anonymous(this=func_name, expressions=func_args)
1263
+
1264
+ over_args: dict[str, Any] = {}
1265
+
1266
+ if partition_by:
1267
+ if isinstance(partition_by, str):
1268
+ over_args["partition_by"] = [exp.column(partition_by)]
1269
+ elif isinstance(partition_by, list):
1270
+ over_args["partition_by"] = [exp.column(col) for col in partition_by]
1271
+ elif isinstance(partition_by, exp.Expression):
1272
+ over_args["partition_by"] = [partition_by]
1273
+
1274
+ if order_by:
1275
+ if isinstance(order_by, str):
1276
+ over_args["order"] = [exp.column(order_by).asc()]
1277
+ elif isinstance(order_by, list):
1278
+ over_args["order"] = [exp.column(col).asc() for col in order_by]
1279
+ elif isinstance(order_by, exp.Expression):
1280
+ over_args["order"] = [order_by]
1281
+
1282
+ return exp.Window(this=func_expr, **over_args)
1283
+
1284
+
1285
+ class Case:
1286
+ """Builder for CASE expressions using the SQL factory.
1287
+
1288
+ Example:
1289
+ ```python
1290
+ from sqlspec import sql
1291
+
1292
+ case_expr = (
1293
+ sql.case()
1294
+ .when(sql.age < 18, "Minor")
1295
+ .when(sql.age < 65, "Adult")
1296
+ .else_("Senior")
1297
+ .end()
1298
+ )
1299
+ ```
1300
+ """
1301
+
1302
+ def __init__(self) -> None:
1303
+ """Initialize the CASE expression builder."""
1304
+ self._conditions: list[exp.If] = []
1305
+ self._default: Optional[exp.Expression] = None
1306
+
1307
+ def when(self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]) -> "Case":
1308
+ """Add a WHEN clause.
1309
+
1310
+ Args:
1311
+ condition: Condition to test.
1312
+ value: Value to return if condition is true.
1313
+
1314
+ Returns:
1315
+ Self for method chaining.
1316
+ """
1317
+ cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
1318
+ val_expr = SQLFactory._to_literal(value)
1319
+
1320
+ # SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
1321
+ when_clause = exp.If(this=cond_expr, true=val_expr)
1322
+ self._conditions.append(when_clause)
1323
+ return self
1324
+
1325
+ def else_(self, value: Union[str, exp.Expression, Any]) -> "Case":
1326
+ """Add an ELSE clause.
1327
+
1328
+ Args:
1329
+ value: Default value to return.
1330
+
1331
+ Returns:
1332
+ Self for method chaining.
1333
+ """
1334
+ self._default = SQLFactory._to_literal(value)
1335
+ return self
1336
+
1337
+ def end(self) -> exp.Expression:
1338
+ """Complete the CASE expression.
1339
+
1340
+ Returns:
1341
+ Complete CASE expression.
1342
+ """
1343
+ return exp.Case(ifs=self._conditions, default=self._default)
1344
+
1345
+
1346
+ # Create a default SQL factory instance
1347
+ sql = SQLFactory()