sqlspec 0.16.1__cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl

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

Potentially problematic release.


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

Files changed (148) hide show
  1. 51ff5a9eadfdefd49f98__mypyc.cpython-39-aarch64-linux-gnu.so +0 -0
  2. sqlspec/__init__.py +92 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +77 -0
  6. sqlspec/_sql.py +1780 -0
  7. sqlspec/_typing.py +680 -0
  8. sqlspec/adapters/__init__.py +0 -0
  9. sqlspec/adapters/adbc/__init__.py +5 -0
  10. sqlspec/adapters/adbc/_types.py +12 -0
  11. sqlspec/adapters/adbc/config.py +361 -0
  12. sqlspec/adapters/adbc/driver.py +512 -0
  13. sqlspec/adapters/aiosqlite/__init__.py +19 -0
  14. sqlspec/adapters/aiosqlite/_types.py +13 -0
  15. sqlspec/adapters/aiosqlite/config.py +253 -0
  16. sqlspec/adapters/aiosqlite/driver.py +248 -0
  17. sqlspec/adapters/asyncmy/__init__.py +19 -0
  18. sqlspec/adapters/asyncmy/_types.py +12 -0
  19. sqlspec/adapters/asyncmy/config.py +180 -0
  20. sqlspec/adapters/asyncmy/driver.py +274 -0
  21. sqlspec/adapters/asyncpg/__init__.py +21 -0
  22. sqlspec/adapters/asyncpg/_types.py +17 -0
  23. sqlspec/adapters/asyncpg/config.py +229 -0
  24. sqlspec/adapters/asyncpg/driver.py +344 -0
  25. sqlspec/adapters/bigquery/__init__.py +18 -0
  26. sqlspec/adapters/bigquery/_types.py +12 -0
  27. sqlspec/adapters/bigquery/config.py +298 -0
  28. sqlspec/adapters/bigquery/driver.py +558 -0
  29. sqlspec/adapters/duckdb/__init__.py +22 -0
  30. sqlspec/adapters/duckdb/_types.py +12 -0
  31. sqlspec/adapters/duckdb/config.py +504 -0
  32. sqlspec/adapters/duckdb/driver.py +368 -0
  33. sqlspec/adapters/oracledb/__init__.py +32 -0
  34. sqlspec/adapters/oracledb/_types.py +14 -0
  35. sqlspec/adapters/oracledb/config.py +317 -0
  36. sqlspec/adapters/oracledb/driver.py +538 -0
  37. sqlspec/adapters/psqlpy/__init__.py +16 -0
  38. sqlspec/adapters/psqlpy/_types.py +11 -0
  39. sqlspec/adapters/psqlpy/config.py +214 -0
  40. sqlspec/adapters/psqlpy/driver.py +530 -0
  41. sqlspec/adapters/psycopg/__init__.py +32 -0
  42. sqlspec/adapters/psycopg/_types.py +17 -0
  43. sqlspec/adapters/psycopg/config.py +426 -0
  44. sqlspec/adapters/psycopg/driver.py +796 -0
  45. sqlspec/adapters/sqlite/__init__.py +15 -0
  46. sqlspec/adapters/sqlite/_types.py +11 -0
  47. sqlspec/adapters/sqlite/config.py +240 -0
  48. sqlspec/adapters/sqlite/driver.py +294 -0
  49. sqlspec/base.py +571 -0
  50. sqlspec/builder/__init__.py +62 -0
  51. sqlspec/builder/_base.py +473 -0
  52. sqlspec/builder/_column.py +320 -0
  53. sqlspec/builder/_ddl.py +1346 -0
  54. sqlspec/builder/_ddl_utils.py +103 -0
  55. sqlspec/builder/_delete.py +76 -0
  56. sqlspec/builder/_insert.py +256 -0
  57. sqlspec/builder/_merge.py +71 -0
  58. sqlspec/builder/_parsing_utils.py +140 -0
  59. sqlspec/builder/_select.py +170 -0
  60. sqlspec/builder/_update.py +188 -0
  61. sqlspec/builder/mixins/__init__.py +55 -0
  62. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  63. sqlspec/builder/mixins/_delete_operations.py +41 -0
  64. sqlspec/builder/mixins/_insert_operations.py +244 -0
  65. sqlspec/builder/mixins/_join_operations.py +122 -0
  66. sqlspec/builder/mixins/_merge_operations.py +476 -0
  67. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  68. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  69. sqlspec/builder/mixins/_select_operations.py +603 -0
  70. sqlspec/builder/mixins/_update_operations.py +187 -0
  71. sqlspec/builder/mixins/_where_clause.py +621 -0
  72. sqlspec/cli.py +247 -0
  73. sqlspec/config.py +395 -0
  74. sqlspec/core/__init__.py +63 -0
  75. sqlspec/core/cache.cpython-39-aarch64-linux-gnu.so +0 -0
  76. sqlspec/core/cache.py +871 -0
  77. sqlspec/core/compiler.cpython-39-aarch64-linux-gnu.so +0 -0
  78. sqlspec/core/compiler.py +417 -0
  79. sqlspec/core/filters.cpython-39-aarch64-linux-gnu.so +0 -0
  80. sqlspec/core/filters.py +830 -0
  81. sqlspec/core/hashing.cpython-39-aarch64-linux-gnu.so +0 -0
  82. sqlspec/core/hashing.py +310 -0
  83. sqlspec/core/parameters.cpython-39-aarch64-linux-gnu.so +0 -0
  84. sqlspec/core/parameters.py +1237 -0
  85. sqlspec/core/result.cpython-39-aarch64-linux-gnu.so +0 -0
  86. sqlspec/core/result.py +677 -0
  87. sqlspec/core/splitter.cpython-39-aarch64-linux-gnu.so +0 -0
  88. sqlspec/core/splitter.py +819 -0
  89. sqlspec/core/statement.cpython-39-aarch64-linux-gnu.so +0 -0
  90. sqlspec/core/statement.py +676 -0
  91. sqlspec/driver/__init__.py +19 -0
  92. sqlspec/driver/_async.py +502 -0
  93. sqlspec/driver/_common.py +631 -0
  94. sqlspec/driver/_sync.py +503 -0
  95. sqlspec/driver/mixins/__init__.py +6 -0
  96. sqlspec/driver/mixins/_result_tools.py +193 -0
  97. sqlspec/driver/mixins/_sql_translator.py +86 -0
  98. sqlspec/exceptions.py +193 -0
  99. sqlspec/extensions/__init__.py +0 -0
  100. sqlspec/extensions/aiosql/__init__.py +10 -0
  101. sqlspec/extensions/aiosql/adapter.py +461 -0
  102. sqlspec/extensions/litestar/__init__.py +6 -0
  103. sqlspec/extensions/litestar/_utils.py +52 -0
  104. sqlspec/extensions/litestar/cli.py +48 -0
  105. sqlspec/extensions/litestar/config.py +92 -0
  106. sqlspec/extensions/litestar/handlers.py +260 -0
  107. sqlspec/extensions/litestar/plugin.py +145 -0
  108. sqlspec/extensions/litestar/providers.py +454 -0
  109. sqlspec/loader.cpython-39-aarch64-linux-gnu.so +0 -0
  110. sqlspec/loader.py +760 -0
  111. sqlspec/migrations/__init__.py +35 -0
  112. sqlspec/migrations/base.py +414 -0
  113. sqlspec/migrations/commands.py +443 -0
  114. sqlspec/migrations/loaders.py +402 -0
  115. sqlspec/migrations/runner.py +213 -0
  116. sqlspec/migrations/tracker.py +140 -0
  117. sqlspec/migrations/utils.py +129 -0
  118. sqlspec/protocols.py +407 -0
  119. sqlspec/py.typed +0 -0
  120. sqlspec/storage/__init__.py +23 -0
  121. sqlspec/storage/backends/__init__.py +0 -0
  122. sqlspec/storage/backends/base.py +163 -0
  123. sqlspec/storage/backends/fsspec.py +386 -0
  124. sqlspec/storage/backends/obstore.py +459 -0
  125. sqlspec/storage/capabilities.py +102 -0
  126. sqlspec/storage/registry.py +239 -0
  127. sqlspec/typing.py +299 -0
  128. sqlspec/utils/__init__.py +3 -0
  129. sqlspec/utils/correlation.py +150 -0
  130. sqlspec/utils/deprecation.py +106 -0
  131. sqlspec/utils/fixtures.cpython-39-aarch64-linux-gnu.so +0 -0
  132. sqlspec/utils/fixtures.py +58 -0
  133. sqlspec/utils/logging.py +127 -0
  134. sqlspec/utils/module_loader.py +89 -0
  135. sqlspec/utils/serializers.py +4 -0
  136. sqlspec/utils/singleton.py +32 -0
  137. sqlspec/utils/sync_tools.cpython-39-aarch64-linux-gnu.so +0 -0
  138. sqlspec/utils/sync_tools.py +237 -0
  139. sqlspec/utils/text.cpython-39-aarch64-linux-gnu.so +0 -0
  140. sqlspec/utils/text.py +96 -0
  141. sqlspec/utils/type_guards.cpython-39-aarch64-linux-gnu.so +0 -0
  142. sqlspec/utils/type_guards.py +1139 -0
  143. sqlspec-0.16.1.dist-info/METADATA +365 -0
  144. sqlspec-0.16.1.dist-info/RECORD +148 -0
  145. sqlspec-0.16.1.dist-info/WHEEL +7 -0
  146. sqlspec-0.16.1.dist-info/entry_points.txt +2 -0
  147. sqlspec-0.16.1.dist-info/licenses/LICENSE +21 -0
  148. sqlspec-0.16.1.dist-info/licenses/NOTICE +29 -0
sqlspec/_sql.py ADDED
@@ -0,0 +1,1780 @@
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, cast
8
+
9
+ import sqlglot
10
+ from mypy_extensions import trait
11
+ from sqlglot import exp
12
+ from sqlglot.dialects.dialect import DialectType
13
+ from sqlglot.errors import ParseError as SQLGlotParseError
14
+
15
+ from sqlspec.builder import (
16
+ AlterTable,
17
+ Column,
18
+ CommentOn,
19
+ CreateIndex,
20
+ CreateMaterializedView,
21
+ CreateSchema,
22
+ CreateTable,
23
+ CreateTableAsSelect,
24
+ CreateView,
25
+ Delete,
26
+ DropIndex,
27
+ DropSchema,
28
+ DropTable,
29
+ DropView,
30
+ Insert,
31
+ Merge,
32
+ RenameTable,
33
+ Select,
34
+ Truncate,
35
+ Update,
36
+ )
37
+ from sqlspec.exceptions import SQLBuilderError
38
+
39
+ if TYPE_CHECKING:
40
+ from sqlspec.builder._column import ColumnExpression
41
+ from sqlspec.core.statement import SQL
42
+
43
+ __all__ = (
44
+ "AlterTable",
45
+ "Case",
46
+ "Column",
47
+ "CommentOn",
48
+ "CreateIndex",
49
+ "CreateMaterializedView",
50
+ "CreateSchema",
51
+ "CreateTable",
52
+ "CreateTableAsSelect",
53
+ "CreateView",
54
+ "Delete",
55
+ "DropIndex",
56
+ "DropSchema",
57
+ "DropTable",
58
+ "DropView",
59
+ "Insert",
60
+ "Merge",
61
+ "RenameTable",
62
+ "SQLFactory",
63
+ "Select",
64
+ "Truncate",
65
+ "Update",
66
+ "WindowFunctionBuilder",
67
+ "sql",
68
+ )
69
+
70
+ logger = logging.getLogger("sqlspec")
71
+
72
+ MIN_SQL_LIKE_STRING_LENGTH = 6
73
+ MIN_DECODE_ARGS = 2
74
+ SQL_STARTERS = {
75
+ "SELECT",
76
+ "INSERT",
77
+ "UPDATE",
78
+ "DELETE",
79
+ "MERGE",
80
+ "WITH",
81
+ "CALL",
82
+ "DECLARE",
83
+ "BEGIN",
84
+ "END",
85
+ "CREATE",
86
+ "DROP",
87
+ "ALTER",
88
+ "TRUNCATE",
89
+ "RENAME",
90
+ "GRANT",
91
+ "REVOKE",
92
+ "SET",
93
+ "SHOW",
94
+ "USE",
95
+ "EXPLAIN",
96
+ "OPTIMIZE",
97
+ "VACUUM",
98
+ "COPY",
99
+ }
100
+
101
+
102
+ class SQLFactory:
103
+ """Unified factory for creating SQL builders and column expressions with a fluent API."""
104
+
105
+ @classmethod
106
+ def detect_sql_type(cls, sql: str, dialect: DialectType = None) -> str:
107
+ try:
108
+ parsed_expr = sqlglot.parse_one(sql, read=dialect)
109
+ if parsed_expr and parsed_expr.key:
110
+ return parsed_expr.key.upper()
111
+ if parsed_expr:
112
+ command_type = type(parsed_expr).__name__.upper()
113
+ if command_type == "COMMAND" and parsed_expr.this:
114
+ return str(parsed_expr.this).upper()
115
+ return command_type
116
+ except SQLGlotParseError:
117
+ logger.debug("Failed to parse SQL for type detection: %s", sql[:100])
118
+ except (ValueError, TypeError, AttributeError) as e:
119
+ logger.warning("Unexpected error during SQL type detection for '%s...': %s", sql[:50], e)
120
+ return "UNKNOWN"
121
+
122
+ def __init__(self, dialect: DialectType = None) -> None:
123
+ """Initialize the SQL factory.
124
+
125
+ Args:
126
+ dialect: Default SQL dialect to use for all builders.
127
+ """
128
+ self.dialect = dialect
129
+
130
+ # ===================
131
+ # Callable Interface
132
+ # ===================
133
+ def __call__(self, statement: str, dialect: DialectType = None) -> "Any":
134
+ """Create a SelectBuilder from a SQL string, only allowing SELECT/CTE queries.
135
+
136
+ Args:
137
+ statement: The SQL statement string.
138
+ parameters: Optional parameters for the query.
139
+ *filters: Optional filters.
140
+ config: Optional config.
141
+ dialect: Optional SQL dialect.
142
+ **kwargs: Additional parameters.
143
+
144
+ Returns:
145
+ SelectBuilder instance.
146
+
147
+ Raises:
148
+ SQLBuilderError: If the SQL is not a SELECT/CTE statement.
149
+ """
150
+
151
+ try:
152
+ parsed_expr = sqlglot.parse_one(statement, read=dialect or self.dialect)
153
+ except Exception as e:
154
+ msg = f"Failed to parse SQL: {e}"
155
+ raise SQLBuilderError(msg) from e
156
+ actual_type = type(parsed_expr).__name__.upper()
157
+ expr_type_map = {
158
+ "SELECT": "SELECT",
159
+ "INSERT": "INSERT",
160
+ "UPDATE": "UPDATE",
161
+ "DELETE": "DELETE",
162
+ "MERGE": "MERGE",
163
+ "WITH": "WITH",
164
+ }
165
+ actual_type_str = expr_type_map.get(actual_type, actual_type)
166
+ if actual_type_str == "SELECT" or (
167
+ actual_type_str == "WITH" and parsed_expr.this and isinstance(parsed_expr.this, exp.Select)
168
+ ):
169
+ builder = Select(dialect=dialect or self.dialect)
170
+ builder._expression = parsed_expr
171
+ return builder
172
+ msg = (
173
+ f"sql(...) only supports SELECT statements. Detected type: {actual_type_str}. "
174
+ f"Use sql.{actual_type_str.lower()}() instead."
175
+ )
176
+ raise SQLBuilderError(msg)
177
+
178
+ # ===================
179
+ # Statement Builders
180
+ # ===================
181
+ def select(self, *columns_or_sql: Union[str, exp.Expression, Column], dialect: DialectType = None) -> "Select":
182
+ builder_dialect = dialect or self.dialect
183
+ if len(columns_or_sql) == 1 and isinstance(columns_or_sql[0], str):
184
+ sql_candidate = columns_or_sql[0].strip()
185
+ if self._looks_like_sql(sql_candidate):
186
+ detected = self.detect_sql_type(sql_candidate, dialect=builder_dialect)
187
+ if detected not in {"SELECT", "WITH"}:
188
+ msg = (
189
+ f"sql.select() expects a SELECT or WITH statement, got {detected}. "
190
+ f"Use sql.{detected.lower()}() if a dedicated builder exists, or ensure the SQL is SELECT/WITH."
191
+ )
192
+ raise SQLBuilderError(msg)
193
+ select_builder = Select(dialect=builder_dialect)
194
+ return self._populate_select_from_sql(select_builder, sql_candidate)
195
+ select_builder = Select(dialect=builder_dialect)
196
+ if columns_or_sql:
197
+ select_builder.select(*columns_or_sql)
198
+ return select_builder
199
+
200
+ def insert(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Insert":
201
+ builder_dialect = dialect or self.dialect
202
+ builder = Insert(dialect=builder_dialect)
203
+ if table_or_sql:
204
+ if self._looks_like_sql(table_or_sql):
205
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
206
+ if detected not in {"INSERT", "SELECT"}:
207
+ msg = (
208
+ f"sql.insert() expects INSERT or SELECT (for insert-from-select), got {detected}. "
209
+ f"Use sql.{detected.lower()}() if a dedicated builder exists, "
210
+ f"or ensure the SQL is INSERT/SELECT."
211
+ )
212
+ raise SQLBuilderError(msg)
213
+ return self._populate_insert_from_sql(builder, table_or_sql)
214
+ return builder.into(table_or_sql)
215
+ return builder
216
+
217
+ def update(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Update":
218
+ builder_dialect = dialect or self.dialect
219
+ builder = Update(dialect=builder_dialect)
220
+ if table_or_sql:
221
+ if self._looks_like_sql(table_or_sql):
222
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
223
+ if detected != "UPDATE":
224
+ msg = f"sql.update() expects UPDATE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
225
+ raise SQLBuilderError(msg)
226
+ return self._populate_update_from_sql(builder, table_or_sql)
227
+ return builder.table(table_or_sql)
228
+ return builder
229
+
230
+ def delete(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Delete":
231
+ builder_dialect = dialect or self.dialect
232
+ builder = Delete(dialect=builder_dialect)
233
+ if table_or_sql and self._looks_like_sql(table_or_sql):
234
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
235
+ if detected != "DELETE":
236
+ msg = f"sql.delete() expects DELETE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
237
+ raise SQLBuilderError(msg)
238
+ return self._populate_delete_from_sql(builder, table_or_sql)
239
+ return builder
240
+
241
+ def merge(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Merge":
242
+ builder_dialect = dialect or self.dialect
243
+ builder = Merge(dialect=builder_dialect)
244
+ if table_or_sql:
245
+ if self._looks_like_sql(table_or_sql):
246
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
247
+ if detected != "MERGE":
248
+ msg = f"sql.merge() expects MERGE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
249
+ raise SQLBuilderError(msg)
250
+ return self._populate_merge_from_sql(builder, table_or_sql)
251
+ return builder.into(table_or_sql)
252
+ return builder
253
+
254
+ # ===================
255
+ # DDL Statement Builders
256
+ # ===================
257
+
258
+ def create_table(self, table_name: str, dialect: DialectType = None) -> "CreateTable":
259
+ """Create a CREATE TABLE builder.
260
+
261
+ Args:
262
+ table_name: Name of the table to create
263
+ dialect: Optional SQL dialect
264
+
265
+ Returns:
266
+ CreateTable builder instance
267
+ """
268
+ builder = CreateTable(table_name)
269
+ builder.dialect = dialect or self.dialect
270
+ return builder
271
+
272
+ def create_table_as_select(self, dialect: DialectType = None) -> "CreateTableAsSelect":
273
+ """Create a CREATE TABLE AS SELECT builder.
274
+
275
+ Args:
276
+ dialect: Optional SQL dialect
277
+
278
+ Returns:
279
+ CreateTableAsSelect builder instance
280
+ """
281
+ builder = CreateTableAsSelect()
282
+ builder.dialect = dialect or self.dialect
283
+ return builder
284
+
285
+ def create_view(self, dialect: DialectType = None) -> "CreateView":
286
+ """Create a CREATE VIEW builder.
287
+
288
+ Args:
289
+ dialect: Optional SQL dialect
290
+
291
+ Returns:
292
+ CreateView builder instance
293
+ """
294
+ builder = CreateView()
295
+ builder.dialect = dialect or self.dialect
296
+ return builder
297
+
298
+ def create_materialized_view(self, dialect: DialectType = None) -> "CreateMaterializedView":
299
+ """Create a CREATE MATERIALIZED VIEW builder.
300
+
301
+ Args:
302
+ dialect: Optional SQL dialect
303
+
304
+ Returns:
305
+ CreateMaterializedView builder instance
306
+ """
307
+ builder = CreateMaterializedView()
308
+ builder.dialect = dialect or self.dialect
309
+ return builder
310
+
311
+ def create_index(self, index_name: str, dialect: DialectType = None) -> "CreateIndex":
312
+ """Create a CREATE INDEX builder.
313
+
314
+ Args:
315
+ index_name: Name of the index to create
316
+ dialect: Optional SQL dialect
317
+
318
+ Returns:
319
+ CreateIndex builder instance
320
+ """
321
+ return CreateIndex(index_name, dialect=dialect or self.dialect)
322
+
323
+ def create_schema(self, dialect: DialectType = None) -> "CreateSchema":
324
+ """Create a CREATE SCHEMA builder.
325
+
326
+ Args:
327
+ dialect: Optional SQL dialect
328
+
329
+ Returns:
330
+ CreateSchema builder instance
331
+ """
332
+ builder = CreateSchema()
333
+ builder.dialect = dialect or self.dialect
334
+ return builder
335
+
336
+ def drop_table(self, table_name: str, dialect: DialectType = None) -> "DropTable":
337
+ """Create a DROP TABLE builder.
338
+
339
+ Args:
340
+ table_name: Name of the table to drop
341
+ dialect: Optional SQL dialect
342
+
343
+ Returns:
344
+ DropTable builder instance
345
+ """
346
+ return DropTable(table_name, dialect=dialect or self.dialect)
347
+
348
+ def drop_view(self, dialect: DialectType = None) -> "DropView":
349
+ """Create a DROP VIEW builder.
350
+
351
+ Args:
352
+ dialect: Optional SQL dialect
353
+
354
+ Returns:
355
+ DropView builder instance
356
+ """
357
+ return DropView(dialect=dialect or self.dialect)
358
+
359
+ def drop_index(self, index_name: str, dialect: DialectType = None) -> "DropIndex":
360
+ """Create a DROP INDEX builder.
361
+
362
+ Args:
363
+ index_name: Name of the index to drop
364
+ dialect: Optional SQL dialect
365
+
366
+ Returns:
367
+ DropIndex builder instance
368
+ """
369
+ return DropIndex(index_name, dialect=dialect or self.dialect)
370
+
371
+ def drop_schema(self, dialect: DialectType = None) -> "DropSchema":
372
+ """Create a DROP SCHEMA builder.
373
+
374
+ Args:
375
+ dialect: Optional SQL dialect
376
+
377
+ Returns:
378
+ DropSchema builder instance
379
+ """
380
+ return DropSchema(dialect=dialect or self.dialect)
381
+
382
+ def alter_table(self, table_name: str, dialect: DialectType = None) -> "AlterTable":
383
+ """Create an ALTER TABLE builder.
384
+
385
+ Args:
386
+ table_name: Name of the table to alter
387
+ dialect: Optional SQL dialect
388
+
389
+ Returns:
390
+ AlterTable builder instance
391
+ """
392
+ builder = AlterTable(table_name)
393
+ builder.dialect = dialect or self.dialect
394
+ return builder
395
+
396
+ def rename_table(self, dialect: DialectType = None) -> "RenameTable":
397
+ """Create a RENAME TABLE builder.
398
+
399
+ Args:
400
+ dialect: Optional SQL dialect
401
+
402
+ Returns:
403
+ RenameTable builder instance
404
+ """
405
+ builder = RenameTable()
406
+ builder.dialect = dialect or self.dialect
407
+ return builder
408
+
409
+ def comment_on(self, dialect: DialectType = None) -> "CommentOn":
410
+ """Create a COMMENT ON builder.
411
+
412
+ Args:
413
+ dialect: Optional SQL dialect
414
+
415
+ Returns:
416
+ CommentOn builder instance
417
+ """
418
+ builder = CommentOn()
419
+ builder.dialect = dialect or self.dialect
420
+ return builder
421
+
422
+ # ===================
423
+ # SQL Analysis Helpers
424
+ # ===================
425
+
426
+ @staticmethod
427
+ def _looks_like_sql(candidate: str, expected_type: Optional[str] = None) -> bool:
428
+ """Efficiently determine if a string looks like SQL.
429
+
430
+ Args:
431
+ candidate: String to check
432
+ expected_type: Expected SQL statement type (SELECT, INSERT, etc.)
433
+
434
+ Returns:
435
+ True if the string appears to be SQL
436
+ """
437
+ if not candidate or len(candidate.strip()) < MIN_SQL_LIKE_STRING_LENGTH:
438
+ return False
439
+
440
+ candidate_upper = candidate.strip().upper()
441
+
442
+ if expected_type:
443
+ return candidate_upper.startswith(expected_type.upper())
444
+
445
+ # More sophisticated check for SQL vs column names
446
+ # Column names that start with SQL keywords are common (user_id, insert_date, etc.)
447
+ if any(candidate_upper.startswith(starter) for starter in SQL_STARTERS):
448
+ # Additional checks to distinguish real SQL from column names:
449
+ # 1. Real SQL typically has spaces (SELECT ... FROM, INSERT INTO, etc.)
450
+ # 2. Check for common SQL syntax patterns
451
+ return " " in candidate
452
+
453
+ return False
454
+
455
+ def _populate_insert_from_sql(self, builder: "Insert", sql_string: str) -> "Insert":
456
+ """Parse SQL string and populate INSERT builder using SQLGlot directly."""
457
+ try:
458
+ # Use SQLGlot directly for parsing - no validation here
459
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
460
+
461
+ if isinstance(parsed_expr, exp.Insert):
462
+ builder._expression = parsed_expr
463
+ return builder
464
+
465
+ if isinstance(parsed_expr, exp.Select):
466
+ # The actual conversion logic can be handled by the builder itself
467
+ logger.info("Detected SELECT statement for INSERT - may need target table specification")
468
+ return builder
469
+
470
+ # For other statement types, just return the builder as-is
471
+ logger.warning("Cannot create INSERT from %s statement", type(parsed_expr).__name__)
472
+
473
+ except Exception as e:
474
+ logger.warning("Failed to parse INSERT SQL, falling back to traditional mode: %s", e)
475
+ return builder
476
+
477
+ def _populate_select_from_sql(self, builder: "Select", sql_string: str) -> "Select":
478
+ """Parse SQL string and populate SELECT builder using SQLGlot directly."""
479
+ try:
480
+ # Use SQLGlot directly for parsing - no validation here
481
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
482
+
483
+ if isinstance(parsed_expr, exp.Select):
484
+ builder._expression = parsed_expr
485
+ return builder
486
+
487
+ logger.warning("Cannot create SELECT from %s statement", type(parsed_expr).__name__)
488
+
489
+ except Exception as e:
490
+ logger.warning("Failed to parse SELECT SQL, falling back to traditional mode: %s", e)
491
+ return builder
492
+
493
+ def _populate_update_from_sql(self, builder: "Update", sql_string: str) -> "Update":
494
+ """Parse SQL string and populate UPDATE builder using SQLGlot directly."""
495
+ try:
496
+ # Use SQLGlot directly for parsing - no validation here
497
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
498
+
499
+ if isinstance(parsed_expr, exp.Update):
500
+ builder._expression = parsed_expr
501
+ return builder
502
+
503
+ logger.warning("Cannot create UPDATE from %s statement", type(parsed_expr).__name__)
504
+
505
+ except Exception as e:
506
+ logger.warning("Failed to parse UPDATE SQL, falling back to traditional mode: %s", e)
507
+ return builder
508
+
509
+ def _populate_delete_from_sql(self, builder: "Delete", sql_string: str) -> "Delete":
510
+ """Parse SQL string and populate DELETE builder using SQLGlot directly."""
511
+ try:
512
+ # Use SQLGlot directly for parsing - no validation here
513
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
514
+
515
+ if isinstance(parsed_expr, exp.Delete):
516
+ builder._expression = parsed_expr
517
+ return builder
518
+
519
+ logger.warning("Cannot create DELETE from %s statement", type(parsed_expr).__name__)
520
+
521
+ except Exception as e:
522
+ logger.warning("Failed to parse DELETE SQL, falling back to traditional mode: %s", e)
523
+ return builder
524
+
525
+ def _populate_merge_from_sql(self, builder: "Merge", sql_string: str) -> "Merge":
526
+ """Parse SQL string and populate MERGE builder using SQLGlot directly."""
527
+ try:
528
+ # Use SQLGlot directly for parsing - no validation here
529
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
530
+
531
+ if isinstance(parsed_expr, exp.Merge):
532
+ builder._expression = parsed_expr
533
+ return builder
534
+
535
+ logger.warning("Cannot create MERGE from %s statement", type(parsed_expr).__name__)
536
+
537
+ except Exception as e:
538
+ logger.warning("Failed to parse MERGE SQL, falling back to traditional mode: %s", e)
539
+ return builder
540
+
541
+ # ===================
542
+ # Column References
543
+ # ===================
544
+
545
+ def column(self, name: str, table: Optional[str] = None) -> Column:
546
+ """Create a column reference.
547
+
548
+ Args:
549
+ name: Column name.
550
+ table: Optional table name.
551
+
552
+ Returns:
553
+ Column object that supports method chaining and operator overloading.
554
+ """
555
+ return Column(name, table)
556
+
557
+ @property
558
+ def case_(self) -> "Case":
559
+ """Create a CASE expression builder with improved syntax.
560
+
561
+ Returns:
562
+ Case builder instance for fluent CASE expression building.
563
+
564
+ Example:
565
+ ```python
566
+ case_expr = (
567
+ sql.case_.when("x = 1", "one")
568
+ .when("x = 2", "two")
569
+ .else_("other")
570
+ .end()
571
+ )
572
+ aliased_case = (
573
+ sql.case_.when("status = 'active'", 1)
574
+ .else_(0)
575
+ .as_("is_active")
576
+ )
577
+ ```
578
+ """
579
+ return Case()
580
+
581
+ @property
582
+ def row_number_(self) -> "WindowFunctionBuilder":
583
+ """Create a ROW_NUMBER() window function builder."""
584
+ return WindowFunctionBuilder("row_number")
585
+
586
+ @property
587
+ def rank_(self) -> "WindowFunctionBuilder":
588
+ """Create a RANK() window function builder."""
589
+ return WindowFunctionBuilder("rank")
590
+
591
+ @property
592
+ def dense_rank_(self) -> "WindowFunctionBuilder":
593
+ """Create a DENSE_RANK() window function builder."""
594
+ return WindowFunctionBuilder("dense_rank")
595
+
596
+ @property
597
+ def lag_(self) -> "WindowFunctionBuilder":
598
+ """Create a LAG() window function builder."""
599
+ return WindowFunctionBuilder("lag")
600
+
601
+ @property
602
+ def lead_(self) -> "WindowFunctionBuilder":
603
+ """Create a LEAD() window function builder."""
604
+ return WindowFunctionBuilder("lead")
605
+
606
+ @property
607
+ def exists_(self) -> "SubqueryBuilder":
608
+ """Create an EXISTS subquery builder."""
609
+ return SubqueryBuilder("exists")
610
+
611
+ @property
612
+ def in_(self) -> "SubqueryBuilder":
613
+ """Create an IN subquery builder."""
614
+ return SubqueryBuilder("in")
615
+
616
+ @property
617
+ def any_(self) -> "SubqueryBuilder":
618
+ """Create an ANY subquery builder."""
619
+ return SubqueryBuilder("any")
620
+
621
+ @property
622
+ def all_(self) -> "SubqueryBuilder":
623
+ """Create an ALL subquery builder."""
624
+ return SubqueryBuilder("all")
625
+
626
+ @property
627
+ def inner_join_(self) -> "JoinBuilder":
628
+ """Create an INNER JOIN builder."""
629
+ return JoinBuilder("inner join")
630
+
631
+ @property
632
+ def left_join_(self) -> "JoinBuilder":
633
+ """Create a LEFT JOIN builder."""
634
+ return JoinBuilder("left join")
635
+
636
+ @property
637
+ def right_join_(self) -> "JoinBuilder":
638
+ """Create a RIGHT JOIN builder."""
639
+ return JoinBuilder("right join")
640
+
641
+ @property
642
+ def full_join_(self) -> "JoinBuilder":
643
+ """Create a FULL OUTER JOIN builder."""
644
+ return JoinBuilder("full join")
645
+
646
+ @property
647
+ def cross_join_(self) -> "JoinBuilder":
648
+ """Create a CROSS JOIN builder."""
649
+ return JoinBuilder("cross join")
650
+
651
+ def __getattr__(self, name: str) -> "Column":
652
+ """Dynamically create column references.
653
+
654
+ Args:
655
+ name: Column name.
656
+
657
+ Returns:
658
+ Column object for the given name.
659
+
660
+ Note:
661
+ Special SQL constructs like case_, row_number_, etc. are now
662
+ handled as properties for better type safety.
663
+ """
664
+ return Column(name)
665
+
666
+ # ===================
667
+ # Raw SQL Expressions
668
+ # ===================
669
+
670
+ @staticmethod
671
+ def raw(sql_fragment: str, **parameters: Any) -> "Union[exp.Expression, SQL]":
672
+ """Create a raw SQL expression from a string fragment with optional parameters.
673
+
674
+ This method makes it explicit that you are passing raw SQL that should
675
+ be parsed and included directly in the query. Useful for complex expressions,
676
+ database-specific functions, or when you need precise control over the SQL.
677
+
678
+ Args:
679
+ sql_fragment: Raw SQL string to parse into an expression.
680
+ **parameters: Named parameters for parameter binding.
681
+
682
+ Returns:
683
+ SQLGlot expression from the parsed SQL fragment (if no parameters).
684
+ SQL statement object (if parameters provided).
685
+
686
+ Raises:
687
+ SQLBuilderError: If the SQL fragment cannot be parsed.
688
+
689
+ Example:
690
+ ```python
691
+ # Raw expression without parameters (current behavior)
692
+ expr = sql.raw("COALESCE(name, 'Unknown')")
693
+
694
+ # Raw SQL with named parameters (new functionality)
695
+ stmt = sql.raw(
696
+ "LOWER(name) LIKE LOWER(:pattern)", pattern=f"%{query}%"
697
+ )
698
+
699
+ # Raw complex expression with parameters
700
+ expr = sql.raw(
701
+ "price BETWEEN :min_price AND :max_price",
702
+ min_price=100,
703
+ max_price=500,
704
+ )
705
+
706
+ # Raw window function
707
+ query = sql.select(
708
+ "name",
709
+ sql.raw(
710
+ "ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC)"
711
+ ),
712
+ ).from_("employees")
713
+ ```
714
+ """
715
+ if not parameters:
716
+ # Original behavior - return pure expression
717
+ try:
718
+ parsed: Optional[exp.Expression] = exp.maybe_parse(sql_fragment)
719
+ if parsed is not None:
720
+ return parsed
721
+ if sql_fragment.strip().replace("_", "").replace(".", "").isalnum():
722
+ return exp.to_identifier(sql_fragment)
723
+ return exp.Literal.string(sql_fragment)
724
+ except Exception as e:
725
+ msg = f"Failed to parse raw SQL fragment '{sql_fragment}': {e}"
726
+ raise SQLBuilderError(msg) from e
727
+
728
+ # New behavior - return SQL statement with parameters
729
+ from sqlspec.core.statement import SQL
730
+
731
+ return SQL(sql_fragment, parameters)
732
+
733
+ # ===================
734
+ # Aggregate Functions
735
+ # ===================
736
+
737
+ @staticmethod
738
+ def count(column: Union[str, exp.Expression] = "*", distinct: bool = False) -> exp.Expression:
739
+ """Create a COUNT expression.
740
+
741
+ Args:
742
+ column: Column to count (default "*").
743
+ distinct: Whether to use COUNT DISTINCT.
744
+
745
+ Returns:
746
+ COUNT expression.
747
+ """
748
+ if column == "*":
749
+ return exp.Count(this=exp.Star(), distinct=distinct)
750
+ col_expr = exp.column(column) if isinstance(column, str) else column
751
+ return exp.Count(this=col_expr, distinct=distinct)
752
+
753
+ def count_distinct(self, column: Union[str, exp.Expression]) -> exp.Expression:
754
+ """Create a COUNT(DISTINCT column) expression.
755
+
756
+ Args:
757
+ column: Column to count distinct values.
758
+
759
+ Returns:
760
+ COUNT DISTINCT expression.
761
+ """
762
+ return self.count(column, distinct=True)
763
+
764
+ @staticmethod
765
+ def sum(column: Union[str, exp.Expression], distinct: bool = False) -> exp.Expression:
766
+ """Create a SUM expression.
767
+
768
+ Args:
769
+ column: Column to sum.
770
+ distinct: Whether to use SUM DISTINCT.
771
+
772
+ Returns:
773
+ SUM expression.
774
+ """
775
+ col_expr = exp.column(column) if isinstance(column, str) else column
776
+ return exp.Sum(this=col_expr, distinct=distinct)
777
+
778
+ @staticmethod
779
+ def avg(column: Union[str, exp.Expression]) -> exp.Expression:
780
+ """Create an AVG expression.
781
+
782
+ Args:
783
+ column: Column to average.
784
+
785
+ Returns:
786
+ AVG expression.
787
+ """
788
+ col_expr = exp.column(column) if isinstance(column, str) else column
789
+ return exp.Avg(this=col_expr)
790
+
791
+ @staticmethod
792
+ def max(column: Union[str, exp.Expression]) -> exp.Expression:
793
+ """Create a MAX expression.
794
+
795
+ Args:
796
+ column: Column to find maximum.
797
+
798
+ Returns:
799
+ MAX expression.
800
+ """
801
+ col_expr = exp.column(column) if isinstance(column, str) else column
802
+ return exp.Max(this=col_expr)
803
+
804
+ @staticmethod
805
+ def min(column: Union[str, exp.Expression]) -> exp.Expression:
806
+ """Create a MIN expression.
807
+
808
+ Args:
809
+ column: Column to find minimum.
810
+
811
+ Returns:
812
+ MIN expression.
813
+ """
814
+ col_expr = exp.column(column) if isinstance(column, str) else column
815
+ return exp.Min(this=col_expr)
816
+
817
+ # ===================
818
+ # Advanced SQL Operations
819
+ # ===================
820
+
821
+ @staticmethod
822
+ def rollup(*columns: Union[str, exp.Expression]) -> exp.Expression:
823
+ """Create a ROLLUP expression for GROUP BY clauses.
824
+
825
+ Args:
826
+ *columns: Columns to include in the rollup.
827
+
828
+ Returns:
829
+ ROLLUP expression.
830
+
831
+ Example:
832
+ ```python
833
+ # GROUP BY ROLLUP(product, region)
834
+ query = (
835
+ sql.select("product", "region", sql.sum("sales"))
836
+ .from_("sales_data")
837
+ .group_by(sql.rollup("product", "region"))
838
+ )
839
+ ```
840
+ """
841
+ column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
842
+ return exp.Rollup(expressions=column_exprs)
843
+
844
+ @staticmethod
845
+ def cube(*columns: Union[str, exp.Expression]) -> exp.Expression:
846
+ """Create a CUBE expression for GROUP BY clauses.
847
+
848
+ Args:
849
+ *columns: Columns to include in the cube.
850
+
851
+ Returns:
852
+ CUBE expression.
853
+
854
+ Example:
855
+ ```python
856
+ # GROUP BY CUBE(product, region)
857
+ query = (
858
+ sql.select("product", "region", sql.sum("sales"))
859
+ .from_("sales_data")
860
+ .group_by(sql.cube("product", "region"))
861
+ )
862
+ ```
863
+ """
864
+ column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
865
+ return exp.Cube(expressions=column_exprs)
866
+
867
+ @staticmethod
868
+ def grouping_sets(*column_sets: Union[tuple[str, ...], list[str]]) -> exp.Expression:
869
+ """Create a GROUPING SETS expression for GROUP BY clauses.
870
+
871
+ Args:
872
+ *column_sets: Sets of columns to group by.
873
+
874
+ Returns:
875
+ GROUPING SETS expression.
876
+
877
+ Example:
878
+ ```python
879
+ # GROUP BY GROUPING SETS ((product), (region), ())
880
+ query = (
881
+ sql.select("product", "region", sql.sum("sales"))
882
+ .from_("sales_data")
883
+ .group_by(
884
+ sql.grouping_sets(("product",), ("region",), ())
885
+ )
886
+ )
887
+ ```
888
+ """
889
+ set_expressions = []
890
+ for column_set in column_sets:
891
+ if isinstance(column_set, (tuple, list)):
892
+ if len(column_set) == 0:
893
+ set_expressions.append(exp.Tuple(expressions=[]))
894
+ else:
895
+ columns = [exp.column(col) for col in column_set]
896
+ set_expressions.append(exp.Tuple(expressions=columns))
897
+ else:
898
+ set_expressions.append(exp.column(column_set))
899
+
900
+ return exp.GroupingSets(expressions=set_expressions)
901
+
902
+ @staticmethod
903
+ def any(values: Union[list[Any], exp.Expression, str]) -> exp.Expression:
904
+ """Create an ANY expression for use with comparison operators.
905
+
906
+ Args:
907
+ values: Values, expression, or subquery for the ANY clause.
908
+
909
+ Returns:
910
+ ANY expression.
911
+
912
+ Example:
913
+ ```python
914
+ # WHERE id = ANY(subquery)
915
+ subquery = sql.select("user_id").from_("active_users")
916
+ query = (
917
+ sql.select("*")
918
+ .from_("users")
919
+ .where(sql.id.eq(sql.any(subquery)))
920
+ )
921
+ ```
922
+ """
923
+ if isinstance(values, list):
924
+ literals = [SQLFactory._to_literal(v) for v in values]
925
+ return exp.Any(this=exp.Array(expressions=literals))
926
+ if isinstance(values, str):
927
+ # Parse as SQL
928
+ parsed = exp.maybe_parse(values) # type: ignore[var-annotated]
929
+ if parsed:
930
+ return exp.Any(this=parsed)
931
+ return exp.Any(this=exp.Literal.string(values))
932
+ return exp.Any(this=values)
933
+
934
+ @staticmethod
935
+ def not_any_(values: Union[list[Any], exp.Expression, str]) -> exp.Expression:
936
+ """Create a NOT ANY expression for use with comparison operators.
937
+
938
+ Args:
939
+ values: Values, expression, or subquery for the NOT ANY clause.
940
+
941
+ Returns:
942
+ NOT ANY expression.
943
+
944
+ Example:
945
+ ```python
946
+ # WHERE id <> ANY(subquery)
947
+ subquery = sql.select("user_id").from_("blocked_users")
948
+ query = (
949
+ sql.select("*")
950
+ .from_("users")
951
+ .where(sql.id.neq(sql.not_any(subquery)))
952
+ )
953
+ ```
954
+ """
955
+ return SQLFactory.any(values) # NOT ANY is handled by the comparison operator
956
+
957
+ # ===================
958
+ # String Functions
959
+ # ===================
960
+
961
+ @staticmethod
962
+ def concat(*expressions: Union[str, exp.Expression]) -> exp.Expression:
963
+ """Create a CONCAT expression.
964
+
965
+ Args:
966
+ *expressions: Expressions to concatenate.
967
+
968
+ Returns:
969
+ CONCAT expression.
970
+ """
971
+ exprs = [exp.column(expr) if isinstance(expr, str) else expr for expr in expressions]
972
+ return exp.Concat(expressions=exprs)
973
+
974
+ @staticmethod
975
+ def upper(column: Union[str, exp.Expression]) -> exp.Expression:
976
+ """Create an UPPER expression.
977
+
978
+ Args:
979
+ column: Column to convert to uppercase.
980
+
981
+ Returns:
982
+ UPPER expression.
983
+ """
984
+ col_expr = exp.column(column) if isinstance(column, str) else column
985
+ return exp.Upper(this=col_expr)
986
+
987
+ @staticmethod
988
+ def lower(column: Union[str, exp.Expression]) -> exp.Expression:
989
+ """Create a LOWER expression.
990
+
991
+ Args:
992
+ column: Column to convert to lowercase.
993
+
994
+ Returns:
995
+ LOWER expression.
996
+ """
997
+ col_expr = exp.column(column) if isinstance(column, str) else column
998
+ return exp.Lower(this=col_expr)
999
+
1000
+ @staticmethod
1001
+ def length(column: Union[str, exp.Expression]) -> exp.Expression:
1002
+ """Create a LENGTH expression.
1003
+
1004
+ Args:
1005
+ column: Column to get length of.
1006
+
1007
+ Returns:
1008
+ LENGTH expression.
1009
+ """
1010
+ col_expr = exp.column(column) if isinstance(column, str) else column
1011
+ return exp.Length(this=col_expr)
1012
+
1013
+ # ===================
1014
+ # Math Functions
1015
+ # ===================
1016
+
1017
+ @staticmethod
1018
+ def round(column: Union[str, exp.Expression], decimals: int = 0) -> exp.Expression:
1019
+ """Create a ROUND expression.
1020
+
1021
+ Args:
1022
+ column: Column to round.
1023
+ decimals: Number of decimal places.
1024
+
1025
+ Returns:
1026
+ ROUND expression.
1027
+ """
1028
+ col_expr = exp.column(column) if isinstance(column, str) else column
1029
+ if decimals == 0:
1030
+ return exp.Round(this=col_expr)
1031
+ return exp.Round(this=col_expr, expression=exp.Literal.number(decimals))
1032
+
1033
+ # ===================
1034
+ # Conversion Functions
1035
+ # ===================
1036
+
1037
+ @staticmethod
1038
+ def _to_literal(value: Any) -> exp.Expression:
1039
+ """Convert a Python value to a SQLGlot literal expression.
1040
+
1041
+ Uses SQLGlot's built-in exp.convert() function for optimal dialect-agnostic
1042
+ literal creation. Handles all Python primitive types correctly:
1043
+ - None -> exp.Null (renders as NULL)
1044
+ - bool -> exp.Boolean (renders as TRUE/FALSE or 1/0 based on dialect)
1045
+ - int/float -> exp.Literal with is_number=True
1046
+ - str -> exp.Literal with is_string=True
1047
+ - exp.Expression -> returned as-is (passthrough)
1048
+
1049
+ Args:
1050
+ value: Python value or SQLGlot expression to convert.
1051
+
1052
+ Returns:
1053
+ SQLGlot expression representing the literal value.
1054
+ """
1055
+ if isinstance(value, exp.Expression):
1056
+ return value
1057
+ return exp.convert(value)
1058
+
1059
+ @staticmethod
1060
+ def decode(column: Union[str, exp.Expression], *args: Union[str, exp.Expression, Any]) -> exp.Expression:
1061
+ """Create a DECODE expression (Oracle-style conditional logic).
1062
+
1063
+ DECODE compares column to each search value and returns the corresponding result.
1064
+ If no match is found, returns the default value (if provided) or NULL.
1065
+
1066
+ Args:
1067
+ column: Column to compare.
1068
+ *args: Alternating search values and results, with optional default at the end.
1069
+ Format: search1, result1, search2, result2, ..., [default]
1070
+
1071
+ Raises:
1072
+ ValueError: If fewer than two search/result pairs are provided.
1073
+
1074
+ Returns:
1075
+ CASE expression equivalent to DECODE.
1076
+
1077
+ Example:
1078
+ ```python
1079
+ # DECODE(status, 'A', 'Active', 'I', 'Inactive', 'Unknown')
1080
+ sql.decode(
1081
+ "status", "A", "Active", "I", "Inactive", "Unknown"
1082
+ )
1083
+ ```
1084
+ """
1085
+ col_expr = exp.column(column) if isinstance(column, str) else column
1086
+
1087
+ if len(args) < MIN_DECODE_ARGS:
1088
+ msg = "DECODE requires at least one search/result pair"
1089
+ raise ValueError(msg)
1090
+
1091
+ conditions = []
1092
+ default = None
1093
+
1094
+ for i in range(0, len(args) - 1, 2):
1095
+ if i + 1 >= len(args):
1096
+ # Odd number of args means last one is default
1097
+ default = SQLFactory._to_literal(args[i])
1098
+ break
1099
+
1100
+ search_val = args[i]
1101
+ result_val = args[i + 1]
1102
+
1103
+ search_expr = SQLFactory._to_literal(search_val)
1104
+ result_expr = SQLFactory._to_literal(result_val)
1105
+
1106
+ condition = exp.EQ(this=col_expr, expression=search_expr)
1107
+ conditions.append(exp.When(this=condition, then=result_expr))
1108
+
1109
+ return exp.Case(ifs=conditions, default=default)
1110
+
1111
+ @staticmethod
1112
+ def cast(column: Union[str, exp.Expression], data_type: str) -> exp.Expression:
1113
+ """Create a CAST expression for type conversion.
1114
+
1115
+ Args:
1116
+ column: Column or expression to cast.
1117
+ data_type: Target data type (e.g., 'INT', 'VARCHAR(100)', 'DECIMAL(10,2)').
1118
+
1119
+ Returns:
1120
+ CAST expression.
1121
+ """
1122
+ col_expr = exp.column(column) if isinstance(column, str) else column
1123
+ return exp.Cast(this=col_expr, to=exp.DataType.build(data_type))
1124
+
1125
+ @staticmethod
1126
+ def coalesce(*expressions: Union[str, exp.Expression]) -> exp.Expression:
1127
+ """Create a COALESCE expression.
1128
+
1129
+ Args:
1130
+ *expressions: Expressions to coalesce.
1131
+
1132
+ Returns:
1133
+ COALESCE expression.
1134
+ """
1135
+ exprs = [exp.column(expr) if isinstance(expr, str) else expr for expr in expressions]
1136
+ return exp.Coalesce(expressions=exprs)
1137
+
1138
+ @staticmethod
1139
+ def nvl(column: Union[str, exp.Expression], substitute_value: Union[str, exp.Expression, Any]) -> exp.Expression:
1140
+ """Create an NVL (Oracle-style) expression using COALESCE.
1141
+
1142
+ Args:
1143
+ column: Column to check for NULL.
1144
+ substitute_value: Value to use if column is NULL.
1145
+
1146
+ Returns:
1147
+ COALESCE expression equivalent to NVL.
1148
+ """
1149
+ col_expr = exp.column(column) if isinstance(column, str) else column
1150
+ sub_expr = SQLFactory._to_literal(substitute_value)
1151
+ return exp.Coalesce(expressions=[col_expr, sub_expr])
1152
+
1153
+ @staticmethod
1154
+ def nvl2(
1155
+ column: Union[str, exp.Expression],
1156
+ value_if_not_null: Union[str, exp.Expression, Any],
1157
+ value_if_null: Union[str, exp.Expression, Any],
1158
+ ) -> exp.Expression:
1159
+ """Create an NVL2 (Oracle-style) expression using CASE.
1160
+
1161
+ NVL2 returns value_if_not_null if column is not NULL,
1162
+ otherwise returns value_if_null.
1163
+
1164
+ Args:
1165
+ column: Column to check for NULL.
1166
+ value_if_not_null: Value to use if column is NOT NULL.
1167
+ value_if_null: Value to use if column is NULL.
1168
+
1169
+ Returns:
1170
+ CASE expression equivalent to NVL2.
1171
+
1172
+ Example:
1173
+ ```python
1174
+ # NVL2(salary, 'Has Salary', 'No Salary')
1175
+ sql.nvl2("salary", "Has Salary", "No Salary")
1176
+ ```
1177
+ """
1178
+ col_expr = exp.column(column) if isinstance(column, str) else column
1179
+ not_null_expr = SQLFactory._to_literal(value_if_not_null)
1180
+ null_expr = SQLFactory._to_literal(value_if_null)
1181
+
1182
+ # Create CASE WHEN column IS NOT NULL THEN value_if_not_null ELSE value_if_null END
1183
+ is_null = exp.Is(this=col_expr, expression=exp.Null())
1184
+ condition = exp.Not(this=is_null)
1185
+ when_clause = exp.If(this=condition, true=not_null_expr)
1186
+
1187
+ return exp.Case(ifs=[when_clause], default=null_expr)
1188
+
1189
+ # ===================
1190
+ # Bulk Operations
1191
+ # ===================
1192
+
1193
+ @staticmethod
1194
+ def bulk_insert(table_name: str, column_count: int, placeholder_style: str = "?") -> exp.Expression:
1195
+ """Create bulk INSERT expression for executemany operations.
1196
+
1197
+ This is specifically for bulk loading operations like CSV ingestion where
1198
+ we need an INSERT expression with placeholders for executemany().
1199
+
1200
+ Args:
1201
+ table_name: Name of the table to insert into
1202
+ column_count: Number of columns (for placeholder generation)
1203
+ placeholder_style: Placeholder style ("?" for SQLite/PostgreSQL, "%s" for MySQL, ":1" for Oracle)
1204
+
1205
+ Returns:
1206
+ INSERT expression with proper placeholders for bulk operations
1207
+
1208
+ Example:
1209
+ ```python
1210
+ from sqlspec import sql
1211
+
1212
+ # SQLite/PostgreSQL style
1213
+ insert_expr = sql.bulk_insert("my_table", 3)
1214
+ # Creates: INSERT INTO "my_table" VALUES (?, ?, ?)
1215
+
1216
+ # MySQL style
1217
+ insert_expr = sql.bulk_insert(
1218
+ "my_table", 3, placeholder_style="%s"
1219
+ )
1220
+ # Creates: INSERT INTO "my_table" VALUES (%s, %s, %s)
1221
+
1222
+ # Oracle style
1223
+ insert_expr = sql.bulk_insert(
1224
+ "my_table", 3, placeholder_style=":1"
1225
+ )
1226
+ # Creates: INSERT INTO "my_table" VALUES (:1, :2, :3)
1227
+ ```
1228
+ """
1229
+ return exp.Insert(
1230
+ this=exp.Table(this=exp.to_identifier(table_name)),
1231
+ expression=exp.Values(
1232
+ expressions=[
1233
+ exp.Tuple(expressions=[exp.Placeholder(this=placeholder_style) for _ in range(column_count)])
1234
+ ]
1235
+ ),
1236
+ )
1237
+
1238
+ def truncate(self, table_name: str) -> "Truncate":
1239
+ """Create a TRUNCATE TABLE builder.
1240
+
1241
+ Args:
1242
+ table_name: Name of the table to truncate
1243
+
1244
+ Returns:
1245
+ TruncateTable builder instance
1246
+
1247
+ Example:
1248
+ ```python
1249
+ from sqlspec import sql
1250
+
1251
+ # Simple truncate
1252
+ truncate_sql = sql.truncate_table("my_table").build().sql
1253
+
1254
+ # Truncate with options
1255
+ truncate_sql = (
1256
+ sql.truncate_table("my_table")
1257
+ .cascade()
1258
+ .restart_identity()
1259
+ .build()
1260
+ .sql
1261
+ )
1262
+ ```
1263
+ """
1264
+ builder = Truncate(dialect=self.dialect)
1265
+ builder._table_name = table_name
1266
+ return builder
1267
+
1268
+ # ===================
1269
+ # Case Expressions
1270
+ # ===================
1271
+
1272
+ @staticmethod
1273
+ def case() -> "Case":
1274
+ """Create a CASE expression builder.
1275
+
1276
+ Returns:
1277
+ CaseExpressionBuilder for building CASE expressions.
1278
+ """
1279
+ return Case()
1280
+
1281
+ # ===================
1282
+ # Window Functions
1283
+ # ===================
1284
+
1285
+ def row_number(
1286
+ self,
1287
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
1288
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
1289
+ ) -> exp.Expression:
1290
+ """Create a ROW_NUMBER() window function.
1291
+
1292
+ Args:
1293
+ partition_by: Columns to partition by.
1294
+ order_by: Columns to order by.
1295
+
1296
+ Returns:
1297
+ ROW_NUMBER window function expression.
1298
+ """
1299
+ return self._create_window_function("ROW_NUMBER", [], partition_by, order_by)
1300
+
1301
+ def rank(
1302
+ self,
1303
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
1304
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
1305
+ ) -> exp.Expression:
1306
+ """Create a RANK() window function.
1307
+
1308
+ Args:
1309
+ partition_by: Columns to partition by.
1310
+ order_by: Columns to order by.
1311
+
1312
+ Returns:
1313
+ RANK window function expression.
1314
+ """
1315
+ return self._create_window_function("RANK", [], partition_by, order_by)
1316
+
1317
+ def dense_rank(
1318
+ self,
1319
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
1320
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
1321
+ ) -> exp.Expression:
1322
+ """Create a DENSE_RANK() window function.
1323
+
1324
+ Args:
1325
+ partition_by: Columns to partition by.
1326
+ order_by: Columns to order by.
1327
+
1328
+ Returns:
1329
+ DENSE_RANK window function expression.
1330
+ """
1331
+ return self._create_window_function("DENSE_RANK", [], partition_by, order_by)
1332
+
1333
+ @staticmethod
1334
+ def _create_window_function(
1335
+ func_name: str,
1336
+ func_args: list[exp.Expression],
1337
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
1338
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
1339
+ ) -> exp.Expression:
1340
+ """Helper to create window function expressions.
1341
+
1342
+ Args:
1343
+ func_name: Name of the window function.
1344
+ func_args: Arguments to the function.
1345
+ partition_by: Columns to partition by.
1346
+ order_by: Columns to order by.
1347
+
1348
+ Returns:
1349
+ Window function expression.
1350
+ """
1351
+ func_expr = exp.Anonymous(this=func_name, expressions=func_args)
1352
+
1353
+ over_args: dict[str, Any] = {}
1354
+
1355
+ if partition_by:
1356
+ if isinstance(partition_by, str):
1357
+ over_args["partition_by"] = [exp.column(partition_by)]
1358
+ elif isinstance(partition_by, list):
1359
+ over_args["partition_by"] = [exp.column(col) for col in partition_by]
1360
+ elif isinstance(partition_by, exp.Expression):
1361
+ over_args["partition_by"] = [partition_by]
1362
+
1363
+ if order_by:
1364
+ if isinstance(order_by, str):
1365
+ over_args["order"] = [exp.column(order_by).asc()]
1366
+ elif isinstance(order_by, list):
1367
+ over_args["order"] = [exp.column(col).asc() for col in order_by]
1368
+ elif isinstance(order_by, exp.Expression):
1369
+ over_args["order"] = [order_by]
1370
+
1371
+ return exp.Window(this=func_expr, **over_args)
1372
+
1373
+
1374
+ @trait
1375
+ class Case:
1376
+ """Builder for CASE expressions using the SQL factory.
1377
+
1378
+ Example:
1379
+ ```python
1380
+ from sqlspec import sql
1381
+
1382
+ case_expr = (
1383
+ sql.case()
1384
+ .when(sql.age < 18, "Minor")
1385
+ .when(sql.age < 65, "Adult")
1386
+ .else_("Senior")
1387
+ .end()
1388
+ )
1389
+ ```
1390
+ """
1391
+
1392
+ def __init__(self) -> None:
1393
+ """Initialize the CASE expression builder."""
1394
+ self._conditions: list[exp.If] = []
1395
+ self._default: Optional[exp.Expression] = None
1396
+
1397
+ def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
1398
+ """Equal to (==) - convert to expression then compare."""
1399
+ from sqlspec.builder._column import ColumnExpression
1400
+
1401
+ case_expr = exp.Case(ifs=self._conditions, default=self._default)
1402
+ if other is None:
1403
+ return ColumnExpression(exp.Is(this=case_expr, expression=exp.Null()))
1404
+ return ColumnExpression(exp.EQ(this=case_expr, expression=exp.convert(other)))
1405
+
1406
+ def __hash__(self) -> int:
1407
+ """Make Case hashable."""
1408
+ return hash(id(self))
1409
+
1410
+ def when(self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]) -> "Case":
1411
+ """Add a WHEN clause.
1412
+
1413
+ Args:
1414
+ condition: Condition to test.
1415
+ value: Value to return if condition is true.
1416
+
1417
+ Returns:
1418
+ Self for method chaining.
1419
+ """
1420
+ cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
1421
+ val_expr = SQLFactory._to_literal(value)
1422
+
1423
+ # SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
1424
+ when_clause = exp.If(this=cond_expr, true=val_expr)
1425
+ self._conditions.append(when_clause)
1426
+ return self
1427
+
1428
+ def else_(self, value: Union[str, exp.Expression, Any]) -> "Case":
1429
+ """Add an ELSE clause.
1430
+
1431
+ Args:
1432
+ value: Default value to return.
1433
+
1434
+ Returns:
1435
+ Self for method chaining.
1436
+ """
1437
+ self._default = SQLFactory._to_literal(value)
1438
+ return self
1439
+
1440
+ def end(self) -> exp.Expression:
1441
+ """Complete the CASE expression.
1442
+
1443
+ Returns:
1444
+ Complete CASE expression.
1445
+ """
1446
+ return exp.Case(ifs=self._conditions, default=self._default)
1447
+
1448
+ def as_(self, alias: str) -> exp.Alias:
1449
+ """Complete the CASE expression with an alias.
1450
+
1451
+ Args:
1452
+ alias: Alias name for the CASE expression.
1453
+
1454
+ Returns:
1455
+ Aliased CASE expression.
1456
+ """
1457
+ case_expr = exp.Case(ifs=self._conditions, default=self._default)
1458
+ return cast("exp.Alias", exp.alias_(case_expr, alias))
1459
+
1460
+
1461
+ @trait
1462
+ class WindowFunctionBuilder:
1463
+ """Builder for window functions with fluent syntax.
1464
+
1465
+ Example:
1466
+ ```python
1467
+ from sqlspec import sql
1468
+
1469
+ # sql.row_number_.partition_by("department").order_by("salary")
1470
+ window_func = (
1471
+ sql.row_number_.partition_by("department")
1472
+ .order_by("salary")
1473
+ .as_("row_num")
1474
+ )
1475
+ ```
1476
+ """
1477
+
1478
+ def __init__(self, function_name: str) -> None:
1479
+ """Initialize the window function builder.
1480
+
1481
+ Args:
1482
+ function_name: Name of the window function (row_number, rank, etc.)
1483
+ """
1484
+ self._function_name = function_name
1485
+ self._partition_by_cols: list[exp.Expression] = []
1486
+ self._order_by_cols: list[exp.Expression] = []
1487
+ self._alias: Optional[str] = None
1488
+
1489
+ def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
1490
+ """Equal to (==) - convert to expression then compare."""
1491
+ from sqlspec.builder._column import ColumnExpression
1492
+
1493
+ window_expr = self._build_expression()
1494
+ if other is None:
1495
+ return ColumnExpression(exp.Is(this=window_expr, expression=exp.Null()))
1496
+ return ColumnExpression(exp.EQ(this=window_expr, expression=exp.convert(other)))
1497
+
1498
+ def __hash__(self) -> int:
1499
+ """Make WindowFunctionBuilder hashable."""
1500
+ return hash(id(self))
1501
+
1502
+ def partition_by(self, *columns: Union[str, exp.Expression]) -> "WindowFunctionBuilder":
1503
+ """Add PARTITION BY clause.
1504
+
1505
+ Args:
1506
+ *columns: Columns to partition by.
1507
+
1508
+ Returns:
1509
+ Self for method chaining.
1510
+ """
1511
+ for col in columns:
1512
+ col_expr = exp.column(col) if isinstance(col, str) else col
1513
+ self._partition_by_cols.append(col_expr)
1514
+ return self
1515
+
1516
+ def order_by(self, *columns: Union[str, exp.Expression]) -> "WindowFunctionBuilder":
1517
+ """Add ORDER BY clause.
1518
+
1519
+ Args:
1520
+ *columns: Columns to order by.
1521
+
1522
+ Returns:
1523
+ Self for method chaining.
1524
+ """
1525
+ for col in columns:
1526
+ if isinstance(col, str):
1527
+ col_expr = exp.column(col).asc()
1528
+ self._order_by_cols.append(col_expr)
1529
+ else:
1530
+ # Convert to ordered expression
1531
+ self._order_by_cols.append(exp.Ordered(this=col, desc=False))
1532
+ return self
1533
+
1534
+ def as_(self, alias: str) -> exp.Expression:
1535
+ """Complete the window function with an alias.
1536
+
1537
+ Args:
1538
+ alias: Alias name for the window function.
1539
+
1540
+ Returns:
1541
+ Aliased window function expression.
1542
+ """
1543
+ window_expr = self._build_expression()
1544
+ return cast("exp.Alias", exp.alias_(window_expr, alias))
1545
+
1546
+ def build(self) -> exp.Expression:
1547
+ """Complete the window function without an alias.
1548
+
1549
+ Returns:
1550
+ Window function expression.
1551
+ """
1552
+ return self._build_expression()
1553
+
1554
+ def _build_expression(self) -> exp.Expression:
1555
+ """Build the complete window function expression."""
1556
+ # Create the function expression
1557
+ func_expr = exp.Anonymous(this=self._function_name.upper(), expressions=[])
1558
+
1559
+ # Build the OVER clause arguments
1560
+ over_args: dict[str, Any] = {}
1561
+
1562
+ if self._partition_by_cols:
1563
+ over_args["partition_by"] = self._partition_by_cols
1564
+
1565
+ if self._order_by_cols:
1566
+ over_args["order"] = exp.Order(expressions=self._order_by_cols)
1567
+
1568
+ return exp.Window(this=func_expr, **over_args)
1569
+
1570
+
1571
+ @trait
1572
+ class SubqueryBuilder:
1573
+ """Builder for subquery operations with fluent syntax.
1574
+
1575
+ Example:
1576
+ ```python
1577
+ from sqlspec import sql
1578
+
1579
+ # sql.exists_(subquery)
1580
+ exists_check = sql.exists_(
1581
+ sql.select("1")
1582
+ .from_("orders")
1583
+ .where_eq("user_id", sql.users.id)
1584
+ )
1585
+
1586
+ # sql.in_(subquery)
1587
+ in_check = sql.in_(
1588
+ sql.select("category_id")
1589
+ .from_("categories")
1590
+ .where_eq("active", True)
1591
+ )
1592
+ ```
1593
+ """
1594
+
1595
+ def __init__(self, operation: str) -> None:
1596
+ """Initialize the subquery builder.
1597
+
1598
+ Args:
1599
+ operation: Type of subquery operation (exists, in, any, all)
1600
+ """
1601
+ self._operation = operation
1602
+
1603
+ def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
1604
+ """Equal to (==) - not typically used but needed for type consistency."""
1605
+ from sqlspec.builder._column import ColumnExpression
1606
+
1607
+ # SubqueryBuilder doesn't have a direct expression, so this is a placeholder
1608
+ # In practice, this shouldn't be called as subqueries are used differently
1609
+ placeholder_expr = exp.Literal.string(f"subquery_{self._operation}")
1610
+ if other is None:
1611
+ return ColumnExpression(exp.Is(this=placeholder_expr, expression=exp.Null()))
1612
+ return ColumnExpression(exp.EQ(this=placeholder_expr, expression=exp.convert(other)))
1613
+
1614
+ def __hash__(self) -> int:
1615
+ """Make SubqueryBuilder hashable."""
1616
+ return hash(id(self))
1617
+
1618
+ def __call__(self, subquery: Union[str, exp.Expression, Any]) -> exp.Expression:
1619
+ """Build the subquery expression.
1620
+
1621
+ Args:
1622
+ subquery: The subquery - can be a SQL string, SelectBuilder, or expression
1623
+
1624
+ Returns:
1625
+ The subquery expression (EXISTS, IN, ANY, ALL, etc.)
1626
+ """
1627
+ subquery_expr: exp.Expression
1628
+ if isinstance(subquery, str):
1629
+ # Parse as SQL
1630
+ parsed: Optional[exp.Expression] = exp.maybe_parse(subquery)
1631
+ if not parsed:
1632
+ msg = f"Could not parse subquery SQL: {subquery}"
1633
+ raise SQLBuilderError(msg)
1634
+ subquery_expr = parsed
1635
+ elif hasattr(subquery, "build") and callable(getattr(subquery, "build", None)):
1636
+ # It's a query builder - build it to get the SQL and parse
1637
+ built_query = subquery.build() # pyright: ignore[reportAttributeAccessIssue]
1638
+ subquery_expr = exp.maybe_parse(built_query.sql)
1639
+ if not subquery_expr:
1640
+ msg = f"Could not parse built query: {built_query.sql}"
1641
+ raise SQLBuilderError(msg)
1642
+ elif isinstance(subquery, exp.Expression):
1643
+ subquery_expr = subquery
1644
+ else:
1645
+ # Try to convert to expression
1646
+ parsed = exp.maybe_parse(str(subquery))
1647
+ if not parsed:
1648
+ msg = f"Could not convert subquery to expression: {subquery}"
1649
+ raise SQLBuilderError(msg)
1650
+ subquery_expr = parsed
1651
+
1652
+ # Build the appropriate expression based on operation
1653
+ if self._operation == "exists":
1654
+ return exp.Exists(this=subquery_expr)
1655
+ if self._operation == "in":
1656
+ # For IN, we create a subquery that can be used with WHERE column IN (subquery)
1657
+ return exp.In(expressions=[subquery_expr])
1658
+ if self._operation == "any":
1659
+ return exp.Any(this=subquery_expr)
1660
+ if self._operation == "all":
1661
+ return exp.All(this=subquery_expr)
1662
+ msg = f"Unknown subquery operation: {self._operation}"
1663
+ raise SQLBuilderError(msg)
1664
+
1665
+
1666
+ @trait
1667
+ class JoinBuilder:
1668
+ """Builder for JOIN operations with fluent syntax.
1669
+
1670
+ Example:
1671
+ ```python
1672
+ from sqlspec import sql
1673
+
1674
+ # sql.left_join_("posts").on("users.id = posts.user_id")
1675
+ join_clause = sql.left_join_("posts").on(
1676
+ "users.id = posts.user_id"
1677
+ )
1678
+
1679
+ # Or with query builder
1680
+ query = (
1681
+ sql.select("users.name", "posts.title")
1682
+ .from_("users")
1683
+ .join(
1684
+ sql.left_join_("posts").on(
1685
+ "users.id = posts.user_id"
1686
+ )
1687
+ )
1688
+ )
1689
+ ```
1690
+ """
1691
+
1692
+ def __init__(self, join_type: str) -> None:
1693
+ """Initialize the join builder.
1694
+
1695
+ Args:
1696
+ join_type: Type of join (inner, left, right, full, cross)
1697
+ """
1698
+ self._join_type = join_type.upper()
1699
+ self._table: Optional[Union[str, exp.Expression]] = None
1700
+ self._condition: Optional[exp.Expression] = None
1701
+ self._alias: Optional[str] = None
1702
+
1703
+ def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
1704
+ """Equal to (==) - not typically used but needed for type consistency."""
1705
+ from sqlspec.builder._column import ColumnExpression
1706
+
1707
+ # JoinBuilder doesn't have a direct expression, so this is a placeholder
1708
+ # In practice, this shouldn't be called as joins are used differently
1709
+ placeholder_expr = exp.Literal.string(f"join_{self._join_type.lower()}")
1710
+ if other is None:
1711
+ return ColumnExpression(exp.Is(this=placeholder_expr, expression=exp.Null()))
1712
+ return ColumnExpression(exp.EQ(this=placeholder_expr, expression=exp.convert(other)))
1713
+
1714
+ def __hash__(self) -> int:
1715
+ """Make JoinBuilder hashable."""
1716
+ return hash(id(self))
1717
+
1718
+ def __call__(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> "JoinBuilder":
1719
+ """Set the table to join.
1720
+
1721
+ Args:
1722
+ table: Table name or expression to join
1723
+ alias: Optional alias for the table
1724
+
1725
+ Returns:
1726
+ Self for method chaining
1727
+ """
1728
+ self._table = table
1729
+ self._alias = alias
1730
+ return self
1731
+
1732
+ def on(self, condition: Union[str, exp.Expression]) -> exp.Expression:
1733
+ """Set the join condition and build the JOIN expression.
1734
+
1735
+ Args:
1736
+ condition: JOIN condition (e.g., "users.id = posts.user_id")
1737
+
1738
+ Returns:
1739
+ Complete JOIN expression
1740
+ """
1741
+ if not self._table:
1742
+ msg = "Table must be set before calling .on()"
1743
+ raise SQLBuilderError(msg)
1744
+
1745
+ # Parse the condition
1746
+ condition_expr: exp.Expression
1747
+ if isinstance(condition, str):
1748
+ parsed: Optional[exp.Expression] = exp.maybe_parse(condition)
1749
+ condition_expr = parsed or exp.condition(condition)
1750
+ else:
1751
+ condition_expr = condition
1752
+
1753
+ # Build table expression
1754
+ table_expr: exp.Expression
1755
+ if isinstance(self._table, str):
1756
+ table_expr = exp.to_table(self._table)
1757
+ if self._alias:
1758
+ table_expr = cast("exp.Expression", exp.alias_(table_expr, self._alias))
1759
+ else:
1760
+ table_expr = self._table
1761
+ if self._alias:
1762
+ table_expr = cast("exp.Expression", exp.alias_(table_expr, self._alias))
1763
+
1764
+ # Create the appropriate join type using same pattern as existing JoinClauseMixin
1765
+ if self._join_type == "INNER JOIN":
1766
+ return exp.Join(this=table_expr, on=condition_expr)
1767
+ if self._join_type == "LEFT JOIN":
1768
+ return exp.Join(this=table_expr, on=condition_expr, side="LEFT")
1769
+ if self._join_type == "RIGHT JOIN":
1770
+ return exp.Join(this=table_expr, on=condition_expr, side="RIGHT")
1771
+ if self._join_type == "FULL JOIN":
1772
+ return exp.Join(this=table_expr, on=condition_expr, side="FULL", kind="OUTER")
1773
+ if self._join_type == "CROSS JOIN":
1774
+ # CROSS JOIN doesn't use ON condition
1775
+ return exp.Join(this=table_expr, kind="CROSS")
1776
+ return exp.Join(this=table_expr, on=condition_expr)
1777
+
1778
+
1779
+ # Create a default SQL factory instance
1780
+ sql = SQLFactory()