sqlspec 0.13.1__py3-none-any.whl → 0.16.2__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (185) hide show
  1. sqlspec/__init__.py +71 -8
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +930 -136
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +116 -285
  10. sqlspec/adapters/adbc/driver.py +462 -340
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +202 -150
  14. sqlspec/adapters/aiosqlite/driver.py +226 -247
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -199
  18. sqlspec/adapters/asyncmy/driver.py +257 -215
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +81 -214
  22. sqlspec/adapters/asyncpg/driver.py +284 -359
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -299
  26. sqlspec/adapters/bigquery/driver.py +474 -634
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +414 -397
  30. sqlspec/adapters/duckdb/driver.py +342 -393
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -458
  34. sqlspec/adapters/oracledb/driver.py +505 -531
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -307
  38. sqlspec/adapters/psqlpy/driver.py +504 -213
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -472
  42. sqlspec/adapters/psycopg/driver.py +704 -825
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +208 -142
  46. sqlspec/adapters/sqlite/driver.py +263 -278
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
  50. sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
  51. sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
  53. sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
  54. sqlspec/builder/_insert.py +421 -0
  55. sqlspec/builder/_merge.py +71 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
  57. sqlspec/builder/_select.py +170 -0
  58. sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
  59. sqlspec/builder/mixins/__init__.py +55 -0
  60. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  61. sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
  62. sqlspec/builder/mixins/_insert_operations.py +244 -0
  63. sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
  64. sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
  65. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  66. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  67. sqlspec/builder/mixins/_select_operations.py +604 -0
  68. sqlspec/builder/mixins/_update_operations.py +202 -0
  69. sqlspec/builder/mixins/_where_clause.py +644 -0
  70. sqlspec/cli.py +247 -0
  71. sqlspec/config.py +183 -138
  72. sqlspec/core/__init__.py +63 -0
  73. sqlspec/core/cache.py +871 -0
  74. sqlspec/core/compiler.py +417 -0
  75. sqlspec/core/filters.py +830 -0
  76. sqlspec/core/hashing.py +310 -0
  77. sqlspec/core/parameters.py +1237 -0
  78. sqlspec/core/result.py +677 -0
  79. sqlspec/{statement → core}/splitter.py +321 -191
  80. sqlspec/core/statement.py +676 -0
  81. sqlspec/driver/__init__.py +7 -10
  82. sqlspec/driver/_async.py +422 -163
  83. sqlspec/driver/_common.py +545 -287
  84. sqlspec/driver/_sync.py +426 -160
  85. sqlspec/driver/mixins/__init__.py +2 -13
  86. sqlspec/driver/mixins/_result_tools.py +193 -0
  87. sqlspec/driver/mixins/_sql_translator.py +65 -14
  88. sqlspec/exceptions.py +5 -252
  89. sqlspec/extensions/aiosql/adapter.py +93 -96
  90. sqlspec/extensions/litestar/__init__.py +2 -1
  91. sqlspec/extensions/litestar/cli.py +48 -0
  92. sqlspec/extensions/litestar/config.py +0 -1
  93. sqlspec/extensions/litestar/handlers.py +15 -26
  94. sqlspec/extensions/litestar/plugin.py +21 -16
  95. sqlspec/extensions/litestar/providers.py +17 -52
  96. sqlspec/loader.py +423 -104
  97. sqlspec/migrations/__init__.py +35 -0
  98. sqlspec/migrations/base.py +414 -0
  99. sqlspec/migrations/commands.py +443 -0
  100. sqlspec/migrations/loaders.py +402 -0
  101. sqlspec/migrations/runner.py +213 -0
  102. sqlspec/migrations/tracker.py +140 -0
  103. sqlspec/migrations/utils.py +129 -0
  104. sqlspec/protocols.py +51 -186
  105. sqlspec/storage/__init__.py +1 -1
  106. sqlspec/storage/backends/base.py +37 -40
  107. sqlspec/storage/backends/fsspec.py +136 -112
  108. sqlspec/storage/backends/obstore.py +138 -160
  109. sqlspec/storage/capabilities.py +5 -4
  110. sqlspec/storage/registry.py +57 -106
  111. sqlspec/typing.py +136 -115
  112. sqlspec/utils/__init__.py +2 -2
  113. sqlspec/utils/correlation.py +0 -3
  114. sqlspec/utils/deprecation.py +6 -6
  115. sqlspec/utils/fixtures.py +6 -6
  116. sqlspec/utils/logging.py +0 -2
  117. sqlspec/utils/module_loader.py +7 -12
  118. sqlspec/utils/singleton.py +0 -1
  119. sqlspec/utils/sync_tools.py +17 -38
  120. sqlspec/utils/text.py +12 -51
  121. sqlspec/utils/type_guards.py +482 -235
  122. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
  123. sqlspec-0.16.2.dist-info/RECORD +134 -0
  124. sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
  125. sqlspec/driver/connection.py +0 -207
  126. sqlspec/driver/mixins/_csv_writer.py +0 -91
  127. sqlspec/driver/mixins/_pipeline.py +0 -512
  128. sqlspec/driver/mixins/_result_utils.py +0 -140
  129. sqlspec/driver/mixins/_storage.py +0 -926
  130. sqlspec/driver/mixins/_type_coercion.py +0 -130
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/service/__init__.py +0 -4
  133. sqlspec/service/_util.py +0 -147
  134. sqlspec/service/base.py +0 -1131
  135. sqlspec/service/pagination.py +0 -26
  136. sqlspec/statement/__init__.py +0 -21
  137. sqlspec/statement/builder/insert.py +0 -288
  138. sqlspec/statement/builder/merge.py +0 -95
  139. sqlspec/statement/builder/mixins/__init__.py +0 -65
  140. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  141. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  142. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  143. sqlspec/statement/builder/mixins/_from.py +0 -63
  144. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  145. sqlspec/statement/builder/mixins/_having.py +0 -35
  146. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  147. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  148. sqlspec/statement/builder/mixins/_insert_values.py +0 -67
  149. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  150. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  151. sqlspec/statement/builder/mixins/_pivot.py +0 -79
  152. sqlspec/statement/builder/mixins/_returning.py +0 -37
  153. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  154. sqlspec/statement/builder/mixins/_set_ops.py +0 -122
  155. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  156. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  157. sqlspec/statement/builder/mixins/_update_set.py +0 -94
  158. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  159. sqlspec/statement/builder/mixins/_where.py +0 -401
  160. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  161. sqlspec/statement/builder/select.py +0 -221
  162. sqlspec/statement/filters.py +0 -596
  163. sqlspec/statement/parameter_manager.py +0 -220
  164. sqlspec/statement/parameters.py +0 -867
  165. sqlspec/statement/pipelines/__init__.py +0 -210
  166. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  167. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  168. sqlspec/statement/pipelines/context.py +0 -115
  169. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  170. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  171. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  172. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  173. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  174. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  175. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  176. sqlspec/statement/pipelines/validators/_performance.py +0 -718
  177. sqlspec/statement/pipelines/validators/_security.py +0 -967
  178. sqlspec/statement/result.py +0 -435
  179. sqlspec/statement/sql.py +0 -1704
  180. sqlspec/statement/sql_compiler.py +0 -140
  181. sqlspec/utils/cached_property.py +0 -25
  182. sqlspec-0.13.1.dist-info/RECORD +0 -150
  183. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
  184. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
  185. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
sqlspec/_sql.py CHANGED
@@ -1,21 +1,71 @@
1
1
  """Unified SQL factory for creating SQL builders and column expressions with a clean API.
2
2
 
3
- This module provides the `sql` factory object for easy SQL construction:
4
- - `sql` provides both statement builders (select, insert, update, etc.) and column expressions
3
+ Provides both statement builders (select, insert, update, etc.) and column expressions.
5
4
  """
6
5
 
7
6
  import logging
8
- from typing import Any, Optional, Union
7
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
9
8
 
10
9
  import sqlglot
10
+ from mypy_extensions import trait
11
11
  from sqlglot import exp
12
12
  from sqlglot.dialects.dialect import DialectType
13
13
  from sqlglot.errors import ParseError as SQLGlotParseError
14
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
+ )
15
37
  from sqlspec.exceptions import SQLBuilderError
16
- from sqlspec.statement.builder import Column, Delete, Insert, Merge, Select, Update
17
38
 
18
- __all__ = ("SQLFactory",)
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
+ )
19
69
 
20
70
  logger = logging.getLogger("sqlspec")
21
71
 
@@ -50,58 +100,18 @@ SQL_STARTERS = {
50
100
 
51
101
 
52
102
  class SQLFactory:
53
- """Unified factory for creating SQL builders and column expressions with a fluent API.
54
-
55
- Provides both statement builders and column expressions through a single, clean interface.
56
- Now supports parsing raw SQL strings into appropriate builders for enhanced flexibility.
57
-
58
- Example:
59
- ```python
60
- from sqlspec import sql
61
-
62
- # Traditional builder usage (unchanged)
63
- query = (
64
- sql.select(sql.id, sql.name)
65
- .from_("users")
66
- .where("age > 18")
67
- )
68
-
69
- # New: Raw SQL parsing
70
- insert_sql = sql.insert(
71
- "INSERT INTO users (name, email) VALUES ('John', 'john@example.com')"
72
- )
73
- select_sql = sql.select(
74
- "SELECT * FROM users WHERE active = 1"
75
- )
76
-
77
- # RETURNING clause detection
78
- returning_insert = sql.insert(
79
- "INSERT INTO users (name) VALUES ('John') RETURNING id"
80
- )
81
- # → When executed, will return SelectResult instead of ExecuteResult
82
-
83
- # Smart INSERT FROM SELECT
84
- insert_from_select = sql.insert(
85
- "SELECT id, name FROM source WHERE active = 1"
86
- )
87
- # → Will prompt for target table or convert to INSERT FROM SELECT pattern
88
- ```
89
- """
103
+ """Unified factory for creating SQL builders and column expressions with a fluent API."""
90
104
 
91
105
  @classmethod
92
106
  def detect_sql_type(cls, sql: str, dialect: DialectType = None) -> str:
93
107
  try:
94
- # Minimal parsing just to get the command type
95
108
  parsed_expr = sqlglot.parse_one(sql, read=dialect)
96
109
  if parsed_expr and parsed_expr.key:
97
110
  return parsed_expr.key.upper()
98
- # Fallback for expressions that might not have a direct 'key'
99
- # or where key is None (e.g. some DDL without explicit command like SET)
100
111
  if parsed_expr:
101
- # Attempt to get the class name as a fallback, e.g., "Set", "Command"
102
112
  command_type = type(parsed_expr).__name__.upper()
103
113
  if command_type == "COMMAND" and parsed_expr.this:
104
- return str(parsed_expr.this).upper() # e.g. "SET", "ALTER"
114
+ return str(parsed_expr.this).upper()
105
115
  return command_type
106
116
  except SQLGlotParseError:
107
117
  logger.debug("Failed to parse SQL for type detection: %s", sql[:100])
@@ -120,15 +130,7 @@ class SQLFactory:
120
130
  # ===================
121
131
  # Callable Interface
122
132
  # ===================
123
- def __call__(
124
- self,
125
- statement: str,
126
- parameters: Optional[Any] = None,
127
- *filters: Any,
128
- config: Optional[Any] = None,
129
- dialect: DialectType = None,
130
- **kwargs: Any,
131
- ) -> "Any":
133
+ def __call__(self, statement: str, dialect: DialectType = None) -> "Any":
132
134
  """Create a SelectBuilder from a SQL string, only allowing SELECT/CTE queries.
133
135
 
134
136
  Args:
@@ -152,7 +154,6 @@ class SQLFactory:
152
154
  msg = f"Failed to parse SQL: {e}"
153
155
  raise SQLBuilderError(msg) from e
154
156
  actual_type = type(parsed_expr).__name__.upper()
155
- # Map sqlglot expression class to type string
156
157
  expr_type_map = {
157
158
  "SELECT": "SELECT",
158
159
  "INSERT": "INSERT",
@@ -177,7 +178,9 @@ class SQLFactory:
177
178
  # ===================
178
179
  # Statement Builders
179
180
  # ===================
180
- def select(self, *columns_or_sql: Union[str, exp.Expression, Column], dialect: DialectType = None) -> "Select":
181
+ def select(
182
+ self, *columns_or_sql: Union[str, exp.Expression, Column, "SQL"], dialect: DialectType = None
183
+ ) -> "Select":
181
184
  builder_dialect = dialect or self.dialect
182
185
  if len(columns_or_sql) == 1 and isinstance(columns_or_sql[0], str):
183
186
  sql_candidate = columns_or_sql[0].strip()
@@ -190,12 +193,8 @@ class SQLFactory:
190
193
  )
191
194
  raise SQLBuilderError(msg)
192
195
  select_builder = Select(dialect=builder_dialect)
193
- if select_builder._expression is None:
194
- select_builder.__post_init__()
195
196
  return self._populate_select_from_sql(select_builder, sql_candidate)
196
197
  select_builder = Select(dialect=builder_dialect)
197
- if select_builder._expression is None:
198
- select_builder.__post_init__()
199
198
  if columns_or_sql:
200
199
  select_builder.select(*columns_or_sql)
201
200
  return select_builder
@@ -203,8 +202,6 @@ class SQLFactory:
203
202
  def insert(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Insert":
204
203
  builder_dialect = dialect or self.dialect
205
204
  builder = Insert(dialect=builder_dialect)
206
- if builder._expression is None:
207
- builder.__post_init__()
208
205
  if table_or_sql:
209
206
  if self._looks_like_sql(table_or_sql):
210
207
  detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
@@ -222,8 +219,6 @@ class SQLFactory:
222
219
  def update(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Update":
223
220
  builder_dialect = dialect or self.dialect
224
221
  builder = Update(dialect=builder_dialect)
225
- if builder._expression is None:
226
- builder.__post_init__()
227
222
  if table_or_sql:
228
223
  if self._looks_like_sql(table_or_sql):
229
224
  detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
@@ -237,8 +232,6 @@ class SQLFactory:
237
232
  def delete(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Delete":
238
233
  builder_dialect = dialect or self.dialect
239
234
  builder = Delete(dialect=builder_dialect)
240
- if builder._expression is None:
241
- builder.__post_init__()
242
235
  if table_or_sql and self._looks_like_sql(table_or_sql):
243
236
  detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
244
237
  if detected != "DELETE":
@@ -250,8 +243,6 @@ class SQLFactory:
250
243
  def merge(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Merge":
251
244
  builder_dialect = dialect or self.dialect
252
245
  builder = Merge(dialect=builder_dialect)
253
- if builder._expression is None:
254
- builder.__post_init__()
255
246
  if table_or_sql:
256
247
  if self._looks_like_sql(table_or_sql):
257
248
  detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
@@ -262,6 +253,174 @@ class SQLFactory:
262
253
  return builder.into(table_or_sql)
263
254
  return builder
264
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
+
265
424
  # ===================
266
425
  # SQL Analysis Helpers
267
426
  # ===================
@@ -300,8 +459,6 @@ class SQLFactory:
300
459
  try:
301
460
  # Use SQLGlot directly for parsing - no validation here
302
461
  parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
303
- if parsed_expr is None:
304
- parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
305
462
 
306
463
  if isinstance(parsed_expr, exp.Insert):
307
464
  builder._expression = parsed_expr
@@ -324,8 +481,6 @@ class SQLFactory:
324
481
  try:
325
482
  # Use SQLGlot directly for parsing - no validation here
326
483
  parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
327
- if parsed_expr is None:
328
- parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
329
484
 
330
485
  if isinstance(parsed_expr, exp.Select):
331
486
  builder._expression = parsed_expr
@@ -342,8 +497,6 @@ class SQLFactory:
342
497
  try:
343
498
  # Use SQLGlot directly for parsing - no validation here
344
499
  parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
345
- if parsed_expr is None:
346
- parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
347
500
 
348
501
  if isinstance(parsed_expr, exp.Update):
349
502
  builder._expression = parsed_expr
@@ -360,8 +513,6 @@ class SQLFactory:
360
513
  try:
361
514
  # Use SQLGlot directly for parsing - no validation here
362
515
  parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
363
- if parsed_expr is None:
364
- parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
365
516
 
366
517
  if isinstance(parsed_expr, exp.Delete):
367
518
  builder._expression = parsed_expr
@@ -378,8 +529,6 @@ class SQLFactory:
378
529
  try:
379
530
  # Use SQLGlot directly for parsing - no validation here
380
531
  parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
381
- if parsed_expr is None:
382
- parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
383
532
 
384
533
  if isinstance(parsed_expr, exp.Merge):
385
534
  builder._expression = parsed_expr
@@ -395,17 +544,194 @@ class SQLFactory:
395
544
  # Column References
396
545
  # ===================
397
546
 
398
- def __getattr__(self, name: str) -> Column:
399
- """Dynamically create column references.
547
+ def column(self, name: str, table: Optional[str] = None) -> Column:
548
+ """Create a column reference.
400
549
 
401
550
  Args:
402
551
  name: Column name.
552
+ table: Optional table name.
403
553
 
404
554
  Returns:
405
555
  Column object that supports method chaining and operator overloading.
406
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
+ """
407
666
  return Column(name)
408
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
+
409
735
  # ===================
410
736
  # Aggregate Functions
411
737
  # ===================
@@ -597,7 +923,7 @@ class SQLFactory:
597
923
  ```
598
924
  """
599
925
  if isinstance(values, list):
600
- literals = [exp.Literal.string(str(v)) if isinstance(v, str) else exp.Literal.number(v) for v in values]
926
+ literals = [SQLFactory._to_literal(v) for v in values]
601
927
  return exp.Any(this=exp.Array(expressions=literals))
602
928
  if isinstance(values, str):
603
929
  # Parse as SQL
@@ -607,6 +933,29 @@ class SQLFactory:
607
933
  return exp.Any(this=exp.Literal.string(values))
608
934
  return exp.Any(this=values)
609
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
+
610
959
  # ===================
611
960
  # String Functions
612
961
  # ===================
@@ -687,6 +1036,28 @@ class SQLFactory:
687
1036
  # Conversion Functions
688
1037
  # ===================
689
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
+
690
1061
  @staticmethod
691
1062
  def decode(column: Union[str, exp.Expression], *args: Union[str, exp.Expression, Any]) -> exp.Expression:
692
1063
  """Create a DECODE expression (Oracle-style conditional logic).
@@ -725,29 +1096,14 @@ class SQLFactory:
725
1096
  for i in range(0, len(args) - 1, 2):
726
1097
  if i + 1 >= len(args):
727
1098
  # Odd number of args means last one is default
728
- default = exp.Literal.string(str(args[i])) if not isinstance(args[i], exp.Expression) else args[i]
1099
+ default = SQLFactory._to_literal(args[i])
729
1100
  break
730
1101
 
731
1102
  search_val = args[i]
732
1103
  result_val = args[i + 1]
733
1104
 
734
- if isinstance(search_val, str):
735
- search_expr = exp.Literal.string(search_val)
736
- elif isinstance(search_val, (int, float)):
737
- search_expr = exp.Literal.number(search_val)
738
- elif isinstance(search_val, exp.Expression):
739
- search_expr = search_val # type: ignore[assignment]
740
- else:
741
- search_expr = exp.Literal.string(str(search_val))
742
-
743
- if isinstance(result_val, str):
744
- result_expr = exp.Literal.string(result_val)
745
- elif isinstance(result_val, (int, float)):
746
- result_expr = exp.Literal.number(result_val)
747
- elif isinstance(result_val, exp.Expression):
748
- result_expr = result_val # type: ignore[assignment]
749
- else:
750
- result_expr = exp.Literal.string(str(result_val))
1105
+ search_expr = SQLFactory._to_literal(search_val)
1106
+ result_expr = SQLFactory._to_literal(result_val)
751
1107
 
752
1108
  condition = exp.EQ(this=col_expr, expression=search_expr)
753
1109
  conditions.append(exp.When(this=condition, then=result_expr))
@@ -793,30 +1149,136 @@ class SQLFactory:
793
1149
  COALESCE expression equivalent to NVL.
794
1150
  """
795
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])
796
1154
 
797
- if isinstance(substitute_value, str):
798
- sub_expr = exp.Literal.string(substitute_value)
799
- elif isinstance(substitute_value, (int, float)):
800
- sub_expr = exp.Literal.number(substitute_value)
801
- elif isinstance(substitute_value, exp.Expression):
802
- sub_expr = substitute_value # type: ignore[assignment]
803
- else:
804
- sub_expr = exp.Literal.string(str(substitute_value))
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.
805
1162
 
806
- return exp.Coalesce(expressions=[col_expr, sub_expr])
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
807
1269
 
808
1270
  # ===================
809
1271
  # Case Expressions
810
1272
  # ===================
811
1273
 
812
1274
  @staticmethod
813
- def case() -> "CaseExpressionBuilder":
1275
+ def case() -> "Case":
814
1276
  """Create a CASE expression builder.
815
1277
 
816
1278
  Returns:
817
1279
  CaseExpressionBuilder for building CASE expressions.
818
1280
  """
819
- return CaseExpressionBuilder()
1281
+ return Case()
820
1282
 
821
1283
  # ===================
822
1284
  # Window Functions
@@ -911,7 +1373,8 @@ class SQLFactory:
911
1373
  return exp.Window(this=func_expr, **over_args)
912
1374
 
913
1375
 
914
- class CaseExpressionBuilder:
1376
+ @trait
1377
+ class Case:
915
1378
  """Builder for CASE expressions using the SQL factory.
916
1379
 
917
1380
  Example:
@@ -930,12 +1393,23 @@ class CaseExpressionBuilder:
930
1393
 
931
1394
  def __init__(self) -> None:
932
1395
  """Initialize the CASE expression builder."""
933
- self._conditions: list[exp.When] = []
1396
+ self._conditions: list[exp.If] = []
934
1397
  self._default: Optional[exp.Expression] = None
935
1398
 
936
- def when(
937
- self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]
938
- ) -> "CaseExpressionBuilder":
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":
939
1413
  """Add a WHEN clause.
940
1414
 
941
1415
  Args:
@@ -946,21 +1420,14 @@ class CaseExpressionBuilder:
946
1420
  Self for method chaining.
947
1421
  """
948
1422
  cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
1423
+ val_expr = SQLFactory._to_literal(value)
949
1424
 
950
- if isinstance(value, str):
951
- val_expr = exp.Literal.string(value)
952
- elif isinstance(value, (int, float)):
953
- val_expr = exp.Literal.number(value)
954
- elif isinstance(value, exp.Expression):
955
- val_expr = value # type: ignore[assignment]
956
- else:
957
- val_expr = exp.Literal.string(str(value))
958
-
959
- when_clause = exp.When(this=cond_expr, then=val_expr)
1425
+ # SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
1426
+ when_clause = exp.If(this=cond_expr, true=val_expr)
960
1427
  self._conditions.append(when_clause)
961
1428
  return self
962
1429
 
963
- def else_(self, value: Union[str, exp.Expression, Any]) -> "CaseExpressionBuilder":
1430
+ def else_(self, value: Union[str, exp.Expression, Any]) -> "Case":
964
1431
  """Add an ELSE clause.
965
1432
 
966
1433
  Args:
@@ -969,14 +1436,7 @@ class CaseExpressionBuilder:
969
1436
  Returns:
970
1437
  Self for method chaining.
971
1438
  """
972
- if isinstance(value, str):
973
- self._default = exp.Literal.string(value)
974
- elif isinstance(value, (int, float)):
975
- self._default = exp.Literal.number(value)
976
- elif isinstance(value, exp.Expression):
977
- self._default = value
978
- else:
979
- self._default = exp.Literal.string(str(value))
1439
+ self._default = SQLFactory._to_literal(value)
980
1440
  return self
981
1441
 
982
1442
  def end(self) -> exp.Expression:
@@ -986,3 +1446,337 @@ class CaseExpressionBuilder:
986
1446
  Complete CASE expression.
987
1447
  """
988
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()