sqlspec 0.14.0__py3-none-any.whl → 0.15.0__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 (158) hide show
  1. sqlspec/__init__.py +50 -25
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +256 -120
  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 +115 -248
  10. sqlspec/adapters/adbc/driver.py +462 -353
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +199 -129
  14. sqlspec/adapters/aiosqlite/driver.py +230 -269
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -168
  18. sqlspec/adapters/asyncmy/driver.py +260 -225
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +82 -181
  22. sqlspec/adapters/asyncpg/driver.py +285 -383
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -258
  26. sqlspec/adapters/bigquery/driver.py +474 -646
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +415 -351
  30. sqlspec/adapters/duckdb/driver.py +343 -413
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -379
  34. sqlspec/adapters/oracledb/driver.py +507 -560
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -254
  38. sqlspec/adapters/psqlpy/driver.py +505 -234
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -403
  42. sqlspec/adapters/psycopg/driver.py +706 -872
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +202 -118
  46. sqlspec/adapters/sqlite/driver.py +264 -303
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder → builder}/_base.py +120 -55
  50. sqlspec/{statement/builder → builder}/_column.py +17 -6
  51. sqlspec/{statement/builder → builder}/_ddl.py +46 -79
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
  53. sqlspec/{statement/builder → builder}/_delete.py +6 -25
  54. sqlspec/{statement/builder → builder}/_insert.py +6 -64
  55. sqlspec/builder/_merge.py +56 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +3 -10
  57. sqlspec/{statement/builder → builder}/_select.py +11 -56
  58. sqlspec/{statement/builder → builder}/_update.py +12 -18
  59. sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
  60. sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
  61. sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +22 -16
  62. sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
  63. sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +3 -5
  64. sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
  65. sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
  66. sqlspec/{statement/builder → builder}/mixins/_select_operations.py +21 -36
  67. sqlspec/{statement/builder → builder}/mixins/_update_operations.py +3 -14
  68. sqlspec/{statement/builder → builder}/mixins/_where_clause.py +52 -79
  69. sqlspec/cli.py +4 -5
  70. sqlspec/config.py +180 -133
  71. sqlspec/core/__init__.py +63 -0
  72. sqlspec/core/cache.py +873 -0
  73. sqlspec/core/compiler.py +396 -0
  74. sqlspec/core/filters.py +828 -0
  75. sqlspec/core/hashing.py +310 -0
  76. sqlspec/core/parameters.py +1209 -0
  77. sqlspec/core/result.py +664 -0
  78. sqlspec/{statement → core}/splitter.py +321 -191
  79. sqlspec/core/statement.py +651 -0
  80. sqlspec/driver/__init__.py +7 -10
  81. sqlspec/driver/_async.py +387 -176
  82. sqlspec/driver/_common.py +527 -289
  83. sqlspec/driver/_sync.py +390 -172
  84. sqlspec/driver/mixins/__init__.py +2 -19
  85. sqlspec/driver/mixins/_result_tools.py +168 -0
  86. sqlspec/driver/mixins/_sql_translator.py +6 -3
  87. sqlspec/exceptions.py +5 -252
  88. sqlspec/extensions/aiosql/adapter.py +93 -96
  89. sqlspec/extensions/litestar/config.py +0 -1
  90. sqlspec/extensions/litestar/handlers.py +15 -26
  91. sqlspec/extensions/litestar/plugin.py +16 -14
  92. sqlspec/extensions/litestar/providers.py +17 -52
  93. sqlspec/loader.py +424 -105
  94. sqlspec/migrations/__init__.py +12 -0
  95. sqlspec/migrations/base.py +92 -68
  96. sqlspec/migrations/commands.py +24 -106
  97. sqlspec/migrations/loaders.py +402 -0
  98. sqlspec/migrations/runner.py +49 -51
  99. sqlspec/migrations/tracker.py +31 -44
  100. sqlspec/migrations/utils.py +64 -24
  101. sqlspec/protocols.py +7 -183
  102. sqlspec/storage/__init__.py +1 -1
  103. sqlspec/storage/backends/base.py +37 -40
  104. sqlspec/storage/backends/fsspec.py +136 -112
  105. sqlspec/storage/backends/obstore.py +138 -160
  106. sqlspec/storage/capabilities.py +5 -4
  107. sqlspec/storage/registry.py +57 -106
  108. sqlspec/typing.py +136 -115
  109. sqlspec/utils/__init__.py +2 -3
  110. sqlspec/utils/correlation.py +0 -3
  111. sqlspec/utils/deprecation.py +6 -6
  112. sqlspec/utils/fixtures.py +6 -6
  113. sqlspec/utils/logging.py +0 -2
  114. sqlspec/utils/module_loader.py +7 -12
  115. sqlspec/utils/singleton.py +0 -1
  116. sqlspec/utils/sync_tools.py +16 -37
  117. sqlspec/utils/text.py +12 -51
  118. sqlspec/utils/type_guards.py +443 -232
  119. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/METADATA +7 -2
  120. sqlspec-0.15.0.dist-info/RECORD +134 -0
  121. sqlspec-0.15.0.dist-info/entry_points.txt +2 -0
  122. sqlspec/driver/connection.py +0 -207
  123. sqlspec/driver/mixins/_cache.py +0 -114
  124. sqlspec/driver/mixins/_csv_writer.py +0 -91
  125. sqlspec/driver/mixins/_pipeline.py +0 -508
  126. sqlspec/driver/mixins/_query_tools.py +0 -796
  127. sqlspec/driver/mixins/_result_utils.py +0 -138
  128. sqlspec/driver/mixins/_storage.py +0 -912
  129. sqlspec/driver/mixins/_type_coercion.py +0 -128
  130. sqlspec/driver/parameters.py +0 -138
  131. sqlspec/statement/__init__.py +0 -21
  132. sqlspec/statement/builder/_merge.py +0 -95
  133. sqlspec/statement/cache.py +0 -50
  134. sqlspec/statement/filters.py +0 -625
  135. sqlspec/statement/parameters.py +0 -996
  136. sqlspec/statement/pipelines/__init__.py +0 -210
  137. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  138. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  139. sqlspec/statement/pipelines/context.py +0 -115
  140. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  141. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  142. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  143. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  144. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  145. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  146. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  147. sqlspec/statement/pipelines/validators/_performance.py +0 -714
  148. sqlspec/statement/pipelines/validators/_security.py +0 -967
  149. sqlspec/statement/result.py +0 -435
  150. sqlspec/statement/sql.py +0 -1774
  151. sqlspec/utils/cached_property.py +0 -25
  152. sqlspec/utils/statement_hashing.py +0 -203
  153. sqlspec-0.14.0.dist-info/RECORD +0 -143
  154. sqlspec-0.14.0.dist-info/entry_points.txt +0 -2
  155. /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
  156. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/WHEEL +0 -0
  157. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/licenses/LICENSE +0 -0
  158. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/_sql.py CHANGED
@@ -1,7 +1,6 @@
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
@@ -12,10 +11,10 @@ from sqlglot import exp
12
11
  from sqlglot.dialects.dialect import DialectType
13
12
  from sqlglot.errors import ParseError as SQLGlotParseError
14
13
 
14
+ from sqlspec.builder import Column, Delete, Insert, Merge, Select, Truncate, Update
15
15
  from sqlspec.exceptions import SQLBuilderError
16
- from sqlspec.statement.builder import Column, Delete, Insert, Merge, Select, Update
17
16
 
18
- __all__ = ("SQLFactory",)
17
+ __all__ = ("Case", "Column", "Delete", "Insert", "Merge", "SQLFactory", "Select", "Truncate", "Update", "sql")
19
18
 
20
19
  logger = logging.getLogger("sqlspec")
21
20
 
@@ -50,58 +49,18 @@ SQL_STARTERS = {
50
49
 
51
50
 
52
51
  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
- """
52
+ """Unified factory for creating SQL builders and column expressions with a fluent API."""
90
53
 
91
54
  @classmethod
92
55
  def detect_sql_type(cls, sql: str, dialect: DialectType = None) -> str:
93
56
  try:
94
- # Minimal parsing just to get the command type
95
57
  parsed_expr = sqlglot.parse_one(sql, read=dialect)
96
58
  if parsed_expr and parsed_expr.key:
97
59
  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
60
  if parsed_expr:
101
- # Attempt to get the class name as a fallback, e.g., "Set", "Command"
102
61
  command_type = type(parsed_expr).__name__.upper()
103
62
  if command_type == "COMMAND" and parsed_expr.this:
104
- return str(parsed_expr.this).upper() # e.g. "SET", "ALTER"
63
+ return str(parsed_expr.this).upper()
105
64
  return command_type
106
65
  except SQLGlotParseError:
107
66
  logger.debug("Failed to parse SQL for type detection: %s", sql[:100])
@@ -120,15 +79,7 @@ class SQLFactory:
120
79
  # ===================
121
80
  # Callable Interface
122
81
  # ===================
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":
82
+ def __call__(self, statement: str, dialect: DialectType = None) -> "Any":
132
83
  """Create a SelectBuilder from a SQL string, only allowing SELECT/CTE queries.
133
84
 
134
85
  Args:
@@ -152,7 +103,6 @@ class SQLFactory:
152
103
  msg = f"Failed to parse SQL: {e}"
153
104
  raise SQLBuilderError(msg) from e
154
105
  actual_type = type(parsed_expr).__name__.upper()
155
- # Map sqlglot expression class to type string
156
106
  expr_type_map = {
157
107
  "SELECT": "SELECT",
158
108
  "INSERT": "INSERT",
@@ -300,8 +250,6 @@ class SQLFactory:
300
250
  try:
301
251
  # Use SQLGlot directly for parsing - no validation here
302
252
  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
253
 
306
254
  if isinstance(parsed_expr, exp.Insert):
307
255
  builder._expression = parsed_expr
@@ -324,8 +272,6 @@ class SQLFactory:
324
272
  try:
325
273
  # Use SQLGlot directly for parsing - no validation here
326
274
  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
275
 
330
276
  if isinstance(parsed_expr, exp.Select):
331
277
  builder._expression = parsed_expr
@@ -342,8 +288,6 @@ class SQLFactory:
342
288
  try:
343
289
  # Use SQLGlot directly for parsing - no validation here
344
290
  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
291
 
348
292
  if isinstance(parsed_expr, exp.Update):
349
293
  builder._expression = parsed_expr
@@ -360,8 +304,6 @@ class SQLFactory:
360
304
  try:
361
305
  # Use SQLGlot directly for parsing - no validation here
362
306
  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
307
 
366
308
  if isinstance(parsed_expr, exp.Delete):
367
309
  builder._expression = parsed_expr
@@ -378,8 +320,6 @@ class SQLFactory:
378
320
  try:
379
321
  # Use SQLGlot directly for parsing - no validation here
380
322
  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
323
 
384
324
  if isinstance(parsed_expr, exp.Merge):
385
325
  builder._expression = parsed_expr
@@ -395,6 +335,18 @@ class SQLFactory:
395
335
  # Column References
396
336
  # ===================
397
337
 
338
+ def column(self, name: str, table: Optional[str] = None) -> Column:
339
+ """Create a column reference.
340
+
341
+ Args:
342
+ name: Column name.
343
+ table: Optional table name.
344
+
345
+ Returns:
346
+ Column object that supports method chaining and operator overloading.
347
+ """
348
+ return Column(name, table)
349
+
398
350
  def __getattr__(self, name: str) -> Column:
399
351
  """Dynamically create column references.
400
352
 
@@ -406,6 +358,66 @@ class SQLFactory:
406
358
  """
407
359
  return Column(name)
408
360
 
361
+ # ===================
362
+ # Raw SQL Expressions
363
+ # ===================
364
+
365
+ @staticmethod
366
+ def raw(sql_fragment: str) -> exp.Expression:
367
+ """Create a raw SQL expression from a string fragment.
368
+
369
+ This method makes it explicit that you are passing raw SQL that should
370
+ be parsed and included directly in the query. Useful for complex expressions,
371
+ database-specific functions, or when you need precise control over the SQL.
372
+
373
+ Args:
374
+ sql_fragment: Raw SQL string to parse into an expression.
375
+
376
+ Returns:
377
+ SQLGlot expression from the parsed SQL fragment.
378
+
379
+ Raises:
380
+ SQLBuilderError: If the SQL fragment cannot be parsed.
381
+
382
+ Example:
383
+ ```python
384
+ # Raw column expression with alias
385
+ query = sql.select(
386
+ sql.raw("user.id AS u_id"), "name"
387
+ ).from_("users")
388
+
389
+ # Raw function call
390
+ query = sql.select(
391
+ sql.raw("COALESCE(name, 'Unknown')")
392
+ ).from_("users")
393
+
394
+ # Raw complex expression
395
+ query = (
396
+ sql.select("*")
397
+ .from_("orders")
398
+ .where(sql.raw("DATE(created_at) = CURRENT_DATE"))
399
+ )
400
+
401
+ # Raw window function
402
+ query = sql.select(
403
+ "name",
404
+ sql.raw(
405
+ "ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC)"
406
+ ),
407
+ ).from_("employees")
408
+ ```
409
+ """
410
+ try:
411
+ parsed: Optional[exp.Expression] = exp.maybe_parse(sql_fragment)
412
+ if parsed is not None:
413
+ return parsed
414
+ if sql_fragment.strip().replace("_", "").replace(".", "").isalnum():
415
+ return exp.to_identifier(sql_fragment)
416
+ return exp.Literal.string(sql_fragment)
417
+ except Exception as e:
418
+ msg = f"Failed to parse raw SQL fragment '{sql_fragment}': {e}"
419
+ raise SQLBuilderError(msg) from e
420
+
409
421
  # ===================
410
422
  # Aggregate Functions
411
423
  # ===================
@@ -597,7 +609,7 @@ class SQLFactory:
597
609
  ```
598
610
  """
599
611
  if isinstance(values, list):
600
- literals = [exp.Literal.string(str(v)) if isinstance(v, str) else exp.Literal.number(v) for v in values]
612
+ literals = [SQLFactory._to_literal(v) for v in values]
601
613
  return exp.Any(this=exp.Array(expressions=literals))
602
614
  if isinstance(values, str):
603
615
  # Parse as SQL
@@ -607,6 +619,29 @@ class SQLFactory:
607
619
  return exp.Any(this=exp.Literal.string(values))
608
620
  return exp.Any(this=values)
609
621
 
622
+ @staticmethod
623
+ def not_any_(values: Union[list[Any], exp.Expression, str]) -> exp.Expression:
624
+ """Create a NOT ANY expression for use with comparison operators.
625
+
626
+ Args:
627
+ values: Values, expression, or subquery for the NOT ANY clause.
628
+
629
+ Returns:
630
+ NOT ANY expression.
631
+
632
+ Example:
633
+ ```python
634
+ # WHERE id <> ANY(subquery)
635
+ subquery = sql.select("user_id").from_("blocked_users")
636
+ query = (
637
+ sql.select("*")
638
+ .from_("users")
639
+ .where(sql.id.neq(sql.not_any(subquery)))
640
+ )
641
+ ```
642
+ """
643
+ return SQLFactory.any(values) # NOT ANY is handled by the comparison operator
644
+
610
645
  # ===================
611
646
  # String Functions
612
647
  # ===================
@@ -687,6 +722,28 @@ class SQLFactory:
687
722
  # Conversion Functions
688
723
  # ===================
689
724
 
725
+ @staticmethod
726
+ def _to_literal(value: Any) -> exp.Expression:
727
+ """Convert a Python value to a SQLGlot literal expression.
728
+
729
+ Uses SQLGlot's built-in exp.convert() function for optimal dialect-agnostic
730
+ literal creation. Handles all Python primitive types correctly:
731
+ - None -> exp.Null (renders as NULL)
732
+ - bool -> exp.Boolean (renders as TRUE/FALSE or 1/0 based on dialect)
733
+ - int/float -> exp.Literal with is_number=True
734
+ - str -> exp.Literal with is_string=True
735
+ - exp.Expression -> returned as-is (passthrough)
736
+
737
+ Args:
738
+ value: Python value or SQLGlot expression to convert.
739
+
740
+ Returns:
741
+ SQLGlot expression representing the literal value.
742
+ """
743
+ if isinstance(value, exp.Expression):
744
+ return value
745
+ return exp.convert(value)
746
+
690
747
  @staticmethod
691
748
  def decode(column: Union[str, exp.Expression], *args: Union[str, exp.Expression, Any]) -> exp.Expression:
692
749
  """Create a DECODE expression (Oracle-style conditional logic).
@@ -725,29 +782,14 @@ class SQLFactory:
725
782
  for i in range(0, len(args) - 1, 2):
726
783
  if i + 1 >= len(args):
727
784
  # 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]
785
+ default = SQLFactory._to_literal(args[i])
729
786
  break
730
787
 
731
788
  search_val = args[i]
732
789
  result_val = args[i + 1]
733
790
 
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))
791
+ search_expr = SQLFactory._to_literal(search_val)
792
+ result_expr = SQLFactory._to_literal(result_val)
751
793
 
752
794
  condition = exp.EQ(this=col_expr, expression=search_expr)
753
795
  conditions.append(exp.When(this=condition, then=result_expr))
@@ -793,30 +835,136 @@ class SQLFactory:
793
835
  COALESCE expression equivalent to NVL.
794
836
  """
795
837
  col_expr = exp.column(column) if isinstance(column, str) else column
838
+ sub_expr = SQLFactory._to_literal(substitute_value)
839
+ return exp.Coalesce(expressions=[col_expr, sub_expr])
796
840
 
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))
841
+ @staticmethod
842
+ def nvl2(
843
+ column: Union[str, exp.Expression],
844
+ value_if_not_null: Union[str, exp.Expression, Any],
845
+ value_if_null: Union[str, exp.Expression, Any],
846
+ ) -> exp.Expression:
847
+ """Create an NVL2 (Oracle-style) expression using CASE.
805
848
 
806
- return exp.Coalesce(expressions=[col_expr, sub_expr])
849
+ NVL2 returns value_if_not_null if column is not NULL,
850
+ otherwise returns value_if_null.
851
+
852
+ Args:
853
+ column: Column to check for NULL.
854
+ value_if_not_null: Value to use if column is NOT NULL.
855
+ value_if_null: Value to use if column is NULL.
856
+
857
+ Returns:
858
+ CASE expression equivalent to NVL2.
859
+
860
+ Example:
861
+ ```python
862
+ # NVL2(salary, 'Has Salary', 'No Salary')
863
+ sql.nvl2("salary", "Has Salary", "No Salary")
864
+ ```
865
+ """
866
+ col_expr = exp.column(column) if isinstance(column, str) else column
867
+ not_null_expr = SQLFactory._to_literal(value_if_not_null)
868
+ null_expr = SQLFactory._to_literal(value_if_null)
869
+
870
+ # Create CASE WHEN column IS NOT NULL THEN value_if_not_null ELSE value_if_null END
871
+ is_null = exp.Is(this=col_expr, expression=exp.Null())
872
+ condition = exp.Not(this=is_null)
873
+ when_clause = exp.If(this=condition, true=not_null_expr)
874
+
875
+ return exp.Case(ifs=[when_clause], default=null_expr)
876
+
877
+ # ===================
878
+ # Bulk Operations
879
+ # ===================
880
+
881
+ @staticmethod
882
+ def bulk_insert(table_name: str, column_count: int, placeholder_style: str = "?") -> exp.Expression:
883
+ """Create bulk INSERT expression for executemany operations.
884
+
885
+ This is specifically for bulk loading operations like CSV ingestion where
886
+ we need an INSERT expression with placeholders for executemany().
887
+
888
+ Args:
889
+ table_name: Name of the table to insert into
890
+ column_count: Number of columns (for placeholder generation)
891
+ placeholder_style: Placeholder style ("?" for SQLite/PostgreSQL, "%s" for MySQL, ":1" for Oracle)
892
+
893
+ Returns:
894
+ INSERT expression with proper placeholders for bulk operations
895
+
896
+ Example:
897
+ ```python
898
+ from sqlspec import sql
899
+
900
+ # SQLite/PostgreSQL style
901
+ insert_expr = sql.bulk_insert("my_table", 3)
902
+ # Creates: INSERT INTO "my_table" VALUES (?, ?, ?)
903
+
904
+ # MySQL style
905
+ insert_expr = sql.bulk_insert(
906
+ "my_table", 3, placeholder_style="%s"
907
+ )
908
+ # Creates: INSERT INTO "my_table" VALUES (%s, %s, %s)
909
+
910
+ # Oracle style
911
+ insert_expr = sql.bulk_insert(
912
+ "my_table", 3, placeholder_style=":1"
913
+ )
914
+ # Creates: INSERT INTO "my_table" VALUES (:1, :2, :3)
915
+ ```
916
+ """
917
+ return exp.Insert(
918
+ this=exp.Table(this=exp.to_identifier(table_name)),
919
+ expression=exp.Values(
920
+ expressions=[
921
+ exp.Tuple(expressions=[exp.Placeholder(this=placeholder_style) for _ in range(column_count)])
922
+ ]
923
+ ),
924
+ )
925
+
926
+ def truncate(self, table_name: str) -> "Truncate":
927
+ """Create a TRUNCATE TABLE builder.
928
+
929
+ Args:
930
+ table_name: Name of the table to truncate
931
+
932
+ Returns:
933
+ TruncateTable builder instance
934
+
935
+ Example:
936
+ ```python
937
+ from sqlspec import sql
938
+
939
+ # Simple truncate
940
+ truncate_sql = sql.truncate_table("my_table").build().sql
941
+
942
+ # Truncate with options
943
+ truncate_sql = (
944
+ sql.truncate_table("my_table")
945
+ .cascade()
946
+ .restart_identity()
947
+ .build()
948
+ .sql
949
+ )
950
+ ```
951
+ """
952
+ builder = Truncate(dialect=self.dialect)
953
+ builder._table_name = table_name
954
+ return builder
807
955
 
808
956
  # ===================
809
957
  # Case Expressions
810
958
  # ===================
811
959
 
812
960
  @staticmethod
813
- def case() -> "CaseExpressionBuilder":
961
+ def case() -> "Case":
814
962
  """Create a CASE expression builder.
815
963
 
816
964
  Returns:
817
965
  CaseExpressionBuilder for building CASE expressions.
818
966
  """
819
- return CaseExpressionBuilder()
967
+ return Case()
820
968
 
821
969
  # ===================
822
970
  # Window Functions
@@ -911,7 +1059,7 @@ class SQLFactory:
911
1059
  return exp.Window(this=func_expr, **over_args)
912
1060
 
913
1061
 
914
- class CaseExpressionBuilder:
1062
+ class Case:
915
1063
  """Builder for CASE expressions using the SQL factory.
916
1064
 
917
1065
  Example:
@@ -930,12 +1078,10 @@ class CaseExpressionBuilder:
930
1078
 
931
1079
  def __init__(self) -> None:
932
1080
  """Initialize the CASE expression builder."""
933
- self._conditions: list[exp.When] = []
1081
+ self._conditions: list[exp.If] = []
934
1082
  self._default: Optional[exp.Expression] = None
935
1083
 
936
- def when(
937
- self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]
938
- ) -> "CaseExpressionBuilder":
1084
+ def when(self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]) -> "Case":
939
1085
  """Add a WHEN clause.
940
1086
 
941
1087
  Args:
@@ -946,21 +1092,14 @@ class CaseExpressionBuilder:
946
1092
  Self for method chaining.
947
1093
  """
948
1094
  cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
1095
+ val_expr = SQLFactory._to_literal(value)
949
1096
 
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)
1097
+ # SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
1098
+ when_clause = exp.If(this=cond_expr, true=val_expr)
960
1099
  self._conditions.append(when_clause)
961
1100
  return self
962
1101
 
963
- def else_(self, value: Union[str, exp.Expression, Any]) -> "CaseExpressionBuilder":
1102
+ def else_(self, value: Union[str, exp.Expression, Any]) -> "Case":
964
1103
  """Add an ELSE clause.
965
1104
 
966
1105
  Args:
@@ -969,14 +1108,7 @@ class CaseExpressionBuilder:
969
1108
  Returns:
970
1109
  Self for method chaining.
971
1110
  """
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))
1111
+ self._default = SQLFactory._to_literal(value)
980
1112
  return self
981
1113
 
982
1114
  def end(self) -> exp.Expression:
@@ -986,3 +1118,7 @@ class CaseExpressionBuilder:
986
1118
  Complete CASE expression.
987
1119
  """
988
1120
  return exp.Case(ifs=self._conditions, default=self._default)
1121
+
1122
+
1123
+ # Create a default SQL factory instance
1124
+ sql = SQLFactory()