sqlspec 0.14.1__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.
- sqlspec/__init__.py +50 -25
- sqlspec/__main__.py +1 -1
- sqlspec/__metadata__.py +1 -3
- sqlspec/_serialization.py +1 -2
- sqlspec/_sql.py +256 -120
- sqlspec/_typing.py +278 -142
- sqlspec/adapters/adbc/__init__.py +4 -3
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +115 -260
- sqlspec/adapters/adbc/driver.py +462 -367
- sqlspec/adapters/aiosqlite/__init__.py +18 -3
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +199 -129
- sqlspec/adapters/aiosqlite/driver.py +230 -269
- sqlspec/adapters/asyncmy/__init__.py +18 -3
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +80 -168
- sqlspec/adapters/asyncmy/driver.py +260 -225
- sqlspec/adapters/asyncpg/__init__.py +19 -4
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +82 -181
- sqlspec/adapters/asyncpg/driver.py +285 -383
- sqlspec/adapters/bigquery/__init__.py +17 -3
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +191 -258
- sqlspec/adapters/bigquery/driver.py +474 -646
- sqlspec/adapters/duckdb/__init__.py +14 -3
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +415 -351
- sqlspec/adapters/duckdb/driver.py +343 -413
- sqlspec/adapters/oracledb/__init__.py +19 -5
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +123 -379
- sqlspec/adapters/oracledb/driver.py +507 -560
- sqlspec/adapters/psqlpy/__init__.py +13 -3
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +93 -254
- sqlspec/adapters/psqlpy/driver.py +505 -234
- sqlspec/adapters/psycopg/__init__.py +19 -5
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +143 -403
- sqlspec/adapters/psycopg/driver.py +706 -872
- sqlspec/adapters/sqlite/__init__.py +14 -3
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +202 -118
- sqlspec/adapters/sqlite/driver.py +264 -303
- sqlspec/base.py +105 -9
- sqlspec/{statement/builder → builder}/__init__.py +12 -14
- sqlspec/{statement/builder → builder}/_base.py +120 -55
- sqlspec/{statement/builder → builder}/_column.py +17 -6
- sqlspec/{statement/builder → builder}/_ddl.py +46 -79
- sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
- sqlspec/{statement/builder → builder}/_delete.py +6 -25
- sqlspec/{statement/builder → builder}/_insert.py +6 -64
- sqlspec/builder/_merge.py +56 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +3 -10
- sqlspec/{statement/builder → builder}/_select.py +11 -56
- sqlspec/{statement/builder → builder}/_update.py +12 -18
- sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
- sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
- sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +22 -16
- sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
- sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +3 -5
- sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
- sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
- sqlspec/{statement/builder → builder}/mixins/_select_operations.py +21 -36
- sqlspec/{statement/builder → builder}/mixins/_update_operations.py +3 -14
- sqlspec/{statement/builder → builder}/mixins/_where_clause.py +52 -79
- sqlspec/cli.py +4 -5
- sqlspec/config.py +180 -133
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.py +873 -0
- sqlspec/core/compiler.py +396 -0
- sqlspec/core/filters.py +828 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.py +1209 -0
- sqlspec/core/result.py +664 -0
- sqlspec/{statement → core}/splitter.py +321 -191
- sqlspec/core/statement.py +651 -0
- sqlspec/driver/__init__.py +7 -10
- sqlspec/driver/_async.py +387 -176
- sqlspec/driver/_common.py +527 -289
- sqlspec/driver/_sync.py +390 -172
- sqlspec/driver/mixins/__init__.py +2 -19
- sqlspec/driver/mixins/_result_tools.py +168 -0
- sqlspec/driver/mixins/_sql_translator.py +6 -3
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +16 -14
- sqlspec/extensions/litestar/providers.py +17 -52
- sqlspec/loader.py +424 -105
- sqlspec/migrations/__init__.py +12 -0
- sqlspec/migrations/base.py +92 -68
- sqlspec/migrations/commands.py +24 -106
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +49 -51
- sqlspec/migrations/tracker.py +31 -44
- sqlspec/migrations/utils.py +64 -24
- sqlspec/protocols.py +7 -183
- sqlspec/storage/__init__.py +1 -1
- sqlspec/storage/backends/base.py +37 -40
- sqlspec/storage/backends/fsspec.py +136 -112
- sqlspec/storage/backends/obstore.py +138 -160
- sqlspec/storage/capabilities.py +5 -4
- sqlspec/storage/registry.py +57 -106
- sqlspec/typing.py +136 -115
- sqlspec/utils/__init__.py +2 -3
- sqlspec/utils/correlation.py +0 -3
- sqlspec/utils/deprecation.py +6 -6
- sqlspec/utils/fixtures.py +6 -6
- sqlspec/utils/logging.py +0 -2
- sqlspec/utils/module_loader.py +7 -12
- sqlspec/utils/singleton.py +0 -1
- sqlspec/utils/sync_tools.py +16 -37
- sqlspec/utils/text.py +12 -51
- sqlspec/utils/type_guards.py +443 -232
- {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/METADATA +7 -2
- sqlspec-0.15.0.dist-info/RECORD +134 -0
- sqlspec/adapters/adbc/transformers.py +0 -108
- sqlspec/driver/connection.py +0 -207
- sqlspec/driver/mixins/_cache.py +0 -114
- sqlspec/driver/mixins/_csv_writer.py +0 -91
- sqlspec/driver/mixins/_pipeline.py +0 -508
- sqlspec/driver/mixins/_query_tools.py +0 -796
- sqlspec/driver/mixins/_result_utils.py +0 -138
- sqlspec/driver/mixins/_storage.py +0 -912
- sqlspec/driver/mixins/_type_coercion.py +0 -128
- sqlspec/driver/parameters.py +0 -138
- sqlspec/statement/__init__.py +0 -21
- sqlspec/statement/builder/_merge.py +0 -95
- sqlspec/statement/cache.py +0 -50
- sqlspec/statement/filters.py +0 -625
- sqlspec/statement/parameters.py +0 -956
- sqlspec/statement/pipelines/__init__.py +0 -210
- sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
- sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
- sqlspec/statement/pipelines/context.py +0 -109
- sqlspec/statement/pipelines/transformers/__init__.py +0 -7
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
- sqlspec/statement/pipelines/validators/__init__.py +0 -23
- sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
- sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
- sqlspec/statement/pipelines/validators/_performance.py +0 -714
- sqlspec/statement/pipelines/validators/_security.py +0 -967
- sqlspec/statement/result.py +0 -435
- sqlspec/statement/sql.py +0 -1774
- sqlspec/utils/cached_property.py +0 -25
- sqlspec/utils/statement_hashing.py +0 -203
- sqlspec-0.14.1.dist-info/RECORD +0 -145
- /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.14.1.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
|
-
|
|
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()
|
|
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 = [
|
|
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 =
|
|
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
|
-
|
|
735
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
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() -> "
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
951
|
-
|
|
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]) -> "
|
|
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
|
-
|
|
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()
|