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.

Files changed (43) hide show
  1. sqlspec/_sql.py +702 -44
  2. sqlspec/builder/_base.py +77 -44
  3. sqlspec/builder/_column.py +0 -4
  4. sqlspec/builder/_ddl.py +15 -52
  5. sqlspec/builder/_ddl_utils.py +0 -1
  6. sqlspec/builder/_delete.py +4 -5
  7. sqlspec/builder/_insert.py +235 -44
  8. sqlspec/builder/_merge.py +17 -2
  9. sqlspec/builder/_parsing_utils.py +42 -14
  10. sqlspec/builder/_select.py +29 -33
  11. sqlspec/builder/_update.py +4 -2
  12. sqlspec/builder/mixins/_cte_and_set_ops.py +47 -20
  13. sqlspec/builder/mixins/_delete_operations.py +6 -1
  14. sqlspec/builder/mixins/_insert_operations.py +126 -24
  15. sqlspec/builder/mixins/_join_operations.py +44 -10
  16. sqlspec/builder/mixins/_merge_operations.py +183 -25
  17. sqlspec/builder/mixins/_order_limit_operations.py +15 -3
  18. sqlspec/builder/mixins/_pivot_operations.py +11 -2
  19. sqlspec/builder/mixins/_select_operations.py +21 -14
  20. sqlspec/builder/mixins/_update_operations.py +80 -32
  21. sqlspec/builder/mixins/_where_clause.py +201 -66
  22. sqlspec/core/cache.py +26 -28
  23. sqlspec/core/compiler.py +58 -37
  24. sqlspec/core/filters.py +12 -10
  25. sqlspec/core/parameters.py +80 -52
  26. sqlspec/core/result.py +30 -17
  27. sqlspec/core/statement.py +47 -22
  28. sqlspec/driver/_async.py +76 -46
  29. sqlspec/driver/_common.py +25 -6
  30. sqlspec/driver/_sync.py +73 -43
  31. sqlspec/driver/mixins/_result_tools.py +62 -37
  32. sqlspec/driver/mixins/_sql_translator.py +61 -11
  33. sqlspec/extensions/litestar/cli.py +1 -1
  34. sqlspec/extensions/litestar/plugin.py +2 -2
  35. sqlspec/protocols.py +7 -0
  36. sqlspec/utils/sync_tools.py +1 -1
  37. sqlspec/utils/type_guards.py +7 -3
  38. {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/METADATA +1 -1
  39. {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/RECORD +43 -43
  40. {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
  41. {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/entry_points.txt +0 -0
  42. {sqlspec-0.15.0.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
  43. {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 Column, Delete, Insert, Merge, Select, Truncate, Update
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
- __all__ = ("Case", "Column", "Delete", "Insert", "Merge", "SQLFactory", "Select", "Truncate", "Update", "sql")
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(self, *columns_or_sql: Union[str, exp.Expression, Column], dialect: DialectType = None) -> "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
- def __getattr__(self, name: str) -> Column:
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 that supports method chaining and operator overloading.
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 column expression with alias
385
- query = sql.select(
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 function call
390
- query = sql.select(
391
- sql.raw("COALESCE(name, 'Unknown')")
392
- ).from_("users")
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
- query = (
396
- sql.select("*")
397
- .from_("orders")
398
- .where(sql.raw("DATE(created_at) = CURRENT_DATE"))
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
- try:
411
- parsed: Optional[exp.Expression] = exp.maybe_parse(sql_fragment)
412
- if parsed is not None:
413
- return parsed
414
- if sql_fragment.strip().replace("_", "").replace(".", "").isalnum():
415
- return exp.to_identifier(sql_fragment)
416
- return exp.Literal.string(sql_fragment)
417
- except Exception as e:
418
- msg = f"Failed to parse raw SQL fragment '{sql_fragment}': {e}"
419
- raise SQLBuilderError(msg) from e
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()