sqlspec 0.15.0__py3-none-any.whl → 0.16.1__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 +699 -43
- 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 +61 -35
- sqlspec/builder/_merge.py +17 -2
- sqlspec/builder/_parsing_utils.py +16 -12
- 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 +11 -4
- sqlspec/builder/mixins/_merge_operations.py +91 -19
- sqlspec/builder/mixins/_order_limit_operations.py +15 -3
- sqlspec/builder/mixins/_pivot_operations.py +11 -2
- sqlspec/builder/mixins/_select_operations.py +16 -10
- sqlspec/builder/mixins/_update_operations.py +43 -10
- sqlspec/builder/mixins/_where_clause.py +177 -65
- 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.1.dist-info}/METADATA +1 -1
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/RECORD +43 -43
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/WHEEL +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.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
|
|
|
@@ -140,12 +191,8 @@ class SQLFactory:
|
|
|
140
191
|
)
|
|
141
192
|
raise SQLBuilderError(msg)
|
|
142
193
|
select_builder = Select(dialect=builder_dialect)
|
|
143
|
-
if select_builder._expression is None:
|
|
144
|
-
select_builder.__post_init__()
|
|
145
194
|
return self._populate_select_from_sql(select_builder, sql_candidate)
|
|
146
195
|
select_builder = Select(dialect=builder_dialect)
|
|
147
|
-
if select_builder._expression is None:
|
|
148
|
-
select_builder.__post_init__()
|
|
149
196
|
if columns_or_sql:
|
|
150
197
|
select_builder.select(*columns_or_sql)
|
|
151
198
|
return select_builder
|
|
@@ -153,8 +200,6 @@ class SQLFactory:
|
|
|
153
200
|
def insert(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Insert":
|
|
154
201
|
builder_dialect = dialect or self.dialect
|
|
155
202
|
builder = Insert(dialect=builder_dialect)
|
|
156
|
-
if builder._expression is None:
|
|
157
|
-
builder.__post_init__()
|
|
158
203
|
if table_or_sql:
|
|
159
204
|
if self._looks_like_sql(table_or_sql):
|
|
160
205
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
@@ -172,8 +217,6 @@ class SQLFactory:
|
|
|
172
217
|
def update(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Update":
|
|
173
218
|
builder_dialect = dialect or self.dialect
|
|
174
219
|
builder = Update(dialect=builder_dialect)
|
|
175
|
-
if builder._expression is None:
|
|
176
|
-
builder.__post_init__()
|
|
177
220
|
if table_or_sql:
|
|
178
221
|
if self._looks_like_sql(table_or_sql):
|
|
179
222
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
@@ -187,8 +230,6 @@ class SQLFactory:
|
|
|
187
230
|
def delete(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Delete":
|
|
188
231
|
builder_dialect = dialect or self.dialect
|
|
189
232
|
builder = Delete(dialect=builder_dialect)
|
|
190
|
-
if builder._expression is None:
|
|
191
|
-
builder.__post_init__()
|
|
192
233
|
if table_or_sql and self._looks_like_sql(table_or_sql):
|
|
193
234
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
194
235
|
if detected != "DELETE":
|
|
@@ -200,8 +241,6 @@ class SQLFactory:
|
|
|
200
241
|
def merge(self, table_or_sql: Optional[str] = None, dialect: DialectType = None) -> "Merge":
|
|
201
242
|
builder_dialect = dialect or self.dialect
|
|
202
243
|
builder = Merge(dialect=builder_dialect)
|
|
203
|
-
if builder._expression is None:
|
|
204
|
-
builder.__post_init__()
|
|
205
244
|
if table_or_sql:
|
|
206
245
|
if self._looks_like_sql(table_or_sql):
|
|
207
246
|
detected = self.detect_sql_type(table_or_sql, dialect=builder_dialect)
|
|
@@ -212,6 +251,174 @@ class SQLFactory:
|
|
|
212
251
|
return builder.into(table_or_sql)
|
|
213
252
|
return builder
|
|
214
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
|
+
|
|
215
422
|
# ===================
|
|
216
423
|
# SQL Analysis Helpers
|
|
217
424
|
# ===================
|
|
@@ -347,14 +554,112 @@ class SQLFactory:
|
|
|
347
554
|
"""
|
|
348
555
|
return Column(name, table)
|
|
349
556
|
|
|
350
|
-
|
|
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":
|
|
351
652
|
"""Dynamically create column references.
|
|
352
653
|
|
|
353
654
|
Args:
|
|
354
655
|
name: Column name.
|
|
355
656
|
|
|
356
657
|
Returns:
|
|
357
|
-
Column object
|
|
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.
|
|
358
663
|
"""
|
|
359
664
|
return Column(name)
|
|
360
665
|
|
|
@@ -363,8 +668,8 @@ class SQLFactory:
|
|
|
363
668
|
# ===================
|
|
364
669
|
|
|
365
670
|
@staticmethod
|
|
366
|
-
def raw(sql_fragment: str) -> exp.Expression:
|
|
367
|
-
"""Create a raw SQL expression from a string fragment.
|
|
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.
|
|
368
673
|
|
|
369
674
|
This method makes it explicit that you are passing raw SQL that should
|
|
370
675
|
be parsed and included directly in the query. Useful for complex expressions,
|
|
@@ -372,30 +677,30 @@ class SQLFactory:
|
|
|
372
677
|
|
|
373
678
|
Args:
|
|
374
679
|
sql_fragment: Raw SQL string to parse into an expression.
|
|
680
|
+
**parameters: Named parameters for parameter binding.
|
|
375
681
|
|
|
376
682
|
Returns:
|
|
377
|
-
SQLGlot expression from the parsed SQL fragment.
|
|
683
|
+
SQLGlot expression from the parsed SQL fragment (if no parameters).
|
|
684
|
+
SQL statement object (if parameters provided).
|
|
378
685
|
|
|
379
686
|
Raises:
|
|
380
687
|
SQLBuilderError: If the SQL fragment cannot be parsed.
|
|
381
688
|
|
|
382
689
|
Example:
|
|
383
690
|
```python
|
|
384
|
-
# Raw
|
|
385
|
-
|
|
386
|
-
sql.raw("user.id AS u_id"), "name"
|
|
387
|
-
).from_("users")
|
|
691
|
+
# Raw expression without parameters (current behavior)
|
|
692
|
+
expr = sql.raw("COALESCE(name, 'Unknown')")
|
|
388
693
|
|
|
389
|
-
# Raw
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
)
|
|
694
|
+
# Raw SQL with named parameters (new functionality)
|
|
695
|
+
stmt = sql.raw(
|
|
696
|
+
"LOWER(name) LIKE LOWER(:pattern)", pattern=f"%{query}%"
|
|
697
|
+
)
|
|
393
698
|
|
|
394
|
-
# Raw complex expression
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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,
|
|
399
704
|
)
|
|
400
705
|
|
|
401
706
|
# Raw window function
|
|
@@ -407,16 +712,23 @@ class SQLFactory:
|
|
|
407
712
|
).from_("employees")
|
|
408
713
|
```
|
|
409
714
|
"""
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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)
|
|
420
732
|
|
|
421
733
|
# ===================
|
|
422
734
|
# Aggregate Functions
|
|
@@ -1059,6 +1371,7 @@ class SQLFactory:
|
|
|
1059
1371
|
return exp.Window(this=func_expr, **over_args)
|
|
1060
1372
|
|
|
1061
1373
|
|
|
1374
|
+
@trait
|
|
1062
1375
|
class Case:
|
|
1063
1376
|
"""Builder for CASE expressions using the SQL factory.
|
|
1064
1377
|
|
|
@@ -1081,6 +1394,19 @@ class Case:
|
|
|
1081
1394
|
self._conditions: list[exp.If] = []
|
|
1082
1395
|
self._default: Optional[exp.Expression] = None
|
|
1083
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
|
+
|
|
1084
1410
|
def when(self, condition: Union[str, exp.Expression], value: Union[str, exp.Expression, Any]) -> "Case":
|
|
1085
1411
|
"""Add a WHEN clause.
|
|
1086
1412
|
|
|
@@ -1119,6 +1445,336 @@ class Case:
|
|
|
1119
1445
|
"""
|
|
1120
1446
|
return exp.Case(ifs=self._conditions, default=self._default)
|
|
1121
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
|
+
|
|
1122
1778
|
|
|
1123
1779
|
# Create a default SQL factory instance
|
|
1124
1780
|
sql = SQLFactory()
|