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