sqlspec 0.11.1__py3-none-any.whl → 0.12.1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (155) hide show
  1. sqlspec/__init__.py +16 -3
  2. sqlspec/_serialization.py +3 -10
  3. sqlspec/_sql.py +1147 -0
  4. sqlspec/_typing.py +343 -41
  5. sqlspec/adapters/adbc/__init__.py +2 -6
  6. sqlspec/adapters/adbc/config.py +474 -149
  7. sqlspec/adapters/adbc/driver.py +330 -621
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -431
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +218 -436
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +417 -487
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +600 -553
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +392 -406
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +548 -921
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -533
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +725 -0
  31. sqlspec/adapters/psycopg/driver.py +734 -694
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +242 -405
  35. sqlspec/base.py +220 -784
  36. sqlspec/config.py +354 -0
  37. sqlspec/driver/__init__.py +22 -0
  38. sqlspec/driver/_async.py +252 -0
  39. sqlspec/driver/_common.py +338 -0
  40. sqlspec/driver/_sync.py +261 -0
  41. sqlspec/driver/mixins/__init__.py +17 -0
  42. sqlspec/driver/mixins/_pipeline.py +523 -0
  43. sqlspec/driver/mixins/_result_utils.py +122 -0
  44. sqlspec/driver/mixins/_sql_translator.py +35 -0
  45. sqlspec/driver/mixins/_storage.py +993 -0
  46. sqlspec/driver/mixins/_type_coercion.py +131 -0
  47. sqlspec/exceptions.py +299 -7
  48. sqlspec/extensions/aiosql/__init__.py +10 -0
  49. sqlspec/extensions/aiosql/adapter.py +474 -0
  50. sqlspec/extensions/litestar/__init__.py +1 -6
  51. sqlspec/extensions/litestar/_utils.py +1 -5
  52. sqlspec/extensions/litestar/config.py +5 -6
  53. sqlspec/extensions/litestar/handlers.py +13 -12
  54. sqlspec/extensions/litestar/plugin.py +22 -24
  55. sqlspec/extensions/litestar/providers.py +37 -55
  56. sqlspec/loader.py +528 -0
  57. sqlspec/service/__init__.py +3 -0
  58. sqlspec/service/base.py +24 -0
  59. sqlspec/service/pagination.py +26 -0
  60. sqlspec/statement/__init__.py +21 -0
  61. sqlspec/statement/builder/__init__.py +54 -0
  62. sqlspec/statement/builder/_ddl_utils.py +119 -0
  63. sqlspec/statement/builder/_parsing_utils.py +135 -0
  64. sqlspec/statement/builder/base.py +328 -0
  65. sqlspec/statement/builder/ddl.py +1379 -0
  66. sqlspec/statement/builder/delete.py +80 -0
  67. sqlspec/statement/builder/insert.py +274 -0
  68. sqlspec/statement/builder/merge.py +95 -0
  69. sqlspec/statement/builder/mixins/__init__.py +65 -0
  70. sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
  71. sqlspec/statement/builder/mixins/_case_builder.py +91 -0
  72. sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
  73. sqlspec/statement/builder/mixins/_delete_from.py +34 -0
  74. sqlspec/statement/builder/mixins/_from.py +61 -0
  75. sqlspec/statement/builder/mixins/_group_by.py +119 -0
  76. sqlspec/statement/builder/mixins/_having.py +35 -0
  77. sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
  78. sqlspec/statement/builder/mixins/_insert_into.py +36 -0
  79. sqlspec/statement/builder/mixins/_insert_values.py +69 -0
  80. sqlspec/statement/builder/mixins/_join.py +110 -0
  81. sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
  82. sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
  83. sqlspec/statement/builder/mixins/_order_by.py +46 -0
  84. sqlspec/statement/builder/mixins/_pivot.py +82 -0
  85. sqlspec/statement/builder/mixins/_returning.py +37 -0
  86. sqlspec/statement/builder/mixins/_select_columns.py +60 -0
  87. sqlspec/statement/builder/mixins/_set_ops.py +122 -0
  88. sqlspec/statement/builder/mixins/_unpivot.py +80 -0
  89. sqlspec/statement/builder/mixins/_update_from.py +54 -0
  90. sqlspec/statement/builder/mixins/_update_set.py +91 -0
  91. sqlspec/statement/builder/mixins/_update_table.py +29 -0
  92. sqlspec/statement/builder/mixins/_where.py +374 -0
  93. sqlspec/statement/builder/mixins/_window_functions.py +86 -0
  94. sqlspec/statement/builder/protocols.py +20 -0
  95. sqlspec/statement/builder/select.py +206 -0
  96. sqlspec/statement/builder/update.py +178 -0
  97. sqlspec/statement/filters.py +571 -0
  98. sqlspec/statement/parameters.py +736 -0
  99. sqlspec/statement/pipelines/__init__.py +67 -0
  100. sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
  101. sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
  102. sqlspec/statement/pipelines/base.py +315 -0
  103. sqlspec/statement/pipelines/context.py +119 -0
  104. sqlspec/statement/pipelines/result_types.py +41 -0
  105. sqlspec/statement/pipelines/transformers/__init__.py +8 -0
  106. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
  107. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
  108. sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
  109. sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
  110. sqlspec/statement/pipelines/validators/__init__.py +23 -0
  111. sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
  112. sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
  113. sqlspec/statement/pipelines/validators/_performance.py +703 -0
  114. sqlspec/statement/pipelines/validators/_security.py +990 -0
  115. sqlspec/statement/pipelines/validators/base.py +67 -0
  116. sqlspec/statement/result.py +527 -0
  117. sqlspec/statement/splitter.py +701 -0
  118. sqlspec/statement/sql.py +1198 -0
  119. sqlspec/storage/__init__.py +15 -0
  120. sqlspec/storage/backends/__init__.py +0 -0
  121. sqlspec/storage/backends/base.py +166 -0
  122. sqlspec/storage/backends/fsspec.py +315 -0
  123. sqlspec/storage/backends/obstore.py +464 -0
  124. sqlspec/storage/protocol.py +170 -0
  125. sqlspec/storage/registry.py +315 -0
  126. sqlspec/typing.py +157 -36
  127. sqlspec/utils/correlation.py +155 -0
  128. sqlspec/utils/deprecation.py +3 -6
  129. sqlspec/utils/fixtures.py +6 -11
  130. sqlspec/utils/logging.py +135 -0
  131. sqlspec/utils/module_loader.py +45 -43
  132. sqlspec/utils/serializers.py +4 -0
  133. sqlspec/utils/singleton.py +6 -8
  134. sqlspec/utils/sync_tools.py +15 -27
  135. sqlspec/utils/text.py +58 -26
  136. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/METADATA +97 -26
  137. sqlspec-0.12.1.dist-info/RECORD +145 -0
  138. sqlspec/adapters/bigquery/config/__init__.py +0 -3
  139. sqlspec/adapters/bigquery/config/_common.py +0 -40
  140. sqlspec/adapters/bigquery/config/_sync.py +0 -87
  141. sqlspec/adapters/oracledb/config/__init__.py +0 -9
  142. sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
  143. sqlspec/adapters/oracledb/config/_common.py +0 -131
  144. sqlspec/adapters/oracledb/config/_sync.py +0 -186
  145. sqlspec/adapters/psycopg/config/__init__.py +0 -19
  146. sqlspec/adapters/psycopg/config/_async.py +0 -169
  147. sqlspec/adapters/psycopg/config/_common.py +0 -56
  148. sqlspec/adapters/psycopg/config/_sync.py +0 -168
  149. sqlspec/filters.py +0 -331
  150. sqlspec/mixins.py +0 -305
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.1.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/NOTICE +0 -0
sqlspec/_sql.py ADDED
@@ -0,0 +1,1147 @@
1
+ """Unified SQL factory for creating SQL builders and column expressions with a clean API.
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
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Optional, Union
9
+
10
+ import sqlglot
11
+ from sqlglot import exp
12
+ from sqlglot.dialects.dialect import DialectType
13
+ from sqlglot.errors import ParseError as SQLGlotParseError
14
+
15
+ from sqlspec.exceptions import SQLBuilderError
16
+ from sqlspec.statement.builder import DeleteBuilder, InsertBuilder, MergeBuilder, SelectBuilder, UpdateBuilder
17
+
18
+ __all__ = ("SQLFactory",)
19
+
20
+ logger = logging.getLogger("sqlspec")
21
+
22
+ MIN_SQL_LIKE_STRING_LENGTH = 6
23
+ MIN_DECODE_ARGS = 2
24
+ SQL_STARTERS = {
25
+ "SELECT",
26
+ "INSERT",
27
+ "UPDATE",
28
+ "DELETE",
29
+ "MERGE",
30
+ "WITH",
31
+ "CALL",
32
+ "DECLARE",
33
+ "BEGIN",
34
+ "END",
35
+ "CREATE",
36
+ "DROP",
37
+ "ALTER",
38
+ "TRUNCATE",
39
+ "RENAME",
40
+ "GRANT",
41
+ "REVOKE",
42
+ "SET",
43
+ "SHOW",
44
+ "USE",
45
+ "EXPLAIN",
46
+ "OPTIMIZE",
47
+ "VACUUM",
48
+ "COPY",
49
+ }
50
+
51
+
52
+ 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
+ """
90
+
91
+ @classmethod
92
+ def detect_sql_type(cls, sql: str, dialect: DialectType = None) -> str:
93
+ try:
94
+ # Minimal parsing just to get the command type
95
+ parsed_expr = sqlglot.parse_one(sql, read=dialect)
96
+ if parsed_expr and parsed_expr.key:
97
+ 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
+ if parsed_expr:
101
+ # Attempt to get the class name as a fallback, e.g., "Set", "Command"
102
+ command_type = type(parsed_expr).__name__.upper()
103
+ # Handle specific cases like "COMMAND" which might be too generic
104
+ if command_type == "COMMAND" and parsed_expr.this:
105
+ return str(parsed_expr.this).upper() # e.g. "SET", "ALTER"
106
+ return command_type
107
+ except SQLGlotParseError:
108
+ logger.debug("Failed to parse SQL for type detection: %s", sql[:100])
109
+ except (ValueError, TypeError, AttributeError) as e:
110
+ logger.warning("Unexpected error during SQL type detection for '%s...': %s", sql[:50], e)
111
+ return "UNKNOWN"
112
+
113
+ def __init__(self, dialect: DialectType = None) -> None:
114
+ """Initialize the SQL factory.
115
+
116
+ Args:
117
+ dialect: Default SQL dialect to use for all builders.
118
+ """
119
+ self.dialect = dialect
120
+
121
+ # ===================
122
+ # Callable Interface
123
+ # ===================
124
+ def __call__(
125
+ self,
126
+ statement: str,
127
+ parameters: Optional[Any] = None,
128
+ *filters: Any,
129
+ config: Optional[Any] = None,
130
+ dialect: DialectType = None,
131
+ **kwargs: Any,
132
+ ) -> "Any":
133
+ """Create a SelectBuilder from a SQL string, only allowing SELECT/CTE queries.
134
+
135
+ Args:
136
+ statement: The SQL statement string.
137
+ parameters: Optional parameters for the query.
138
+ *filters: Optional filters.
139
+ config: Optional config.
140
+ dialect: Optional SQL dialect.
141
+ **kwargs: Additional parameters.
142
+
143
+ Returns:
144
+ SelectBuilder instance.
145
+
146
+ Raises:
147
+ SQLBuilderError: If the SQL is not a SELECT/CTE statement.
148
+ """
149
+
150
+ try:
151
+ parsed_expr = sqlglot.parse_one(statement, read=dialect or self.dialect)
152
+ except Exception as e:
153
+ msg = f"Failed to parse SQL: {e}"
154
+ raise SQLBuilderError(msg) from e
155
+ actual_type = type(parsed_expr).__name__.upper()
156
+ # Map sqlglot expression class to type string
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
+ # Only allow SELECT or WITH (if WITH wraps SELECT)
167
+ if actual_type_str == "SELECT" or (
168
+ actual_type_str == "WITH" and parsed_expr.this and isinstance(parsed_expr.this, exp.Select)
169
+ ):
170
+ builder = SelectBuilder(dialect=dialect or self.dialect)
171
+ builder._expression = parsed_expr
172
+ return builder
173
+ # If not SELECT, raise with helpful message
174
+ msg = (
175
+ f"sql(...) only supports SELECT statements. Detected type: {actual_type_str}. "
176
+ f"Use sql.{actual_type_str.lower()}() instead."
177
+ )
178
+ raise SQLBuilderError(msg)
179
+
180
+ # ===================
181
+ # Statement Builders
182
+ # ===================
183
+ def select(self, *columns_or_sql: Union[str, exp.Expression], dialect: DialectType = None) -> "SelectBuilder":
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
+ # Check if it actually looks like SQL before parsing
188
+ if self._looks_like_sql(sql_candidate):
189
+ # Validate type
190
+ detected = self.detect_sql_type(sql_candidate, dialect=builder_dialect)
191
+ if detected not in {"SELECT", "WITH"}:
192
+ msg = (
193
+ f"sql.select() expects a SELECT or WITH statement, got {detected}. "
194
+ f"Use sql.{detected.lower()}() if a dedicated builder exists, or ensure the SQL is SELECT/WITH."
195
+ )
196
+ raise SQLBuilderError(msg)
197
+ select_builder = SelectBuilder(dialect=builder_dialect)
198
+ if select_builder._expression is None:
199
+ select_builder.__post_init__()
200
+ return self._populate_select_from_sql(select_builder, sql_candidate)
201
+ # Otherwise treat as column name and fall through to normal column handling
202
+ select_builder = SelectBuilder(dialect=builder_dialect)
203
+ if select_builder._expression is None:
204
+ select_builder.__post_init__()
205
+ if columns_or_sql:
206
+ select_builder.select(*columns_or_sql)
207
+ return select_builder
208
+
209
+ def insert(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "InsertBuilder":
210
+ builder_dialect = dialect or self.dialect
211
+ builder = InsertBuilder(dialect=builder_dialect)
212
+ if builder._expression is None:
213
+ builder.__post_init__()
214
+ if table_or_sql:
215
+ if self._looks_like_sql(table_or_sql):
216
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
217
+ if detected not in {"INSERT", "SELECT"}:
218
+ msg = (
219
+ f"sql.insert() expects INSERT or SELECT (for insert-from-select), got {detected}. "
220
+ f"Use sql.{detected.lower()}() if a dedicated builder exists, "
221
+ f"or ensure the SQL is INSERT/SELECT."
222
+ )
223
+ raise SQLBuilderError(msg)
224
+ return self._populate_insert_from_sql(builder, table_or_sql)
225
+ return builder.into(table_or_sql)
226
+ return builder
227
+
228
+ def update(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "UpdateBuilder":
229
+ builder_dialect = dialect or self.dialect
230
+ builder = UpdateBuilder(dialect=builder_dialect)
231
+ if builder._expression is None:
232
+ builder.__post_init__()
233
+ if table_or_sql:
234
+ if self._looks_like_sql(table_or_sql):
235
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
236
+ if detected != "UPDATE":
237
+ msg = f"sql.update() expects UPDATE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
238
+ raise SQLBuilderError(msg)
239
+ return self._populate_update_from_sql(builder, table_or_sql)
240
+ return builder.table(table_or_sql)
241
+ return builder
242
+
243
+ def delete(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "DeleteBuilder":
244
+ builder_dialect = dialect or self.dialect
245
+ builder = DeleteBuilder(dialect=builder_dialect)
246
+ if builder._expression is None:
247
+ builder.__post_init__()
248
+ if table_or_sql and self._looks_like_sql(table_or_sql):
249
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
250
+ if detected != "DELETE":
251
+ msg = f"sql.delete() expects DELETE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
252
+ raise SQLBuilderError(msg)
253
+ return self._populate_delete_from_sql(builder, table_or_sql)
254
+ return builder
255
+
256
+ def merge(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "MergeBuilder":
257
+ builder_dialect = dialect or self.dialect
258
+ builder = MergeBuilder(dialect=builder_dialect)
259
+ if builder._expression is None:
260
+ builder.__post_init__()
261
+ if table_or_sql:
262
+ if self._looks_like_sql(table_or_sql):
263
+ detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
264
+ if detected != "MERGE":
265
+ msg = f"sql.merge() expects MERGE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
266
+ raise SQLBuilderError(msg)
267
+ return self._populate_merge_from_sql(builder, table_or_sql)
268
+ return builder.into(table_or_sql)
269
+ return builder
270
+
271
+ # ===================
272
+ # SQL Analysis Helpers
273
+ # ===================
274
+
275
+ @staticmethod
276
+ def _looks_like_sql(candidate: str, expected_type: Optional[str] = None) -> bool:
277
+ """Efficiently determine if a string looks like SQL.
278
+
279
+ Args:
280
+ candidate: String to check
281
+ expected_type: Expected SQL statement type (SELECT, INSERT, etc.)
282
+
283
+ Returns:
284
+ True if the string appears to be SQL
285
+ """
286
+ if not candidate or len(candidate.strip()) < MIN_SQL_LIKE_STRING_LENGTH:
287
+ return False
288
+
289
+ candidate_upper = candidate.strip().upper()
290
+
291
+ # Check for SQL keywords at the beginning
292
+ if expected_type:
293
+ return candidate_upper.startswith(expected_type.upper())
294
+
295
+ # More sophisticated check for SQL vs column names
296
+ # Column names that start with SQL keywords are common (user_id, insert_date, etc.)
297
+ if any(candidate_upper.startswith(starter) for starter in SQL_STARTERS):
298
+ # Additional checks to distinguish real SQL from column names:
299
+ # 1. Real SQL typically has spaces (SELECT ... FROM, INSERT INTO, etc.)
300
+ # 2. Check for common SQL syntax patterns
301
+ return " " in candidate
302
+
303
+ return False
304
+
305
+ def _populate_insert_from_sql(self, builder: "InsertBuilder", sql_string: str) -> "InsertBuilder":
306
+ """Parse SQL string and populate INSERT builder using SQLGlot directly."""
307
+ try:
308
+ # Use SQLGlot directly for parsing - no validation here
309
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
310
+ if parsed_expr is None:
311
+ parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
312
+
313
+ if isinstance(parsed_expr, exp.Insert):
314
+ # Set the internal expression to the parsed one
315
+ builder._expression = parsed_expr
316
+ return builder
317
+
318
+ if isinstance(parsed_expr, exp.Select):
319
+ # Handle INSERT FROM SELECT case - just return builder for now
320
+ # The actual conversion logic can be handled by the builder itself
321
+ logger.info("Detected SELECT statement for INSERT - may need target table specification")
322
+ return builder
323
+
324
+ # For other statement types, just return the builder as-is
325
+ logger.warning("Cannot create INSERT from %s statement", type(parsed_expr).__name__)
326
+
327
+ except Exception as e:
328
+ logger.warning("Failed to parse INSERT SQL, falling back to traditional mode: %s", e)
329
+ return builder
330
+
331
+ def _populate_select_from_sql(self, builder: "SelectBuilder", sql_string: str) -> "SelectBuilder":
332
+ """Parse SQL string and populate SELECT builder using SQLGlot directly."""
333
+ try:
334
+ # Use SQLGlot directly for parsing - no validation here
335
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
336
+ if parsed_expr is None:
337
+ parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
338
+
339
+ if isinstance(parsed_expr, exp.Select):
340
+ # Set the internal expression to the parsed one
341
+ builder._expression = parsed_expr
342
+ return builder
343
+
344
+ logger.warning("Cannot create SELECT from %s statement", type(parsed_expr).__name__)
345
+
346
+ except Exception as e:
347
+ logger.warning("Failed to parse SELECT SQL, falling back to traditional mode: %s", e)
348
+ return builder
349
+
350
+ def _populate_update_from_sql(self, builder: "UpdateBuilder", sql_string: str) -> "UpdateBuilder":
351
+ """Parse SQL string and populate UPDATE builder using SQLGlot directly."""
352
+ try:
353
+ # Use SQLGlot directly for parsing - no validation here
354
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
355
+ if parsed_expr is None:
356
+ parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
357
+
358
+ if isinstance(parsed_expr, exp.Update):
359
+ # Set the internal expression to the parsed one
360
+ builder._expression = parsed_expr
361
+ return builder
362
+
363
+ logger.warning("Cannot create UPDATE from %s statement", type(parsed_expr).__name__)
364
+
365
+ except Exception as e:
366
+ logger.warning("Failed to parse UPDATE SQL, falling back to traditional mode: %s", e)
367
+ return builder
368
+
369
+ def _populate_delete_from_sql(self, builder: "DeleteBuilder", sql_string: str) -> "DeleteBuilder":
370
+ """Parse SQL string and populate DELETE builder using SQLGlot directly."""
371
+ try:
372
+ # Use SQLGlot directly for parsing - no validation here
373
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
374
+ if parsed_expr is None:
375
+ parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
376
+
377
+ if isinstance(parsed_expr, exp.Delete):
378
+ # Set the internal expression to the parsed one
379
+ builder._expression = parsed_expr
380
+ return builder
381
+
382
+ logger.warning("Cannot create DELETE from %s statement", type(parsed_expr).__name__)
383
+
384
+ except Exception as e:
385
+ logger.warning("Failed to parse DELETE SQL, falling back to traditional mode: %s", e)
386
+ return builder
387
+
388
+ def _populate_merge_from_sql(self, builder: "MergeBuilder", sql_string: str) -> "MergeBuilder":
389
+ """Parse SQL string and populate MERGE builder using SQLGlot directly."""
390
+ try:
391
+ # Use SQLGlot directly for parsing - no validation here
392
+ parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
393
+ if parsed_expr is None:
394
+ parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
395
+
396
+ if isinstance(parsed_expr, exp.Merge):
397
+ # Set the internal expression to the parsed one
398
+ builder._expression = parsed_expr
399
+ return builder
400
+
401
+ logger.warning("Cannot create MERGE from %s statement", type(parsed_expr).__name__)
402
+
403
+ except Exception as e:
404
+ logger.warning("Failed to parse MERGE SQL, falling back to traditional mode: %s", e)
405
+ return builder
406
+
407
+ # ===================
408
+ # Column References
409
+ # ===================
410
+
411
+ def __getattr__(self, name: str) -> exp.Column:
412
+ """Dynamically create column references.
413
+
414
+ Args:
415
+ name: Column name.
416
+
417
+ Returns:
418
+ Column expression for the specified column name.
419
+ """
420
+ return exp.column(name)
421
+
422
+ # ===================
423
+ # Aggregate Functions
424
+ # ===================
425
+
426
+ @staticmethod
427
+ def count(column: Union[str, exp.Expression] = "*", distinct: bool = False) -> exp.Expression:
428
+ """Create a COUNT expression.
429
+
430
+ Args:
431
+ column: Column to count (default "*").
432
+ distinct: Whether to use COUNT DISTINCT.
433
+
434
+ Returns:
435
+ COUNT expression.
436
+ """
437
+ if column == "*":
438
+ return exp.Count(this=exp.Star(), distinct=distinct)
439
+ col_expr = exp.column(column) if isinstance(column, str) else column
440
+ return exp.Count(this=col_expr, distinct=distinct)
441
+
442
+ def count_distinct(self, column: Union[str, exp.Expression]) -> exp.Expression:
443
+ """Create a COUNT(DISTINCT column) expression.
444
+
445
+ Args:
446
+ column: Column to count distinct values.
447
+
448
+ Returns:
449
+ COUNT DISTINCT expression.
450
+ """
451
+ return self.count(column, distinct=True)
452
+
453
+ @staticmethod
454
+ def sum(column: Union[str, exp.Expression], distinct: bool = False) -> exp.Expression:
455
+ """Create a SUM expression.
456
+
457
+ Args:
458
+ column: Column to sum.
459
+ distinct: Whether to use SUM DISTINCT.
460
+
461
+ Returns:
462
+ SUM expression.
463
+ """
464
+ col_expr = exp.column(column) if isinstance(column, str) else column
465
+ return exp.Sum(this=col_expr, distinct=distinct)
466
+
467
+ @staticmethod
468
+ def avg(column: Union[str, exp.Expression]) -> exp.Expression:
469
+ """Create an AVG expression.
470
+
471
+ Args:
472
+ column: Column to average.
473
+
474
+ Returns:
475
+ AVG expression.
476
+ """
477
+ col_expr = exp.column(column) if isinstance(column, str) else column
478
+ return exp.Avg(this=col_expr)
479
+
480
+ @staticmethod
481
+ def max(column: Union[str, exp.Expression]) -> exp.Expression:
482
+ """Create a MAX expression.
483
+
484
+ Args:
485
+ column: Column to find maximum.
486
+
487
+ Returns:
488
+ MAX expression.
489
+ """
490
+ col_expr = exp.column(column) if isinstance(column, str) else column
491
+ return exp.Max(this=col_expr)
492
+
493
+ @staticmethod
494
+ def min(column: Union[str, exp.Expression]) -> exp.Expression:
495
+ """Create a MIN expression.
496
+
497
+ Args:
498
+ column: Column to find minimum.
499
+
500
+ Returns:
501
+ MIN expression.
502
+ """
503
+ col_expr = exp.column(column) if isinstance(column, str) else column
504
+ return exp.Min(this=col_expr)
505
+
506
+ # ===================
507
+ # Advanced SQL Operations
508
+ # ===================
509
+
510
+ @staticmethod
511
+ def rollup(*columns: Union[str, exp.Expression]) -> exp.Expression:
512
+ """Create a ROLLUP expression for GROUP BY clauses.
513
+
514
+ Args:
515
+ *columns: Columns to include in the rollup.
516
+
517
+ Returns:
518
+ ROLLUP expression.
519
+
520
+ Example:
521
+ ```python
522
+ # GROUP BY ROLLUP(product, region)
523
+ query = (
524
+ sql.select("product", "region", sql.sum("sales"))
525
+ .from_("sales_data")
526
+ .group_by(sql.rollup("product", "region"))
527
+ )
528
+ ```
529
+ """
530
+ column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
531
+ return exp.Rollup(expressions=column_exprs)
532
+
533
+ @staticmethod
534
+ def cube(*columns: Union[str, exp.Expression]) -> exp.Expression:
535
+ """Create a CUBE expression for GROUP BY clauses.
536
+
537
+ Args:
538
+ *columns: Columns to include in the cube.
539
+
540
+ Returns:
541
+ CUBE expression.
542
+
543
+ Example:
544
+ ```python
545
+ # GROUP BY CUBE(product, region)
546
+ query = (
547
+ sql.select("product", "region", sql.sum("sales"))
548
+ .from_("sales_data")
549
+ .group_by(sql.cube("product", "region"))
550
+ )
551
+ ```
552
+ """
553
+ column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
554
+ return exp.Cube(expressions=column_exprs)
555
+
556
+ @staticmethod
557
+ def grouping_sets(*column_sets: Union[tuple[str, ...], list[str]]) -> exp.Expression:
558
+ """Create a GROUPING SETS expression for GROUP BY clauses.
559
+
560
+ Args:
561
+ *column_sets: Sets of columns to group by.
562
+
563
+ Returns:
564
+ GROUPING SETS expression.
565
+
566
+ Example:
567
+ ```python
568
+ # GROUP BY GROUPING SETS ((product), (region), ())
569
+ query = (
570
+ sql.select("product", "region", sql.sum("sales"))
571
+ .from_("sales_data")
572
+ .group_by(
573
+ sql.grouping_sets(("product",), ("region",), ())
574
+ )
575
+ )
576
+ ```
577
+ """
578
+ set_expressions = []
579
+ for column_set in column_sets:
580
+ if isinstance(column_set, (tuple, list)):
581
+ if len(column_set) == 0:
582
+ # Empty set for grand total
583
+ set_expressions.append(exp.Tuple(expressions=[]))
584
+ else:
585
+ columns = [exp.column(col) for col in column_set]
586
+ set_expressions.append(exp.Tuple(expressions=columns))
587
+ else:
588
+ set_expressions.append(exp.column(column_set))
589
+
590
+ return exp.GroupingSets(expressions=set_expressions)
591
+
592
+ @staticmethod
593
+ def any(values: Union[list[Any], exp.Expression, str]) -> exp.Expression:
594
+ """Create an ANY expression for use with comparison operators.
595
+
596
+ Args:
597
+ values: Values, expression, or subquery for the ANY clause.
598
+
599
+ Returns:
600
+ ANY expression.
601
+
602
+ Example:
603
+ ```python
604
+ # WHERE id = ANY(subquery)
605
+ subquery = sql.select("user_id").from_("active_users")
606
+ query = (
607
+ sql.select("*")
608
+ .from_("users")
609
+ .where(sql.id.eq(sql.any(subquery)))
610
+ )
611
+ ```
612
+ """
613
+ if isinstance(values, list):
614
+ # Convert list to array literal
615
+ literals = [exp.Literal.string(str(v)) if isinstance(v, str) else exp.Literal.number(v) for v in values]
616
+ return exp.Any(this=exp.Array(expressions=literals))
617
+ if isinstance(values, str):
618
+ # Parse as SQL
619
+ parsed = exp.maybe_parse(values) # type: ignore[var-annotated]
620
+ if parsed:
621
+ return exp.Any(this=parsed)
622
+ return exp.Any(this=exp.Literal.string(values))
623
+ return exp.Any(this=values)
624
+
625
+ # ===================
626
+ # String Functions
627
+ # ===================
628
+
629
+ @staticmethod
630
+ def concat(*expressions: Union[str, exp.Expression]) -> exp.Expression:
631
+ """Create a CONCAT expression.
632
+
633
+ Args:
634
+ *expressions: Expressions to concatenate.
635
+
636
+ Returns:
637
+ CONCAT expression.
638
+ """
639
+ exprs = [exp.column(expr) if isinstance(expr, str) else expr for expr in expressions]
640
+ return exp.Concat(expressions=exprs)
641
+
642
+ @staticmethod
643
+ def upper(column: Union[str, exp.Expression]) -> exp.Expression:
644
+ """Create an UPPER expression.
645
+
646
+ Args:
647
+ column: Column to convert to uppercase.
648
+
649
+ Returns:
650
+ UPPER expression.
651
+ """
652
+ col_expr = exp.column(column) if isinstance(column, str) else column
653
+ return exp.Upper(this=col_expr)
654
+
655
+ @staticmethod
656
+ def lower(column: Union[str, exp.Expression]) -> exp.Expression:
657
+ """Create a LOWER expression.
658
+
659
+ Args:
660
+ column: Column to convert to lowercase.
661
+
662
+ Returns:
663
+ LOWER expression.
664
+ """
665
+ col_expr = exp.column(column) if isinstance(column, str) else column
666
+ return exp.Lower(this=col_expr)
667
+
668
+ @staticmethod
669
+ def length(column: Union[str, exp.Expression]) -> exp.Expression:
670
+ """Create a LENGTH expression.
671
+
672
+ Args:
673
+ column: Column to get length of.
674
+
675
+ Returns:
676
+ LENGTH expression.
677
+ """
678
+ col_expr = exp.column(column) if isinstance(column, str) else column
679
+ return exp.Length(this=col_expr)
680
+
681
+ # ===================
682
+ # Math Functions
683
+ # ===================
684
+
685
+ @staticmethod
686
+ def round(column: Union[str, exp.Expression], decimals: int = 0) -> exp.Expression:
687
+ """Create a ROUND expression.
688
+
689
+ Args:
690
+ column: Column to round.
691
+ decimals: Number of decimal places.
692
+
693
+ Returns:
694
+ ROUND expression.
695
+ """
696
+ col_expr = exp.column(column) if isinstance(column, str) else column
697
+ if decimals == 0:
698
+ return exp.Round(this=col_expr)
699
+ return exp.Round(this=col_expr, expression=exp.Literal.number(decimals))
700
+
701
+ # ===================
702
+ # Conversion Functions
703
+ # ===================
704
+
705
+ @staticmethod
706
+ def decode(column: Union[str, exp.Expression], *args: Union[str, exp.Expression, Any]) -> exp.Expression:
707
+ """Create a DECODE expression (Oracle-style conditional logic).
708
+
709
+ DECODE compares column to each search value and returns the corresponding result.
710
+ If no match is found, returns the default value (if provided) or NULL.
711
+
712
+ Args:
713
+ column: Column to compare.
714
+ *args: Alternating search values and results, with optional default at the end.
715
+ Format: search1, result1, search2, result2, ..., [default]
716
+
717
+ Raises:
718
+ ValueError: If fewer than two search/result pairs are provided.
719
+
720
+ Returns:
721
+ CASE expression equivalent to DECODE.
722
+
723
+ Example:
724
+ ```python
725
+ # DECODE(status, 'A', 'Active', 'I', 'Inactive', 'Unknown')
726
+ sql.decode(
727
+ "status", "A", "Active", "I", "Inactive", "Unknown"
728
+ )
729
+ ```
730
+ """
731
+ col_expr = exp.column(column) if isinstance(column, str) else column
732
+
733
+ if len(args) < MIN_DECODE_ARGS:
734
+ msg = "DECODE requires at least one search/result pair"
735
+ raise ValueError(msg)
736
+
737
+ # Build CASE expression
738
+ conditions = []
739
+ default = None
740
+
741
+ # Process search/result pairs
742
+ for i in range(0, len(args) - 1, 2):
743
+ if i + 1 >= len(args):
744
+ # Odd number of args means last one is default
745
+ default = exp.Literal.string(str(args[i])) if not isinstance(args[i], exp.Expression) else args[i]
746
+ break
747
+
748
+ search_val = args[i]
749
+ result_val = args[i + 1]
750
+
751
+ # Create search expression
752
+ if isinstance(search_val, str):
753
+ search_expr = exp.Literal.string(search_val)
754
+ elif isinstance(search_val, (int, float)):
755
+ search_expr = exp.Literal.number(search_val)
756
+ elif isinstance(search_val, exp.Expression):
757
+ search_expr = search_val # type: ignore[assignment]
758
+ else:
759
+ search_expr = exp.Literal.string(str(search_val))
760
+
761
+ # Create result expression
762
+ if isinstance(result_val, str):
763
+ result_expr = exp.Literal.string(result_val)
764
+ elif isinstance(result_val, (int, float)):
765
+ result_expr = exp.Literal.number(result_val)
766
+ elif isinstance(result_val, exp.Expression):
767
+ result_expr = result_val # type: ignore[assignment]
768
+ else:
769
+ result_expr = exp.Literal.string(str(result_val))
770
+
771
+ # Create WHEN condition
772
+ condition = exp.EQ(this=col_expr, expression=search_expr)
773
+ conditions.append(exp.When(this=condition, then=result_expr))
774
+
775
+ return exp.Case(ifs=conditions, default=default)
776
+
777
+ @staticmethod
778
+ def to_date(date_string: Union[str, exp.Expression], format_mask: Optional[str] = None) -> exp.Expression:
779
+ """Create a TO_DATE expression for converting strings to dates.
780
+
781
+ Args:
782
+ date_string: String or expression containing the date to convert.
783
+ format_mask: Optional format mask (e.g., 'YYYY-MM-DD', 'DD/MM/YYYY').
784
+
785
+ Returns:
786
+ TO_DATE function expression.
787
+ """
788
+ date_expr = exp.column(date_string) if isinstance(date_string, str) else date_string
789
+
790
+ if format_mask:
791
+ format_expr = exp.Literal.string(format_mask)
792
+ return exp.Anonymous(this="TO_DATE", expressions=[date_expr, format_expr])
793
+ return exp.Anonymous(this="TO_DATE", expressions=[date_expr])
794
+
795
+ @staticmethod
796
+ def to_char(column: Union[str, exp.Expression], format_mask: Optional[str] = None) -> exp.Expression:
797
+ """Create a TO_CHAR expression for converting values to strings.
798
+
799
+ Args:
800
+ column: Column or expression to convert to string.
801
+ format_mask: Optional format mask for dates/numbers.
802
+
803
+ Returns:
804
+ TO_CHAR function expression.
805
+ """
806
+ col_expr = exp.column(column) if isinstance(column, str) else column
807
+
808
+ if format_mask:
809
+ format_expr = exp.Literal.string(format_mask)
810
+ return exp.Anonymous(this="TO_CHAR", expressions=[col_expr, format_expr])
811
+ return exp.Anonymous(this="TO_CHAR", expressions=[col_expr])
812
+
813
+ @staticmethod
814
+ def to_string(column: Union[str, exp.Expression]) -> exp.Expression:
815
+ """Create a TO_STRING expression for converting values to strings.
816
+
817
+ Args:
818
+ column: Column or expression to convert to string.
819
+
820
+ Returns:
821
+ TO_STRING or CAST AS STRING expression.
822
+ """
823
+ col_expr = exp.column(column) if isinstance(column, str) else column
824
+ # Use CAST for broader compatibility
825
+ return exp.Cast(this=col_expr, to=exp.DataType.build("STRING"))
826
+
827
+ @staticmethod
828
+ def to_number(column: Union[str, exp.Expression], format_mask: Optional[str] = None) -> exp.Expression:
829
+ """Create a TO_NUMBER expression for converting strings to numbers.
830
+
831
+ Args:
832
+ column: Column or expression to convert to number.
833
+ format_mask: Optional format mask for the conversion.
834
+
835
+ Returns:
836
+ TO_NUMBER function expression.
837
+ """
838
+ col_expr = exp.column(column) if isinstance(column, str) else column
839
+
840
+ if format_mask:
841
+ format_expr = exp.Literal.string(format_mask)
842
+ return exp.Anonymous(this="TO_NUMBER", expressions=[col_expr, format_expr])
843
+ return exp.Anonymous(this="TO_NUMBER", expressions=[col_expr])
844
+
845
+ @staticmethod
846
+ def cast(column: Union[str, exp.Expression], data_type: str) -> exp.Expression:
847
+ """Create a CAST expression for type conversion.
848
+
849
+ Args:
850
+ column: Column or expression to cast.
851
+ data_type: Target data type (e.g., 'INT', 'VARCHAR(100)', 'DECIMAL(10,2)').
852
+
853
+ Returns:
854
+ CAST expression.
855
+ """
856
+ col_expr = exp.column(column) if isinstance(column, str) else column
857
+ return exp.Cast(this=col_expr, to=exp.DataType.build(data_type))
858
+
859
+ # ===================
860
+ # JSON Functions
861
+ # ===================
862
+
863
+ @staticmethod
864
+ def to_json(column: Union[str, exp.Expression]) -> exp.Expression:
865
+ """Create a TO_JSON expression for converting values to JSON.
866
+
867
+ Args:
868
+ column: Column or expression to convert to JSON.
869
+
870
+ Returns:
871
+ TO_JSON function expression.
872
+ """
873
+ col_expr = exp.column(column) if isinstance(column, str) else column
874
+ return exp.Anonymous(this="TO_JSON", expressions=[col_expr])
875
+
876
+ @staticmethod
877
+ def from_json(json_column: Union[str, exp.Expression], schema: Optional[str] = None) -> exp.Expression:
878
+ """Create a FROM_JSON expression for parsing JSON strings.
879
+
880
+ Args:
881
+ json_column: Column or expression containing JSON string.
882
+ schema: Optional schema specification for the JSON structure.
883
+
884
+ Returns:
885
+ FROM_JSON function expression.
886
+ """
887
+ json_expr = exp.column(json_column) if isinstance(json_column, str) else json_column
888
+
889
+ if schema:
890
+ schema_expr = exp.Literal.string(schema)
891
+ return exp.Anonymous(this="FROM_JSON", expressions=[json_expr, schema_expr])
892
+ return exp.Anonymous(this="FROM_JSON", expressions=[json_expr])
893
+
894
+ @staticmethod
895
+ def json_extract(json_column: Union[str, exp.Expression], path: str) -> exp.Expression:
896
+ """Create a JSON_EXTRACT expression for extracting values from JSON.
897
+
898
+ Args:
899
+ json_column: Column or expression containing JSON.
900
+ path: JSON path to extract (e.g., '$.field', '$.array[0]').
901
+
902
+ Returns:
903
+ JSON_EXTRACT function expression.
904
+ """
905
+ json_expr = exp.column(json_column) if isinstance(json_column, str) else json_column
906
+ path_expr = exp.Literal.string(path)
907
+ return exp.Anonymous(this="JSON_EXTRACT", expressions=[json_expr, path_expr])
908
+
909
+ @staticmethod
910
+ def json_value(json_column: Union[str, exp.Expression], path: str) -> exp.Expression:
911
+ """Create a JSON_VALUE expression for extracting scalar values from JSON.
912
+
913
+ Args:
914
+ json_column: Column or expression containing JSON.
915
+ path: JSON path to extract scalar value.
916
+
917
+ Returns:
918
+ JSON_VALUE function expression.
919
+ """
920
+ json_expr = exp.column(json_column) if isinstance(json_column, str) else json_column
921
+ path_expr = exp.Literal.string(path)
922
+ return exp.Anonymous(this="JSON_VALUE", expressions=[json_expr, path_expr])
923
+
924
+ # ===================
925
+ # NULL Functions
926
+ # ===================
927
+
928
+ @staticmethod
929
+ def coalesce(*expressions: Union[str, exp.Expression]) -> exp.Expression:
930
+ """Create a COALESCE expression.
931
+
932
+ Args:
933
+ *expressions: Expressions to coalesce.
934
+
935
+ Returns:
936
+ COALESCE expression.
937
+ """
938
+ exprs = [exp.column(expr) if isinstance(expr, str) else expr for expr in expressions]
939
+ return exp.Coalesce(expressions=exprs)
940
+
941
+ @staticmethod
942
+ def nvl(column: Union[str, exp.Expression], substitute_value: Union[str, exp.Expression, Any]) -> exp.Expression:
943
+ """Create an NVL (Oracle-style) expression using COALESCE.
944
+
945
+ Args:
946
+ column: Column to check for NULL.
947
+ substitute_value: Value to use if column is NULL.
948
+
949
+ Returns:
950
+ COALESCE expression equivalent to NVL.
951
+ """
952
+ col_expr = exp.column(column) if isinstance(column, str) else column
953
+
954
+ if isinstance(substitute_value, str):
955
+ sub_expr = exp.Literal.string(substitute_value)
956
+ elif isinstance(substitute_value, (int, float)):
957
+ sub_expr = exp.Literal.number(substitute_value)
958
+ elif isinstance(substitute_value, exp.Expression):
959
+ sub_expr = substitute_value # type: ignore[assignment]
960
+ else:
961
+ sub_expr = exp.Literal.string(str(substitute_value))
962
+
963
+ return exp.Coalesce(expressions=[col_expr, sub_expr])
964
+
965
+ # ===================
966
+ # Case Expressions
967
+ # ===================
968
+
969
+ @staticmethod
970
+ def case() -> "CaseExpressionBuilder":
971
+ """Create a CASE expression builder.
972
+
973
+ Returns:
974
+ CaseExpressionBuilder for building CASE expressions.
975
+ """
976
+ return CaseExpressionBuilder()
977
+
978
+ # ===================
979
+ # Window Functions
980
+ # ===================
981
+
982
+ def row_number(
983
+ self,
984
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
985
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
986
+ ) -> exp.Expression:
987
+ """Create a ROW_NUMBER() window function.
988
+
989
+ Args:
990
+ partition_by: Columns to partition by.
991
+ order_by: Columns to order by.
992
+
993
+ Returns:
994
+ ROW_NUMBER window function expression.
995
+ """
996
+ return self._create_window_function("ROW_NUMBER", [], partition_by, order_by)
997
+
998
+ def rank(
999
+ self,
1000
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
1001
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
1002
+ ) -> exp.Expression:
1003
+ """Create a RANK() window function.
1004
+
1005
+ Args:
1006
+ partition_by: Columns to partition by.
1007
+ order_by: Columns to order by.
1008
+
1009
+ Returns:
1010
+ RANK window function expression.
1011
+ """
1012
+ return self._create_window_function("RANK", [], partition_by, order_by)
1013
+
1014
+ def dense_rank(
1015
+ self,
1016
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
1017
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
1018
+ ) -> exp.Expression:
1019
+ """Create a DENSE_RANK() window function.
1020
+
1021
+ Args:
1022
+ partition_by: Columns to partition by.
1023
+ order_by: Columns to order by.
1024
+
1025
+ Returns:
1026
+ DENSE_RANK window function expression.
1027
+ """
1028
+ return self._create_window_function("DENSE_RANK", [], partition_by, order_by)
1029
+
1030
+ @staticmethod
1031
+ def _create_window_function(
1032
+ func_name: str,
1033
+ func_args: list[exp.Expression],
1034
+ partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
1035
+ order_by: Optional[Union[str, list[str], exp.Expression]] = None,
1036
+ ) -> exp.Expression:
1037
+ """Helper to create window function expressions.
1038
+
1039
+ Args:
1040
+ func_name: Name of the window function.
1041
+ func_args: Arguments to the function.
1042
+ partition_by: Columns to partition by.
1043
+ order_by: Columns to order by.
1044
+
1045
+ Returns:
1046
+ Window function expression.
1047
+ """
1048
+ # Create the function call
1049
+ func_expr = exp.Anonymous(this=func_name, expressions=func_args)
1050
+
1051
+ # Build OVER clause
1052
+ over_args: dict[str, Any] = {}
1053
+
1054
+ if partition_by:
1055
+ if isinstance(partition_by, str):
1056
+ over_args["partition_by"] = [exp.column(partition_by)]
1057
+ elif isinstance(partition_by, list):
1058
+ over_args["partition_by"] = [exp.column(col) for col in partition_by]
1059
+ elif isinstance(partition_by, exp.Expression):
1060
+ over_args["partition_by"] = [partition_by]
1061
+
1062
+ if order_by:
1063
+ if isinstance(order_by, str):
1064
+ over_args["order"] = [exp.column(order_by).asc()]
1065
+ elif isinstance(order_by, list):
1066
+ over_args["order"] = [exp.column(col).asc() for col in order_by]
1067
+ elif isinstance(order_by, exp.Expression):
1068
+ over_args["order"] = [order_by]
1069
+
1070
+ return exp.Window(this=func_expr, **over_args)
1071
+
1072
+
1073
+ class CaseExpressionBuilder:
1074
+ """Builder for CASE expressions using the SQL factory.
1075
+
1076
+ Example:
1077
+ ```python
1078
+ from sqlspec import sql
1079
+
1080
+ case_expr = (
1081
+ sql.case()
1082
+ .when(sql.age < 18, "Minor")
1083
+ .when(sql.age < 65, "Adult")
1084
+ .else_("Senior")
1085
+ .end()
1086
+ )
1087
+ ```
1088
+ """
1089
+
1090
+ def __init__(self) -> None:
1091
+ """Initialize the CASE expression builder."""
1092
+ self._conditions: list[exp.When] = []
1093
+ self._default: Optional[exp.Expression] = None
1094
+
1095
+ def when(
1096
+ self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]
1097
+ ) -> "CaseExpressionBuilder":
1098
+ """Add a WHEN clause.
1099
+
1100
+ Args:
1101
+ condition: Condition to test.
1102
+ value: Value to return if condition is true.
1103
+
1104
+ Returns:
1105
+ Self for method chaining.
1106
+ """
1107
+ cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
1108
+
1109
+ if isinstance(value, str):
1110
+ val_expr = exp.Literal.string(value)
1111
+ elif isinstance(value, (int, float)):
1112
+ val_expr = exp.Literal.number(value)
1113
+ elif isinstance(value, exp.Expression):
1114
+ val_expr = value # type: ignore[assignment]
1115
+ else:
1116
+ val_expr = exp.Literal.string(str(value))
1117
+
1118
+ when_clause = exp.When(this=cond_expr, then=val_expr)
1119
+ self._conditions.append(when_clause)
1120
+ return self
1121
+
1122
+ def else_(self, value: Union[str, exp.Expression, Any]) -> "CaseExpressionBuilder":
1123
+ """Add an ELSE clause.
1124
+
1125
+ Args:
1126
+ value: Default value to return.
1127
+
1128
+ Returns:
1129
+ Self for method chaining.
1130
+ """
1131
+ if isinstance(value, str):
1132
+ self._default = exp.Literal.string(value)
1133
+ elif isinstance(value, (int, float)):
1134
+ self._default = exp.Literal.number(value)
1135
+ elif isinstance(value, exp.Expression):
1136
+ self._default = value
1137
+ else:
1138
+ self._default = exp.Literal.string(str(value))
1139
+ return self
1140
+
1141
+ def end(self) -> exp.Expression:
1142
+ """Complete the CASE expression.
1143
+
1144
+ Returns:
1145
+ Complete CASE expression.
1146
+ """
1147
+ return exp.Case(ifs=self._conditions, default=self._default)