sql_fusion 1.1.0__tar.gz → 1.2.1__tar.gz

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.
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sql_fusion
3
- Version: 1.1.0
3
+ Version: 1.2.1
4
4
  Summary: Python query builder with a focus on composability and reusability.
5
5
  Author: Mastermind-U
6
6
  Author-email: Mastermind-U <rex49513@gmail.com>
7
- Requires-Python: >=3.14
7
+ Requires-Python: >=3.11
8
8
  Project-URL: Homepage, https://github.com/Mastermind-U/sql_fusion
9
9
  Project-URL: Documentation, https://github.com/Mastermind-U/sql_fusion/blob/main/README.md
10
10
  Project-URL: Repository, https://github.com/Mastermind-U/sql_fusion.git
@@ -40,6 +40,7 @@ That makes it easy to plug into your own connection layer.
40
40
  - [Quickstart: psycopg3](#quickstart-psycopg3)
41
41
  - [Query Basics](#query-basics)
42
42
  - [Subquery Example](#subquery-example)
43
+ - [Set Operations](#set-operations)
43
44
  - [Method Reference](#method-reference)
44
45
  - [Functions](#functions)
45
46
  - [CTEs](#ctes)
@@ -77,6 +78,7 @@ In short, the goal is to keep the ergonomics of a lightweight builder while stil
77
78
  - automatic table aliases
78
79
  - composable conditions with `AND`, `OR`, and `NOT`
79
80
  - joins, subqueries, and CTEs
81
+ - set operations with `UNION`, `INTERSECT`, and `EXCEPT`
80
82
  - ordering and grouping with `GROUP BY`, `ROLLUP`, `CUBE`, and `GROUPING SETS`
81
83
  - aggregate and custom SQL functions through `func`
82
84
  - backend-specific SQL rewrites through compile expressions
@@ -113,10 +115,13 @@ from sql_fusion import (
113
115
  Column,
114
116
  Table,
115
117
  delete,
118
+ except_,
116
119
  func,
117
120
  insert,
121
+ intersect,
118
122
  select,
119
- text,
123
+ union,
124
+ text_op,
120
125
  update,
121
126
  )
122
127
  ```
@@ -400,6 +405,94 @@ paid_orders = (
400
405
  query, params = select().from_(paid_orders).compile()
401
406
  ```
402
407
 
408
+ ## Set Operations
409
+
410
+ SQL Fusion supports compound queries through three small wrapper classes:
411
+
412
+ - `union(query1, query2, all=False, by_name=False)`
413
+ - `intersect(q1, q2, all_=False)`
414
+ - `except_(q1, q2, all_=False)`
415
+
416
+ Each builder accepts two query objects and returns a new query that compiles to
417
+ the matching SQL set operation.
418
+
419
+ ### `union(...)`
420
+
421
+ ```python
422
+ users = Table("users")
423
+ archived_users = Table("archived_users")
424
+
425
+ active_users = select(users.id, users.name).from_(users).where_by(status="active")
426
+ archived_active_users = (
427
+ select(archived_users.id, archived_users.name)
428
+ .from_(archived_users)
429
+ .where_by(status="active")
430
+ )
431
+
432
+ query, params = union(active_users, archived_active_users).compile()
433
+ ```
434
+
435
+ Use `all=True` for `UNION ALL`:
436
+
437
+ ```python
438
+ query, params = union(active_users, archived_active_users, all=True).compile()
439
+ ```
440
+
441
+ Use `by_name=True` when the two result sets expose the same logical columns in
442
+ different orders:
443
+
444
+ ```python
445
+ left = select(users.id, users.name).from_(users)
446
+ right = select(archived_users.name, archived_users.id).from_(archived_users)
447
+
448
+ query, params = union(left, right, all=True, by_name=True).compile()
449
+ ```
450
+
451
+ ### `intersect(...)`
452
+
453
+ ```python
454
+ users = Table("users")
455
+ premium_users = Table("premium_users")
456
+
457
+ active_users = select(users.id).from_(users).where_by(status="active")
458
+ premium_active_users = (
459
+ select(premium_users.id).from_(premium_users).where_by(status="active")
460
+ )
461
+
462
+ query, params = intersect(active_users, premium_active_users).compile()
463
+ ```
464
+
465
+ Use `all_=True` for `INTERSECT ALL`:
466
+
467
+ ```python
468
+ query, params = intersect(
469
+ active_users,
470
+ premium_active_users,
471
+ all_=True,
472
+ ).compile()
473
+ ```
474
+
475
+ ### `except_(...)`
476
+
477
+ ```python
478
+ users = Table("users")
479
+ banned_users = Table("banned_users")
480
+
481
+ all_users = select(users.id).from_(users)
482
+ banned_user_ids = select(banned_users.id).from_(banned_users)
483
+
484
+ query, params = except_(all_users, banned_user_ids).compile()
485
+ ```
486
+
487
+ Use `all_=True` for `EXCEPT ALL`:
488
+
489
+ ```python
490
+ query, params = except_(all_users, banned_user_ids, all_=True).compile()
491
+ ```
492
+
493
+ These builders preserve the parameter order from left to right, so the returned
494
+ `params` tuple can be passed directly to DB-API drivers.
495
+
403
496
  ### Having Example
404
497
 
405
498
  ```python
@@ -26,6 +26,7 @@ That makes it easy to plug into your own connection layer.
26
26
  - [Quickstart: psycopg3](#quickstart-psycopg3)
27
27
  - [Query Basics](#query-basics)
28
28
  - [Subquery Example](#subquery-example)
29
+ - [Set Operations](#set-operations)
29
30
  - [Method Reference](#method-reference)
30
31
  - [Functions](#functions)
31
32
  - [CTEs](#ctes)
@@ -63,6 +64,7 @@ In short, the goal is to keep the ergonomics of a lightweight builder while stil
63
64
  - automatic table aliases
64
65
  - composable conditions with `AND`, `OR`, and `NOT`
65
66
  - joins, subqueries, and CTEs
67
+ - set operations with `UNION`, `INTERSECT`, and `EXCEPT`
66
68
  - ordering and grouping with `GROUP BY`, `ROLLUP`, `CUBE`, and `GROUPING SETS`
67
69
  - aggregate and custom SQL functions through `func`
68
70
  - backend-specific SQL rewrites through compile expressions
@@ -99,10 +101,13 @@ from sql_fusion import (
99
101
  Column,
100
102
  Table,
101
103
  delete,
104
+ except_,
102
105
  func,
103
106
  insert,
107
+ intersect,
104
108
  select,
105
- text,
109
+ union,
110
+ text_op,
106
111
  update,
107
112
  )
108
113
  ```
@@ -386,6 +391,94 @@ paid_orders = (
386
391
  query, params = select().from_(paid_orders).compile()
387
392
  ```
388
393
 
394
+ ## Set Operations
395
+
396
+ SQL Fusion supports compound queries through three small wrapper classes:
397
+
398
+ - `union(query1, query2, all=False, by_name=False)`
399
+ - `intersect(q1, q2, all_=False)`
400
+ - `except_(q1, q2, all_=False)`
401
+
402
+ Each builder accepts two query objects and returns a new query that compiles to
403
+ the matching SQL set operation.
404
+
405
+ ### `union(...)`
406
+
407
+ ```python
408
+ users = Table("users")
409
+ archived_users = Table("archived_users")
410
+
411
+ active_users = select(users.id, users.name).from_(users).where_by(status="active")
412
+ archived_active_users = (
413
+ select(archived_users.id, archived_users.name)
414
+ .from_(archived_users)
415
+ .where_by(status="active")
416
+ )
417
+
418
+ query, params = union(active_users, archived_active_users).compile()
419
+ ```
420
+
421
+ Use `all=True` for `UNION ALL`:
422
+
423
+ ```python
424
+ query, params = union(active_users, archived_active_users, all=True).compile()
425
+ ```
426
+
427
+ Use `by_name=True` when the two result sets expose the same logical columns in
428
+ different orders:
429
+
430
+ ```python
431
+ left = select(users.id, users.name).from_(users)
432
+ right = select(archived_users.name, archived_users.id).from_(archived_users)
433
+
434
+ query, params = union(left, right, all=True, by_name=True).compile()
435
+ ```
436
+
437
+ ### `intersect(...)`
438
+
439
+ ```python
440
+ users = Table("users")
441
+ premium_users = Table("premium_users")
442
+
443
+ active_users = select(users.id).from_(users).where_by(status="active")
444
+ premium_active_users = (
445
+ select(premium_users.id).from_(premium_users).where_by(status="active")
446
+ )
447
+
448
+ query, params = intersect(active_users, premium_active_users).compile()
449
+ ```
450
+
451
+ Use `all_=True` for `INTERSECT ALL`:
452
+
453
+ ```python
454
+ query, params = intersect(
455
+ active_users,
456
+ premium_active_users,
457
+ all_=True,
458
+ ).compile()
459
+ ```
460
+
461
+ ### `except_(...)`
462
+
463
+ ```python
464
+ users = Table("users")
465
+ banned_users = Table("banned_users")
466
+
467
+ all_users = select(users.id).from_(users)
468
+ banned_user_ids = select(banned_users.id).from_(banned_users)
469
+
470
+ query, params = except_(all_users, banned_user_ids).compile()
471
+ ```
472
+
473
+ Use `all_=True` for `EXCEPT ALL`:
474
+
475
+ ```python
476
+ query, params = except_(all_users, banned_user_ids, all_=True).compile()
477
+ ```
478
+
479
+ These builders preserve the parameter order from left to right, so the returned
480
+ `params` tuple can be passed directly to DB-API drivers.
481
+
389
482
  ### Having Example
390
483
 
391
484
  ```python
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "sql_fusion"
3
- version = "1.1.0"
3
+ version = "1.2.1"
4
4
  description = "Python query builder with a focus on composability and reusability."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Mastermind-U", email = "rex49513@gmail.com" }]
7
- requires-python = ">=3.14"
7
+ requires-python = ">=3.11"
8
8
  dependencies = []
9
9
 
10
10
  [project.urls]
@@ -2,6 +2,7 @@ from .composite_table import Alias, Column, Table, func, text_op
2
2
  from .query.delete import delete
3
3
  from .query.insert import insert
4
4
  from .query.select import select
5
+ from .query.sets import except_, intersect, union
5
6
  from .query.update import update
6
7
 
7
8
  __all__ = [
@@ -9,9 +10,12 @@ __all__ = [
9
10
  "Column",
10
11
  "Table",
11
12
  "delete",
13
+ "except_",
12
14
  "func",
13
15
  "insert",
16
+ "intersect",
14
17
  "select",
15
18
  "text_op",
19
+ "union",
16
20
  "update",
17
21
  ]
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections.abc import Iterable
2
4
  from copy import copy
3
5
  from typing import Any, Callable, Self
@@ -511,7 +513,8 @@ class Condition:
511
513
  )
512
514
  sql, op_params = operator.to_sql_ref(subquery_sql)
513
515
  return apply_negation(
514
- sql, col_params + subquery_params + op_params
516
+ sql,
517
+ col_params + subquery_params + op_params,
515
518
  )
516
519
 
517
520
  sql, op_params = operator.to_sql(self.value)
@@ -0,0 +1,15 @@
1
+ from .delete import delete
2
+ from .insert import insert
3
+ from .select import select
4
+ from .sets import except_, intersect, union
5
+ from .update import update
6
+
7
+ __all__ = [
8
+ "delete",
9
+ "except_",
10
+ "insert",
11
+ "intersect",
12
+ "select",
13
+ "union",
14
+ "update",
15
+ ]
@@ -0,0 +1,107 @@
1
+ from typing import Any
2
+
3
+ from sql_fusion.composite_table import AbstractQuery, AliasRegistry
4
+
5
+
6
+ class _set_operation(AbstractQuery):
7
+ def __init__(
8
+ self,
9
+ query1: AbstractQuery,
10
+ query2: AbstractQuery,
11
+ ) -> None:
12
+ super().__init__(table=None, columns=())
13
+ self._query1 = query1
14
+ self._query2 = query2
15
+
16
+ def _operator_sql(self) -> str:
17
+ raise NotImplementedError()
18
+
19
+ def _render_query(
20
+ self,
21
+ query: AbstractQuery,
22
+ alias_registry: AliasRegistry,
23
+ ) -> tuple[str, tuple[Any, ...]]:
24
+ return query.build_query(alias_registry)
25
+
26
+ def build_query(
27
+ self,
28
+ alias_registry: AliasRegistry | None = None,
29
+ ) -> tuple[str, tuple[Any, ...]]:
30
+ registry = alias_registry or self._alias_registry
31
+ params: list[Any] = []
32
+
33
+ with_sql, with_params = self._build_with_clause(registry)
34
+ params.extend(with_params)
35
+
36
+ left_sql, left_params = self._render_query(self._query1, registry)
37
+ right_sql, right_params = self._render_query(self._query2, registry)
38
+
39
+ query_parts: list[str] = []
40
+ if with_sql:
41
+ query_parts.append(with_sql)
42
+ query_parts.append(
43
+ f"{left_sql} {self._operator_sql()} {right_sql}",
44
+ )
45
+
46
+ params.extend(left_params)
47
+ params.extend(right_params)
48
+
49
+ return self._apply_compile_expressions(
50
+ " ".join(query_parts),
51
+ tuple(params),
52
+ )
53
+
54
+
55
+ class union(_set_operation):
56
+ def __init__(
57
+ self,
58
+ query1: AbstractQuery,
59
+ query2: AbstractQuery,
60
+ all: bool = False, # noqa: A002
61
+ by_name: bool = False,
62
+ ) -> None:
63
+ super().__init__(query1, query2)
64
+ self._all = all
65
+ self._by_name = by_name
66
+
67
+ def _operator_sql(self) -> str:
68
+ operator = "UNION"
69
+ if self._all:
70
+ operator += " ALL"
71
+ if self._by_name:
72
+ operator += " BY NAME"
73
+ return operator
74
+
75
+
76
+ class intersect(_set_operation):
77
+ def __init__(
78
+ self,
79
+ query1: AbstractQuery,
80
+ query2: AbstractQuery,
81
+ all_: bool = False,
82
+ ) -> None:
83
+ super().__init__(query1, query2)
84
+ self._all = all_
85
+
86
+ def _operator_sql(self) -> str:
87
+ operator = "INTERSECT"
88
+ if self._all:
89
+ operator += " ALL"
90
+ return operator
91
+
92
+
93
+ class except_(_set_operation):
94
+ def __init__(
95
+ self,
96
+ query1: AbstractQuery,
97
+ query2: AbstractQuery,
98
+ all_: bool = False,
99
+ ) -> None:
100
+ super().__init__(query1, query2)
101
+ self._all = all_
102
+
103
+ def _operator_sql(self) -> str:
104
+ operator = "EXCEPT"
105
+ if self._all:
106
+ operator += " ALL"
107
+ return operator
File without changes