sqlspec 0.15.0__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/_sql.py +702 -44
- sqlspec/builder/_base.py +77 -44
- sqlspec/builder/_column.py +0 -4
- sqlspec/builder/_ddl.py +15 -52
- sqlspec/builder/_ddl_utils.py +0 -1
- sqlspec/builder/_delete.py +4 -5
- sqlspec/builder/_insert.py +235 -44
- sqlspec/builder/_merge.py +17 -2
- sqlspec/builder/_parsing_utils.py +42 -14
- sqlspec/builder/_select.py +29 -33
- sqlspec/builder/_update.py +4 -2
- sqlspec/builder/mixins/_cte_and_set_ops.py +47 -20
- sqlspec/builder/mixins/_delete_operations.py +6 -1
- sqlspec/builder/mixins/_insert_operations.py +126 -24
- sqlspec/builder/mixins/_join_operations.py +44 -10
- sqlspec/builder/mixins/_merge_operations.py +183 -25
- sqlspec/builder/mixins/_order_limit_operations.py +15 -3
- sqlspec/builder/mixins/_pivot_operations.py +11 -2
- sqlspec/builder/mixins/_select_operations.py +21 -14
- sqlspec/builder/mixins/_update_operations.py +80 -32
- sqlspec/builder/mixins/_where_clause.py +201 -66
- sqlspec/core/cache.py +26 -28
- sqlspec/core/compiler.py +58 -37
- sqlspec/core/filters.py +12 -10
- sqlspec/core/parameters.py +80 -52
- sqlspec/core/result.py +30 -17
- sqlspec/core/statement.py +47 -22
- sqlspec/driver/_async.py +76 -46
- sqlspec/driver/_common.py +25 -6
- sqlspec/driver/_sync.py +73 -43
- sqlspec/driver/mixins/_result_tools.py +62 -37
- sqlspec/driver/mixins/_sql_translator.py +61 -11
- sqlspec/extensions/litestar/cli.py +1 -1
- sqlspec/extensions/litestar/plugin.py +2 -2
- sqlspec/protocols.py +7 -0
- sqlspec/utils/sync_tools.py +1 -1
- sqlspec/utils/type_guards.py +7 -3
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/METADATA +1 -1
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/RECORD +43 -43
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
sqlspec/_sql.py
CHANGED
|
@@ -4,17 +4,68 @@ Provides both statement builders (select, insert, update, etc.) and column expre
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
|
-
from typing import Any, Optional, Union
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
8
8
|
|
|
9
9
|
import sqlglot
|
|
10
|
+
from mypy_extensions import trait
|
|
10
11
|
from sqlglot import exp
|
|
11
12
|
from sqlglot.dialects.dialect import DialectType
|
|
12
13
|
from sqlglot.errors import ParseError as SQLGlotParseError
|
|
13
14
|
|
|
14
|
-
from sqlspec.builder import
|
|
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
38
|
|
|
17
|
-
|
|
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
|
+
)
|
|
18
69
|
|
|
19
70
|
logger = logging.getLogger("sqlspec")
|
|
20
71
|
|
|
@@ -127,7 +178,9 @@ class SQLFactory:
|
|
|
127
178
|
# ===================
|
|
128
179
|
# Statement Builders
|
|
129
180
|
# ===================
|
|
130
|
-
def select(
|
|
181
|
+
def select(
|
|
182
|
+
self, *columns_or_sql: Union[str, exp.Expression, Column, "SQL"], dialect: DialectType = None
|
|
183
|
+
) -> "Select":
|
|
131
184
|
builder_dialect = dialect or self.dialect
|
|
132
185
|
if len(columns_or_sql) == 1 and isinstance(columns_or_sql[0], str):
|
|
133
186
|
sql_candidate = columns_or_sql[0].strip()
|
|
@@ -140,12 +193,8 @@ class SQLFactory:
|
|
|
140
193
|
)
|
|
141
194
|
raise SQLBuilderError(msg)
|
|
142
195
|
select_builder = Select(dialect=builder_dialect)
|
|
143
|
-
if select_builder._expression is None:
|
|
144
|
-
select_builder.__post_init__()
|
|
145
196
|
return self._populate_select_from_sql(select_builder, sql_candidate)
|
|
146
197
|
select_builder = Select(dialect=builder_dialect)
|
|
147
|
-
if select_builder._expression is None:
|
|
148
|
-
select_builder.__post_init__()
|
|
149
198
|
if columns_or_sql:
|
|
150
199
|
select_builder.select(*columns_or_sql)
|
|
151
200
|
return select_builder
|
|
@@ -153,8 +202,6 @@ class SQLFactory:
|
|
|
153
202
|
def insert(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Insert":
|
|
154
203
|
builder_dialect = dialect or self.dialect
|
|
155
204
|
builder = Insert(dialect=builder_dialect)
|
|
156
|
-
if builder._expression is None:
|
|
157
|
-
builder.__post_init__()
|
|
158
205
|
if table_or_sql:
|
|
159
206
|
if self._looks_like_sql(table_or_sql):
|
|
160
207
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
@@ -172,8 +219,6 @@ class SQLFactory:
|
|
|
172
219
|
def update(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Update":
|
|
173
220
|
builder_dialect = dialect or self.dialect
|
|
174
221
|
builder = Update(dialect=builder_dialect)
|
|
175
|
-
if builder._expression is None:
|
|
176
|
-
builder.__post_init__()
|
|
177
222
|
if table_or_sql:
|
|
178
223
|
if self._looks_like_sql(table_or_sql):
|
|
179
224
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
@@ -187,8 +232,6 @@ class SQLFactory:
|
|
|
187
232
|
def delete(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Delete":
|
|
188
233
|
builder_dialect = dialect or self.dialect
|
|
189
234
|
builder = Delete(dialect=builder_dialect)
|
|
190
|
-
if builder._expression is None:
|
|
191
|
-
builder.__post_init__()
|
|
192
235
|
if table_or_sql and self._looks_like_sql(table_or_sql):
|
|
193
236
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
194
237
|
if detected != "DELETE":
|
|
@@ -200,8 +243,6 @@ class SQLFactory:
|
|
|
200
243
|
def merge(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Merge":
|
|
201
244
|
builder_dialect = dialect or self.dialect
|
|
202
245
|
builder = Merge(dialect=builder_dialect)
|
|
203
|
-
if builder._expression is None:
|
|
204
|
-
builder.__post_init__()
|
|
205
246
|
if table_or_sql:
|
|
206
247
|
if self._looks_like_sql(table_or_sql):
|
|
207
248
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
@@ -212,6 +253,174 @@ class SQLFactory:
|
|
|
212
253
|
return builder.into(table_or_sql)
|
|
213
254
|
return builder
|
|
214
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
|
+
|
|
215
424
|
# ===================
|
|
216
425
|
# SQL Analysis Helpers
|
|
217
426
|
# ===================
|
|
@@ -347,14 +556,112 @@ class SQLFactory:
|
|
|
347
556
|
"""
|
|
348
557
|
return Column(name, table)
|
|
349
558
|
|
|
350
|
-
|
|
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":
|
|
351
654
|
"""Dynamically create column references.
|
|
352
655
|
|
|
353
656
|
Args:
|
|
354
657
|
name: Column name.
|
|
355
658
|
|
|
356
659
|
Returns:
|
|
357
|
-
Column object
|
|
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.
|
|
358
665
|
"""
|
|
359
666
|
return Column(name)
|
|
360
667
|
|
|
@@ -363,8 +670,8 @@ class SQLFactory:
|
|
|
363
670
|
# ===================
|
|
364
671
|
|
|
365
672
|
@staticmethod
|
|
366
|
-
def raw(sql_fragment: str) -> exp.Expression:
|
|
367
|
-
"""Create a raw SQL expression from a string fragment.
|
|
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.
|
|
368
675
|
|
|
369
676
|
This method makes it explicit that you are passing raw SQL that should
|
|
370
677
|
be parsed and included directly in the query. Useful for complex expressions,
|
|
@@ -372,30 +679,30 @@ class SQLFactory:
|
|
|
372
679
|
|
|
373
680
|
Args:
|
|
374
681
|
sql_fragment: Raw SQL string to parse into an expression.
|
|
682
|
+
**parameters: Named parameters for parameter binding.
|
|
375
683
|
|
|
376
684
|
Returns:
|
|
377
|
-
SQLGlot expression from the parsed SQL fragment.
|
|
685
|
+
SQLGlot expression from the parsed SQL fragment (if no parameters).
|
|
686
|
+
SQL statement object (if parameters provided).
|
|
378
687
|
|
|
379
688
|
Raises:
|
|
380
689
|
SQLBuilderError: If the SQL fragment cannot be parsed.
|
|
381
690
|
|
|
382
691
|
Example:
|
|
383
692
|
```python
|
|
384
|
-
# Raw
|
|
385
|
-
|
|
386
|
-
sql.raw("user.id AS u_id"), "name"
|
|
387
|
-
).from_("users")
|
|
693
|
+
# Raw expression without parameters (current behavior)
|
|
694
|
+
expr = sql.raw("COALESCE(name, 'Unknown')")
|
|
388
695
|
|
|
389
|
-
# Raw
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
)
|
|
696
|
+
# Raw SQL with named parameters (new functionality)
|
|
697
|
+
stmt = sql.raw(
|
|
698
|
+
"LOWER(name) LIKE LOWER(:pattern)", pattern=f"%{query}%"
|
|
699
|
+
)
|
|
393
700
|
|
|
394
|
-
# Raw complex expression
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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,
|
|
399
706
|
)
|
|
400
707
|
|
|
401
708
|
# Raw window function
|
|
@@ -407,16 +714,23 @@ class SQLFactory:
|
|
|
407
714
|
).from_("employees")
|
|
408
715
|
```
|
|
409
716
|
"""
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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)
|
|
420
734
|
|
|
421
735
|
# ===================
|
|
422
736
|
# Aggregate Functions
|
|
@@ -1059,6 +1373,7 @@ class SQLFactory:
|
|
|
1059
1373
|
return exp.Window(this=func_expr, **over_args)
|
|
1060
1374
|
|
|
1061
1375
|
|
|
1376
|
+
@trait
|
|
1062
1377
|
class Case:
|
|
1063
1378
|
"""Builder for CASE expressions using the SQL factory.
|
|
1064
1379
|
|
|
@@ -1081,6 +1396,19 @@ class Case:
|
|
|
1081
1396
|
self._conditions: list[exp.If] = []
|
|
1082
1397
|
self._default: Optional[exp.Expression] = None
|
|
1083
1398
|
|
|
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
|
+
|
|
1084
1412
|
def when(self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]) -> "Case":
|
|
1085
1413
|
"""Add a WHEN clause.
|
|
1086
1414
|
|
|
@@ -1119,6 +1447,336 @@ class Case:
|
|
|
1119
1447
|
"""
|
|
1120
1448
|
return exp.Case(ifs=self._conditions, default=self._default)
|
|
1121
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
|
+
|
|
1122
1780
|
|
|
1123
1781
|
# Create a default SQL factory instance
|
|
1124
1782
|
sql = SQLFactory()
|