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.
- {sql_fusion-1.1.0 → sql_fusion-1.2.1}/PKG-INFO +96 -3
- {sql_fusion-1.1.0 → sql_fusion-1.2.1}/README.md +94 -1
- {sql_fusion-1.1.0 → sql_fusion-1.2.1}/pyproject.toml +2 -2
- {sql_fusion-1.1.0 → sql_fusion-1.2.1}/src/sql_fusion/__init__.py +4 -0
- {sql_fusion-1.1.0 → sql_fusion-1.2.1}/src/sql_fusion/composite_table.py +4 -1
- sql_fusion-1.2.1/src/sql_fusion/query/__init__.py +15 -0
- sql_fusion-1.2.1/src/sql_fusion/query/sets.py +107 -0
- sql_fusion-1.1.0/src/sql_fusion/query/__init__.py +0 -0
- {sql_fusion-1.1.0 → sql_fusion-1.2.1}/src/sql_fusion/operators.py +0 -0
- {sql_fusion-1.1.0 → sql_fusion-1.2.1}/src/sql_fusion/query/delete.py +0 -0
- {sql_fusion-1.1.0 → sql_fusion-1.2.1}/src/sql_fusion/query/insert.py +0 -0
- {sql_fusion-1.1.0 → sql_fusion-1.2.1}/src/sql_fusion/query/select.py +0 -0
- {sql_fusion-1.1.0 → sql_fusion-1.2.1}/src/sql_fusion/query/update.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sql_fusion
|
|
3
|
-
Version: 1.1
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|