sqlspec 0.13.1__py3-none-any.whl → 0.16.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/__init__.py +71 -8
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +1 -3
- sqlspec/_serialization.py +1 -2
- sqlspec/_sql.py +930 -136
- sqlspec/_typing.py +278 -142
- sqlspec/adapters/adbc/__init__.py +4 -3
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +116 -285
- sqlspec/adapters/adbc/driver.py +462 -340
- sqlspec/adapters/aiosqlite/__init__.py +18 -3
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +202 -150
- sqlspec/adapters/aiosqlite/driver.py +226 -247
- sqlspec/adapters/asyncmy/__init__.py +18 -3
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +80 -199
- sqlspec/adapters/asyncmy/driver.py +257 -215
- sqlspec/adapters/asyncpg/__init__.py +19 -4
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +81 -214
- sqlspec/adapters/asyncpg/driver.py +284 -359
- sqlspec/adapters/bigquery/__init__.py +17 -3
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +191 -299
- sqlspec/adapters/bigquery/driver.py +474 -634
- sqlspec/adapters/duckdb/__init__.py +14 -3
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +414 -397
- sqlspec/adapters/duckdb/driver.py +342 -393
- sqlspec/adapters/oracledb/__init__.py +19 -5
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +123 -458
- sqlspec/adapters/oracledb/driver.py +505 -531
- sqlspec/adapters/psqlpy/__init__.py +13 -3
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +93 -307
- sqlspec/adapters/psqlpy/driver.py +504 -213
- sqlspec/adapters/psycopg/__init__.py +19 -5
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +143 -472
- sqlspec/adapters/psycopg/driver.py +704 -825
- sqlspec/adapters/sqlite/__init__.py +14 -3
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +208 -142
- sqlspec/adapters/sqlite/driver.py +263 -278
- sqlspec/base.py +105 -9
- sqlspec/{statement/builder → builder}/__init__.py +12 -14
- sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
- sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
- sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
- sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
- sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
- sqlspec/builder/_insert.py +421 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
- sqlspec/builder/_select.py +170 -0
- sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
- sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +604 -0
- sqlspec/builder/mixins/_update_operations.py +202 -0
- sqlspec/builder/mixins/_where_clause.py +644 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +183 -138
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.py +677 -0
- sqlspec/{statement → core}/splitter.py +321 -191
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +7 -10
- sqlspec/driver/_async.py +422 -163
- sqlspec/driver/_common.py +545 -287
- sqlspec/driver/_sync.py +426 -160
- sqlspec/driver/mixins/__init__.py +2 -13
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +65 -14
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/__init__.py +2 -1
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +21 -16
- sqlspec/extensions/litestar/providers.py +17 -52
- sqlspec/loader.py +423 -104
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +51 -186
- 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 -2
- 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 +17 -38
- sqlspec/utils/text.py +12 -51
- sqlspec/utils/type_guards.py +482 -235
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
- sqlspec-0.16.2.dist-info/RECORD +134 -0
- sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
- sqlspec/driver/connection.py +0 -207
- sqlspec/driver/mixins/_csv_writer.py +0 -91
- sqlspec/driver/mixins/_pipeline.py +0 -512
- sqlspec/driver/mixins/_result_utils.py +0 -140
- sqlspec/driver/mixins/_storage.py +0 -926
- sqlspec/driver/mixins/_type_coercion.py +0 -130
- sqlspec/driver/parameters.py +0 -138
- sqlspec/service/__init__.py +0 -4
- sqlspec/service/_util.py +0 -147
- sqlspec/service/base.py +0 -1131
- sqlspec/service/pagination.py +0 -26
- sqlspec/statement/__init__.py +0 -21
- sqlspec/statement/builder/insert.py +0 -288
- sqlspec/statement/builder/merge.py +0 -95
- sqlspec/statement/builder/mixins/__init__.py +0 -65
- sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
- sqlspec/statement/builder/mixins/_case_builder.py +0 -91
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
- sqlspec/statement/builder/mixins/_from.py +0 -63
- sqlspec/statement/builder/mixins/_group_by.py +0 -118
- sqlspec/statement/builder/mixins/_having.py +0 -35
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
- sqlspec/statement/builder/mixins/_insert_into.py +0 -36
- sqlspec/statement/builder/mixins/_insert_values.py +0 -67
- sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
- sqlspec/statement/builder/mixins/_order_by.py +0 -46
- sqlspec/statement/builder/mixins/_pivot.py +0 -79
- sqlspec/statement/builder/mixins/_returning.py +0 -37
- sqlspec/statement/builder/mixins/_select_columns.py +0 -61
- sqlspec/statement/builder/mixins/_set_ops.py +0 -122
- sqlspec/statement/builder/mixins/_unpivot.py +0 -77
- sqlspec/statement/builder/mixins/_update_from.py +0 -55
- sqlspec/statement/builder/mixins/_update_set.py +0 -94
- sqlspec/statement/builder/mixins/_update_table.py +0 -29
- sqlspec/statement/builder/mixins/_where.py +0 -401
- sqlspec/statement/builder/mixins/_window_functions.py +0 -86
- sqlspec/statement/builder/select.py +0 -221
- sqlspec/statement/filters.py +0 -596
- sqlspec/statement/parameter_manager.py +0 -220
- sqlspec/statement/parameters.py +0 -867
- 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 -115
- 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 -718
- sqlspec/statement/pipelines/validators/_security.py +0 -967
- sqlspec/statement/result.py +0 -435
- sqlspec/statement/sql.py +0 -1704
- sqlspec/statement/sql_compiler.py +0 -140
- sqlspec/utils/cached_property.py +0 -25
- sqlspec-0.13.1.dist-info/RECORD +0 -150
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
sqlspec/_sql.py
CHANGED
|
@@ -1,21 +1,71 @@
|
|
|
1
1
|
"""Unified SQL factory for creating SQL builders and column expressions with a clean API.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
- `sql` provides both statement builders (select, insert, update, etc.) and column expressions
|
|
3
|
+
Provides both statement builders (select, insert, update, etc.) and column expressions.
|
|
5
4
|
"""
|
|
6
5
|
|
|
7
6
|
import logging
|
|
8
|
-
from typing import Any, Optional, Union
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
9
8
|
|
|
10
9
|
import sqlglot
|
|
10
|
+
from mypy_extensions import trait
|
|
11
11
|
from sqlglot import exp
|
|
12
12
|
from sqlglot.dialects.dialect import DialectType
|
|
13
13
|
from sqlglot.errors import ParseError as SQLGlotParseError
|
|
14
14
|
|
|
15
|
+
from sqlspec.builder import (
|
|
16
|
+
AlterTable,
|
|
17
|
+
Column,
|
|
18
|
+
CommentOn,
|
|
19
|
+
CreateIndex,
|
|
20
|
+
CreateMaterializedView,
|
|
21
|
+
CreateSchema,
|
|
22
|
+
CreateTable,
|
|
23
|
+
CreateTableAsSelect,
|
|
24
|
+
CreateView,
|
|
25
|
+
Delete,
|
|
26
|
+
DropIndex,
|
|
27
|
+
DropSchema,
|
|
28
|
+
DropTable,
|
|
29
|
+
DropView,
|
|
30
|
+
Insert,
|
|
31
|
+
Merge,
|
|
32
|
+
RenameTable,
|
|
33
|
+
Select,
|
|
34
|
+
Truncate,
|
|
35
|
+
Update,
|
|
36
|
+
)
|
|
15
37
|
from sqlspec.exceptions import SQLBuilderError
|
|
16
|
-
from sqlspec.statement.builder import Column, Delete, Insert, Merge, Select, Update
|
|
17
38
|
|
|
18
|
-
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from sqlspec.builder._column import ColumnExpression
|
|
41
|
+
from sqlspec.core.statement import SQL
|
|
42
|
+
|
|
43
|
+
__all__ = (
|
|
44
|
+
"AlterTable",
|
|
45
|
+
"Case",
|
|
46
|
+
"Column",
|
|
47
|
+
"CommentOn",
|
|
48
|
+
"CreateIndex",
|
|
49
|
+
"CreateMaterializedView",
|
|
50
|
+
"CreateSchema",
|
|
51
|
+
"CreateTable",
|
|
52
|
+
"CreateTableAsSelect",
|
|
53
|
+
"CreateView",
|
|
54
|
+
"Delete",
|
|
55
|
+
"DropIndex",
|
|
56
|
+
"DropSchema",
|
|
57
|
+
"DropTable",
|
|
58
|
+
"DropView",
|
|
59
|
+
"Insert",
|
|
60
|
+
"Merge",
|
|
61
|
+
"RenameTable",
|
|
62
|
+
"SQLFactory",
|
|
63
|
+
"Select",
|
|
64
|
+
"Truncate",
|
|
65
|
+
"Update",
|
|
66
|
+
"WindowFunctionBuilder",
|
|
67
|
+
"sql",
|
|
68
|
+
)
|
|
19
69
|
|
|
20
70
|
logger = logging.getLogger("sqlspec")
|
|
21
71
|
|
|
@@ -50,58 +100,18 @@ SQL_STARTERS = {
|
|
|
50
100
|
|
|
51
101
|
|
|
52
102
|
class SQLFactory:
|
|
53
|
-
"""Unified factory for creating SQL builders and column expressions with a fluent API.
|
|
54
|
-
|
|
55
|
-
Provides both statement builders and column expressions through a single, clean interface.
|
|
56
|
-
Now supports parsing raw SQL strings into appropriate builders for enhanced flexibility.
|
|
57
|
-
|
|
58
|
-
Example:
|
|
59
|
-
```python
|
|
60
|
-
from sqlspec import sql
|
|
61
|
-
|
|
62
|
-
# Traditional builder usage (unchanged)
|
|
63
|
-
query = (
|
|
64
|
-
sql.select(sql.id, sql.name)
|
|
65
|
-
.from_("users")
|
|
66
|
-
.where("age > 18")
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
# New: Raw SQL parsing
|
|
70
|
-
insert_sql = sql.insert(
|
|
71
|
-
"INSERT INTO users (name, email) VALUES ('John', 'john@example.com')"
|
|
72
|
-
)
|
|
73
|
-
select_sql = sql.select(
|
|
74
|
-
"SELECT * FROM users WHERE active = 1"
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
# RETURNING clause detection
|
|
78
|
-
returning_insert = sql.insert(
|
|
79
|
-
"INSERT INTO users (name) VALUES ('John') RETURNING id"
|
|
80
|
-
)
|
|
81
|
-
# → When executed, will return SelectResult instead of ExecuteResult
|
|
82
|
-
|
|
83
|
-
# Smart INSERT FROM SELECT
|
|
84
|
-
insert_from_select = sql.insert(
|
|
85
|
-
"SELECT id, name FROM source WHERE active = 1"
|
|
86
|
-
)
|
|
87
|
-
# → Will prompt for target table or convert to INSERT FROM SELECT pattern
|
|
88
|
-
```
|
|
89
|
-
"""
|
|
103
|
+
"""Unified factory for creating SQL builders and column expressions with a fluent API."""
|
|
90
104
|
|
|
91
105
|
@classmethod
|
|
92
106
|
def detect_sql_type(cls, sql: str, dialect: DialectType = None) -> str:
|
|
93
107
|
try:
|
|
94
|
-
# Minimal parsing just to get the command type
|
|
95
108
|
parsed_expr = sqlglot.parse_one(sql, read=dialect)
|
|
96
109
|
if parsed_expr and parsed_expr.key:
|
|
97
110
|
return parsed_expr.key.upper()
|
|
98
|
-
# Fallback for expressions that might not have a direct 'key'
|
|
99
|
-
# or where key is None (e.g. some DDL without explicit command like SET)
|
|
100
111
|
if parsed_expr:
|
|
101
|
-
# Attempt to get the class name as a fallback, e.g., "Set", "Command"
|
|
102
112
|
command_type = type(parsed_expr).__name__.upper()
|
|
103
113
|
if command_type == "COMMAND" and parsed_expr.this:
|
|
104
|
-
return str(parsed_expr.this).upper()
|
|
114
|
+
return str(parsed_expr.this).upper()
|
|
105
115
|
return command_type
|
|
106
116
|
except SQLGlotParseError:
|
|
107
117
|
logger.debug("Failed to parse SQL for type detection: %s", sql[:100])
|
|
@@ -120,15 +130,7 @@ class SQLFactory:
|
|
|
120
130
|
# ===================
|
|
121
131
|
# Callable Interface
|
|
122
132
|
# ===================
|
|
123
|
-
def __call__(
|
|
124
|
-
self,
|
|
125
|
-
statement: str,
|
|
126
|
-
parameters: Optional[Any] = None,
|
|
127
|
-
*filters: Any,
|
|
128
|
-
config: Optional[Any] = None,
|
|
129
|
-
dialect: DialectType = None,
|
|
130
|
-
**kwargs: Any,
|
|
131
|
-
) -> "Any":
|
|
133
|
+
def __call__(self, statement: str, dialect: DialectType = None) -> "Any":
|
|
132
134
|
"""Create a SelectBuilder from a SQL string, only allowing SELECT/CTE queries.
|
|
133
135
|
|
|
134
136
|
Args:
|
|
@@ -152,7 +154,6 @@ class SQLFactory:
|
|
|
152
154
|
msg = f"Failed to parse SQL: {e}"
|
|
153
155
|
raise SQLBuilderError(msg) from e
|
|
154
156
|
actual_type = type(parsed_expr).__name__.upper()
|
|
155
|
-
# Map sqlglot expression class to type string
|
|
156
157
|
expr_type_map = {
|
|
157
158
|
"SELECT": "SELECT",
|
|
158
159
|
"INSERT": "INSERT",
|
|
@@ -177,7 +178,9 @@ class SQLFactory:
|
|
|
177
178
|
# ===================
|
|
178
179
|
# Statement Builders
|
|
179
180
|
# ===================
|
|
180
|
-
def select(
|
|
181
|
+
def select(
|
|
182
|
+
self, *columns_or_sql: Union[str, exp.Expression, Column, "SQL"], dialect: DialectType = None
|
|
183
|
+
) -> "Select":
|
|
181
184
|
builder_dialect = dialect or self.dialect
|
|
182
185
|
if len(columns_or_sql) == 1 and isinstance(columns_or_sql[0], str):
|
|
183
186
|
sql_candidate = columns_or_sql[0].strip()
|
|
@@ -190,12 +193,8 @@ class SQLFactory:
|
|
|
190
193
|
)
|
|
191
194
|
raise SQLBuilderError(msg)
|
|
192
195
|
select_builder = Select(dialect=builder_dialect)
|
|
193
|
-
if select_builder._expression is None:
|
|
194
|
-
select_builder.__post_init__()
|
|
195
196
|
return self._populate_select_from_sql(select_builder, sql_candidate)
|
|
196
197
|
select_builder = Select(dialect=builder_dialect)
|
|
197
|
-
if select_builder._expression is None:
|
|
198
|
-
select_builder.__post_init__()
|
|
199
198
|
if columns_or_sql:
|
|
200
199
|
select_builder.select(*columns_or_sql)
|
|
201
200
|
return select_builder
|
|
@@ -203,8 +202,6 @@ class SQLFactory:
|
|
|
203
202
|
def insert(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Insert":
|
|
204
203
|
builder_dialect = dialect or self.dialect
|
|
205
204
|
builder = Insert(dialect=builder_dialect)
|
|
206
|
-
if builder._expression is None:
|
|
207
|
-
builder.__post_init__()
|
|
208
205
|
if table_or_sql:
|
|
209
206
|
if self._looks_like_sql(table_or_sql):
|
|
210
207
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
@@ -222,8 +219,6 @@ class SQLFactory:
|
|
|
222
219
|
def update(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Update":
|
|
223
220
|
builder_dialect = dialect or self.dialect
|
|
224
221
|
builder = Update(dialect=builder_dialect)
|
|
225
|
-
if builder._expression is None:
|
|
226
|
-
builder.__post_init__()
|
|
227
222
|
if table_or_sql:
|
|
228
223
|
if self._looks_like_sql(table_or_sql):
|
|
229
224
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
@@ -237,8 +232,6 @@ class SQLFactory:
|
|
|
237
232
|
def delete(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Delete":
|
|
238
233
|
builder_dialect = dialect or self.dialect
|
|
239
234
|
builder = Delete(dialect=builder_dialect)
|
|
240
|
-
if builder._expression is None:
|
|
241
|
-
builder.__post_init__()
|
|
242
235
|
if table_or_sql and self._looks_like_sql(table_or_sql):
|
|
243
236
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
244
237
|
if detected != "DELETE":
|
|
@@ -250,8 +243,6 @@ class SQLFactory:
|
|
|
250
243
|
def merge(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Merge":
|
|
251
244
|
builder_dialect = dialect or self.dialect
|
|
252
245
|
builder = Merge(dialect=builder_dialect)
|
|
253
|
-
if builder._expression is None:
|
|
254
|
-
builder.__post_init__()
|
|
255
246
|
if table_or_sql:
|
|
256
247
|
if self._looks_like_sql(table_or_sql):
|
|
257
248
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
@@ -262,6 +253,174 @@ class SQLFactory:
|
|
|
262
253
|
return builder.into(table_or_sql)
|
|
263
254
|
return builder
|
|
264
255
|
|
|
256
|
+
# ===================
|
|
257
|
+
# DDL Statement Builders
|
|
258
|
+
# ===================
|
|
259
|
+
|
|
260
|
+
def create_table(self, table_name: str, dialect: DialectType = None) -> "CreateTable":
|
|
261
|
+
"""Create a CREATE TABLE builder.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
table_name: Name of the table to create
|
|
265
|
+
dialect: Optional SQL dialect
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
CreateTable builder instance
|
|
269
|
+
"""
|
|
270
|
+
builder = CreateTable(table_name)
|
|
271
|
+
builder.dialect = dialect or self.dialect
|
|
272
|
+
return builder
|
|
273
|
+
|
|
274
|
+
def create_table_as_select(self, dialect: DialectType = None) -> "CreateTableAsSelect":
|
|
275
|
+
"""Create a CREATE TABLE AS SELECT builder.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
dialect: Optional SQL dialect
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
CreateTableAsSelect builder instance
|
|
282
|
+
"""
|
|
283
|
+
builder = CreateTableAsSelect()
|
|
284
|
+
builder.dialect = dialect or self.dialect
|
|
285
|
+
return builder
|
|
286
|
+
|
|
287
|
+
def create_view(self, dialect: DialectType = None) -> "CreateView":
|
|
288
|
+
"""Create a CREATE VIEW builder.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
dialect: Optional SQL dialect
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
CreateView builder instance
|
|
295
|
+
"""
|
|
296
|
+
builder = CreateView()
|
|
297
|
+
builder.dialect = dialect or self.dialect
|
|
298
|
+
return builder
|
|
299
|
+
|
|
300
|
+
def create_materialized_view(self, dialect: DialectType = None) -> "CreateMaterializedView":
|
|
301
|
+
"""Create a CREATE MATERIALIZED VIEW builder.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
dialect: Optional SQL dialect
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
CreateMaterializedView builder instance
|
|
308
|
+
"""
|
|
309
|
+
builder = CreateMaterializedView()
|
|
310
|
+
builder.dialect = dialect or self.dialect
|
|
311
|
+
return builder
|
|
312
|
+
|
|
313
|
+
def create_index(self, index_name: str, dialect: DialectType = None) -> "CreateIndex":
|
|
314
|
+
"""Create a CREATE INDEX builder.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
index_name: Name of the index to create
|
|
318
|
+
dialect: Optional SQL dialect
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
CreateIndex builder instance
|
|
322
|
+
"""
|
|
323
|
+
return CreateIndex(index_name, dialect=dialect or self.dialect)
|
|
324
|
+
|
|
325
|
+
def create_schema(self, dialect: DialectType = None) -> "CreateSchema":
|
|
326
|
+
"""Create a CREATE SCHEMA builder.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
dialect: Optional SQL dialect
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
CreateSchema builder instance
|
|
333
|
+
"""
|
|
334
|
+
builder = CreateSchema()
|
|
335
|
+
builder.dialect = dialect or self.dialect
|
|
336
|
+
return builder
|
|
337
|
+
|
|
338
|
+
def drop_table(self, table_name: str, dialect: DialectType = None) -> "DropTable":
|
|
339
|
+
"""Create a DROP TABLE builder.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
table_name: Name of the table to drop
|
|
343
|
+
dialect: Optional SQL dialect
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
DropTable builder instance
|
|
347
|
+
"""
|
|
348
|
+
return DropTable(table_name, dialect=dialect or self.dialect)
|
|
349
|
+
|
|
350
|
+
def drop_view(self, dialect: DialectType = None) -> "DropView":
|
|
351
|
+
"""Create a DROP VIEW builder.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
dialect: Optional SQL dialect
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
DropView builder instance
|
|
358
|
+
"""
|
|
359
|
+
return DropView(dialect=dialect or self.dialect)
|
|
360
|
+
|
|
361
|
+
def drop_index(self, index_name: str, dialect: DialectType = None) -> "DropIndex":
|
|
362
|
+
"""Create a DROP INDEX builder.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
index_name: Name of the index to drop
|
|
366
|
+
dialect: Optional SQL dialect
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
DropIndex builder instance
|
|
370
|
+
"""
|
|
371
|
+
return DropIndex(index_name, dialect=dialect or self.dialect)
|
|
372
|
+
|
|
373
|
+
def drop_schema(self, dialect: DialectType = None) -> "DropSchema":
|
|
374
|
+
"""Create a DROP SCHEMA builder.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
dialect: Optional SQL dialect
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
DropSchema builder instance
|
|
381
|
+
"""
|
|
382
|
+
return DropSchema(dialect=dialect or self.dialect)
|
|
383
|
+
|
|
384
|
+
def alter_table(self, table_name: str, dialect: DialectType = None) -> "AlterTable":
|
|
385
|
+
"""Create an ALTER TABLE builder.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
table_name: Name of the table to alter
|
|
389
|
+
dialect: Optional SQL dialect
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
AlterTable builder instance
|
|
393
|
+
"""
|
|
394
|
+
builder = AlterTable(table_name)
|
|
395
|
+
builder.dialect = dialect or self.dialect
|
|
396
|
+
return builder
|
|
397
|
+
|
|
398
|
+
def rename_table(self, dialect: DialectType = None) -> "RenameTable":
|
|
399
|
+
"""Create a RENAME TABLE builder.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
dialect: Optional SQL dialect
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
RenameTable builder instance
|
|
406
|
+
"""
|
|
407
|
+
builder = RenameTable()
|
|
408
|
+
builder.dialect = dialect or self.dialect
|
|
409
|
+
return builder
|
|
410
|
+
|
|
411
|
+
def comment_on(self, dialect: DialectType = None) -> "CommentOn":
|
|
412
|
+
"""Create a COMMENT ON builder.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
dialect: Optional SQL dialect
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
CommentOn builder instance
|
|
419
|
+
"""
|
|
420
|
+
builder = CommentOn()
|
|
421
|
+
builder.dialect = dialect or self.dialect
|
|
422
|
+
return builder
|
|
423
|
+
|
|
265
424
|
# ===================
|
|
266
425
|
# SQL Analysis Helpers
|
|
267
426
|
# ===================
|
|
@@ -300,8 +459,6 @@ class SQLFactory:
|
|
|
300
459
|
try:
|
|
301
460
|
# Use SQLGlot directly for parsing - no validation here
|
|
302
461
|
parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
|
|
303
|
-
if parsed_expr is None:
|
|
304
|
-
parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
|
|
305
462
|
|
|
306
463
|
if isinstance(parsed_expr, exp.Insert):
|
|
307
464
|
builder._expression = parsed_expr
|
|
@@ -324,8 +481,6 @@ class SQLFactory:
|
|
|
324
481
|
try:
|
|
325
482
|
# Use SQLGlot directly for parsing - no validation here
|
|
326
483
|
parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
|
|
327
|
-
if parsed_expr is None:
|
|
328
|
-
parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
|
|
329
484
|
|
|
330
485
|
if isinstance(parsed_expr, exp.Select):
|
|
331
486
|
builder._expression = parsed_expr
|
|
@@ -342,8 +497,6 @@ class SQLFactory:
|
|
|
342
497
|
try:
|
|
343
498
|
# Use SQLGlot directly for parsing - no validation here
|
|
344
499
|
parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
|
|
345
|
-
if parsed_expr is None:
|
|
346
|
-
parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
|
|
347
500
|
|
|
348
501
|
if isinstance(parsed_expr, exp.Update):
|
|
349
502
|
builder._expression = parsed_expr
|
|
@@ -360,8 +513,6 @@ class SQLFactory:
|
|
|
360
513
|
try:
|
|
361
514
|
# Use SQLGlot directly for parsing - no validation here
|
|
362
515
|
parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
|
|
363
|
-
if parsed_expr is None:
|
|
364
|
-
parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
|
|
365
516
|
|
|
366
517
|
if isinstance(parsed_expr, exp.Delete):
|
|
367
518
|
builder._expression = parsed_expr
|
|
@@ -378,8 +529,6 @@ class SQLFactory:
|
|
|
378
529
|
try:
|
|
379
530
|
# Use SQLGlot directly for parsing - no validation here
|
|
380
531
|
parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
|
|
381
|
-
if parsed_expr is None:
|
|
382
|
-
parsed_expr = sqlglot.parse_one(sql_string, read=self.dialect)
|
|
383
532
|
|
|
384
533
|
if isinstance(parsed_expr, exp.Merge):
|
|
385
534
|
builder._expression = parsed_expr
|
|
@@ -395,17 +544,194 @@ class SQLFactory:
|
|
|
395
544
|
# Column References
|
|
396
545
|
# ===================
|
|
397
546
|
|
|
398
|
-
def
|
|
399
|
-
"""
|
|
547
|
+
def column(self, name: str, table: Optional[str] = None) -> Column:
|
|
548
|
+
"""Create a column reference.
|
|
400
549
|
|
|
401
550
|
Args:
|
|
402
551
|
name: Column name.
|
|
552
|
+
table: Optional table name.
|
|
403
553
|
|
|
404
554
|
Returns:
|
|
405
555
|
Column object that supports method chaining and operator overloading.
|
|
406
556
|
"""
|
|
557
|
+
return Column(name, table)
|
|
558
|
+
|
|
559
|
+
@property
|
|
560
|
+
def case_(self) -> "Case":
|
|
561
|
+
"""Create a CASE expression builder with improved syntax.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Case builder instance for fluent CASE expression building.
|
|
565
|
+
|
|
566
|
+
Example:
|
|
567
|
+
```python
|
|
568
|
+
case_expr = (
|
|
569
|
+
sql.case_.when("x = 1", "one")
|
|
570
|
+
.when("x = 2", "two")
|
|
571
|
+
.else_("other")
|
|
572
|
+
.end()
|
|
573
|
+
)
|
|
574
|
+
aliased_case = (
|
|
575
|
+
sql.case_.when("status = 'active'", 1)
|
|
576
|
+
.else_(0)
|
|
577
|
+
.as_("is_active")
|
|
578
|
+
)
|
|
579
|
+
```
|
|
580
|
+
"""
|
|
581
|
+
return Case()
|
|
582
|
+
|
|
583
|
+
@property
|
|
584
|
+
def row_number_(self) -> "WindowFunctionBuilder":
|
|
585
|
+
"""Create a ROW_NUMBER() window function builder."""
|
|
586
|
+
return WindowFunctionBuilder("row_number")
|
|
587
|
+
|
|
588
|
+
@property
|
|
589
|
+
def rank_(self) -> "WindowFunctionBuilder":
|
|
590
|
+
"""Create a RANK() window function builder."""
|
|
591
|
+
return WindowFunctionBuilder("rank")
|
|
592
|
+
|
|
593
|
+
@property
|
|
594
|
+
def dense_rank_(self) -> "WindowFunctionBuilder":
|
|
595
|
+
"""Create a DENSE_RANK() window function builder."""
|
|
596
|
+
return WindowFunctionBuilder("dense_rank")
|
|
597
|
+
|
|
598
|
+
@property
|
|
599
|
+
def lag_(self) -> "WindowFunctionBuilder":
|
|
600
|
+
"""Create a LAG() window function builder."""
|
|
601
|
+
return WindowFunctionBuilder("lag")
|
|
602
|
+
|
|
603
|
+
@property
|
|
604
|
+
def lead_(self) -> "WindowFunctionBuilder":
|
|
605
|
+
"""Create a LEAD() window function builder."""
|
|
606
|
+
return WindowFunctionBuilder("lead")
|
|
607
|
+
|
|
608
|
+
@property
|
|
609
|
+
def exists_(self) -> "SubqueryBuilder":
|
|
610
|
+
"""Create an EXISTS subquery builder."""
|
|
611
|
+
return SubqueryBuilder("exists")
|
|
612
|
+
|
|
613
|
+
@property
|
|
614
|
+
def in_(self) -> "SubqueryBuilder":
|
|
615
|
+
"""Create an IN subquery builder."""
|
|
616
|
+
return SubqueryBuilder("in")
|
|
617
|
+
|
|
618
|
+
@property
|
|
619
|
+
def any_(self) -> "SubqueryBuilder":
|
|
620
|
+
"""Create an ANY subquery builder."""
|
|
621
|
+
return SubqueryBuilder("any")
|
|
622
|
+
|
|
623
|
+
@property
|
|
624
|
+
def all_(self) -> "SubqueryBuilder":
|
|
625
|
+
"""Create an ALL subquery builder."""
|
|
626
|
+
return SubqueryBuilder("all")
|
|
627
|
+
|
|
628
|
+
@property
|
|
629
|
+
def inner_join_(self) -> "JoinBuilder":
|
|
630
|
+
"""Create an INNER JOIN builder."""
|
|
631
|
+
return JoinBuilder("inner join")
|
|
632
|
+
|
|
633
|
+
@property
|
|
634
|
+
def left_join_(self) -> "JoinBuilder":
|
|
635
|
+
"""Create a LEFT JOIN builder."""
|
|
636
|
+
return JoinBuilder("left join")
|
|
637
|
+
|
|
638
|
+
@property
|
|
639
|
+
def right_join_(self) -> "JoinBuilder":
|
|
640
|
+
"""Create a RIGHT JOIN builder."""
|
|
641
|
+
return JoinBuilder("right join")
|
|
642
|
+
|
|
643
|
+
@property
|
|
644
|
+
def full_join_(self) -> "JoinBuilder":
|
|
645
|
+
"""Create a FULL OUTER JOIN builder."""
|
|
646
|
+
return JoinBuilder("full join")
|
|
647
|
+
|
|
648
|
+
@property
|
|
649
|
+
def cross_join_(self) -> "JoinBuilder":
|
|
650
|
+
"""Create a CROSS JOIN builder."""
|
|
651
|
+
return JoinBuilder("cross join")
|
|
652
|
+
|
|
653
|
+
def __getattr__(self, name: str) -> "Column":
|
|
654
|
+
"""Dynamically create column references.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
name: Column name.
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
Column object for the given name.
|
|
661
|
+
|
|
662
|
+
Note:
|
|
663
|
+
Special SQL constructs like case_, row_number_, etc. are now
|
|
664
|
+
handled as properties for better type safety.
|
|
665
|
+
"""
|
|
407
666
|
return Column(name)
|
|
408
667
|
|
|
668
|
+
# ===================
|
|
669
|
+
# Raw SQL Expressions
|
|
670
|
+
# ===================
|
|
671
|
+
|
|
672
|
+
@staticmethod
|
|
673
|
+
def raw(sql_fragment: str, **parameters: Any) -> "Union[exp.Expression, SQL]":
|
|
674
|
+
"""Create a raw SQL expression from a string fragment with optional parameters.
|
|
675
|
+
|
|
676
|
+
This method makes it explicit that you are passing raw SQL that should
|
|
677
|
+
be parsed and included directly in the query. Useful for complex expressions,
|
|
678
|
+
database-specific functions, or when you need precise control over the SQL.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
sql_fragment: Raw SQL string to parse into an expression.
|
|
682
|
+
**parameters: Named parameters for parameter binding.
|
|
683
|
+
|
|
684
|
+
Returns:
|
|
685
|
+
SQLGlot expression from the parsed SQL fragment (if no parameters).
|
|
686
|
+
SQL statement object (if parameters provided).
|
|
687
|
+
|
|
688
|
+
Raises:
|
|
689
|
+
SQLBuilderError: If the SQL fragment cannot be parsed.
|
|
690
|
+
|
|
691
|
+
Example:
|
|
692
|
+
```python
|
|
693
|
+
# Raw expression without parameters (current behavior)
|
|
694
|
+
expr = sql.raw("COALESCE(name, 'Unknown')")
|
|
695
|
+
|
|
696
|
+
# Raw SQL with named parameters (new functionality)
|
|
697
|
+
stmt = sql.raw(
|
|
698
|
+
"LOWER(name) LIKE LOWER(:pattern)", pattern=f"%{query}%"
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
# Raw complex expression with parameters
|
|
702
|
+
expr = sql.raw(
|
|
703
|
+
"price BETWEEN :min_price AND :max_price",
|
|
704
|
+
min_price=100,
|
|
705
|
+
max_price=500,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
# Raw window function
|
|
709
|
+
query = sql.select(
|
|
710
|
+
"name",
|
|
711
|
+
sql.raw(
|
|
712
|
+
"ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC)"
|
|
713
|
+
),
|
|
714
|
+
).from_("employees")
|
|
715
|
+
```
|
|
716
|
+
"""
|
|
717
|
+
if not parameters:
|
|
718
|
+
# Original behavior - return pure expression
|
|
719
|
+
try:
|
|
720
|
+
parsed: Optional[exp.Expression] = exp.maybe_parse(sql_fragment)
|
|
721
|
+
if parsed is not None:
|
|
722
|
+
return parsed
|
|
723
|
+
if sql_fragment.strip().replace("_", "").replace(".", "").isalnum():
|
|
724
|
+
return exp.to_identifier(sql_fragment)
|
|
725
|
+
return exp.Literal.string(sql_fragment)
|
|
726
|
+
except Exception as e:
|
|
727
|
+
msg = f"Failed to parse raw SQL fragment '{sql_fragment}': {e}"
|
|
728
|
+
raise SQLBuilderError(msg) from e
|
|
729
|
+
|
|
730
|
+
# New behavior - return SQL statement with parameters
|
|
731
|
+
from sqlspec.core.statement import SQL
|
|
732
|
+
|
|
733
|
+
return SQL(sql_fragment, parameters)
|
|
734
|
+
|
|
409
735
|
# ===================
|
|
410
736
|
# Aggregate Functions
|
|
411
737
|
# ===================
|
|
@@ -597,7 +923,7 @@ class SQLFactory:
|
|
|
597
923
|
```
|
|
598
924
|
"""
|
|
599
925
|
if isinstance(values, list):
|
|
600
|
-
literals = [
|
|
926
|
+
literals = [SQLFactory._to_literal(v) for v in values]
|
|
601
927
|
return exp.Any(this=exp.Array(expressions=literals))
|
|
602
928
|
if isinstance(values, str):
|
|
603
929
|
# Parse as SQL
|
|
@@ -607,6 +933,29 @@ class SQLFactory:
|
|
|
607
933
|
return exp.Any(this=exp.Literal.string(values))
|
|
608
934
|
return exp.Any(this=values)
|
|
609
935
|
|
|
936
|
+
@staticmethod
|
|
937
|
+
def not_any_(values: Union[list[Any], exp.Expression, str]) -> exp.Expression:
|
|
938
|
+
"""Create a NOT ANY expression for use with comparison operators.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
values: Values, expression, or subquery for the NOT ANY clause.
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
NOT ANY expression.
|
|
945
|
+
|
|
946
|
+
Example:
|
|
947
|
+
```python
|
|
948
|
+
# WHERE id <> ANY(subquery)
|
|
949
|
+
subquery = sql.select("user_id").from_("blocked_users")
|
|
950
|
+
query = (
|
|
951
|
+
sql.select("*")
|
|
952
|
+
.from_("users")
|
|
953
|
+
.where(sql.id.neq(sql.not_any(subquery)))
|
|
954
|
+
)
|
|
955
|
+
```
|
|
956
|
+
"""
|
|
957
|
+
return SQLFactory.any(values) # NOT ANY is handled by the comparison operator
|
|
958
|
+
|
|
610
959
|
# ===================
|
|
611
960
|
# String Functions
|
|
612
961
|
# ===================
|
|
@@ -687,6 +1036,28 @@ class SQLFactory:
|
|
|
687
1036
|
# Conversion Functions
|
|
688
1037
|
# ===================
|
|
689
1038
|
|
|
1039
|
+
@staticmethod
|
|
1040
|
+
def _to_literal(value: Any) -> exp.Expression:
|
|
1041
|
+
"""Convert a Python value to a SQLGlot literal expression.
|
|
1042
|
+
|
|
1043
|
+
Uses SQLGlot's built-in exp.convert() function for optimal dialect-agnostic
|
|
1044
|
+
literal creation. Handles all Python primitive types correctly:
|
|
1045
|
+
- None -> exp.Null (renders as NULL)
|
|
1046
|
+
- bool -> exp.Boolean (renders as TRUE/FALSE or 1/0 based on dialect)
|
|
1047
|
+
- int/float -> exp.Literal with is_number=True
|
|
1048
|
+
- str -> exp.Literal with is_string=True
|
|
1049
|
+
- exp.Expression -> returned as-is (passthrough)
|
|
1050
|
+
|
|
1051
|
+
Args:
|
|
1052
|
+
value: Python value or SQLGlot expression to convert.
|
|
1053
|
+
|
|
1054
|
+
Returns:
|
|
1055
|
+
SQLGlot expression representing the literal value.
|
|
1056
|
+
"""
|
|
1057
|
+
if isinstance(value, exp.Expression):
|
|
1058
|
+
return value
|
|
1059
|
+
return exp.convert(value)
|
|
1060
|
+
|
|
690
1061
|
@staticmethod
|
|
691
1062
|
def decode(column: Union[str, exp.Expression], *args: Union[str, exp.Expression, Any]) -> exp.Expression:
|
|
692
1063
|
"""Create a DECODE expression (Oracle-style conditional logic).
|
|
@@ -725,29 +1096,14 @@ class SQLFactory:
|
|
|
725
1096
|
for i in range(0, len(args) - 1, 2):
|
|
726
1097
|
if i + 1 >= len(args):
|
|
727
1098
|
# Odd number of args means last one is default
|
|
728
|
-
default =
|
|
1099
|
+
default = SQLFactory._to_literal(args[i])
|
|
729
1100
|
break
|
|
730
1101
|
|
|
731
1102
|
search_val = args[i]
|
|
732
1103
|
result_val = args[i + 1]
|
|
733
1104
|
|
|
734
|
-
|
|
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))
|
|
1105
|
+
search_expr = SQLFactory._to_literal(search_val)
|
|
1106
|
+
result_expr = SQLFactory._to_literal(result_val)
|
|
751
1107
|
|
|
752
1108
|
condition = exp.EQ(this=col_expr, expression=search_expr)
|
|
753
1109
|
conditions.append(exp.When(this=condition, then=result_expr))
|
|
@@ -793,30 +1149,136 @@ class SQLFactory:
|
|
|
793
1149
|
COALESCE expression equivalent to NVL.
|
|
794
1150
|
"""
|
|
795
1151
|
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
1152
|
+
sub_expr = SQLFactory._to_literal(substitute_value)
|
|
1153
|
+
return exp.Coalesce(expressions=[col_expr, sub_expr])
|
|
796
1154
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
sub_expr = exp.Literal.string(str(substitute_value))
|
|
1155
|
+
@staticmethod
|
|
1156
|
+
def nvl2(
|
|
1157
|
+
column: Union[str, exp.Expression],
|
|
1158
|
+
value_if_not_null: Union[str, exp.Expression, Any],
|
|
1159
|
+
value_if_null: Union[str, exp.Expression, Any],
|
|
1160
|
+
) -> exp.Expression:
|
|
1161
|
+
"""Create an NVL2 (Oracle-style) expression using CASE.
|
|
805
1162
|
|
|
806
|
-
|
|
1163
|
+
NVL2 returns value_if_not_null if column is not NULL,
|
|
1164
|
+
otherwise returns value_if_null.
|
|
1165
|
+
|
|
1166
|
+
Args:
|
|
1167
|
+
column: Column to check for NULL.
|
|
1168
|
+
value_if_not_null: Value to use if column is NOT NULL.
|
|
1169
|
+
value_if_null: Value to use if column is NULL.
|
|
1170
|
+
|
|
1171
|
+
Returns:
|
|
1172
|
+
CASE expression equivalent to NVL2.
|
|
1173
|
+
|
|
1174
|
+
Example:
|
|
1175
|
+
```python
|
|
1176
|
+
# NVL2(salary, 'Has Salary', 'No Salary')
|
|
1177
|
+
sql.nvl2("salary", "Has Salary", "No Salary")
|
|
1178
|
+
```
|
|
1179
|
+
"""
|
|
1180
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
1181
|
+
not_null_expr = SQLFactory._to_literal(value_if_not_null)
|
|
1182
|
+
null_expr = SQLFactory._to_literal(value_if_null)
|
|
1183
|
+
|
|
1184
|
+
# Create CASE WHEN column IS NOT NULL THEN value_if_not_null ELSE value_if_null END
|
|
1185
|
+
is_null = exp.Is(this=col_expr, expression=exp.Null())
|
|
1186
|
+
condition = exp.Not(this=is_null)
|
|
1187
|
+
when_clause = exp.If(this=condition, true=not_null_expr)
|
|
1188
|
+
|
|
1189
|
+
return exp.Case(ifs=[when_clause], default=null_expr)
|
|
1190
|
+
|
|
1191
|
+
# ===================
|
|
1192
|
+
# Bulk Operations
|
|
1193
|
+
# ===================
|
|
1194
|
+
|
|
1195
|
+
@staticmethod
|
|
1196
|
+
def bulk_insert(table_name: str, column_count: int, placeholder_style: str = "?") -> exp.Expression:
|
|
1197
|
+
"""Create bulk INSERT expression for executemany operations.
|
|
1198
|
+
|
|
1199
|
+
This is specifically for bulk loading operations like CSV ingestion where
|
|
1200
|
+
we need an INSERT expression with placeholders for executemany().
|
|
1201
|
+
|
|
1202
|
+
Args:
|
|
1203
|
+
table_name: Name of the table to insert into
|
|
1204
|
+
column_count: Number of columns (for placeholder generation)
|
|
1205
|
+
placeholder_style: Placeholder style ("?" for SQLite/PostgreSQL, "%s" for MySQL, ":1" for Oracle)
|
|
1206
|
+
|
|
1207
|
+
Returns:
|
|
1208
|
+
INSERT expression with proper placeholders for bulk operations
|
|
1209
|
+
|
|
1210
|
+
Example:
|
|
1211
|
+
```python
|
|
1212
|
+
from sqlspec import sql
|
|
1213
|
+
|
|
1214
|
+
# SQLite/PostgreSQL style
|
|
1215
|
+
insert_expr = sql.bulk_insert("my_table", 3)
|
|
1216
|
+
# Creates: INSERT INTO "my_table" VALUES (?, ?, ?)
|
|
1217
|
+
|
|
1218
|
+
# MySQL style
|
|
1219
|
+
insert_expr = sql.bulk_insert(
|
|
1220
|
+
"my_table", 3, placeholder_style="%s"
|
|
1221
|
+
)
|
|
1222
|
+
# Creates: INSERT INTO "my_table" VALUES (%s, %s, %s)
|
|
1223
|
+
|
|
1224
|
+
# Oracle style
|
|
1225
|
+
insert_expr = sql.bulk_insert(
|
|
1226
|
+
"my_table", 3, placeholder_style=":1"
|
|
1227
|
+
)
|
|
1228
|
+
# Creates: INSERT INTO "my_table" VALUES (:1, :2, :3)
|
|
1229
|
+
```
|
|
1230
|
+
"""
|
|
1231
|
+
return exp.Insert(
|
|
1232
|
+
this=exp.Table(this=exp.to_identifier(table_name)),
|
|
1233
|
+
expression=exp.Values(
|
|
1234
|
+
expressions=[
|
|
1235
|
+
exp.Tuple(expressions=[exp.Placeholder(this=placeholder_style) for _ in range(column_count)])
|
|
1236
|
+
]
|
|
1237
|
+
),
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
def truncate(self, table_name: str) -> "Truncate":
|
|
1241
|
+
"""Create a TRUNCATE TABLE builder.
|
|
1242
|
+
|
|
1243
|
+
Args:
|
|
1244
|
+
table_name: Name of the table to truncate
|
|
1245
|
+
|
|
1246
|
+
Returns:
|
|
1247
|
+
TruncateTable builder instance
|
|
1248
|
+
|
|
1249
|
+
Example:
|
|
1250
|
+
```python
|
|
1251
|
+
from sqlspec import sql
|
|
1252
|
+
|
|
1253
|
+
# Simple truncate
|
|
1254
|
+
truncate_sql = sql.truncate_table("my_table").build().sql
|
|
1255
|
+
|
|
1256
|
+
# Truncate with options
|
|
1257
|
+
truncate_sql = (
|
|
1258
|
+
sql.truncate_table("my_table")
|
|
1259
|
+
.cascade()
|
|
1260
|
+
.restart_identity()
|
|
1261
|
+
.build()
|
|
1262
|
+
.sql
|
|
1263
|
+
)
|
|
1264
|
+
```
|
|
1265
|
+
"""
|
|
1266
|
+
builder = Truncate(dialect=self.dialect)
|
|
1267
|
+
builder._table_name = table_name
|
|
1268
|
+
return builder
|
|
807
1269
|
|
|
808
1270
|
# ===================
|
|
809
1271
|
# Case Expressions
|
|
810
1272
|
# ===================
|
|
811
1273
|
|
|
812
1274
|
@staticmethod
|
|
813
|
-
def case() -> "
|
|
1275
|
+
def case() -> "Case":
|
|
814
1276
|
"""Create a CASE expression builder.
|
|
815
1277
|
|
|
816
1278
|
Returns:
|
|
817
1279
|
CaseExpressionBuilder for building CASE expressions.
|
|
818
1280
|
"""
|
|
819
|
-
return
|
|
1281
|
+
return Case()
|
|
820
1282
|
|
|
821
1283
|
# ===================
|
|
822
1284
|
# Window Functions
|
|
@@ -911,7 +1373,8 @@ class SQLFactory:
|
|
|
911
1373
|
return exp.Window(this=func_expr, **over_args)
|
|
912
1374
|
|
|
913
1375
|
|
|
914
|
-
|
|
1376
|
+
@trait
|
|
1377
|
+
class Case:
|
|
915
1378
|
"""Builder for CASE expressions using the SQL factory.
|
|
916
1379
|
|
|
917
1380
|
Example:
|
|
@@ -930,12 +1393,23 @@ class CaseExpressionBuilder:
|
|
|
930
1393
|
|
|
931
1394
|
def __init__(self) -> None:
|
|
932
1395
|
"""Initialize the CASE expression builder."""
|
|
933
|
-
self._conditions: list[exp.
|
|
1396
|
+
self._conditions: list[exp.If] = []
|
|
934
1397
|
self._default: Optional[exp.Expression] = None
|
|
935
1398
|
|
|
936
|
-
def
|
|
937
|
-
|
|
938
|
-
|
|
1399
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
1400
|
+
"""Equal to (==) - convert to expression then compare."""
|
|
1401
|
+
from sqlspec.builder._column import ColumnExpression
|
|
1402
|
+
|
|
1403
|
+
case_expr = exp.Case(ifs=self._conditions, default=self._default)
|
|
1404
|
+
if other is None:
|
|
1405
|
+
return ColumnExpression(exp.Is(this=case_expr, expression=exp.Null()))
|
|
1406
|
+
return ColumnExpression(exp.EQ(this=case_expr, expression=exp.convert(other)))
|
|
1407
|
+
|
|
1408
|
+
def __hash__(self) -> int:
|
|
1409
|
+
"""Make Case hashable."""
|
|
1410
|
+
return hash(id(self))
|
|
1411
|
+
|
|
1412
|
+
def when(self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]) -> "Case":
|
|
939
1413
|
"""Add a WHEN clause.
|
|
940
1414
|
|
|
941
1415
|
Args:
|
|
@@ -946,21 +1420,14 @@ class CaseExpressionBuilder:
|
|
|
946
1420
|
Self for method chaining.
|
|
947
1421
|
"""
|
|
948
1422
|
cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
|
|
1423
|
+
val_expr = SQLFactory._to_literal(value)
|
|
949
1424
|
|
|
950
|
-
|
|
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)
|
|
1425
|
+
# SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
|
|
1426
|
+
when_clause = exp.If(this=cond_expr, true=val_expr)
|
|
960
1427
|
self._conditions.append(when_clause)
|
|
961
1428
|
return self
|
|
962
1429
|
|
|
963
|
-
def else_(self, value: Union[str, exp.Expression, Any]) -> "
|
|
1430
|
+
def else_(self, value: Union[str, exp.Expression, Any]) -> "Case":
|
|
964
1431
|
"""Add an ELSE clause.
|
|
965
1432
|
|
|
966
1433
|
Args:
|
|
@@ -969,14 +1436,7 @@ class CaseExpressionBuilder:
|
|
|
969
1436
|
Returns:
|
|
970
1437
|
Self for method chaining.
|
|
971
1438
|
"""
|
|
972
|
-
|
|
973
|
-
self._default = exp.Literal.string(value)
|
|
974
|
-
elif isinstance(value, (int, float)):
|
|
975
|
-
self._default = exp.Literal.number(value)
|
|
976
|
-
elif isinstance(value, exp.Expression):
|
|
977
|
-
self._default = value
|
|
978
|
-
else:
|
|
979
|
-
self._default = exp.Literal.string(str(value))
|
|
1439
|
+
self._default = SQLFactory._to_literal(value)
|
|
980
1440
|
return self
|
|
981
1441
|
|
|
982
1442
|
def end(self) -> exp.Expression:
|
|
@@ -986,3 +1446,337 @@ class CaseExpressionBuilder:
|
|
|
986
1446
|
Complete CASE expression.
|
|
987
1447
|
"""
|
|
988
1448
|
return exp.Case(ifs=self._conditions, default=self._default)
|
|
1449
|
+
|
|
1450
|
+
def as_(self, alias: str) -> exp.Alias:
|
|
1451
|
+
"""Complete the CASE expression with an alias.
|
|
1452
|
+
|
|
1453
|
+
Args:
|
|
1454
|
+
alias: Alias name for the CASE expression.
|
|
1455
|
+
|
|
1456
|
+
Returns:
|
|
1457
|
+
Aliased CASE expression.
|
|
1458
|
+
"""
|
|
1459
|
+
case_expr = exp.Case(ifs=self._conditions, default=self._default)
|
|
1460
|
+
return cast("exp.Alias", exp.alias_(case_expr, alias))
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
@trait
|
|
1464
|
+
class WindowFunctionBuilder:
|
|
1465
|
+
"""Builder for window functions with fluent syntax.
|
|
1466
|
+
|
|
1467
|
+
Example:
|
|
1468
|
+
```python
|
|
1469
|
+
from sqlspec import sql
|
|
1470
|
+
|
|
1471
|
+
# sql.row_number_.partition_by("department").order_by("salary")
|
|
1472
|
+
window_func = (
|
|
1473
|
+
sql.row_number_.partition_by("department")
|
|
1474
|
+
.order_by("salary")
|
|
1475
|
+
.as_("row_num")
|
|
1476
|
+
)
|
|
1477
|
+
```
|
|
1478
|
+
"""
|
|
1479
|
+
|
|
1480
|
+
def __init__(self, function_name: str) -> None:
|
|
1481
|
+
"""Initialize the window function builder.
|
|
1482
|
+
|
|
1483
|
+
Args:
|
|
1484
|
+
function_name: Name of the window function (row_number, rank, etc.)
|
|
1485
|
+
"""
|
|
1486
|
+
self._function_name = function_name
|
|
1487
|
+
self._partition_by_cols: list[exp.Expression] = []
|
|
1488
|
+
self._order_by_cols: list[exp.Expression] = []
|
|
1489
|
+
self._alias: Optional[str] = None
|
|
1490
|
+
|
|
1491
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
1492
|
+
"""Equal to (==) - convert to expression then compare."""
|
|
1493
|
+
from sqlspec.builder._column import ColumnExpression
|
|
1494
|
+
|
|
1495
|
+
window_expr = self._build_expression()
|
|
1496
|
+
if other is None:
|
|
1497
|
+
return ColumnExpression(exp.Is(this=window_expr, expression=exp.Null()))
|
|
1498
|
+
return ColumnExpression(exp.EQ(this=window_expr, expression=exp.convert(other)))
|
|
1499
|
+
|
|
1500
|
+
def __hash__(self) -> int:
|
|
1501
|
+
"""Make WindowFunctionBuilder hashable."""
|
|
1502
|
+
return hash(id(self))
|
|
1503
|
+
|
|
1504
|
+
def partition_by(self, *columns: Union[str, exp.Expression]) -> "WindowFunctionBuilder":
|
|
1505
|
+
"""Add PARTITION BY clause.
|
|
1506
|
+
|
|
1507
|
+
Args:
|
|
1508
|
+
*columns: Columns to partition by.
|
|
1509
|
+
|
|
1510
|
+
Returns:
|
|
1511
|
+
Self for method chaining.
|
|
1512
|
+
"""
|
|
1513
|
+
for col in columns:
|
|
1514
|
+
col_expr = exp.column(col) if isinstance(col, str) else col
|
|
1515
|
+
self._partition_by_cols.append(col_expr)
|
|
1516
|
+
return self
|
|
1517
|
+
|
|
1518
|
+
def order_by(self, *columns: Union[str, exp.Expression]) -> "WindowFunctionBuilder":
|
|
1519
|
+
"""Add ORDER BY clause.
|
|
1520
|
+
|
|
1521
|
+
Args:
|
|
1522
|
+
*columns: Columns to order by.
|
|
1523
|
+
|
|
1524
|
+
Returns:
|
|
1525
|
+
Self for method chaining.
|
|
1526
|
+
"""
|
|
1527
|
+
for col in columns:
|
|
1528
|
+
if isinstance(col, str):
|
|
1529
|
+
col_expr = exp.column(col).asc()
|
|
1530
|
+
self._order_by_cols.append(col_expr)
|
|
1531
|
+
else:
|
|
1532
|
+
# Convert to ordered expression
|
|
1533
|
+
self._order_by_cols.append(exp.Ordered(this=col, desc=False))
|
|
1534
|
+
return self
|
|
1535
|
+
|
|
1536
|
+
def as_(self, alias: str) -> exp.Alias:
|
|
1537
|
+
"""Complete the window function with an alias.
|
|
1538
|
+
|
|
1539
|
+
Args:
|
|
1540
|
+
alias: Alias name for the window function.
|
|
1541
|
+
|
|
1542
|
+
Returns:
|
|
1543
|
+
Aliased window function expression.
|
|
1544
|
+
"""
|
|
1545
|
+
window_expr = self._build_expression()
|
|
1546
|
+
return cast("exp.Alias", exp.alias_(window_expr, alias))
|
|
1547
|
+
|
|
1548
|
+
def build(self) -> exp.Expression:
|
|
1549
|
+
"""Complete the window function without an alias.
|
|
1550
|
+
|
|
1551
|
+
Returns:
|
|
1552
|
+
Window function expression.
|
|
1553
|
+
"""
|
|
1554
|
+
return self._build_expression()
|
|
1555
|
+
|
|
1556
|
+
def _build_expression(self) -> exp.Expression:
|
|
1557
|
+
"""Build the complete window function expression."""
|
|
1558
|
+
# Create the function expression
|
|
1559
|
+
func_expr = exp.Anonymous(this=self._function_name.upper(), expressions=[])
|
|
1560
|
+
|
|
1561
|
+
# Build the OVER clause arguments
|
|
1562
|
+
over_args: dict[str, Any] = {}
|
|
1563
|
+
|
|
1564
|
+
if self._partition_by_cols:
|
|
1565
|
+
over_args["partition_by"] = self._partition_by_cols
|
|
1566
|
+
|
|
1567
|
+
if self._order_by_cols:
|
|
1568
|
+
over_args["order"] = exp.Order(expressions=self._order_by_cols)
|
|
1569
|
+
|
|
1570
|
+
return exp.Window(this=func_expr, **over_args)
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
@trait
|
|
1574
|
+
class SubqueryBuilder:
|
|
1575
|
+
"""Builder for subquery operations with fluent syntax.
|
|
1576
|
+
|
|
1577
|
+
Example:
|
|
1578
|
+
```python
|
|
1579
|
+
from sqlspec import sql
|
|
1580
|
+
|
|
1581
|
+
# sql.exists_(subquery)
|
|
1582
|
+
exists_check = sql.exists_(
|
|
1583
|
+
sql.select("1")
|
|
1584
|
+
.from_("orders")
|
|
1585
|
+
.where_eq("user_id", sql.users.id)
|
|
1586
|
+
)
|
|
1587
|
+
|
|
1588
|
+
# sql.in_(subquery)
|
|
1589
|
+
in_check = sql.in_(
|
|
1590
|
+
sql.select("category_id")
|
|
1591
|
+
.from_("categories")
|
|
1592
|
+
.where_eq("active", True)
|
|
1593
|
+
)
|
|
1594
|
+
```
|
|
1595
|
+
"""
|
|
1596
|
+
|
|
1597
|
+
def __init__(self, operation: str) -> None:
|
|
1598
|
+
"""Initialize the subquery builder.
|
|
1599
|
+
|
|
1600
|
+
Args:
|
|
1601
|
+
operation: Type of subquery operation (exists, in, any, all)
|
|
1602
|
+
"""
|
|
1603
|
+
self._operation = operation
|
|
1604
|
+
|
|
1605
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
1606
|
+
"""Equal to (==) - not typically used but needed for type consistency."""
|
|
1607
|
+
from sqlspec.builder._column import ColumnExpression
|
|
1608
|
+
|
|
1609
|
+
# SubqueryBuilder doesn't have a direct expression, so this is a placeholder
|
|
1610
|
+
# In practice, this shouldn't be called as subqueries are used differently
|
|
1611
|
+
placeholder_expr = exp.Literal.string(f"subquery_{self._operation}")
|
|
1612
|
+
if other is None:
|
|
1613
|
+
return ColumnExpression(exp.Is(this=placeholder_expr, expression=exp.Null()))
|
|
1614
|
+
return ColumnExpression(exp.EQ(this=placeholder_expr, expression=exp.convert(other)))
|
|
1615
|
+
|
|
1616
|
+
def __hash__(self) -> int:
|
|
1617
|
+
"""Make SubqueryBuilder hashable."""
|
|
1618
|
+
return hash(id(self))
|
|
1619
|
+
|
|
1620
|
+
def __call__(self, subquery: Union[str, exp.Expression, Any]) -> exp.Expression:
|
|
1621
|
+
"""Build the subquery expression.
|
|
1622
|
+
|
|
1623
|
+
Args:
|
|
1624
|
+
subquery: The subquery - can be a SQL string, SelectBuilder, or expression
|
|
1625
|
+
|
|
1626
|
+
Returns:
|
|
1627
|
+
The subquery expression (EXISTS, IN, ANY, ALL, etc.)
|
|
1628
|
+
"""
|
|
1629
|
+
subquery_expr: exp.Expression
|
|
1630
|
+
if isinstance(subquery, str):
|
|
1631
|
+
# Parse as SQL
|
|
1632
|
+
parsed: Optional[exp.Expression] = exp.maybe_parse(subquery)
|
|
1633
|
+
if not parsed:
|
|
1634
|
+
msg = f"Could not parse subquery SQL: {subquery}"
|
|
1635
|
+
raise SQLBuilderError(msg)
|
|
1636
|
+
subquery_expr = parsed
|
|
1637
|
+
elif hasattr(subquery, "build") and callable(getattr(subquery, "build", None)):
|
|
1638
|
+
# It's a query builder - build it to get the SQL and parse
|
|
1639
|
+
built_query = subquery.build() # pyright: ignore[reportAttributeAccessIssue]
|
|
1640
|
+
subquery_expr = exp.maybe_parse(built_query.sql)
|
|
1641
|
+
if not subquery_expr:
|
|
1642
|
+
msg = f"Could not parse built query: {built_query.sql}"
|
|
1643
|
+
raise SQLBuilderError(msg)
|
|
1644
|
+
elif isinstance(subquery, exp.Expression):
|
|
1645
|
+
subquery_expr = subquery
|
|
1646
|
+
else:
|
|
1647
|
+
# Try to convert to expression
|
|
1648
|
+
parsed = exp.maybe_parse(str(subquery))
|
|
1649
|
+
if not parsed:
|
|
1650
|
+
msg = f"Could not convert subquery to expression: {subquery}"
|
|
1651
|
+
raise SQLBuilderError(msg)
|
|
1652
|
+
subquery_expr = parsed
|
|
1653
|
+
|
|
1654
|
+
# Build the appropriate expression based on operation
|
|
1655
|
+
if self._operation == "exists":
|
|
1656
|
+
return exp.Exists(this=subquery_expr)
|
|
1657
|
+
if self._operation == "in":
|
|
1658
|
+
# For IN, we create a subquery that can be used with WHERE column IN (subquery)
|
|
1659
|
+
return exp.In(expressions=[subquery_expr])
|
|
1660
|
+
if self._operation == "any":
|
|
1661
|
+
return exp.Any(this=subquery_expr)
|
|
1662
|
+
if self._operation == "all":
|
|
1663
|
+
return exp.All(this=subquery_expr)
|
|
1664
|
+
msg = f"Unknown subquery operation: {self._operation}"
|
|
1665
|
+
raise SQLBuilderError(msg)
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
@trait
|
|
1669
|
+
class JoinBuilder:
|
|
1670
|
+
"""Builder for JOIN operations with fluent syntax.
|
|
1671
|
+
|
|
1672
|
+
Example:
|
|
1673
|
+
```python
|
|
1674
|
+
from sqlspec import sql
|
|
1675
|
+
|
|
1676
|
+
# sql.left_join_("posts").on("users.id = posts.user_id")
|
|
1677
|
+
join_clause = sql.left_join_("posts").on(
|
|
1678
|
+
"users.id = posts.user_id"
|
|
1679
|
+
)
|
|
1680
|
+
|
|
1681
|
+
# Or with query builder
|
|
1682
|
+
query = (
|
|
1683
|
+
sql.select("users.name", "posts.title")
|
|
1684
|
+
.from_("users")
|
|
1685
|
+
.join(
|
|
1686
|
+
sql.left_join_("posts").on(
|
|
1687
|
+
"users.id = posts.user_id"
|
|
1688
|
+
)
|
|
1689
|
+
)
|
|
1690
|
+
)
|
|
1691
|
+
```
|
|
1692
|
+
"""
|
|
1693
|
+
|
|
1694
|
+
def __init__(self, join_type: str) -> None:
|
|
1695
|
+
"""Initialize the join builder.
|
|
1696
|
+
|
|
1697
|
+
Args:
|
|
1698
|
+
join_type: Type of join (inner, left, right, full, cross)
|
|
1699
|
+
"""
|
|
1700
|
+
self._join_type = join_type.upper()
|
|
1701
|
+
self._table: Optional[Union[str, exp.Expression]] = None
|
|
1702
|
+
self._condition: Optional[exp.Expression] = None
|
|
1703
|
+
self._alias: Optional[str] = None
|
|
1704
|
+
|
|
1705
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
1706
|
+
"""Equal to (==) - not typically used but needed for type consistency."""
|
|
1707
|
+
from sqlspec.builder._column import ColumnExpression
|
|
1708
|
+
|
|
1709
|
+
# JoinBuilder doesn't have a direct expression, so this is a placeholder
|
|
1710
|
+
# In practice, this shouldn't be called as joins are used differently
|
|
1711
|
+
placeholder_expr = exp.Literal.string(f"join_{self._join_type.lower()}")
|
|
1712
|
+
if other is None:
|
|
1713
|
+
return ColumnExpression(exp.Is(this=placeholder_expr, expression=exp.Null()))
|
|
1714
|
+
return ColumnExpression(exp.EQ(this=placeholder_expr, expression=exp.convert(other)))
|
|
1715
|
+
|
|
1716
|
+
def __hash__(self) -> int:
|
|
1717
|
+
"""Make JoinBuilder hashable."""
|
|
1718
|
+
return hash(id(self))
|
|
1719
|
+
|
|
1720
|
+
def __call__(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> "JoinBuilder":
|
|
1721
|
+
"""Set the table to join.
|
|
1722
|
+
|
|
1723
|
+
Args:
|
|
1724
|
+
table: Table name or expression to join
|
|
1725
|
+
alias: Optional alias for the table
|
|
1726
|
+
|
|
1727
|
+
Returns:
|
|
1728
|
+
Self for method chaining
|
|
1729
|
+
"""
|
|
1730
|
+
self._table = table
|
|
1731
|
+
self._alias = alias
|
|
1732
|
+
return self
|
|
1733
|
+
|
|
1734
|
+
def on(self, condition: Union[str, exp.Expression]) -> exp.Expression:
|
|
1735
|
+
"""Set the join condition and build the JOIN expression.
|
|
1736
|
+
|
|
1737
|
+
Args:
|
|
1738
|
+
condition: JOIN condition (e.g., "users.id = posts.user_id")
|
|
1739
|
+
|
|
1740
|
+
Returns:
|
|
1741
|
+
Complete JOIN expression
|
|
1742
|
+
"""
|
|
1743
|
+
if not self._table:
|
|
1744
|
+
msg = "Table must be set before calling .on()"
|
|
1745
|
+
raise SQLBuilderError(msg)
|
|
1746
|
+
|
|
1747
|
+
# Parse the condition
|
|
1748
|
+
condition_expr: exp.Expression
|
|
1749
|
+
if isinstance(condition, str):
|
|
1750
|
+
parsed: Optional[exp.Expression] = exp.maybe_parse(condition)
|
|
1751
|
+
condition_expr = parsed or exp.condition(condition)
|
|
1752
|
+
else:
|
|
1753
|
+
condition_expr = condition
|
|
1754
|
+
|
|
1755
|
+
# Build table expression
|
|
1756
|
+
table_expr: exp.Expression
|
|
1757
|
+
if isinstance(self._table, str):
|
|
1758
|
+
table_expr = exp.to_table(self._table)
|
|
1759
|
+
if self._alias:
|
|
1760
|
+
table_expr = exp.alias_(table_expr, self._alias)
|
|
1761
|
+
else:
|
|
1762
|
+
table_expr = self._table
|
|
1763
|
+
if self._alias:
|
|
1764
|
+
table_expr = exp.alias_(table_expr, self._alias)
|
|
1765
|
+
|
|
1766
|
+
# Create the appropriate join type using same pattern as existing JoinClauseMixin
|
|
1767
|
+
if self._join_type == "INNER JOIN":
|
|
1768
|
+
return exp.Join(this=table_expr, on=condition_expr)
|
|
1769
|
+
if self._join_type == "LEFT JOIN":
|
|
1770
|
+
return exp.Join(this=table_expr, on=condition_expr, side="LEFT")
|
|
1771
|
+
if self._join_type == "RIGHT JOIN":
|
|
1772
|
+
return exp.Join(this=table_expr, on=condition_expr, side="RIGHT")
|
|
1773
|
+
if self._join_type == "FULL JOIN":
|
|
1774
|
+
return exp.Join(this=table_expr, on=condition_expr, side="FULL", kind="OUTER")
|
|
1775
|
+
if self._join_type == "CROSS JOIN":
|
|
1776
|
+
# CROSS JOIN doesn't use ON condition
|
|
1777
|
+
return exp.Join(this=table_expr, kind="CROSS")
|
|
1778
|
+
return exp.Join(this=table_expr, on=condition_expr)
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
# Create a default SQL factory instance
|
|
1782
|
+
sql = SQLFactory()
|