sqlspec 0.14.1__py3-none-any.whl → 0.16.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 (159) hide show
  1. sqlspec/__init__.py +50 -25
  2. sqlspec/__main__.py +1 -1
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +480 -121
  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 -260
  10. sqlspec/adapters/adbc/driver.py +462 -367
  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 +18 -65
  55. sqlspec/builder/_merge.py +56 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +8 -11
  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 +34 -18
  62. sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
  63. sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +19 -9
  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 +25 -38
  67. sqlspec/{statement/builder → builder}/mixins/_update_operations.py +15 -16
  68. sqlspec/{statement/builder → builder}/mixins/_where_clause.py +210 -137
  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 +830 -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 +666 -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 +164 -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/cli.py +1 -1
  90. sqlspec/extensions/litestar/config.py +0 -1
  91. sqlspec/extensions/litestar/handlers.py +15 -26
  92. sqlspec/extensions/litestar/plugin.py +18 -16
  93. sqlspec/extensions/litestar/providers.py +17 -52
  94. sqlspec/loader.py +424 -105
  95. sqlspec/migrations/__init__.py +12 -0
  96. sqlspec/migrations/base.py +92 -68
  97. sqlspec/migrations/commands.py +24 -106
  98. sqlspec/migrations/loaders.py +402 -0
  99. sqlspec/migrations/runner.py +49 -51
  100. sqlspec/migrations/tracker.py +31 -44
  101. sqlspec/migrations/utils.py +64 -24
  102. sqlspec/protocols.py +7 -183
  103. sqlspec/storage/__init__.py +1 -1
  104. sqlspec/storage/backends/base.py +37 -40
  105. sqlspec/storage/backends/fsspec.py +136 -112
  106. sqlspec/storage/backends/obstore.py +138 -160
  107. sqlspec/storage/capabilities.py +5 -4
  108. sqlspec/storage/registry.py +57 -106
  109. sqlspec/typing.py +136 -115
  110. sqlspec/utils/__init__.py +2 -3
  111. sqlspec/utils/correlation.py +0 -3
  112. sqlspec/utils/deprecation.py +6 -6
  113. sqlspec/utils/fixtures.py +6 -6
  114. sqlspec/utils/logging.py +0 -2
  115. sqlspec/utils/module_loader.py +7 -12
  116. sqlspec/utils/singleton.py +0 -1
  117. sqlspec/utils/sync_tools.py +17 -38
  118. sqlspec/utils/text.py +12 -51
  119. sqlspec/utils/type_guards.py +443 -232
  120. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/METADATA +7 -2
  121. sqlspec-0.16.0.dist-info/RECORD +134 -0
  122. sqlspec/adapters/adbc/transformers.py +0 -108
  123. sqlspec/driver/connection.py +0 -207
  124. sqlspec/driver/mixins/_cache.py +0 -114
  125. sqlspec/driver/mixins/_csv_writer.py +0 -91
  126. sqlspec/driver/mixins/_pipeline.py +0 -508
  127. sqlspec/driver/mixins/_query_tools.py +0 -796
  128. sqlspec/driver/mixins/_result_utils.py +0 -138
  129. sqlspec/driver/mixins/_storage.py +0 -912
  130. sqlspec/driver/mixins/_type_coercion.py +0 -128
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/statement/__init__.py +0 -21
  133. sqlspec/statement/builder/_merge.py +0 -95
  134. sqlspec/statement/cache.py +0 -50
  135. sqlspec/statement/filters.py +0 -625
  136. sqlspec/statement/parameters.py +0 -956
  137. sqlspec/statement/pipelines/__init__.py +0 -210
  138. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  139. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  140. sqlspec/statement/pipelines/context.py +0 -109
  141. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  142. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  143. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  144. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  145. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  146. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  147. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  148. sqlspec/statement/pipelines/validators/_performance.py +0 -714
  149. sqlspec/statement/pipelines/validators/_security.py +0 -967
  150. sqlspec/statement/result.py +0 -435
  151. sqlspec/statement/sql.py +0 -1774
  152. sqlspec/utils/cached_property.py +0 -25
  153. sqlspec/utils/statement_hashing.py +0 -203
  154. sqlspec-0.14.1.dist-info/RECORD +0 -145
  155. /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
  156. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/WHEEL +0 -0
  157. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/entry_points.txt +0 -0
  158. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/LICENSE +0 -0
  159. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/_sql.py CHANGED
@@ -1,21 +1,68 @@
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
9
8
 
10
9
  import sqlglot
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 (
15
+ AlterTable,
16
+ Column,
17
+ CommentOn,
18
+ CreateIndex,
19
+ CreateMaterializedView,
20
+ CreateSchema,
21
+ CreateTable,
22
+ CreateTableAsSelect,
23
+ CreateView,
24
+ Delete,
25
+ DropIndex,
26
+ DropSchema,
27
+ DropTable,
28
+ DropView,
29
+ Insert,
30
+ Merge,
31
+ RenameTable,
32
+ Select,
33
+ Truncate,
34
+ Update,
35
+ )
15
36
  from sqlspec.exceptions import SQLBuilderError
16
- from sqlspec.statement.builder import Column, Delete, Insert, Merge, Select, Update
17
37
 
18
- __all__ = ("SQLFactory",)
38
+ if TYPE_CHECKING:
39
+ from sqlspec.core.statement import SQL
40
+
41
+ __all__ = (
42
+ "AlterTable",
43
+ "Case",
44
+ "Column",
45
+ "CommentOn",
46
+ "CreateIndex",
47
+ "CreateMaterializedView",
48
+ "CreateSchema",
49
+ "CreateTable",
50
+ "CreateTableAsSelect",
51
+ "CreateView",
52
+ "Delete",
53
+ "DropIndex",
54
+ "DropSchema",
55
+ "DropTable",
56
+ "DropView",
57
+ "Insert",
58
+ "Merge",
59
+ "RenameTable",
60
+ "SQLFactory",
61
+ "Select",
62
+ "Truncate",
63
+ "Update",
64
+ "sql",
65
+ )
19
66
 
20
67
  logger = logging.getLogger("sqlspec")
21
68
 
@@ -50,58 +97,18 @@ SQL_STARTERS = {
50
97
 
51
98
 
52
99
  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
- """
100
+ """Unified factory for creating SQL builders and column expressions with a fluent API."""
90
101
 
91
102
  @classmethod
92
103
  def detect_sql_type(cls, sql: str, dialect: DialectType = None) -> str:
93
104
  try:
94
- # Minimal parsing just to get the command type
95
105
  parsed_expr = sqlglot.parse_one(sql, read=dialect)
96
106
  if parsed_expr and parsed_expr.key:
97
107
  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
108
  if parsed_expr:
101
- # Attempt to get the class name as a fallback, e.g., "Set", "Command"
102
109
  command_type = type(parsed_expr).__name__.upper()
103
110
  if command_type == "COMMAND" and parsed_expr.this:
104
- return str(parsed_expr.this).upper() # e.g. "SET", "ALTER"
111
+ return str(parsed_expr.this).upper()
105
112
  return command_type
106
113
  except SQLGlotParseError:
107
114
  logger.debug("Failed to parse SQL for type detection: %s", sql[:100])
@@ -120,15 +127,7 @@ class SQLFactory:
120
127
  # ===================
121
128
  # Callable Interface
122
129
  # ===================
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":
130
+ def __call__(self, statement: str, dialect: DialectType = None) -> "Any":
132
131
  """Create a SelectBuilder from a SQL string, only allowing SELECT/CTE queries.
133
132
 
134
133
  Args:
@@ -152,7 +151,6 @@ class SQLFactory:
152
151
  msg = f"Failed to parse SQL: {e}"
153
152
  raise SQLBuilderError(msg) from e
154
153
  actual_type = type(parsed_expr).__name__.upper()
155
- # Map sqlglot expression class to type string
156
154
  expr_type_map = {
157
155
  "SELECT": "SELECT",
158
156
  "INSERT": "INSERT",
@@ -262,6 +260,174 @@ class SQLFactory:
262
260
  return builder.into(table_or_sql)
263
261
  return builder
264
262
 
263
+ # ===================
264
+ # DDL Statement Builders
265
+ # ===================
266
+
267
+ def create_table(self, table_name: str, dialect: DialectType = None) -> "CreateTable":
268
+ """Create a CREATE TABLE builder.
269
+
270
+ Args:
271
+ table_name: Name of the table to create
272
+ dialect: Optional SQL dialect
273
+
274
+ Returns:
275
+ CreateTable builder instance
276
+ """
277
+ builder = CreateTable(table_name)
278
+ builder.dialect = dialect or self.dialect
279
+ return builder
280
+
281
+ def create_table_as_select(self, dialect: DialectType = None) -> "CreateTableAsSelect":
282
+ """Create a CREATE TABLE AS SELECT builder.
283
+
284
+ Args:
285
+ dialect: Optional SQL dialect
286
+
287
+ Returns:
288
+ CreateTableAsSelect builder instance
289
+ """
290
+ builder = CreateTableAsSelect()
291
+ builder.dialect = dialect or self.dialect
292
+ return builder
293
+
294
+ def create_view(self, dialect: DialectType = None) -> "CreateView":
295
+ """Create a CREATE VIEW builder.
296
+
297
+ Args:
298
+ dialect: Optional SQL dialect
299
+
300
+ Returns:
301
+ CreateView builder instance
302
+ """
303
+ builder = CreateView()
304
+ builder.dialect = dialect or self.dialect
305
+ return builder
306
+
307
+ def create_materialized_view(self, dialect: DialectType = None) -> "CreateMaterializedView":
308
+ """Create a CREATE MATERIALIZED VIEW builder.
309
+
310
+ Args:
311
+ dialect: Optional SQL dialect
312
+
313
+ Returns:
314
+ CreateMaterializedView builder instance
315
+ """
316
+ builder = CreateMaterializedView()
317
+ builder.dialect = dialect or self.dialect
318
+ return builder
319
+
320
+ def create_index(self, index_name: str, dialect: DialectType = None) -> "CreateIndex":
321
+ """Create a CREATE INDEX builder.
322
+
323
+ Args:
324
+ index_name: Name of the index to create
325
+ dialect: Optional SQL dialect
326
+
327
+ Returns:
328
+ CreateIndex builder instance
329
+ """
330
+ return CreateIndex(index_name, dialect=dialect or self.dialect)
331
+
332
+ def create_schema(self, dialect: DialectType = None) -> "CreateSchema":
333
+ """Create a CREATE SCHEMA builder.
334
+
335
+ Args:
336
+ dialect: Optional SQL dialect
337
+
338
+ Returns:
339
+ CreateSchema builder instance
340
+ """
341
+ builder = CreateSchema()
342
+ builder.dialect = dialect or self.dialect
343
+ return builder
344
+
345
+ def drop_table(self, table_name: str, dialect: DialectType = None) -> "DropTable":
346
+ """Create a DROP TABLE builder.
347
+
348
+ Args:
349
+ table_name: Name of the table to drop
350
+ dialect: Optional SQL dialect
351
+
352
+ Returns:
353
+ DropTable builder instance
354
+ """
355
+ return DropTable(table_name, dialect=dialect or self.dialect)
356
+
357
+ def drop_view(self, dialect: DialectType = None) -> "DropView":
358
+ """Create a DROP VIEW builder.
359
+
360
+ Args:
361
+ dialect: Optional SQL dialect
362
+
363
+ Returns:
364
+ DropView builder instance
365
+ """
366
+ return DropView(dialect=dialect or self.dialect)
367
+
368
+ def drop_index(self, index_name: str, dialect: DialectType = None) -> "DropIndex":
369
+ """Create a DROP INDEX builder.
370
+
371
+ Args:
372
+ index_name: Name of the index to drop
373
+ dialect: Optional SQL dialect
374
+
375
+ Returns:
376
+ DropIndex builder instance
377
+ """
378
+ return DropIndex(index_name, dialect=dialect or self.dialect)
379
+
380
+ def drop_schema(self, dialect: DialectType = None) -> "DropSchema":
381
+ """Create a DROP SCHEMA builder.
382
+
383
+ Args:
384
+ dialect: Optional SQL dialect
385
+
386
+ Returns:
387
+ DropSchema builder instance
388
+ """
389
+ return DropSchema(dialect=dialect or self.dialect)
390
+
391
+ def alter_table(self, table_name: str, dialect: DialectType = None) -> "AlterTable":
392
+ """Create an ALTER TABLE builder.
393
+
394
+ Args:
395
+ table_name: Name of the table to alter
396
+ dialect: Optional SQL dialect
397
+
398
+ Returns:
399
+ AlterTable builder instance
400
+ """
401
+ builder = AlterTable(table_name)
402
+ builder.dialect = dialect or self.dialect
403
+ return builder
404
+
405
+ def rename_table(self, dialect: DialectType = None) -> "RenameTable":
406
+ """Create a RENAME TABLE builder.
407
+
408
+ Args:
409
+ dialect: Optional SQL dialect
410
+
411
+ Returns:
412
+ RenameTable builder instance
413
+ """
414
+ builder = RenameTable()
415
+ builder.dialect = dialect or self.dialect
416
+ return builder
417
+
418
+ def comment_on(self, dialect: DialectType = None) -> "CommentOn":
419
+ """Create a COMMENT ON builder.
420
+
421
+ Args:
422
+ dialect: Optional SQL dialect
423
+
424
+ Returns:
425
+ CommentOn builder instance
426
+ """
427
+ builder = CommentOn()
428
+ builder.dialect = dialect or self.dialect
429
+ return builder
430
+
265
431
  # ===================
266
432
  # SQL Analysis Helpers
267
433
  # ===================
@@ -300,8 +466,6 @@ class SQLFactory:
300
466
  try:
301
467
  # Use SQLGlot directly for parsing - no validation here
302
468
  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
469
 
306
470
  if isinstance(parsed_expr, exp.Insert):
307
471
  builder._expression = parsed_expr
@@ -324,8 +488,6 @@ class SQLFactory:
324
488
  try:
325
489
  # Use SQLGlot directly for parsing - no validation here
326
490
  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
491
 
330
492
  if isinstance(parsed_expr, exp.Select):
331
493
  builder._expression = parsed_expr
@@ -342,8 +504,6 @@ class SQLFactory:
342
504
  try:
343
505
  # Use SQLGlot directly for parsing - no validation here
344
506
  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
507
 
348
508
  if isinstance(parsed_expr, exp.Update):
349
509
  builder._expression = parsed_expr
@@ -360,8 +520,6 @@ class SQLFactory:
360
520
  try:
361
521
  # Use SQLGlot directly for parsing - no validation here
362
522
  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
523
 
366
524
  if isinstance(parsed_expr, exp.Delete):
367
525
  builder._expression = parsed_expr
@@ -378,8 +536,6 @@ class SQLFactory:
378
536
  try:
379
537
  # Use SQLGlot directly for parsing - no validation here
380
538
  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
539
 
384
540
  if isinstance(parsed_expr, exp.Merge):
385
541
  builder._expression = parsed_expr
@@ -395,6 +551,18 @@ class SQLFactory:
395
551
  # Column References
396
552
  # ===================
397
553
 
554
+ def column(self, name: str, table: Optional[str] = None) -> Column:
555
+ """Create a column reference.
556
+
557
+ Args:
558
+ name: Column name.
559
+ table: Optional table name.
560
+
561
+ Returns:
562
+ Column object that supports method chaining and operator overloading.
563
+ """
564
+ return Column(name, table)
565
+
398
566
  def __getattr__(self, name: str) -> Column:
399
567
  """Dynamically create column references.
400
568
 
@@ -406,6 +574,73 @@ class SQLFactory:
406
574
  """
407
575
  return Column(name)
408
576
 
577
+ # ===================
578
+ # Raw SQL Expressions
579
+ # ===================
580
+
581
+ @staticmethod
582
+ def raw(sql_fragment: str, **parameters: Any) -> "Union[exp.Expression, SQL]":
583
+ """Create a raw SQL expression from a string fragment with optional parameters.
584
+
585
+ This method makes it explicit that you are passing raw SQL that should
586
+ be parsed and included directly in the query. Useful for complex expressions,
587
+ database-specific functions, or when you need precise control over the SQL.
588
+
589
+ Args:
590
+ sql_fragment: Raw SQL string to parse into an expression.
591
+ **parameters: Named parameters for parameter binding.
592
+
593
+ Returns:
594
+ SQLGlot expression from the parsed SQL fragment (if no parameters).
595
+ SQL statement object (if parameters provided).
596
+
597
+ Raises:
598
+ SQLBuilderError: If the SQL fragment cannot be parsed.
599
+
600
+ Example:
601
+ ```python
602
+ # Raw expression without parameters (current behavior)
603
+ expr = sql.raw("COALESCE(name, 'Unknown')")
604
+
605
+ # Raw SQL with named parameters (new functionality)
606
+ stmt = sql.raw(
607
+ "LOWER(name) LIKE LOWER(:pattern)", pattern=f"%{query}%"
608
+ )
609
+
610
+ # Raw complex expression with parameters
611
+ expr = sql.raw(
612
+ "price BETWEEN :min_price AND :max_price",
613
+ min_price=100,
614
+ max_price=500,
615
+ )
616
+
617
+ # Raw window function
618
+ query = sql.select(
619
+ "name",
620
+ sql.raw(
621
+ "ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC)"
622
+ ),
623
+ ).from_("employees")
624
+ ```
625
+ """
626
+ if not parameters:
627
+ # Original behavior - return pure expression
628
+ try:
629
+ parsed: Optional[exp.Expression] = exp.maybe_parse(sql_fragment)
630
+ if parsed is not None:
631
+ return parsed
632
+ if sql_fragment.strip().replace("_", "").replace(".", "").isalnum():
633
+ return exp.to_identifier(sql_fragment)
634
+ return exp.Literal.string(sql_fragment)
635
+ except Exception as e:
636
+ msg = f"Failed to parse raw SQL fragment '{sql_fragment}': {e}"
637
+ raise SQLBuilderError(msg) from e
638
+
639
+ # New behavior - return SQL statement with parameters
640
+ from sqlspec.core.statement import SQL
641
+
642
+ return SQL(sql_fragment, parameters)
643
+
409
644
  # ===================
410
645
  # Aggregate Functions
411
646
  # ===================
@@ -597,7 +832,7 @@ class SQLFactory:
597
832
  ```
598
833
  """
599
834
  if isinstance(values, list):
600
- literals = [exp.Literal.string(str(v)) if isinstance(v, str) else exp.Literal.number(v) for v in values]
835
+ literals = [SQLFactory._to_literal(v) for v in values]
601
836
  return exp.Any(this=exp.Array(expressions=literals))
602
837
  if isinstance(values, str):
603
838
  # Parse as SQL
@@ -607,6 +842,29 @@ class SQLFactory:
607
842
  return exp.Any(this=exp.Literal.string(values))
608
843
  return exp.Any(this=values)
609
844
 
845
+ @staticmethod
846
+ def not_any_(values: Union[list[Any], exp.Expression, str]) -> exp.Expression:
847
+ """Create a NOT ANY expression for use with comparison operators.
848
+
849
+ Args:
850
+ values: Values, expression, or subquery for the NOT ANY clause.
851
+
852
+ Returns:
853
+ NOT ANY expression.
854
+
855
+ Example:
856
+ ```python
857
+ # WHERE id <> ANY(subquery)
858
+ subquery = sql.select("user_id").from_("blocked_users")
859
+ query = (
860
+ sql.select("*")
861
+ .from_("users")
862
+ .where(sql.id.neq(sql.not_any(subquery)))
863
+ )
864
+ ```
865
+ """
866
+ return SQLFactory.any(values) # NOT ANY is handled by the comparison operator
867
+
610
868
  # ===================
611
869
  # String Functions
612
870
  # ===================
@@ -687,6 +945,28 @@ class SQLFactory:
687
945
  # Conversion Functions
688
946
  # ===================
689
947
 
948
+ @staticmethod
949
+ def _to_literal(value: Any) -> exp.Expression:
950
+ """Convert a Python value to a SQLGlot literal expression.
951
+
952
+ Uses SQLGlot's built-in exp.convert() function for optimal dialect-agnostic
953
+ literal creation. Handles all Python primitive types correctly:
954
+ - None -> exp.Null (renders as NULL)
955
+ - bool -> exp.Boolean (renders as TRUE/FALSE or 1/0 based on dialect)
956
+ - int/float -> exp.Literal with is_number=True
957
+ - str -> exp.Literal with is_string=True
958
+ - exp.Expression -> returned as-is (passthrough)
959
+
960
+ Args:
961
+ value: Python value or SQLGlot expression to convert.
962
+
963
+ Returns:
964
+ SQLGlot expression representing the literal value.
965
+ """
966
+ if isinstance(value, exp.Expression):
967
+ return value
968
+ return exp.convert(value)
969
+
690
970
  @staticmethod
691
971
  def decode(column: Union[str, exp.Expression], *args: Union[str, exp.Expression, Any]) -> exp.Expression:
692
972
  """Create a DECODE expression (Oracle-style conditional logic).
@@ -725,29 +1005,14 @@ class SQLFactory:
725
1005
  for i in range(0, len(args) - 1, 2):
726
1006
  if i + 1 >= len(args):
727
1007
  # 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]
1008
+ default = SQLFactory._to_literal(args[i])
729
1009
  break
730
1010
 
731
1011
  search_val = args[i]
732
1012
  result_val = args[i + 1]
733
1013
 
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))
1014
+ search_expr = SQLFactory._to_literal(search_val)
1015
+ result_expr = SQLFactory._to_literal(result_val)
751
1016
 
752
1017
  condition = exp.EQ(this=col_expr, expression=search_expr)
753
1018
  conditions.append(exp.When(this=condition, then=result_expr))
@@ -793,30 +1058,136 @@ class SQLFactory:
793
1058
  COALESCE expression equivalent to NVL.
794
1059
  """
795
1060
  col_expr = exp.column(column) if isinstance(column, str) else column
1061
+ sub_expr = SQLFactory._to_literal(substitute_value)
1062
+ return exp.Coalesce(expressions=[col_expr, sub_expr])
796
1063
 
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))
1064
+ @staticmethod
1065
+ def nvl2(
1066
+ column: Union[str, exp.Expression],
1067
+ value_if_not_null: Union[str, exp.Expression, Any],
1068
+ value_if_null: Union[str, exp.Expression, Any],
1069
+ ) -> exp.Expression:
1070
+ """Create an NVL2 (Oracle-style) expression using CASE.
805
1071
 
806
- return exp.Coalesce(expressions=[col_expr, sub_expr])
1072
+ NVL2 returns value_if_not_null if column is not NULL,
1073
+ otherwise returns value_if_null.
1074
+
1075
+ Args:
1076
+ column: Column to check for NULL.
1077
+ value_if_not_null: Value to use if column is NOT NULL.
1078
+ value_if_null: Value to use if column is NULL.
1079
+
1080
+ Returns:
1081
+ CASE expression equivalent to NVL2.
1082
+
1083
+ Example:
1084
+ ```python
1085
+ # NVL2(salary, 'Has Salary', 'No Salary')
1086
+ sql.nvl2("salary", "Has Salary", "No Salary")
1087
+ ```
1088
+ """
1089
+ col_expr = exp.column(column) if isinstance(column, str) else column
1090
+ not_null_expr = SQLFactory._to_literal(value_if_not_null)
1091
+ null_expr = SQLFactory._to_literal(value_if_null)
1092
+
1093
+ # Create CASE WHEN column IS NOT NULL THEN value_if_not_null ELSE value_if_null END
1094
+ is_null = exp.Is(this=col_expr, expression=exp.Null())
1095
+ condition = exp.Not(this=is_null)
1096
+ when_clause = exp.If(this=condition, true=not_null_expr)
1097
+
1098
+ return exp.Case(ifs=[when_clause], default=null_expr)
1099
+
1100
+ # ===================
1101
+ # Bulk Operations
1102
+ # ===================
1103
+
1104
+ @staticmethod
1105
+ def bulk_insert(table_name: str, column_count: int, placeholder_style: str = "?") -> exp.Expression:
1106
+ """Create bulk INSERT expression for executemany operations.
1107
+
1108
+ This is specifically for bulk loading operations like CSV ingestion where
1109
+ we need an INSERT expression with placeholders for executemany().
1110
+
1111
+ Args:
1112
+ table_name: Name of the table to insert into
1113
+ column_count: Number of columns (for placeholder generation)
1114
+ placeholder_style: Placeholder style ("?" for SQLite/PostgreSQL, "%s" for MySQL, ":1" for Oracle)
1115
+
1116
+ Returns:
1117
+ INSERT expression with proper placeholders for bulk operations
1118
+
1119
+ Example:
1120
+ ```python
1121
+ from sqlspec import sql
1122
+
1123
+ # SQLite/PostgreSQL style
1124
+ insert_expr = sql.bulk_insert("my_table", 3)
1125
+ # Creates: INSERT INTO "my_table" VALUES (?, ?, ?)
1126
+
1127
+ # MySQL style
1128
+ insert_expr = sql.bulk_insert(
1129
+ "my_table", 3, placeholder_style="%s"
1130
+ )
1131
+ # Creates: INSERT INTO "my_table" VALUES (%s, %s, %s)
1132
+
1133
+ # Oracle style
1134
+ insert_expr = sql.bulk_insert(
1135
+ "my_table", 3, placeholder_style=":1"
1136
+ )
1137
+ # Creates: INSERT INTO "my_table" VALUES (:1, :2, :3)
1138
+ ```
1139
+ """
1140
+ return exp.Insert(
1141
+ this=exp.Table(this=exp.to_identifier(table_name)),
1142
+ expression=exp.Values(
1143
+ expressions=[
1144
+ exp.Tuple(expressions=[exp.Placeholder(this=placeholder_style) for _ in range(column_count)])
1145
+ ]
1146
+ ),
1147
+ )
1148
+
1149
+ def truncate(self, table_name: str) -> "Truncate":
1150
+ """Create a TRUNCATE TABLE builder.
1151
+
1152
+ Args:
1153
+ table_name: Name of the table to truncate
1154
+
1155
+ Returns:
1156
+ TruncateTable builder instance
1157
+
1158
+ Example:
1159
+ ```python
1160
+ from sqlspec import sql
1161
+
1162
+ # Simple truncate
1163
+ truncate_sql = sql.truncate_table("my_table").build().sql
1164
+
1165
+ # Truncate with options
1166
+ truncate_sql = (
1167
+ sql.truncate_table("my_table")
1168
+ .cascade()
1169
+ .restart_identity()
1170
+ .build()
1171
+ .sql
1172
+ )
1173
+ ```
1174
+ """
1175
+ builder = Truncate(dialect=self.dialect)
1176
+ builder._table_name = table_name
1177
+ return builder
807
1178
 
808
1179
  # ===================
809
1180
  # Case Expressions
810
1181
  # ===================
811
1182
 
812
1183
  @staticmethod
813
- def case() -> "CaseExpressionBuilder":
1184
+ def case() -> "Case":
814
1185
  """Create a CASE expression builder.
815
1186
 
816
1187
  Returns:
817
1188
  CaseExpressionBuilder for building CASE expressions.
818
1189
  """
819
- return CaseExpressionBuilder()
1190
+ return Case()
820
1191
 
821
1192
  # ===================
822
1193
  # Window Functions
@@ -911,7 +1282,7 @@ class SQLFactory:
911
1282
  return exp.Window(this=func_expr, **over_args)
912
1283
 
913
1284
 
914
- class CaseExpressionBuilder:
1285
+ class Case:
915
1286
  """Builder for CASE expressions using the SQL factory.
916
1287
 
917
1288
  Example:
@@ -930,12 +1301,10 @@ class CaseExpressionBuilder:
930
1301
 
931
1302
  def __init__(self) -> None:
932
1303
  """Initialize the CASE expression builder."""
933
- self._conditions: list[exp.When] = []
1304
+ self._conditions: list[exp.If] = []
934
1305
  self._default: Optional[exp.Expression] = None
935
1306
 
936
- def when(
937
- self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]
938
- ) -> "CaseExpressionBuilder":
1307
+ def when(self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]) -> "Case":
939
1308
  """Add a WHEN clause.
940
1309
 
941
1310
  Args:
@@ -946,21 +1315,14 @@ class CaseExpressionBuilder:
946
1315
  Self for method chaining.
947
1316
  """
948
1317
  cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
1318
+ val_expr = SQLFactory._to_literal(value)
949
1319
 
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)
1320
+ # SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
1321
+ when_clause = exp.If(this=cond_expr, true=val_expr)
960
1322
  self._conditions.append(when_clause)
961
1323
  return self
962
1324
 
963
- def else_(self, value: Union[str, exp.Expression, Any]) -> "CaseExpressionBuilder":
1325
+ def else_(self, value: Union[str, exp.Expression, Any]) -> "Case":
964
1326
  """Add an ELSE clause.
965
1327
 
966
1328
  Args:
@@ -969,14 +1331,7 @@ class CaseExpressionBuilder:
969
1331
  Returns:
970
1332
  Self for method chaining.
971
1333
  """
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))
1334
+ self._default = SQLFactory._to_literal(value)
980
1335
  return self
981
1336
 
982
1337
  def end(self) -> exp.Expression:
@@ -986,3 +1341,7 @@ class CaseExpressionBuilder:
986
1341
  Complete CASE expression.
987
1342
  """
988
1343
  return exp.Case(ifs=self._conditions, default=self._default)
1344
+
1345
+
1346
+ # Create a default SQL factory instance
1347
+ sql = SQLFactory()