sqlspec 0.16.1__cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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.
- 51ff5a9eadfdefd49f98__mypyc.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/__init__.py +92 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +77 -0
- sqlspec/_sql.py +1780 -0
- sqlspec/_typing.py +680 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +361 -0
- sqlspec/adapters/adbc/driver.py +512 -0
- sqlspec/adapters/aiosqlite/__init__.py +19 -0
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +253 -0
- sqlspec/adapters/aiosqlite/driver.py +248 -0
- sqlspec/adapters/asyncmy/__init__.py +19 -0
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +180 -0
- sqlspec/adapters/asyncmy/driver.py +274 -0
- sqlspec/adapters/asyncpg/__init__.py +21 -0
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +229 -0
- sqlspec/adapters/asyncpg/driver.py +344 -0
- sqlspec/adapters/bigquery/__init__.py +18 -0
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +298 -0
- sqlspec/adapters/bigquery/driver.py +558 -0
- sqlspec/adapters/duckdb/__init__.py +22 -0
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +504 -0
- sqlspec/adapters/duckdb/driver.py +368 -0
- sqlspec/adapters/oracledb/__init__.py +32 -0
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +317 -0
- sqlspec/adapters/oracledb/driver.py +538 -0
- sqlspec/adapters/psqlpy/__init__.py +16 -0
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +214 -0
- sqlspec/adapters/psqlpy/driver.py +530 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +426 -0
- sqlspec/adapters/psycopg/driver.py +796 -0
- sqlspec/adapters/sqlite/__init__.py +15 -0
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +240 -0
- sqlspec/adapters/sqlite/driver.py +294 -0
- sqlspec/base.py +571 -0
- sqlspec/builder/__init__.py +62 -0
- sqlspec/builder/_base.py +473 -0
- sqlspec/builder/_column.py +320 -0
- sqlspec/builder/_ddl.py +1346 -0
- sqlspec/builder/_ddl_utils.py +103 -0
- sqlspec/builder/_delete.py +76 -0
- sqlspec/builder/_insert.py +256 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/builder/_parsing_utils.py +140 -0
- sqlspec/builder/_select.py +170 -0
- sqlspec/builder/_update.py +188 -0
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/builder/mixins/_delete_operations.py +41 -0
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/builder/mixins/_join_operations.py +122 -0
- sqlspec/builder/mixins/_merge_operations.py +476 -0
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +603 -0
- sqlspec/builder/mixins/_update_operations.py +187 -0
- sqlspec/builder/mixins/_where_clause.py +621 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +395 -0
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result.py +677 -0
- sqlspec/core/splitter.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +819 -0
- sqlspec/core/statement.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +19 -0
- sqlspec/driver/_async.py +502 -0
- sqlspec/driver/_common.py +631 -0
- sqlspec/driver/_sync.py +503 -0
- sqlspec/driver/mixins/__init__.py +6 -0
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +86 -0
- sqlspec/exceptions.py +193 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +461 -0
- sqlspec/extensions/litestar/__init__.py +6 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +92 -0
- sqlspec/extensions/litestar/handlers.py +260 -0
- sqlspec/extensions/litestar/plugin.py +145 -0
- sqlspec/extensions/litestar/providers.py +454 -0
- sqlspec/loader.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/loader.py +760 -0
- 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 +407 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +23 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +163 -0
- sqlspec/storage/backends/fsspec.py +386 -0
- sqlspec/storage/backends/obstore.py +459 -0
- sqlspec/storage/capabilities.py +102 -0
- sqlspec/storage/registry.py +239 -0
- sqlspec/typing.py +299 -0
- sqlspec/utils/__init__.py +3 -0
- sqlspec/utils/correlation.py +150 -0
- sqlspec/utils/deprecation.py +106 -0
- sqlspec/utils/fixtures.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/fixtures.py +58 -0
- sqlspec/utils/logging.py +127 -0
- sqlspec/utils/module_loader.py +89 -0
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +32 -0
- sqlspec/utils/sync_tools.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +237 -0
- sqlspec/utils/text.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +96 -0
- sqlspec/utils/type_guards.cpython-311-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_guards.py +1139 -0
- sqlspec-0.16.1.dist-info/METADATA +365 -0
- sqlspec-0.16.1.dist-info/RECORD +148 -0
- sqlspec-0.16.1.dist-info/WHEEL +7 -0
- sqlspec-0.16.1.dist-info/entry_points.txt +2 -0
- sqlspec-0.16.1.dist-info/licenses/LICENSE +21 -0
- sqlspec-0.16.1.dist-info/licenses/NOTICE +29 -0
sqlspec/_sql.py
ADDED
|
@@ -0,0 +1,1780 @@
|
|
|
1
|
+
"""Unified SQL factory for creating SQL builders and column expressions with a clean API.
|
|
2
|
+
|
|
3
|
+
Provides both statement builders (select, insert, update, etc.) and column expressions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
8
|
+
|
|
9
|
+
import sqlglot
|
|
10
|
+
from mypy_extensions import trait
|
|
11
|
+
from sqlglot import exp
|
|
12
|
+
from sqlglot.dialects.dialect import DialectType
|
|
13
|
+
from sqlglot.errors import ParseError as SQLGlotParseError
|
|
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
|
+
)
|
|
37
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
38
|
+
|
|
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
|
+
)
|
|
69
|
+
|
|
70
|
+
logger = logging.getLogger("sqlspec")
|
|
71
|
+
|
|
72
|
+
MIN_SQL_LIKE_STRING_LENGTH = 6
|
|
73
|
+
MIN_DECODE_ARGS = 2
|
|
74
|
+
SQL_STARTERS = {
|
|
75
|
+
"SELECT",
|
|
76
|
+
"INSERT",
|
|
77
|
+
"UPDATE",
|
|
78
|
+
"DELETE",
|
|
79
|
+
"MERGE",
|
|
80
|
+
"WITH",
|
|
81
|
+
"CALL",
|
|
82
|
+
"DECLARE",
|
|
83
|
+
"BEGIN",
|
|
84
|
+
"END",
|
|
85
|
+
"CREATE",
|
|
86
|
+
"DROP",
|
|
87
|
+
"ALTER",
|
|
88
|
+
"TRUNCATE",
|
|
89
|
+
"RENAME",
|
|
90
|
+
"GRANT",
|
|
91
|
+
"REVOKE",
|
|
92
|
+
"SET",
|
|
93
|
+
"SHOW",
|
|
94
|
+
"USE",
|
|
95
|
+
"EXPLAIN",
|
|
96
|
+
"OPTIMIZE",
|
|
97
|
+
"VACUUM",
|
|
98
|
+
"COPY",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SQLFactory:
|
|
103
|
+
"""Unified factory for creating SQL builders and column expressions with a fluent API."""
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def detect_sql_type(cls, sql: str, dialect: DialectType = None) -> str:
|
|
107
|
+
try:
|
|
108
|
+
parsed_expr = sqlglot.parse_one(sql, read=dialect)
|
|
109
|
+
if parsed_expr and parsed_expr.key:
|
|
110
|
+
return parsed_expr.key.upper()
|
|
111
|
+
if parsed_expr:
|
|
112
|
+
command_type = type(parsed_expr).__name__.upper()
|
|
113
|
+
if command_type == "COMMAND" and parsed_expr.this:
|
|
114
|
+
return str(parsed_expr.this).upper()
|
|
115
|
+
return command_type
|
|
116
|
+
except SQLGlotParseError:
|
|
117
|
+
logger.debug("Failed to parse SQL for type detection: %s", sql[:100])
|
|
118
|
+
except (ValueError, TypeError, AttributeError) as e:
|
|
119
|
+
logger.warning("Unexpected error during SQL type detection for '%s...': %s", sql[:50], e)
|
|
120
|
+
return "UNKNOWN"
|
|
121
|
+
|
|
122
|
+
def __init__(self, dialect: DialectType = None) -> None:
|
|
123
|
+
"""Initialize the SQL factory.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
dialect: Default SQL dialect to use for all builders.
|
|
127
|
+
"""
|
|
128
|
+
self.dialect = dialect
|
|
129
|
+
|
|
130
|
+
# ===================
|
|
131
|
+
# Callable Interface
|
|
132
|
+
# ===================
|
|
133
|
+
def __call__(self, statement: str, dialect: DialectType = None) -> "Any":
|
|
134
|
+
"""Create a SelectBuilder from a SQL string, only allowing SELECT/CTE queries.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
statement: The SQL statement string.
|
|
138
|
+
parameters: Optional parameters for the query.
|
|
139
|
+
*filters: Optional filters.
|
|
140
|
+
config: Optional config.
|
|
141
|
+
dialect: Optional SQL dialect.
|
|
142
|
+
**kwargs: Additional parameters.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
SelectBuilder instance.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
SQLBuilderError: If the SQL is not a SELECT/CTE statement.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
parsed_expr = sqlglot.parse_one(statement, read=dialect or self.dialect)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
msg = f"Failed to parse SQL: {e}"
|
|
155
|
+
raise SQLBuilderError(msg) from e
|
|
156
|
+
actual_type = type(parsed_expr).__name__.upper()
|
|
157
|
+
expr_type_map = {
|
|
158
|
+
"SELECT": "SELECT",
|
|
159
|
+
"INSERT": "INSERT",
|
|
160
|
+
"UPDATE": "UPDATE",
|
|
161
|
+
"DELETE": "DELETE",
|
|
162
|
+
"MERGE": "MERGE",
|
|
163
|
+
"WITH": "WITH",
|
|
164
|
+
}
|
|
165
|
+
actual_type_str = expr_type_map.get(actual_type, actual_type)
|
|
166
|
+
if actual_type_str == "SELECT" or (
|
|
167
|
+
actual_type_str == "WITH" and parsed_expr.this and isinstance(parsed_expr.this, exp.Select)
|
|
168
|
+
):
|
|
169
|
+
builder = Select(dialect=dialect or self.dialect)
|
|
170
|
+
builder._expression = parsed_expr
|
|
171
|
+
return builder
|
|
172
|
+
msg = (
|
|
173
|
+
f"sql(...) only supports SELECT statements. Detected type: {actual_type_str}. "
|
|
174
|
+
f"Use sql.{actual_type_str.lower()}() instead."
|
|
175
|
+
)
|
|
176
|
+
raise SQLBuilderError(msg)
|
|
177
|
+
|
|
178
|
+
# ===================
|
|
179
|
+
# Statement Builders
|
|
180
|
+
# ===================
|
|
181
|
+
def select(self, *columns_or_sql: Union[str, exp.Expression, Column], dialect: DialectType = None) -> "Select":
|
|
182
|
+
builder_dialect = dialect or self.dialect
|
|
183
|
+
if len(columns_or_sql) == 1 and isinstance(columns_or_sql[0], str):
|
|
184
|
+
sql_candidate = columns_or_sql[0].strip()
|
|
185
|
+
if self._looks_like_sql(sql_candidate):
|
|
186
|
+
detected = self.detect_sql_type(sql_candidate, dialect=builder_dialect)
|
|
187
|
+
if detected not in {"SELECT", "WITH"}:
|
|
188
|
+
msg = (
|
|
189
|
+
f"sql.select() expects a SELECT or WITH statement, got {detected}. "
|
|
190
|
+
f"Use sql.{detected.lower()}() if a dedicated builder exists, or ensure the SQL is SELECT/WITH."
|
|
191
|
+
)
|
|
192
|
+
raise SQLBuilderError(msg)
|
|
193
|
+
select_builder = Select(dialect=builder_dialect)
|
|
194
|
+
return self._populate_select_from_sql(select_builder, sql_candidate)
|
|
195
|
+
select_builder = Select(dialect=builder_dialect)
|
|
196
|
+
if columns_or_sql:
|
|
197
|
+
select_builder.select(*columns_or_sql)
|
|
198
|
+
return select_builder
|
|
199
|
+
|
|
200
|
+
def insert(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Insert":
|
|
201
|
+
builder_dialect = dialect or self.dialect
|
|
202
|
+
builder = Insert(dialect=builder_dialect)
|
|
203
|
+
if table_or_sql:
|
|
204
|
+
if self._looks_like_sql(table_or_sql):
|
|
205
|
+
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
206
|
+
if detected not in {"INSERT", "SELECT"}:
|
|
207
|
+
msg = (
|
|
208
|
+
f"sql.insert() expects INSERT or SELECT (for insert-from-select), got {detected}. "
|
|
209
|
+
f"Use sql.{detected.lower()}() if a dedicated builder exists, "
|
|
210
|
+
f"or ensure the SQL is INSERT/SELECT."
|
|
211
|
+
)
|
|
212
|
+
raise SQLBuilderError(msg)
|
|
213
|
+
return self._populate_insert_from_sql(builder, table_or_sql)
|
|
214
|
+
return builder.into(table_or_sql)
|
|
215
|
+
return builder
|
|
216
|
+
|
|
217
|
+
def update(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Update":
|
|
218
|
+
builder_dialect = dialect or self.dialect
|
|
219
|
+
builder = Update(dialect=builder_dialect)
|
|
220
|
+
if table_or_sql:
|
|
221
|
+
if self._looks_like_sql(table_or_sql):
|
|
222
|
+
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
223
|
+
if detected != "UPDATE":
|
|
224
|
+
msg = f"sql.update() expects UPDATE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
|
|
225
|
+
raise SQLBuilderError(msg)
|
|
226
|
+
return self._populate_update_from_sql(builder, table_or_sql)
|
|
227
|
+
return builder.table(table_or_sql)
|
|
228
|
+
return builder
|
|
229
|
+
|
|
230
|
+
def delete(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Delete":
|
|
231
|
+
builder_dialect = dialect or self.dialect
|
|
232
|
+
builder = Delete(dialect=builder_dialect)
|
|
233
|
+
if table_or_sql and self._looks_like_sql(table_or_sql):
|
|
234
|
+
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
235
|
+
if detected != "DELETE":
|
|
236
|
+
msg = f"sql.delete() expects DELETE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
|
|
237
|
+
raise SQLBuilderError(msg)
|
|
238
|
+
return self._populate_delete_from_sql(builder, table_or_sql)
|
|
239
|
+
return builder
|
|
240
|
+
|
|
241
|
+
def merge(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Merge":
|
|
242
|
+
builder_dialect = dialect or self.dialect
|
|
243
|
+
builder = Merge(dialect=builder_dialect)
|
|
244
|
+
if table_or_sql:
|
|
245
|
+
if self._looks_like_sql(table_or_sql):
|
|
246
|
+
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
247
|
+
if detected != "MERGE":
|
|
248
|
+
msg = f"sql.merge() expects MERGE statement, got {detected}. Use sql.{detected.lower()}() if a dedicated builder exists."
|
|
249
|
+
raise SQLBuilderError(msg)
|
|
250
|
+
return self._populate_merge_from_sql(builder, table_or_sql)
|
|
251
|
+
return builder.into(table_or_sql)
|
|
252
|
+
return builder
|
|
253
|
+
|
|
254
|
+
# ===================
|
|
255
|
+
# DDL Statement Builders
|
|
256
|
+
# ===================
|
|
257
|
+
|
|
258
|
+
def create_table(self, table_name: str, dialect: DialectType = None) -> "CreateTable":
|
|
259
|
+
"""Create a CREATE TABLE builder.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
table_name: Name of the table to create
|
|
263
|
+
dialect: Optional SQL dialect
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
CreateTable builder instance
|
|
267
|
+
"""
|
|
268
|
+
builder = CreateTable(table_name)
|
|
269
|
+
builder.dialect = dialect or self.dialect
|
|
270
|
+
return builder
|
|
271
|
+
|
|
272
|
+
def create_table_as_select(self, dialect: DialectType = None) -> "CreateTableAsSelect":
|
|
273
|
+
"""Create a CREATE TABLE AS SELECT builder.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
dialect: Optional SQL dialect
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
CreateTableAsSelect builder instance
|
|
280
|
+
"""
|
|
281
|
+
builder = CreateTableAsSelect()
|
|
282
|
+
builder.dialect = dialect or self.dialect
|
|
283
|
+
return builder
|
|
284
|
+
|
|
285
|
+
def create_view(self, dialect: DialectType = None) -> "CreateView":
|
|
286
|
+
"""Create a CREATE VIEW builder.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
dialect: Optional SQL dialect
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
CreateView builder instance
|
|
293
|
+
"""
|
|
294
|
+
builder = CreateView()
|
|
295
|
+
builder.dialect = dialect or self.dialect
|
|
296
|
+
return builder
|
|
297
|
+
|
|
298
|
+
def create_materialized_view(self, dialect: DialectType = None) -> "CreateMaterializedView":
|
|
299
|
+
"""Create a CREATE MATERIALIZED VIEW builder.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
dialect: Optional SQL dialect
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
CreateMaterializedView builder instance
|
|
306
|
+
"""
|
|
307
|
+
builder = CreateMaterializedView()
|
|
308
|
+
builder.dialect = dialect or self.dialect
|
|
309
|
+
return builder
|
|
310
|
+
|
|
311
|
+
def create_index(self, index_name: str, dialect: DialectType = None) -> "CreateIndex":
|
|
312
|
+
"""Create a CREATE INDEX builder.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
index_name: Name of the index to create
|
|
316
|
+
dialect: Optional SQL dialect
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
CreateIndex builder instance
|
|
320
|
+
"""
|
|
321
|
+
return CreateIndex(index_name, dialect=dialect or self.dialect)
|
|
322
|
+
|
|
323
|
+
def create_schema(self, dialect: DialectType = None) -> "CreateSchema":
|
|
324
|
+
"""Create a CREATE SCHEMA builder.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
dialect: Optional SQL dialect
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
CreateSchema builder instance
|
|
331
|
+
"""
|
|
332
|
+
builder = CreateSchema()
|
|
333
|
+
builder.dialect = dialect or self.dialect
|
|
334
|
+
return builder
|
|
335
|
+
|
|
336
|
+
def drop_table(self, table_name: str, dialect: DialectType = None) -> "DropTable":
|
|
337
|
+
"""Create a DROP TABLE builder.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
table_name: Name of the table to drop
|
|
341
|
+
dialect: Optional SQL dialect
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
DropTable builder instance
|
|
345
|
+
"""
|
|
346
|
+
return DropTable(table_name, dialect=dialect or self.dialect)
|
|
347
|
+
|
|
348
|
+
def drop_view(self, dialect: DialectType = None) -> "DropView":
|
|
349
|
+
"""Create a DROP VIEW builder.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
dialect: Optional SQL dialect
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
DropView builder instance
|
|
356
|
+
"""
|
|
357
|
+
return DropView(dialect=dialect or self.dialect)
|
|
358
|
+
|
|
359
|
+
def drop_index(self, index_name: str, dialect: DialectType = None) -> "DropIndex":
|
|
360
|
+
"""Create a DROP INDEX builder.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
index_name: Name of the index to drop
|
|
364
|
+
dialect: Optional SQL dialect
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
DropIndex builder instance
|
|
368
|
+
"""
|
|
369
|
+
return DropIndex(index_name, dialect=dialect or self.dialect)
|
|
370
|
+
|
|
371
|
+
def drop_schema(self, dialect: DialectType = None) -> "DropSchema":
|
|
372
|
+
"""Create a DROP SCHEMA builder.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
dialect: Optional SQL dialect
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
DropSchema builder instance
|
|
379
|
+
"""
|
|
380
|
+
return DropSchema(dialect=dialect or self.dialect)
|
|
381
|
+
|
|
382
|
+
def alter_table(self, table_name: str, dialect: DialectType = None) -> "AlterTable":
|
|
383
|
+
"""Create an ALTER TABLE builder.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
table_name: Name of the table to alter
|
|
387
|
+
dialect: Optional SQL dialect
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
AlterTable builder instance
|
|
391
|
+
"""
|
|
392
|
+
builder = AlterTable(table_name)
|
|
393
|
+
builder.dialect = dialect or self.dialect
|
|
394
|
+
return builder
|
|
395
|
+
|
|
396
|
+
def rename_table(self, dialect: DialectType = None) -> "RenameTable":
|
|
397
|
+
"""Create a RENAME TABLE builder.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
dialect: Optional SQL dialect
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
RenameTable builder instance
|
|
404
|
+
"""
|
|
405
|
+
builder = RenameTable()
|
|
406
|
+
builder.dialect = dialect or self.dialect
|
|
407
|
+
return builder
|
|
408
|
+
|
|
409
|
+
def comment_on(self, dialect: DialectType = None) -> "CommentOn":
|
|
410
|
+
"""Create a COMMENT ON builder.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
dialect: Optional SQL dialect
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
CommentOn builder instance
|
|
417
|
+
"""
|
|
418
|
+
builder = CommentOn()
|
|
419
|
+
builder.dialect = dialect or self.dialect
|
|
420
|
+
return builder
|
|
421
|
+
|
|
422
|
+
# ===================
|
|
423
|
+
# SQL Analysis Helpers
|
|
424
|
+
# ===================
|
|
425
|
+
|
|
426
|
+
@staticmethod
|
|
427
|
+
def _looks_like_sql(candidate: str, expected_type: Optional[str] = None) -> bool:
|
|
428
|
+
"""Efficiently determine if a string looks like SQL.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
candidate: String to check
|
|
432
|
+
expected_type: Expected SQL statement type (SELECT, INSERT, etc.)
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
True if the string appears to be SQL
|
|
436
|
+
"""
|
|
437
|
+
if not candidate or len(candidate.strip()) < MIN_SQL_LIKE_STRING_LENGTH:
|
|
438
|
+
return False
|
|
439
|
+
|
|
440
|
+
candidate_upper = candidate.strip().upper()
|
|
441
|
+
|
|
442
|
+
if expected_type:
|
|
443
|
+
return candidate_upper.startswith(expected_type.upper())
|
|
444
|
+
|
|
445
|
+
# More sophisticated check for SQL vs column names
|
|
446
|
+
# Column names that start with SQL keywords are common (user_id, insert_date, etc.)
|
|
447
|
+
if any(candidate_upper.startswith(starter) for starter in SQL_STARTERS):
|
|
448
|
+
# Additional checks to distinguish real SQL from column names:
|
|
449
|
+
# 1. Real SQL typically has spaces (SELECT ... FROM, INSERT INTO, etc.)
|
|
450
|
+
# 2. Check for common SQL syntax patterns
|
|
451
|
+
return " " in candidate
|
|
452
|
+
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
def _populate_insert_from_sql(self, builder: "Insert", sql_string: str) -> "Insert":
|
|
456
|
+
"""Parse SQL string and populate INSERT builder using SQLGlot directly."""
|
|
457
|
+
try:
|
|
458
|
+
# Use SQLGlot directly for parsing - no validation here
|
|
459
|
+
parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
|
|
460
|
+
|
|
461
|
+
if isinstance(parsed_expr, exp.Insert):
|
|
462
|
+
builder._expression = parsed_expr
|
|
463
|
+
return builder
|
|
464
|
+
|
|
465
|
+
if isinstance(parsed_expr, exp.Select):
|
|
466
|
+
# The actual conversion logic can be handled by the builder itself
|
|
467
|
+
logger.info("Detected SELECT statement for INSERT - may need target table specification")
|
|
468
|
+
return builder
|
|
469
|
+
|
|
470
|
+
# For other statement types, just return the builder as-is
|
|
471
|
+
logger.warning("Cannot create INSERT from %s statement", type(parsed_expr).__name__)
|
|
472
|
+
|
|
473
|
+
except Exception as e:
|
|
474
|
+
logger.warning("Failed to parse INSERT SQL, falling back to traditional mode: %s", e)
|
|
475
|
+
return builder
|
|
476
|
+
|
|
477
|
+
def _populate_select_from_sql(self, builder: "Select", sql_string: str) -> "Select":
|
|
478
|
+
"""Parse SQL string and populate SELECT builder using SQLGlot directly."""
|
|
479
|
+
try:
|
|
480
|
+
# Use SQLGlot directly for parsing - no validation here
|
|
481
|
+
parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
|
|
482
|
+
|
|
483
|
+
if isinstance(parsed_expr, exp.Select):
|
|
484
|
+
builder._expression = parsed_expr
|
|
485
|
+
return builder
|
|
486
|
+
|
|
487
|
+
logger.warning("Cannot create SELECT from %s statement", type(parsed_expr).__name__)
|
|
488
|
+
|
|
489
|
+
except Exception as e:
|
|
490
|
+
logger.warning("Failed to parse SELECT SQL, falling back to traditional mode: %s", e)
|
|
491
|
+
return builder
|
|
492
|
+
|
|
493
|
+
def _populate_update_from_sql(self, builder: "Update", sql_string: str) -> "Update":
|
|
494
|
+
"""Parse SQL string and populate UPDATE builder using SQLGlot directly."""
|
|
495
|
+
try:
|
|
496
|
+
# Use SQLGlot directly for parsing - no validation here
|
|
497
|
+
parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
|
|
498
|
+
|
|
499
|
+
if isinstance(parsed_expr, exp.Update):
|
|
500
|
+
builder._expression = parsed_expr
|
|
501
|
+
return builder
|
|
502
|
+
|
|
503
|
+
logger.warning("Cannot create UPDATE from %s statement", type(parsed_expr).__name__)
|
|
504
|
+
|
|
505
|
+
except Exception as e:
|
|
506
|
+
logger.warning("Failed to parse UPDATE SQL, falling back to traditional mode: %s", e)
|
|
507
|
+
return builder
|
|
508
|
+
|
|
509
|
+
def _populate_delete_from_sql(self, builder: "Delete", sql_string: str) -> "Delete":
|
|
510
|
+
"""Parse SQL string and populate DELETE builder using SQLGlot directly."""
|
|
511
|
+
try:
|
|
512
|
+
# Use SQLGlot directly for parsing - no validation here
|
|
513
|
+
parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
|
|
514
|
+
|
|
515
|
+
if isinstance(parsed_expr, exp.Delete):
|
|
516
|
+
builder._expression = parsed_expr
|
|
517
|
+
return builder
|
|
518
|
+
|
|
519
|
+
logger.warning("Cannot create DELETE from %s statement", type(parsed_expr).__name__)
|
|
520
|
+
|
|
521
|
+
except Exception as e:
|
|
522
|
+
logger.warning("Failed to parse DELETE SQL, falling back to traditional mode: %s", e)
|
|
523
|
+
return builder
|
|
524
|
+
|
|
525
|
+
def _populate_merge_from_sql(self, builder: "Merge", sql_string: str) -> "Merge":
|
|
526
|
+
"""Parse SQL string and populate MERGE builder using SQLGlot directly."""
|
|
527
|
+
try:
|
|
528
|
+
# Use SQLGlot directly for parsing - no validation here
|
|
529
|
+
parsed_expr = exp.maybe_parse(sql_string, dialect=self.dialect) # type: ignore[var-annotated]
|
|
530
|
+
|
|
531
|
+
if isinstance(parsed_expr, exp.Merge):
|
|
532
|
+
builder._expression = parsed_expr
|
|
533
|
+
return builder
|
|
534
|
+
|
|
535
|
+
logger.warning("Cannot create MERGE from %s statement", type(parsed_expr).__name__)
|
|
536
|
+
|
|
537
|
+
except Exception as e:
|
|
538
|
+
logger.warning("Failed to parse MERGE SQL, falling back to traditional mode: %s", e)
|
|
539
|
+
return builder
|
|
540
|
+
|
|
541
|
+
# ===================
|
|
542
|
+
# Column References
|
|
543
|
+
# ===================
|
|
544
|
+
|
|
545
|
+
def column(self, name: str, table: Optional[str] = None) -> Column:
|
|
546
|
+
"""Create a column reference.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
name: Column name.
|
|
550
|
+
table: Optional table name.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Column object that supports method chaining and operator overloading.
|
|
554
|
+
"""
|
|
555
|
+
return Column(name, table)
|
|
556
|
+
|
|
557
|
+
@property
|
|
558
|
+
def case_(self) -> "Case":
|
|
559
|
+
"""Create a CASE expression builder with improved syntax.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
Case builder instance for fluent CASE expression building.
|
|
563
|
+
|
|
564
|
+
Example:
|
|
565
|
+
```python
|
|
566
|
+
case_expr = (
|
|
567
|
+
sql.case_.when("x = 1", "one")
|
|
568
|
+
.when("x = 2", "two")
|
|
569
|
+
.else_("other")
|
|
570
|
+
.end()
|
|
571
|
+
)
|
|
572
|
+
aliased_case = (
|
|
573
|
+
sql.case_.when("status = 'active'", 1)
|
|
574
|
+
.else_(0)
|
|
575
|
+
.as_("is_active")
|
|
576
|
+
)
|
|
577
|
+
```
|
|
578
|
+
"""
|
|
579
|
+
return Case()
|
|
580
|
+
|
|
581
|
+
@property
|
|
582
|
+
def row_number_(self) -> "WindowFunctionBuilder":
|
|
583
|
+
"""Create a ROW_NUMBER() window function builder."""
|
|
584
|
+
return WindowFunctionBuilder("row_number")
|
|
585
|
+
|
|
586
|
+
@property
|
|
587
|
+
def rank_(self) -> "WindowFunctionBuilder":
|
|
588
|
+
"""Create a RANK() window function builder."""
|
|
589
|
+
return WindowFunctionBuilder("rank")
|
|
590
|
+
|
|
591
|
+
@property
|
|
592
|
+
def dense_rank_(self) -> "WindowFunctionBuilder":
|
|
593
|
+
"""Create a DENSE_RANK() window function builder."""
|
|
594
|
+
return WindowFunctionBuilder("dense_rank")
|
|
595
|
+
|
|
596
|
+
@property
|
|
597
|
+
def lag_(self) -> "WindowFunctionBuilder":
|
|
598
|
+
"""Create a LAG() window function builder."""
|
|
599
|
+
return WindowFunctionBuilder("lag")
|
|
600
|
+
|
|
601
|
+
@property
|
|
602
|
+
def lead_(self) -> "WindowFunctionBuilder":
|
|
603
|
+
"""Create a LEAD() window function builder."""
|
|
604
|
+
return WindowFunctionBuilder("lead")
|
|
605
|
+
|
|
606
|
+
@property
|
|
607
|
+
def exists_(self) -> "SubqueryBuilder":
|
|
608
|
+
"""Create an EXISTS subquery builder."""
|
|
609
|
+
return SubqueryBuilder("exists")
|
|
610
|
+
|
|
611
|
+
@property
|
|
612
|
+
def in_(self) -> "SubqueryBuilder":
|
|
613
|
+
"""Create an IN subquery builder."""
|
|
614
|
+
return SubqueryBuilder("in")
|
|
615
|
+
|
|
616
|
+
@property
|
|
617
|
+
def any_(self) -> "SubqueryBuilder":
|
|
618
|
+
"""Create an ANY subquery builder."""
|
|
619
|
+
return SubqueryBuilder("any")
|
|
620
|
+
|
|
621
|
+
@property
|
|
622
|
+
def all_(self) -> "SubqueryBuilder":
|
|
623
|
+
"""Create an ALL subquery builder."""
|
|
624
|
+
return SubqueryBuilder("all")
|
|
625
|
+
|
|
626
|
+
@property
|
|
627
|
+
def inner_join_(self) -> "JoinBuilder":
|
|
628
|
+
"""Create an INNER JOIN builder."""
|
|
629
|
+
return JoinBuilder("inner join")
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def left_join_(self) -> "JoinBuilder":
|
|
633
|
+
"""Create a LEFT JOIN builder."""
|
|
634
|
+
return JoinBuilder("left join")
|
|
635
|
+
|
|
636
|
+
@property
|
|
637
|
+
def right_join_(self) -> "JoinBuilder":
|
|
638
|
+
"""Create a RIGHT JOIN builder."""
|
|
639
|
+
return JoinBuilder("right join")
|
|
640
|
+
|
|
641
|
+
@property
|
|
642
|
+
def full_join_(self) -> "JoinBuilder":
|
|
643
|
+
"""Create a FULL OUTER JOIN builder."""
|
|
644
|
+
return JoinBuilder("full join")
|
|
645
|
+
|
|
646
|
+
@property
|
|
647
|
+
def cross_join_(self) -> "JoinBuilder":
|
|
648
|
+
"""Create a CROSS JOIN builder."""
|
|
649
|
+
return JoinBuilder("cross join")
|
|
650
|
+
|
|
651
|
+
def __getattr__(self, name: str) -> "Column":
|
|
652
|
+
"""Dynamically create column references.
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
name: Column name.
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
Column object for the given name.
|
|
659
|
+
|
|
660
|
+
Note:
|
|
661
|
+
Special SQL constructs like case_, row_number_, etc. are now
|
|
662
|
+
handled as properties for better type safety.
|
|
663
|
+
"""
|
|
664
|
+
return Column(name)
|
|
665
|
+
|
|
666
|
+
# ===================
|
|
667
|
+
# Raw SQL Expressions
|
|
668
|
+
# ===================
|
|
669
|
+
|
|
670
|
+
@staticmethod
|
|
671
|
+
def raw(sql_fragment: str, **parameters: Any) -> "Union[exp.Expression, SQL]":
|
|
672
|
+
"""Create a raw SQL expression from a string fragment with optional parameters.
|
|
673
|
+
|
|
674
|
+
This method makes it explicit that you are passing raw SQL that should
|
|
675
|
+
be parsed and included directly in the query. Useful for complex expressions,
|
|
676
|
+
database-specific functions, or when you need precise control over the SQL.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
sql_fragment: Raw SQL string to parse into an expression.
|
|
680
|
+
**parameters: Named parameters for parameter binding.
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
SQLGlot expression from the parsed SQL fragment (if no parameters).
|
|
684
|
+
SQL statement object (if parameters provided).
|
|
685
|
+
|
|
686
|
+
Raises:
|
|
687
|
+
SQLBuilderError: If the SQL fragment cannot be parsed.
|
|
688
|
+
|
|
689
|
+
Example:
|
|
690
|
+
```python
|
|
691
|
+
# Raw expression without parameters (current behavior)
|
|
692
|
+
expr = sql.raw("COALESCE(name, 'Unknown')")
|
|
693
|
+
|
|
694
|
+
# Raw SQL with named parameters (new functionality)
|
|
695
|
+
stmt = sql.raw(
|
|
696
|
+
"LOWER(name) LIKE LOWER(:pattern)", pattern=f"%{query}%"
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# Raw complex expression with parameters
|
|
700
|
+
expr = sql.raw(
|
|
701
|
+
"price BETWEEN :min_price AND :max_price",
|
|
702
|
+
min_price=100,
|
|
703
|
+
max_price=500,
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
# Raw window function
|
|
707
|
+
query = sql.select(
|
|
708
|
+
"name",
|
|
709
|
+
sql.raw(
|
|
710
|
+
"ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC)"
|
|
711
|
+
),
|
|
712
|
+
).from_("employees")
|
|
713
|
+
```
|
|
714
|
+
"""
|
|
715
|
+
if not parameters:
|
|
716
|
+
# Original behavior - return pure expression
|
|
717
|
+
try:
|
|
718
|
+
parsed: Optional[exp.Expression] = exp.maybe_parse(sql_fragment)
|
|
719
|
+
if parsed is not None:
|
|
720
|
+
return parsed
|
|
721
|
+
if sql_fragment.strip().replace("_", "").replace(".", "").isalnum():
|
|
722
|
+
return exp.to_identifier(sql_fragment)
|
|
723
|
+
return exp.Literal.string(sql_fragment)
|
|
724
|
+
except Exception as e:
|
|
725
|
+
msg = f"Failed to parse raw SQL fragment '{sql_fragment}': {e}"
|
|
726
|
+
raise SQLBuilderError(msg) from e
|
|
727
|
+
|
|
728
|
+
# New behavior - return SQL statement with parameters
|
|
729
|
+
from sqlspec.core.statement import SQL
|
|
730
|
+
|
|
731
|
+
return SQL(sql_fragment, parameters)
|
|
732
|
+
|
|
733
|
+
# ===================
|
|
734
|
+
# Aggregate Functions
|
|
735
|
+
# ===================
|
|
736
|
+
|
|
737
|
+
@staticmethod
|
|
738
|
+
def count(column: Union[str, exp.Expression] = "*", distinct: bool = False) -> exp.Expression:
|
|
739
|
+
"""Create a COUNT expression.
|
|
740
|
+
|
|
741
|
+
Args:
|
|
742
|
+
column: Column to count (default "*").
|
|
743
|
+
distinct: Whether to use COUNT DISTINCT.
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
COUNT expression.
|
|
747
|
+
"""
|
|
748
|
+
if column == "*":
|
|
749
|
+
return exp.Count(this=exp.Star(), distinct=distinct)
|
|
750
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
751
|
+
return exp.Count(this=col_expr, distinct=distinct)
|
|
752
|
+
|
|
753
|
+
def count_distinct(self, column: Union[str, exp.Expression]) -> exp.Expression:
|
|
754
|
+
"""Create a COUNT(DISTINCT column) expression.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
column: Column to count distinct values.
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
COUNT DISTINCT expression.
|
|
761
|
+
"""
|
|
762
|
+
return self.count(column, distinct=True)
|
|
763
|
+
|
|
764
|
+
@staticmethod
|
|
765
|
+
def sum(column: Union[str, exp.Expression], distinct: bool = False) -> exp.Expression:
|
|
766
|
+
"""Create a SUM expression.
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
column: Column to sum.
|
|
770
|
+
distinct: Whether to use SUM DISTINCT.
|
|
771
|
+
|
|
772
|
+
Returns:
|
|
773
|
+
SUM expression.
|
|
774
|
+
"""
|
|
775
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
776
|
+
return exp.Sum(this=col_expr, distinct=distinct)
|
|
777
|
+
|
|
778
|
+
@staticmethod
|
|
779
|
+
def avg(column: Union[str, exp.Expression]) -> exp.Expression:
|
|
780
|
+
"""Create an AVG expression.
|
|
781
|
+
|
|
782
|
+
Args:
|
|
783
|
+
column: Column to average.
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
AVG expression.
|
|
787
|
+
"""
|
|
788
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
789
|
+
return exp.Avg(this=col_expr)
|
|
790
|
+
|
|
791
|
+
@staticmethod
|
|
792
|
+
def max(column: Union[str, exp.Expression]) -> exp.Expression:
|
|
793
|
+
"""Create a MAX expression.
|
|
794
|
+
|
|
795
|
+
Args:
|
|
796
|
+
column: Column to find maximum.
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
MAX expression.
|
|
800
|
+
"""
|
|
801
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
802
|
+
return exp.Max(this=col_expr)
|
|
803
|
+
|
|
804
|
+
@staticmethod
|
|
805
|
+
def min(column: Union[str, exp.Expression]) -> exp.Expression:
|
|
806
|
+
"""Create a MIN expression.
|
|
807
|
+
|
|
808
|
+
Args:
|
|
809
|
+
column: Column to find minimum.
|
|
810
|
+
|
|
811
|
+
Returns:
|
|
812
|
+
MIN expression.
|
|
813
|
+
"""
|
|
814
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
815
|
+
return exp.Min(this=col_expr)
|
|
816
|
+
|
|
817
|
+
# ===================
|
|
818
|
+
# Advanced SQL Operations
|
|
819
|
+
# ===================
|
|
820
|
+
|
|
821
|
+
@staticmethod
|
|
822
|
+
def rollup(*columns: Union[str, exp.Expression]) -> exp.Expression:
|
|
823
|
+
"""Create a ROLLUP expression for GROUP BY clauses.
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
*columns: Columns to include in the rollup.
|
|
827
|
+
|
|
828
|
+
Returns:
|
|
829
|
+
ROLLUP expression.
|
|
830
|
+
|
|
831
|
+
Example:
|
|
832
|
+
```python
|
|
833
|
+
# GROUP BY ROLLUP(product, region)
|
|
834
|
+
query = (
|
|
835
|
+
sql.select("product", "region", sql.sum("sales"))
|
|
836
|
+
.from_("sales_data")
|
|
837
|
+
.group_by(sql.rollup("product", "region"))
|
|
838
|
+
)
|
|
839
|
+
```
|
|
840
|
+
"""
|
|
841
|
+
column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
|
|
842
|
+
return exp.Rollup(expressions=column_exprs)
|
|
843
|
+
|
|
844
|
+
@staticmethod
|
|
845
|
+
def cube(*columns: Union[str, exp.Expression]) -> exp.Expression:
|
|
846
|
+
"""Create a CUBE expression for GROUP BY clauses.
|
|
847
|
+
|
|
848
|
+
Args:
|
|
849
|
+
*columns: Columns to include in the cube.
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
CUBE expression.
|
|
853
|
+
|
|
854
|
+
Example:
|
|
855
|
+
```python
|
|
856
|
+
# GROUP BY CUBE(product, region)
|
|
857
|
+
query = (
|
|
858
|
+
sql.select("product", "region", sql.sum("sales"))
|
|
859
|
+
.from_("sales_data")
|
|
860
|
+
.group_by(sql.cube("product", "region"))
|
|
861
|
+
)
|
|
862
|
+
```
|
|
863
|
+
"""
|
|
864
|
+
column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
|
|
865
|
+
return exp.Cube(expressions=column_exprs)
|
|
866
|
+
|
|
867
|
+
@staticmethod
|
|
868
|
+
def grouping_sets(*column_sets: Union[tuple[str, ...], list[str]]) -> exp.Expression:
|
|
869
|
+
"""Create a GROUPING SETS expression for GROUP BY clauses.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
*column_sets: Sets of columns to group by.
|
|
873
|
+
|
|
874
|
+
Returns:
|
|
875
|
+
GROUPING SETS expression.
|
|
876
|
+
|
|
877
|
+
Example:
|
|
878
|
+
```python
|
|
879
|
+
# GROUP BY GROUPING SETS ((product), (region), ())
|
|
880
|
+
query = (
|
|
881
|
+
sql.select("product", "region", sql.sum("sales"))
|
|
882
|
+
.from_("sales_data")
|
|
883
|
+
.group_by(
|
|
884
|
+
sql.grouping_sets(("product",), ("region",), ())
|
|
885
|
+
)
|
|
886
|
+
)
|
|
887
|
+
```
|
|
888
|
+
"""
|
|
889
|
+
set_expressions = []
|
|
890
|
+
for column_set in column_sets:
|
|
891
|
+
if isinstance(column_set, (tuple, list)):
|
|
892
|
+
if len(column_set) == 0:
|
|
893
|
+
set_expressions.append(exp.Tuple(expressions=[]))
|
|
894
|
+
else:
|
|
895
|
+
columns = [exp.column(col) for col in column_set]
|
|
896
|
+
set_expressions.append(exp.Tuple(expressions=columns))
|
|
897
|
+
else:
|
|
898
|
+
set_expressions.append(exp.column(column_set))
|
|
899
|
+
|
|
900
|
+
return exp.GroupingSets(expressions=set_expressions)
|
|
901
|
+
|
|
902
|
+
@staticmethod
|
|
903
|
+
def any(values: Union[list[Any], exp.Expression, str]) -> exp.Expression:
|
|
904
|
+
"""Create an ANY expression for use with comparison operators.
|
|
905
|
+
|
|
906
|
+
Args:
|
|
907
|
+
values: Values, expression, or subquery for the ANY clause.
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
ANY expression.
|
|
911
|
+
|
|
912
|
+
Example:
|
|
913
|
+
```python
|
|
914
|
+
# WHERE id = ANY(subquery)
|
|
915
|
+
subquery = sql.select("user_id").from_("active_users")
|
|
916
|
+
query = (
|
|
917
|
+
sql.select("*")
|
|
918
|
+
.from_("users")
|
|
919
|
+
.where(sql.id.eq(sql.any(subquery)))
|
|
920
|
+
)
|
|
921
|
+
```
|
|
922
|
+
"""
|
|
923
|
+
if isinstance(values, list):
|
|
924
|
+
literals = [SQLFactory._to_literal(v) for v in values]
|
|
925
|
+
return exp.Any(this=exp.Array(expressions=literals))
|
|
926
|
+
if isinstance(values, str):
|
|
927
|
+
# Parse as SQL
|
|
928
|
+
parsed = exp.maybe_parse(values) # type: ignore[var-annotated]
|
|
929
|
+
if parsed:
|
|
930
|
+
return exp.Any(this=parsed)
|
|
931
|
+
return exp.Any(this=exp.Literal.string(values))
|
|
932
|
+
return exp.Any(this=values)
|
|
933
|
+
|
|
934
|
+
@staticmethod
|
|
935
|
+
def not_any_(values: Union[list[Any], exp.Expression, str]) -> exp.Expression:
|
|
936
|
+
"""Create a NOT ANY expression for use with comparison operators.
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
values: Values, expression, or subquery for the NOT ANY clause.
|
|
940
|
+
|
|
941
|
+
Returns:
|
|
942
|
+
NOT ANY expression.
|
|
943
|
+
|
|
944
|
+
Example:
|
|
945
|
+
```python
|
|
946
|
+
# WHERE id <> ANY(subquery)
|
|
947
|
+
subquery = sql.select("user_id").from_("blocked_users")
|
|
948
|
+
query = (
|
|
949
|
+
sql.select("*")
|
|
950
|
+
.from_("users")
|
|
951
|
+
.where(sql.id.neq(sql.not_any(subquery)))
|
|
952
|
+
)
|
|
953
|
+
```
|
|
954
|
+
"""
|
|
955
|
+
return SQLFactory.any(values) # NOT ANY is handled by the comparison operator
|
|
956
|
+
|
|
957
|
+
# ===================
|
|
958
|
+
# String Functions
|
|
959
|
+
# ===================
|
|
960
|
+
|
|
961
|
+
@staticmethod
|
|
962
|
+
def concat(*expressions: Union[str, exp.Expression]) -> exp.Expression:
|
|
963
|
+
"""Create a CONCAT expression.
|
|
964
|
+
|
|
965
|
+
Args:
|
|
966
|
+
*expressions: Expressions to concatenate.
|
|
967
|
+
|
|
968
|
+
Returns:
|
|
969
|
+
CONCAT expression.
|
|
970
|
+
"""
|
|
971
|
+
exprs = [exp.column(expr) if isinstance(expr, str) else expr for expr in expressions]
|
|
972
|
+
return exp.Concat(expressions=exprs)
|
|
973
|
+
|
|
974
|
+
@staticmethod
|
|
975
|
+
def upper(column: Union[str, exp.Expression]) -> exp.Expression:
|
|
976
|
+
"""Create an UPPER expression.
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
column: Column to convert to uppercase.
|
|
980
|
+
|
|
981
|
+
Returns:
|
|
982
|
+
UPPER expression.
|
|
983
|
+
"""
|
|
984
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
985
|
+
return exp.Upper(this=col_expr)
|
|
986
|
+
|
|
987
|
+
@staticmethod
|
|
988
|
+
def lower(column: Union[str, exp.Expression]) -> exp.Expression:
|
|
989
|
+
"""Create a LOWER expression.
|
|
990
|
+
|
|
991
|
+
Args:
|
|
992
|
+
column: Column to convert to lowercase.
|
|
993
|
+
|
|
994
|
+
Returns:
|
|
995
|
+
LOWER expression.
|
|
996
|
+
"""
|
|
997
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
998
|
+
return exp.Lower(this=col_expr)
|
|
999
|
+
|
|
1000
|
+
@staticmethod
|
|
1001
|
+
def length(column: Union[str, exp.Expression]) -> exp.Expression:
|
|
1002
|
+
"""Create a LENGTH expression.
|
|
1003
|
+
|
|
1004
|
+
Args:
|
|
1005
|
+
column: Column to get length of.
|
|
1006
|
+
|
|
1007
|
+
Returns:
|
|
1008
|
+
LENGTH expression.
|
|
1009
|
+
"""
|
|
1010
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
1011
|
+
return exp.Length(this=col_expr)
|
|
1012
|
+
|
|
1013
|
+
# ===================
|
|
1014
|
+
# Math Functions
|
|
1015
|
+
# ===================
|
|
1016
|
+
|
|
1017
|
+
@staticmethod
|
|
1018
|
+
def round(column: Union[str, exp.Expression], decimals: int = 0) -> exp.Expression:
|
|
1019
|
+
"""Create a ROUND expression.
|
|
1020
|
+
|
|
1021
|
+
Args:
|
|
1022
|
+
column: Column to round.
|
|
1023
|
+
decimals: Number of decimal places.
|
|
1024
|
+
|
|
1025
|
+
Returns:
|
|
1026
|
+
ROUND expression.
|
|
1027
|
+
"""
|
|
1028
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
1029
|
+
if decimals == 0:
|
|
1030
|
+
return exp.Round(this=col_expr)
|
|
1031
|
+
return exp.Round(this=col_expr, expression=exp.Literal.number(decimals))
|
|
1032
|
+
|
|
1033
|
+
# ===================
|
|
1034
|
+
# Conversion Functions
|
|
1035
|
+
# ===================
|
|
1036
|
+
|
|
1037
|
+
@staticmethod
|
|
1038
|
+
def _to_literal(value: Any) -> exp.Expression:
|
|
1039
|
+
"""Convert a Python value to a SQLGlot literal expression.
|
|
1040
|
+
|
|
1041
|
+
Uses SQLGlot's built-in exp.convert() function for optimal dialect-agnostic
|
|
1042
|
+
literal creation. Handles all Python primitive types correctly:
|
|
1043
|
+
- None -> exp.Null (renders as NULL)
|
|
1044
|
+
- bool -> exp.Boolean (renders as TRUE/FALSE or 1/0 based on dialect)
|
|
1045
|
+
- int/float -> exp.Literal with is_number=True
|
|
1046
|
+
- str -> exp.Literal with is_string=True
|
|
1047
|
+
- exp.Expression -> returned as-is (passthrough)
|
|
1048
|
+
|
|
1049
|
+
Args:
|
|
1050
|
+
value: Python value or SQLGlot expression to convert.
|
|
1051
|
+
|
|
1052
|
+
Returns:
|
|
1053
|
+
SQLGlot expression representing the literal value.
|
|
1054
|
+
"""
|
|
1055
|
+
if isinstance(value, exp.Expression):
|
|
1056
|
+
return value
|
|
1057
|
+
return exp.convert(value)
|
|
1058
|
+
|
|
1059
|
+
@staticmethod
|
|
1060
|
+
def decode(column: Union[str, exp.Expression], *args: Union[str, exp.Expression, Any]) -> exp.Expression:
|
|
1061
|
+
"""Create a DECODE expression (Oracle-style conditional logic).
|
|
1062
|
+
|
|
1063
|
+
DECODE compares column to each search value and returns the corresponding result.
|
|
1064
|
+
If no match is found, returns the default value (if provided) or NULL.
|
|
1065
|
+
|
|
1066
|
+
Args:
|
|
1067
|
+
column: Column to compare.
|
|
1068
|
+
*args: Alternating search values and results, with optional default at the end.
|
|
1069
|
+
Format: search1, result1, search2, result2, ..., [default]
|
|
1070
|
+
|
|
1071
|
+
Raises:
|
|
1072
|
+
ValueError: If fewer than two search/result pairs are provided.
|
|
1073
|
+
|
|
1074
|
+
Returns:
|
|
1075
|
+
CASE expression equivalent to DECODE.
|
|
1076
|
+
|
|
1077
|
+
Example:
|
|
1078
|
+
```python
|
|
1079
|
+
# DECODE(status, 'A', 'Active', 'I', 'Inactive', 'Unknown')
|
|
1080
|
+
sql.decode(
|
|
1081
|
+
"status", "A", "Active", "I", "Inactive", "Unknown"
|
|
1082
|
+
)
|
|
1083
|
+
```
|
|
1084
|
+
"""
|
|
1085
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
1086
|
+
|
|
1087
|
+
if len(args) < MIN_DECODE_ARGS:
|
|
1088
|
+
msg = "DECODE requires at least one search/result pair"
|
|
1089
|
+
raise ValueError(msg)
|
|
1090
|
+
|
|
1091
|
+
conditions = []
|
|
1092
|
+
default = None
|
|
1093
|
+
|
|
1094
|
+
for i in range(0, len(args) - 1, 2):
|
|
1095
|
+
if i + 1 >= len(args):
|
|
1096
|
+
# Odd number of args means last one is default
|
|
1097
|
+
default = SQLFactory._to_literal(args[i])
|
|
1098
|
+
break
|
|
1099
|
+
|
|
1100
|
+
search_val = args[i]
|
|
1101
|
+
result_val = args[i + 1]
|
|
1102
|
+
|
|
1103
|
+
search_expr = SQLFactory._to_literal(search_val)
|
|
1104
|
+
result_expr = SQLFactory._to_literal(result_val)
|
|
1105
|
+
|
|
1106
|
+
condition = exp.EQ(this=col_expr, expression=search_expr)
|
|
1107
|
+
conditions.append(exp.When(this=condition, then=result_expr))
|
|
1108
|
+
|
|
1109
|
+
return exp.Case(ifs=conditions, default=default)
|
|
1110
|
+
|
|
1111
|
+
@staticmethod
|
|
1112
|
+
def cast(column: Union[str, exp.Expression], data_type: str) -> exp.Expression:
|
|
1113
|
+
"""Create a CAST expression for type conversion.
|
|
1114
|
+
|
|
1115
|
+
Args:
|
|
1116
|
+
column: Column or expression to cast.
|
|
1117
|
+
data_type: Target data type (e.g., 'INT', 'VARCHAR(100)', 'DECIMAL(10,2)').
|
|
1118
|
+
|
|
1119
|
+
Returns:
|
|
1120
|
+
CAST expression.
|
|
1121
|
+
"""
|
|
1122
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
1123
|
+
return exp.Cast(this=col_expr, to=exp.DataType.build(data_type))
|
|
1124
|
+
|
|
1125
|
+
@staticmethod
|
|
1126
|
+
def coalesce(*expressions: Union[str, exp.Expression]) -> exp.Expression:
|
|
1127
|
+
"""Create a COALESCE expression.
|
|
1128
|
+
|
|
1129
|
+
Args:
|
|
1130
|
+
*expressions: Expressions to coalesce.
|
|
1131
|
+
|
|
1132
|
+
Returns:
|
|
1133
|
+
COALESCE expression.
|
|
1134
|
+
"""
|
|
1135
|
+
exprs = [exp.column(expr) if isinstance(expr, str) else expr for expr in expressions]
|
|
1136
|
+
return exp.Coalesce(expressions=exprs)
|
|
1137
|
+
|
|
1138
|
+
@staticmethod
|
|
1139
|
+
def nvl(column: Union[str, exp.Expression], substitute_value: Union[str, exp.Expression, Any]) -> exp.Expression:
|
|
1140
|
+
"""Create an NVL (Oracle-style) expression using COALESCE.
|
|
1141
|
+
|
|
1142
|
+
Args:
|
|
1143
|
+
column: Column to check for NULL.
|
|
1144
|
+
substitute_value: Value to use if column is NULL.
|
|
1145
|
+
|
|
1146
|
+
Returns:
|
|
1147
|
+
COALESCE expression equivalent to NVL.
|
|
1148
|
+
"""
|
|
1149
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
1150
|
+
sub_expr = SQLFactory._to_literal(substitute_value)
|
|
1151
|
+
return exp.Coalesce(expressions=[col_expr, sub_expr])
|
|
1152
|
+
|
|
1153
|
+
@staticmethod
|
|
1154
|
+
def nvl2(
|
|
1155
|
+
column: Union[str, exp.Expression],
|
|
1156
|
+
value_if_not_null: Union[str, exp.Expression, Any],
|
|
1157
|
+
value_if_null: Union[str, exp.Expression, Any],
|
|
1158
|
+
) -> exp.Expression:
|
|
1159
|
+
"""Create an NVL2 (Oracle-style) expression using CASE.
|
|
1160
|
+
|
|
1161
|
+
NVL2 returns value_if_not_null if column is not NULL,
|
|
1162
|
+
otherwise returns value_if_null.
|
|
1163
|
+
|
|
1164
|
+
Args:
|
|
1165
|
+
column: Column to check for NULL.
|
|
1166
|
+
value_if_not_null: Value to use if column is NOT NULL.
|
|
1167
|
+
value_if_null: Value to use if column is NULL.
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
CASE expression equivalent to NVL2.
|
|
1171
|
+
|
|
1172
|
+
Example:
|
|
1173
|
+
```python
|
|
1174
|
+
# NVL2(salary, 'Has Salary', 'No Salary')
|
|
1175
|
+
sql.nvl2("salary", "Has Salary", "No Salary")
|
|
1176
|
+
```
|
|
1177
|
+
"""
|
|
1178
|
+
col_expr = exp.column(column) if isinstance(column, str) else column
|
|
1179
|
+
not_null_expr = SQLFactory._to_literal(value_if_not_null)
|
|
1180
|
+
null_expr = SQLFactory._to_literal(value_if_null)
|
|
1181
|
+
|
|
1182
|
+
# Create CASE WHEN column IS NOT NULL THEN value_if_not_null ELSE value_if_null END
|
|
1183
|
+
is_null = exp.Is(this=col_expr, expression=exp.Null())
|
|
1184
|
+
condition = exp.Not(this=is_null)
|
|
1185
|
+
when_clause = exp.If(this=condition, true=not_null_expr)
|
|
1186
|
+
|
|
1187
|
+
return exp.Case(ifs=[when_clause], default=null_expr)
|
|
1188
|
+
|
|
1189
|
+
# ===================
|
|
1190
|
+
# Bulk Operations
|
|
1191
|
+
# ===================
|
|
1192
|
+
|
|
1193
|
+
@staticmethod
|
|
1194
|
+
def bulk_insert(table_name: str, column_count: int, placeholder_style: str = "?") -> exp.Expression:
|
|
1195
|
+
"""Create bulk INSERT expression for executemany operations.
|
|
1196
|
+
|
|
1197
|
+
This is specifically for bulk loading operations like CSV ingestion where
|
|
1198
|
+
we need an INSERT expression with placeholders for executemany().
|
|
1199
|
+
|
|
1200
|
+
Args:
|
|
1201
|
+
table_name: Name of the table to insert into
|
|
1202
|
+
column_count: Number of columns (for placeholder generation)
|
|
1203
|
+
placeholder_style: Placeholder style ("?" for SQLite/PostgreSQL, "%s" for MySQL, ":1" for Oracle)
|
|
1204
|
+
|
|
1205
|
+
Returns:
|
|
1206
|
+
INSERT expression with proper placeholders for bulk operations
|
|
1207
|
+
|
|
1208
|
+
Example:
|
|
1209
|
+
```python
|
|
1210
|
+
from sqlspec import sql
|
|
1211
|
+
|
|
1212
|
+
# SQLite/PostgreSQL style
|
|
1213
|
+
insert_expr = sql.bulk_insert("my_table", 3)
|
|
1214
|
+
# Creates: INSERT INTO "my_table" VALUES (?, ?, ?)
|
|
1215
|
+
|
|
1216
|
+
# MySQL style
|
|
1217
|
+
insert_expr = sql.bulk_insert(
|
|
1218
|
+
"my_table", 3, placeholder_style="%s"
|
|
1219
|
+
)
|
|
1220
|
+
# Creates: INSERT INTO "my_table" VALUES (%s, %s, %s)
|
|
1221
|
+
|
|
1222
|
+
# Oracle style
|
|
1223
|
+
insert_expr = sql.bulk_insert(
|
|
1224
|
+
"my_table", 3, placeholder_style=":1"
|
|
1225
|
+
)
|
|
1226
|
+
# Creates: INSERT INTO "my_table" VALUES (:1, :2, :3)
|
|
1227
|
+
```
|
|
1228
|
+
"""
|
|
1229
|
+
return exp.Insert(
|
|
1230
|
+
this=exp.Table(this=exp.to_identifier(table_name)),
|
|
1231
|
+
expression=exp.Values(
|
|
1232
|
+
expressions=[
|
|
1233
|
+
exp.Tuple(expressions=[exp.Placeholder(this=placeholder_style) for _ in range(column_count)])
|
|
1234
|
+
]
|
|
1235
|
+
),
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
def truncate(self, table_name: str) -> "Truncate":
|
|
1239
|
+
"""Create a TRUNCATE TABLE builder.
|
|
1240
|
+
|
|
1241
|
+
Args:
|
|
1242
|
+
table_name: Name of the table to truncate
|
|
1243
|
+
|
|
1244
|
+
Returns:
|
|
1245
|
+
TruncateTable builder instance
|
|
1246
|
+
|
|
1247
|
+
Example:
|
|
1248
|
+
```python
|
|
1249
|
+
from sqlspec import sql
|
|
1250
|
+
|
|
1251
|
+
# Simple truncate
|
|
1252
|
+
truncate_sql = sql.truncate_table("my_table").build().sql
|
|
1253
|
+
|
|
1254
|
+
# Truncate with options
|
|
1255
|
+
truncate_sql = (
|
|
1256
|
+
sql.truncate_table("my_table")
|
|
1257
|
+
.cascade()
|
|
1258
|
+
.restart_identity()
|
|
1259
|
+
.build()
|
|
1260
|
+
.sql
|
|
1261
|
+
)
|
|
1262
|
+
```
|
|
1263
|
+
"""
|
|
1264
|
+
builder = Truncate(dialect=self.dialect)
|
|
1265
|
+
builder._table_name = table_name
|
|
1266
|
+
return builder
|
|
1267
|
+
|
|
1268
|
+
# ===================
|
|
1269
|
+
# Case Expressions
|
|
1270
|
+
# ===================
|
|
1271
|
+
|
|
1272
|
+
@staticmethod
|
|
1273
|
+
def case() -> "Case":
|
|
1274
|
+
"""Create a CASE expression builder.
|
|
1275
|
+
|
|
1276
|
+
Returns:
|
|
1277
|
+
CaseExpressionBuilder for building CASE expressions.
|
|
1278
|
+
"""
|
|
1279
|
+
return Case()
|
|
1280
|
+
|
|
1281
|
+
# ===================
|
|
1282
|
+
# Window Functions
|
|
1283
|
+
# ===================
|
|
1284
|
+
|
|
1285
|
+
def row_number(
|
|
1286
|
+
self,
|
|
1287
|
+
partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
|
|
1288
|
+
order_by: Optional[Union[str, list[str], exp.Expression]] = None,
|
|
1289
|
+
) -> exp.Expression:
|
|
1290
|
+
"""Create a ROW_NUMBER() window function.
|
|
1291
|
+
|
|
1292
|
+
Args:
|
|
1293
|
+
partition_by: Columns to partition by.
|
|
1294
|
+
order_by: Columns to order by.
|
|
1295
|
+
|
|
1296
|
+
Returns:
|
|
1297
|
+
ROW_NUMBER window function expression.
|
|
1298
|
+
"""
|
|
1299
|
+
return self._create_window_function("ROW_NUMBER", [], partition_by, order_by)
|
|
1300
|
+
|
|
1301
|
+
def rank(
|
|
1302
|
+
self,
|
|
1303
|
+
partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
|
|
1304
|
+
order_by: Optional[Union[str, list[str], exp.Expression]] = None,
|
|
1305
|
+
) -> exp.Expression:
|
|
1306
|
+
"""Create a RANK() window function.
|
|
1307
|
+
|
|
1308
|
+
Args:
|
|
1309
|
+
partition_by: Columns to partition by.
|
|
1310
|
+
order_by: Columns to order by.
|
|
1311
|
+
|
|
1312
|
+
Returns:
|
|
1313
|
+
RANK window function expression.
|
|
1314
|
+
"""
|
|
1315
|
+
return self._create_window_function("RANK", [], partition_by, order_by)
|
|
1316
|
+
|
|
1317
|
+
def dense_rank(
|
|
1318
|
+
self,
|
|
1319
|
+
partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
|
|
1320
|
+
order_by: Optional[Union[str, list[str], exp.Expression]] = None,
|
|
1321
|
+
) -> exp.Expression:
|
|
1322
|
+
"""Create a DENSE_RANK() window function.
|
|
1323
|
+
|
|
1324
|
+
Args:
|
|
1325
|
+
partition_by: Columns to partition by.
|
|
1326
|
+
order_by: Columns to order by.
|
|
1327
|
+
|
|
1328
|
+
Returns:
|
|
1329
|
+
DENSE_RANK window function expression.
|
|
1330
|
+
"""
|
|
1331
|
+
return self._create_window_function("DENSE_RANK", [], partition_by, order_by)
|
|
1332
|
+
|
|
1333
|
+
@staticmethod
|
|
1334
|
+
def _create_window_function(
|
|
1335
|
+
func_name: str,
|
|
1336
|
+
func_args: list[exp.Expression],
|
|
1337
|
+
partition_by: Optional[Union[str, list[str], exp.Expression]] = None,
|
|
1338
|
+
order_by: Optional[Union[str, list[str], exp.Expression]] = None,
|
|
1339
|
+
) -> exp.Expression:
|
|
1340
|
+
"""Helper to create window function expressions.
|
|
1341
|
+
|
|
1342
|
+
Args:
|
|
1343
|
+
func_name: Name of the window function.
|
|
1344
|
+
func_args: Arguments to the function.
|
|
1345
|
+
partition_by: Columns to partition by.
|
|
1346
|
+
order_by: Columns to order by.
|
|
1347
|
+
|
|
1348
|
+
Returns:
|
|
1349
|
+
Window function expression.
|
|
1350
|
+
"""
|
|
1351
|
+
func_expr = exp.Anonymous(this=func_name, expressions=func_args)
|
|
1352
|
+
|
|
1353
|
+
over_args: dict[str, Any] = {}
|
|
1354
|
+
|
|
1355
|
+
if partition_by:
|
|
1356
|
+
if isinstance(partition_by, str):
|
|
1357
|
+
over_args["partition_by"] = [exp.column(partition_by)]
|
|
1358
|
+
elif isinstance(partition_by, list):
|
|
1359
|
+
over_args["partition_by"] = [exp.column(col) for col in partition_by]
|
|
1360
|
+
elif isinstance(partition_by, exp.Expression):
|
|
1361
|
+
over_args["partition_by"] = [partition_by]
|
|
1362
|
+
|
|
1363
|
+
if order_by:
|
|
1364
|
+
if isinstance(order_by, str):
|
|
1365
|
+
over_args["order"] = [exp.column(order_by).asc()]
|
|
1366
|
+
elif isinstance(order_by, list):
|
|
1367
|
+
over_args["order"] = [exp.column(col).asc() for col in order_by]
|
|
1368
|
+
elif isinstance(order_by, exp.Expression):
|
|
1369
|
+
over_args["order"] = [order_by]
|
|
1370
|
+
|
|
1371
|
+
return exp.Window(this=func_expr, **over_args)
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
@trait
|
|
1375
|
+
class Case:
|
|
1376
|
+
"""Builder for CASE expressions using the SQL factory.
|
|
1377
|
+
|
|
1378
|
+
Example:
|
|
1379
|
+
```python
|
|
1380
|
+
from sqlspec import sql
|
|
1381
|
+
|
|
1382
|
+
case_expr = (
|
|
1383
|
+
sql.case()
|
|
1384
|
+
.when(sql.age < 18, "Minor")
|
|
1385
|
+
.when(sql.age < 65, "Adult")
|
|
1386
|
+
.else_("Senior")
|
|
1387
|
+
.end()
|
|
1388
|
+
)
|
|
1389
|
+
```
|
|
1390
|
+
"""
|
|
1391
|
+
|
|
1392
|
+
def __init__(self) -> None:
|
|
1393
|
+
"""Initialize the CASE expression builder."""
|
|
1394
|
+
self._conditions: list[exp.If] = []
|
|
1395
|
+
self._default: Optional[exp.Expression] = None
|
|
1396
|
+
|
|
1397
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
1398
|
+
"""Equal to (==) - convert to expression then compare."""
|
|
1399
|
+
from sqlspec.builder._column import ColumnExpression
|
|
1400
|
+
|
|
1401
|
+
case_expr = exp.Case(ifs=self._conditions, default=self._default)
|
|
1402
|
+
if other is None:
|
|
1403
|
+
return ColumnExpression(exp.Is(this=case_expr, expression=exp.Null()))
|
|
1404
|
+
return ColumnExpression(exp.EQ(this=case_expr, expression=exp.convert(other)))
|
|
1405
|
+
|
|
1406
|
+
def __hash__(self) -> int:
|
|
1407
|
+
"""Make Case hashable."""
|
|
1408
|
+
return hash(id(self))
|
|
1409
|
+
|
|
1410
|
+
def when(self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]) -> "Case":
|
|
1411
|
+
"""Add a WHEN clause.
|
|
1412
|
+
|
|
1413
|
+
Args:
|
|
1414
|
+
condition: Condition to test.
|
|
1415
|
+
value: Value to return if condition is true.
|
|
1416
|
+
|
|
1417
|
+
Returns:
|
|
1418
|
+
Self for method chaining.
|
|
1419
|
+
"""
|
|
1420
|
+
cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
|
|
1421
|
+
val_expr = SQLFactory._to_literal(value)
|
|
1422
|
+
|
|
1423
|
+
# SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
|
|
1424
|
+
when_clause = exp.If(this=cond_expr, true=val_expr)
|
|
1425
|
+
self._conditions.append(when_clause)
|
|
1426
|
+
return self
|
|
1427
|
+
|
|
1428
|
+
def else_(self, value: Union[str, exp.Expression, Any]) -> "Case":
|
|
1429
|
+
"""Add an ELSE clause.
|
|
1430
|
+
|
|
1431
|
+
Args:
|
|
1432
|
+
value: Default value to return.
|
|
1433
|
+
|
|
1434
|
+
Returns:
|
|
1435
|
+
Self for method chaining.
|
|
1436
|
+
"""
|
|
1437
|
+
self._default = SQLFactory._to_literal(value)
|
|
1438
|
+
return self
|
|
1439
|
+
|
|
1440
|
+
def end(self) -> exp.Expression:
|
|
1441
|
+
"""Complete the CASE expression.
|
|
1442
|
+
|
|
1443
|
+
Returns:
|
|
1444
|
+
Complete CASE expression.
|
|
1445
|
+
"""
|
|
1446
|
+
return exp.Case(ifs=self._conditions, default=self._default)
|
|
1447
|
+
|
|
1448
|
+
def as_(self, alias: str) -> exp.Alias:
|
|
1449
|
+
"""Complete the CASE expression with an alias.
|
|
1450
|
+
|
|
1451
|
+
Args:
|
|
1452
|
+
alias: Alias name for the CASE expression.
|
|
1453
|
+
|
|
1454
|
+
Returns:
|
|
1455
|
+
Aliased CASE expression.
|
|
1456
|
+
"""
|
|
1457
|
+
case_expr = exp.Case(ifs=self._conditions, default=self._default)
|
|
1458
|
+
return cast("exp.Alias", exp.alias_(case_expr, alias))
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
@trait
|
|
1462
|
+
class WindowFunctionBuilder:
|
|
1463
|
+
"""Builder for window functions with fluent syntax.
|
|
1464
|
+
|
|
1465
|
+
Example:
|
|
1466
|
+
```python
|
|
1467
|
+
from sqlspec import sql
|
|
1468
|
+
|
|
1469
|
+
# sql.row_number_.partition_by("department").order_by("salary")
|
|
1470
|
+
window_func = (
|
|
1471
|
+
sql.row_number_.partition_by("department")
|
|
1472
|
+
.order_by("salary")
|
|
1473
|
+
.as_("row_num")
|
|
1474
|
+
)
|
|
1475
|
+
```
|
|
1476
|
+
"""
|
|
1477
|
+
|
|
1478
|
+
def __init__(self, function_name: str) -> None:
|
|
1479
|
+
"""Initialize the window function builder.
|
|
1480
|
+
|
|
1481
|
+
Args:
|
|
1482
|
+
function_name: Name of the window function (row_number, rank, etc.)
|
|
1483
|
+
"""
|
|
1484
|
+
self._function_name = function_name
|
|
1485
|
+
self._partition_by_cols: list[exp.Expression] = []
|
|
1486
|
+
self._order_by_cols: list[exp.Expression] = []
|
|
1487
|
+
self._alias: Optional[str] = None
|
|
1488
|
+
|
|
1489
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
1490
|
+
"""Equal to (==) - convert to expression then compare."""
|
|
1491
|
+
from sqlspec.builder._column import ColumnExpression
|
|
1492
|
+
|
|
1493
|
+
window_expr = self._build_expression()
|
|
1494
|
+
if other is None:
|
|
1495
|
+
return ColumnExpression(exp.Is(this=window_expr, expression=exp.Null()))
|
|
1496
|
+
return ColumnExpression(exp.EQ(this=window_expr, expression=exp.convert(other)))
|
|
1497
|
+
|
|
1498
|
+
def __hash__(self) -> int:
|
|
1499
|
+
"""Make WindowFunctionBuilder hashable."""
|
|
1500
|
+
return hash(id(self))
|
|
1501
|
+
|
|
1502
|
+
def partition_by(self, *columns: Union[str, exp.Expression]) -> "WindowFunctionBuilder":
|
|
1503
|
+
"""Add PARTITION BY clause.
|
|
1504
|
+
|
|
1505
|
+
Args:
|
|
1506
|
+
*columns: Columns to partition by.
|
|
1507
|
+
|
|
1508
|
+
Returns:
|
|
1509
|
+
Self for method chaining.
|
|
1510
|
+
"""
|
|
1511
|
+
for col in columns:
|
|
1512
|
+
col_expr = exp.column(col) if isinstance(col, str) else col
|
|
1513
|
+
self._partition_by_cols.append(col_expr)
|
|
1514
|
+
return self
|
|
1515
|
+
|
|
1516
|
+
def order_by(self, *columns: Union[str, exp.Expression]) -> "WindowFunctionBuilder":
|
|
1517
|
+
"""Add ORDER BY clause.
|
|
1518
|
+
|
|
1519
|
+
Args:
|
|
1520
|
+
*columns: Columns to order by.
|
|
1521
|
+
|
|
1522
|
+
Returns:
|
|
1523
|
+
Self for method chaining.
|
|
1524
|
+
"""
|
|
1525
|
+
for col in columns:
|
|
1526
|
+
if isinstance(col, str):
|
|
1527
|
+
col_expr = exp.column(col).asc()
|
|
1528
|
+
self._order_by_cols.append(col_expr)
|
|
1529
|
+
else:
|
|
1530
|
+
# Convert to ordered expression
|
|
1531
|
+
self._order_by_cols.append(exp.Ordered(this=col, desc=False))
|
|
1532
|
+
return self
|
|
1533
|
+
|
|
1534
|
+
def as_(self, alias: str) -> exp.Expression:
|
|
1535
|
+
"""Complete the window function with an alias.
|
|
1536
|
+
|
|
1537
|
+
Args:
|
|
1538
|
+
alias: Alias name for the window function.
|
|
1539
|
+
|
|
1540
|
+
Returns:
|
|
1541
|
+
Aliased window function expression.
|
|
1542
|
+
"""
|
|
1543
|
+
window_expr = self._build_expression()
|
|
1544
|
+
return cast("exp.Alias", exp.alias_(window_expr, alias))
|
|
1545
|
+
|
|
1546
|
+
def build(self) -> exp.Expression:
|
|
1547
|
+
"""Complete the window function without an alias.
|
|
1548
|
+
|
|
1549
|
+
Returns:
|
|
1550
|
+
Window function expression.
|
|
1551
|
+
"""
|
|
1552
|
+
return self._build_expression()
|
|
1553
|
+
|
|
1554
|
+
def _build_expression(self) -> exp.Expression:
|
|
1555
|
+
"""Build the complete window function expression."""
|
|
1556
|
+
# Create the function expression
|
|
1557
|
+
func_expr = exp.Anonymous(this=self._function_name.upper(), expressions=[])
|
|
1558
|
+
|
|
1559
|
+
# Build the OVER clause arguments
|
|
1560
|
+
over_args: dict[str, Any] = {}
|
|
1561
|
+
|
|
1562
|
+
if self._partition_by_cols:
|
|
1563
|
+
over_args["partition_by"] = self._partition_by_cols
|
|
1564
|
+
|
|
1565
|
+
if self._order_by_cols:
|
|
1566
|
+
over_args["order"] = exp.Order(expressions=self._order_by_cols)
|
|
1567
|
+
|
|
1568
|
+
return exp.Window(this=func_expr, **over_args)
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
@trait
|
|
1572
|
+
class SubqueryBuilder:
|
|
1573
|
+
"""Builder for subquery operations with fluent syntax.
|
|
1574
|
+
|
|
1575
|
+
Example:
|
|
1576
|
+
```python
|
|
1577
|
+
from sqlspec import sql
|
|
1578
|
+
|
|
1579
|
+
# sql.exists_(subquery)
|
|
1580
|
+
exists_check = sql.exists_(
|
|
1581
|
+
sql.select("1")
|
|
1582
|
+
.from_("orders")
|
|
1583
|
+
.where_eq("user_id", sql.users.id)
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
# sql.in_(subquery)
|
|
1587
|
+
in_check = sql.in_(
|
|
1588
|
+
sql.select("category_id")
|
|
1589
|
+
.from_("categories")
|
|
1590
|
+
.where_eq("active", True)
|
|
1591
|
+
)
|
|
1592
|
+
```
|
|
1593
|
+
"""
|
|
1594
|
+
|
|
1595
|
+
def __init__(self, operation: str) -> None:
|
|
1596
|
+
"""Initialize the subquery builder.
|
|
1597
|
+
|
|
1598
|
+
Args:
|
|
1599
|
+
operation: Type of subquery operation (exists, in, any, all)
|
|
1600
|
+
"""
|
|
1601
|
+
self._operation = operation
|
|
1602
|
+
|
|
1603
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
1604
|
+
"""Equal to (==) - not typically used but needed for type consistency."""
|
|
1605
|
+
from sqlspec.builder._column import ColumnExpression
|
|
1606
|
+
|
|
1607
|
+
# SubqueryBuilder doesn't have a direct expression, so this is a placeholder
|
|
1608
|
+
# In practice, this shouldn't be called as subqueries are used differently
|
|
1609
|
+
placeholder_expr = exp.Literal.string(f"subquery_{self._operation}")
|
|
1610
|
+
if other is None:
|
|
1611
|
+
return ColumnExpression(exp.Is(this=placeholder_expr, expression=exp.Null()))
|
|
1612
|
+
return ColumnExpression(exp.EQ(this=placeholder_expr, expression=exp.convert(other)))
|
|
1613
|
+
|
|
1614
|
+
def __hash__(self) -> int:
|
|
1615
|
+
"""Make SubqueryBuilder hashable."""
|
|
1616
|
+
return hash(id(self))
|
|
1617
|
+
|
|
1618
|
+
def __call__(self, subquery: Union[str, exp.Expression, Any]) -> exp.Expression:
|
|
1619
|
+
"""Build the subquery expression.
|
|
1620
|
+
|
|
1621
|
+
Args:
|
|
1622
|
+
subquery: The subquery - can be a SQL string, SelectBuilder, or expression
|
|
1623
|
+
|
|
1624
|
+
Returns:
|
|
1625
|
+
The subquery expression (EXISTS, IN, ANY, ALL, etc.)
|
|
1626
|
+
"""
|
|
1627
|
+
subquery_expr: exp.Expression
|
|
1628
|
+
if isinstance(subquery, str):
|
|
1629
|
+
# Parse as SQL
|
|
1630
|
+
parsed: Optional[exp.Expression] = exp.maybe_parse(subquery)
|
|
1631
|
+
if not parsed:
|
|
1632
|
+
msg = f"Could not parse subquery SQL: {subquery}"
|
|
1633
|
+
raise SQLBuilderError(msg)
|
|
1634
|
+
subquery_expr = parsed
|
|
1635
|
+
elif hasattr(subquery, "build") and callable(getattr(subquery, "build", None)):
|
|
1636
|
+
# It's a query builder - build it to get the SQL and parse
|
|
1637
|
+
built_query = subquery.build() # pyright: ignore[reportAttributeAccessIssue]
|
|
1638
|
+
subquery_expr = exp.maybe_parse(built_query.sql)
|
|
1639
|
+
if not subquery_expr:
|
|
1640
|
+
msg = f"Could not parse built query: {built_query.sql}"
|
|
1641
|
+
raise SQLBuilderError(msg)
|
|
1642
|
+
elif isinstance(subquery, exp.Expression):
|
|
1643
|
+
subquery_expr = subquery
|
|
1644
|
+
else:
|
|
1645
|
+
# Try to convert to expression
|
|
1646
|
+
parsed = exp.maybe_parse(str(subquery))
|
|
1647
|
+
if not parsed:
|
|
1648
|
+
msg = f"Could not convert subquery to expression: {subquery}"
|
|
1649
|
+
raise SQLBuilderError(msg)
|
|
1650
|
+
subquery_expr = parsed
|
|
1651
|
+
|
|
1652
|
+
# Build the appropriate expression based on operation
|
|
1653
|
+
if self._operation == "exists":
|
|
1654
|
+
return exp.Exists(this=subquery_expr)
|
|
1655
|
+
if self._operation == "in":
|
|
1656
|
+
# For IN, we create a subquery that can be used with WHERE column IN (subquery)
|
|
1657
|
+
return exp.In(expressions=[subquery_expr])
|
|
1658
|
+
if self._operation == "any":
|
|
1659
|
+
return exp.Any(this=subquery_expr)
|
|
1660
|
+
if self._operation == "all":
|
|
1661
|
+
return exp.All(this=subquery_expr)
|
|
1662
|
+
msg = f"Unknown subquery operation: {self._operation}"
|
|
1663
|
+
raise SQLBuilderError(msg)
|
|
1664
|
+
|
|
1665
|
+
|
|
1666
|
+
@trait
|
|
1667
|
+
class JoinBuilder:
|
|
1668
|
+
"""Builder for JOIN operations with fluent syntax.
|
|
1669
|
+
|
|
1670
|
+
Example:
|
|
1671
|
+
```python
|
|
1672
|
+
from sqlspec import sql
|
|
1673
|
+
|
|
1674
|
+
# sql.left_join_("posts").on("users.id = posts.user_id")
|
|
1675
|
+
join_clause = sql.left_join_("posts").on(
|
|
1676
|
+
"users.id = posts.user_id"
|
|
1677
|
+
)
|
|
1678
|
+
|
|
1679
|
+
# Or with query builder
|
|
1680
|
+
query = (
|
|
1681
|
+
sql.select("users.name", "posts.title")
|
|
1682
|
+
.from_("users")
|
|
1683
|
+
.join(
|
|
1684
|
+
sql.left_join_("posts").on(
|
|
1685
|
+
"users.id = posts.user_id"
|
|
1686
|
+
)
|
|
1687
|
+
)
|
|
1688
|
+
)
|
|
1689
|
+
```
|
|
1690
|
+
"""
|
|
1691
|
+
|
|
1692
|
+
def __init__(self, join_type: str) -> None:
|
|
1693
|
+
"""Initialize the join builder.
|
|
1694
|
+
|
|
1695
|
+
Args:
|
|
1696
|
+
join_type: Type of join (inner, left, right, full, cross)
|
|
1697
|
+
"""
|
|
1698
|
+
self._join_type = join_type.upper()
|
|
1699
|
+
self._table: Optional[Union[str, exp.Expression]] = None
|
|
1700
|
+
self._condition: Optional[exp.Expression] = None
|
|
1701
|
+
self._alias: Optional[str] = None
|
|
1702
|
+
|
|
1703
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
1704
|
+
"""Equal to (==) - not typically used but needed for type consistency."""
|
|
1705
|
+
from sqlspec.builder._column import ColumnExpression
|
|
1706
|
+
|
|
1707
|
+
# JoinBuilder doesn't have a direct expression, so this is a placeholder
|
|
1708
|
+
# In practice, this shouldn't be called as joins are used differently
|
|
1709
|
+
placeholder_expr = exp.Literal.string(f"join_{self._join_type.lower()}")
|
|
1710
|
+
if other is None:
|
|
1711
|
+
return ColumnExpression(exp.Is(this=placeholder_expr, expression=exp.Null()))
|
|
1712
|
+
return ColumnExpression(exp.EQ(this=placeholder_expr, expression=exp.convert(other)))
|
|
1713
|
+
|
|
1714
|
+
def __hash__(self) -> int:
|
|
1715
|
+
"""Make JoinBuilder hashable."""
|
|
1716
|
+
return hash(id(self))
|
|
1717
|
+
|
|
1718
|
+
def __call__(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> "JoinBuilder":
|
|
1719
|
+
"""Set the table to join.
|
|
1720
|
+
|
|
1721
|
+
Args:
|
|
1722
|
+
table: Table name or expression to join
|
|
1723
|
+
alias: Optional alias for the table
|
|
1724
|
+
|
|
1725
|
+
Returns:
|
|
1726
|
+
Self for method chaining
|
|
1727
|
+
"""
|
|
1728
|
+
self._table = table
|
|
1729
|
+
self._alias = alias
|
|
1730
|
+
return self
|
|
1731
|
+
|
|
1732
|
+
def on(self, condition: Union[str, exp.Expression]) -> exp.Expression:
|
|
1733
|
+
"""Set the join condition and build the JOIN expression.
|
|
1734
|
+
|
|
1735
|
+
Args:
|
|
1736
|
+
condition: JOIN condition (e.g., "users.id = posts.user_id")
|
|
1737
|
+
|
|
1738
|
+
Returns:
|
|
1739
|
+
Complete JOIN expression
|
|
1740
|
+
"""
|
|
1741
|
+
if not self._table:
|
|
1742
|
+
msg = "Table must be set before calling .on()"
|
|
1743
|
+
raise SQLBuilderError(msg)
|
|
1744
|
+
|
|
1745
|
+
# Parse the condition
|
|
1746
|
+
condition_expr: exp.Expression
|
|
1747
|
+
if isinstance(condition, str):
|
|
1748
|
+
parsed: Optional[exp.Expression] = exp.maybe_parse(condition)
|
|
1749
|
+
condition_expr = parsed or exp.condition(condition)
|
|
1750
|
+
else:
|
|
1751
|
+
condition_expr = condition
|
|
1752
|
+
|
|
1753
|
+
# Build table expression
|
|
1754
|
+
table_expr: exp.Expression
|
|
1755
|
+
if isinstance(self._table, str):
|
|
1756
|
+
table_expr = exp.to_table(self._table)
|
|
1757
|
+
if self._alias:
|
|
1758
|
+
table_expr = cast("exp.Expression", exp.alias_(table_expr, self._alias))
|
|
1759
|
+
else:
|
|
1760
|
+
table_expr = self._table
|
|
1761
|
+
if self._alias:
|
|
1762
|
+
table_expr = cast("exp.Expression", exp.alias_(table_expr, self._alias))
|
|
1763
|
+
|
|
1764
|
+
# Create the appropriate join type using same pattern as existing JoinClauseMixin
|
|
1765
|
+
if self._join_type == "INNER JOIN":
|
|
1766
|
+
return exp.Join(this=table_expr, on=condition_expr)
|
|
1767
|
+
if self._join_type == "LEFT JOIN":
|
|
1768
|
+
return exp.Join(this=table_expr, on=condition_expr, side="LEFT")
|
|
1769
|
+
if self._join_type == "RIGHT JOIN":
|
|
1770
|
+
return exp.Join(this=table_expr, on=condition_expr, side="RIGHT")
|
|
1771
|
+
if self._join_type == "FULL JOIN":
|
|
1772
|
+
return exp.Join(this=table_expr, on=condition_expr, side="FULL", kind="OUTER")
|
|
1773
|
+
if self._join_type == "CROSS JOIN":
|
|
1774
|
+
# CROSS JOIN doesn't use ON condition
|
|
1775
|
+
return exp.Join(this=table_expr, kind="CROSS")
|
|
1776
|
+
return exp.Join(this=table_expr, on=condition_expr)
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
# Create a default SQL factory instance
|
|
1780
|
+
sql = SQLFactory()
|