sql_fusion 1.0.0__tar.gz → 1.1.0__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.0.0/README.md → sql_fusion-1.1.0/PKG-INFO +128 -2
- sql_fusion-1.0.0/PKG-INFO → sql_fusion-1.1.0/README.md +114 -11
- {sql_fusion-1.0.0 → sql_fusion-1.1.0}/pyproject.toml +11 -3
- {sql_fusion-1.0.0 → sql_fusion-1.1.0}/src/sql_fusion/__init__.py +3 -1
- {sql_fusion-1.0.0 → sql_fusion-1.1.0}/src/sql_fusion/composite_table.py +56 -9
- {sql_fusion-1.0.0 → sql_fusion-1.1.0}/src/sql_fusion/operators.py +13 -24
- {sql_fusion-1.0.0 → sql_fusion-1.1.0}/src/sql_fusion/query/__init__.py +0 -0
- {sql_fusion-1.0.0 → sql_fusion-1.1.0}/src/sql_fusion/query/delete.py +0 -0
- {sql_fusion-1.0.0 → sql_fusion-1.1.0}/src/sql_fusion/query/insert.py +0 -0
- {sql_fusion-1.0.0 → sql_fusion-1.1.0}/src/sql_fusion/query/select.py +0 -0
- {sql_fusion-1.0.0 → sql_fusion-1.1.0}/src/sql_fusion/query/update.py +0 -0
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sql_fusion
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Python query builder with a focus on composability and reusability.
|
|
5
|
+
Author: Mastermind-U
|
|
6
|
+
Author-email: Mastermind-U <rex49513@gmail.com>
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Project-URL: Homepage, https://github.com/Mastermind-U/sql_fusion
|
|
9
|
+
Project-URL: Documentation, https://github.com/Mastermind-U/sql_fusion/blob/main/README.md
|
|
10
|
+
Project-URL: Repository, https://github.com/Mastermind-U/sql_fusion.git
|
|
11
|
+
Project-URL: Source, https://github.com/Mastermind-U/sql_fusion
|
|
12
|
+
Project-URL: Issues, https://github.com/Mastermind-U/sql_fusion/issues
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
1
15
|
# SQL Fusion
|
|
2
16
|
|
|
3
17
|
SQL Fusion is a lightweight, chainable SQL query builder for Python with zero dependencies.
|
|
@@ -17,6 +31,7 @@ That makes it easy to plug into your own connection layer.
|
|
|
17
31
|
|
|
18
32
|
## Table of Contents
|
|
19
33
|
|
|
34
|
+
- [Motivation](#motivation)
|
|
20
35
|
- [What You Get](#what-you-get)
|
|
21
36
|
- [Installation](#installation)
|
|
22
37
|
- [Public API](#public-api)
|
|
@@ -30,6 +45,31 @@ That makes it easy to plug into your own connection layer.
|
|
|
30
45
|
- [CTEs](#ctes)
|
|
31
46
|
- [Custom Compile Expressions](#custom-compile-expressions)
|
|
32
47
|
- [What To Remember](#what-to-remember)
|
|
48
|
+
- [Feature Comparison](#feature-comparison)
|
|
49
|
+
|
|
50
|
+
## Motivation
|
|
51
|
+
|
|
52
|
+
SQL builders often look similar from the outside, but they make very different trade-offs in practice:
|
|
53
|
+
|
|
54
|
+
- some are template-driven and mainly render filter fragments
|
|
55
|
+
- some are lightweight CRUD helpers with a small API surface
|
|
56
|
+
- some are broad SQL toolkits with dialect systems and advanced composition features
|
|
57
|
+
- some keep SQL parameterized, while others render a finished SQL string directly
|
|
58
|
+
|
|
59
|
+
This README compares SQL Fusion with several other Python query builders so it is easier to see where the library fits and what it is intentionally optimized for.
|
|
60
|
+
|
|
61
|
+
### Why SQL Fusion?
|
|
62
|
+
|
|
63
|
+
SQL Fusion is built for the middle ground:
|
|
64
|
+
|
|
65
|
+
- it stays small and chainable instead of turning into a full ORM
|
|
66
|
+
- it keeps SQL parameterized by default, so the caller controls execution safely
|
|
67
|
+
- it gives you SQL-like syntax inside Python, with type hints and a clean separation of clauses, inspired by SQLAlchemy Core but without the heavy machinery of a full expression system
|
|
68
|
+
- it supports real SQL building blocks like joins, subqueries, CTEs, and grouping helpers without forcing a dialect-specific API
|
|
69
|
+
- it adds automatic alias management so common queries stay readable even as they grow
|
|
70
|
+
- it exposes `compile_expression()` for the cases where the final SQL needs a backend-specific rewrite
|
|
71
|
+
|
|
72
|
+
In short, the goal is to keep the ergonomics of a lightweight builder while still covering the parts of SQL that matter in real applications.
|
|
33
73
|
|
|
34
74
|
## What You Get
|
|
35
75
|
|
|
@@ -37,13 +77,21 @@ That makes it easy to plug into your own connection layer.
|
|
|
37
77
|
- automatic table aliases
|
|
38
78
|
- composable conditions with `AND`, `OR`, and `NOT`
|
|
39
79
|
- joins, subqueries, and CTEs
|
|
40
|
-
- ordering
|
|
80
|
+
- ordering and grouping with `GROUP BY`, `ROLLUP`, `CUBE`, and `GROUPING SETS`
|
|
41
81
|
- aggregate and custom SQL functions through `func`
|
|
42
82
|
- backend-specific SQL rewrites through compile expressions
|
|
43
83
|
|
|
44
84
|
## Installation
|
|
45
85
|
|
|
46
86
|
The project targets Python 3.14 or newer.
|
|
87
|
+
Install it from PyPI:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install sql_fusion
|
|
91
|
+
```
|
|
92
|
+
```bash
|
|
93
|
+
uv add sql_fusion
|
|
94
|
+
```
|
|
47
95
|
|
|
48
96
|
For local development:
|
|
49
97
|
|
|
@@ -60,17 +108,30 @@ pip install -e .
|
|
|
60
108
|
## Public API
|
|
61
109
|
|
|
62
110
|
```python
|
|
63
|
-
from sql_fusion import
|
|
111
|
+
from sql_fusion import (
|
|
112
|
+
Alias,
|
|
113
|
+
Column,
|
|
114
|
+
Table,
|
|
115
|
+
delete,
|
|
116
|
+
func,
|
|
117
|
+
insert,
|
|
118
|
+
select,
|
|
119
|
+
text,
|
|
120
|
+
update,
|
|
121
|
+
)
|
|
64
122
|
```
|
|
65
123
|
|
|
66
124
|
### Core Objects
|
|
67
125
|
|
|
68
126
|
- `Table` represents a real table or a subquery.
|
|
127
|
+
- `Column` is the reusable column object used by `Table` when you want to
|
|
128
|
+
predeclare columns.
|
|
69
129
|
- `select` creates a `SELECT` builder.
|
|
70
130
|
- `insert` creates an `INSERT` builder.
|
|
71
131
|
- `update` creates an `UPDATE` builder.
|
|
72
132
|
- `delete` creates a `DELETE` builder.
|
|
73
133
|
- `func` is a dynamic SQL function registry.
|
|
134
|
+
- `text_op` builds a condition with a raw SQL operator such as `@>`.
|
|
74
135
|
- `Alias` represents a reusable SQL alias for aggregate expressions and
|
|
75
136
|
`HAVING` conditions.
|
|
76
137
|
|
|
@@ -205,6 +266,26 @@ users = Table("users", alias="u")
|
|
|
205
266
|
`Table` can also wrap a subquery. In practice, you usually pass a query
|
|
206
267
|
builder directly to `from_()` or `join()`, and the library wraps it for you.
|
|
207
268
|
|
|
269
|
+
If you want explicit, hint-friendly columns on a table instance, pass them
|
|
270
|
+
when you create it:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from sql_fusion import Column, Table, select
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
users = Table(
|
|
277
|
+
"users",
|
|
278
|
+
Column("id"),
|
|
279
|
+
Column("name"),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
query = select(users.id, users.name).from_(users)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
This style keeps the column list declared in one place and is verified at
|
|
287
|
+
runtime when you access `users.id` / `users.name`.
|
|
288
|
+
|
|
208
289
|
### Conditions
|
|
209
290
|
|
|
210
291
|
Columns support the usual comparison operators:
|
|
@@ -222,6 +303,10 @@ They also support SQL helpers:
|
|
|
222
303
|
- `.ilike(pattern)`
|
|
223
304
|
- `.in_(values)`
|
|
224
305
|
- `.not_in(values)`
|
|
306
|
+
- `text(column, operator, value)` for backend-specific operators such as
|
|
307
|
+
PostgreSQL array containment (`@>`).
|
|
308
|
+
|
|
309
|
+
Use `|` for SQL `OR`. Python's `or` cannot be overloaded for SQL expressions.
|
|
225
310
|
|
|
226
311
|
Conditions can be combined with:
|
|
227
312
|
|
|
@@ -243,6 +328,19 @@ query = (
|
|
|
243
328
|
)
|
|
244
329
|
```
|
|
245
330
|
|
|
331
|
+
For PostgreSQL-style array containment, `text()` lets you pass the operator
|
|
332
|
+
symbol directly:
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
users = Table("users", Column("name"), Column("tags"))
|
|
336
|
+
|
|
337
|
+
query = (
|
|
338
|
+
select(users.name)
|
|
339
|
+
.from_(users)
|
|
340
|
+
.where(users.name == "bob" or text_op(users.tags, "@>", ["coffee"]))
|
|
341
|
+
)
|
|
342
|
+
```
|
|
343
|
+
|
|
246
344
|
### Join Example
|
|
247
345
|
|
|
248
346
|
```python
|
|
@@ -569,3 +667,31 @@ The library also exposes a few built-in compile-time helpers:
|
|
|
569
667
|
- query builders are chainable
|
|
570
668
|
- repeated calls to many methods merge rather than overwrite
|
|
571
669
|
- backend support still depends on the database you execute against
|
|
670
|
+
|
|
671
|
+
## Feature Comparison
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
| Project | Focus | SQL coverage | SQL injection protected | Automatic alias management | Advanced features | Dialect/output model | Takeaway |
|
|
675
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
676
|
+
| Py-QueryBuilder | Template-driven filter rendering | No direct CRUD builder; it renders a `WHERE` fragment into a Jinja template | Yes, via JinjaSQL qmark placeholders and a separate params list | No, subquery and join aliases are template-defined rather than auto-managed by the builder | Nested rule groups, operator mapping, field pruning | Jinja2 + JinjaSQL, SQL formatting | Best for UI-driven search forms, not for composing full statements |
|
|
677
|
+
| simple-query-builder-python | Small mutable CRUD helper | `SELECT`, `INSERT`, `UPDATE`, `DELETE` | Mostly yes, because execution uses `?` placeholders and a params tuple; `get_sql(with_values=True)` can inline values for display | No, subquery and join aliases are supplied manually in the input data | `JOIN`, `GROUP BY`, `HAVING`, `UNION`, `EXCEPT`, `INTERSECT`, `LIMIT`, `OFFSET` | SQLite-first, raw SQL string builder | Simple and approachable, but the SQL surface is modest |
|
|
678
|
+
| sqlquerybuilder | Django-ORM-style queryset wrapper | Basic read/write queries | No, it renders a ready SQL string with values embedded into the query text | No, subquery and join aliases are handled manually in query strings | Filters and excludes, joins, grouping, ordering, `extra()`, slicing, `with_nolock()` | SQLite-oriented, with SQL Server pagination branches in code | Convenient for ORM-like chaining, but not aimed at deep SQL composition |
|
|
679
|
+
| python-sql | Rich Pythonic SQL builder | `SELECT`, `INSERT`, `UPDATE`, `DELETE` | Yes, it keeps placeholders separate from args and can switch param styles via flavor | Partial, it can auto-alias tables and some subqueries, while join aliases are still often explicit | `JOIN`, subqueries, CTEs, `DISTINCT ON`, windows, `RETURNING`, `MERGE`, `UNION` / `INTERSECT` / `EXCEPT` | Dialect/flavor system with multiple param styles | Very broad SQL coverage and strong backend flexibility |
|
|
680
|
+
| PyPika | Mature fluent query builder | `SELECT`, `INSERT`, `UPDATE`, `DELETE` | No by default, it renders literal SQL strings with values injected into the output | Partial, it auto-aliases some subqueries and duplicate joins, but most table and join aliases are explicit | `JOIN`, subqueries, CTEs, set operations, analytics/window helpers, DDL support | Dialect-aware with vendor-specific extensions | One of the broadest and most extensible builders in the set |
|
|
681
|
+
| SQLFactory | General-purpose SQL builder | `SELECT`, `INSERT`, `UPDATE`, `DELETE` | Yes, it emits placeholders and keeps args separately | No, subquery and join aliases are mostly explicit and part of the statement shape | `JOIN`, subselects, CTEs, window functions, set operations, `INSERT ... SELECT`, MySQL-style duplicate-key handling | MySQL / SQLite / PostgreSQL / Oracle / custom dialects, async execution helpers | Full-featured and explicit, with a heavier API than lightweight builders |
|
|
682
|
+
| SQL Fusion | Lightweight chainable builder | `SELECT`, `INSERT`, `UPDATE`, `DELETE` | Yes, it returns `(sql, params)` and leaves binding to the caller | Yes, it auto-assigns stable table aliases and reuses them for subqueries and joins | `JOIN` variants including `CROSS`, `SEMI`, `ANTI`, subqueries, recursive CTEs, `ROLLUP`, `CUBE`, `GROUPING SETS`, functions, comments, `EXPLAIN` / `ANALYZE`, `DELETE RETURNING` | Backend-agnostic, `compile_expression()` hook for rewrites | Best when you want a compact, composable builder with post-processing hooks and no execution layer |
|
|
683
|
+
|
|
684
|
+
## Syntax Comparison
|
|
685
|
+
|
|
686
|
+
The examples below are representative shapes, not copy-paste snippets for every library. Where a library exposes a `Table`
|
|
687
|
+
object, the snippet uses it.
|
|
688
|
+
|
|
689
|
+
| Project | Typical syntax shape |
|
|
690
|
+
| --- | --- |
|
|
691
|
+
| SQL Fusion | `users = Table("users"); orders = Table("orders"); select(users.id, users.name).from_(users).join(orders, users.id == orders.user_id).where(users.active == True).compile()` |
|
|
692
|
+
| PyPika | `users = Table("users"); orders = Table("orders"); Query.from_(users).join(orders).on(users.id == orders.user_id).select(users.id, users.name).where(users.active == True).get_sql()` |
|
|
693
|
+
| python-sql | `user = Table("users"); tuple(user.select(user.name, where=user.active == True))` |
|
|
694
|
+
| SQLFactory | `users = Table("users"); orders = Table("orders"); Select(users.id, users.name, table=users, join=[Join(orders, Eq("users.id", "orders.user_id"))]).where(Eq("users.active", True))` |
|
|
695
|
+
| simple-query-builder-python | `qb.select("users").where([["active", "=", True]]).join("orders", on=[["users.id", "=", "orders.user_id"]]).all()` |
|
|
696
|
+
| sqlquerybuilder | `Queryset("users").filter(active=True).join("orders", on="users.id=orders.user_id")` |
|
|
697
|
+
| Py-QueryBuilder | `QueryBuilder("app.users", filters).render("query.sql", query)` |
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: sql_fusion
|
|
3
|
-
Version: 1.0.0
|
|
4
|
-
Summary: Add your description here
|
|
5
|
-
Author: Mastermind-U
|
|
6
|
-
Author-email: Mastermind-U <rex49513@gmail.com>
|
|
7
|
-
Requires-Python: >=3.14
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
1
|
# SQL Fusion
|
|
11
2
|
|
|
12
3
|
SQL Fusion is a lightweight, chainable SQL query builder for Python with zero dependencies.
|
|
@@ -26,6 +17,7 @@ That makes it easy to plug into your own connection layer.
|
|
|
26
17
|
|
|
27
18
|
## Table of Contents
|
|
28
19
|
|
|
20
|
+
- [Motivation](#motivation)
|
|
29
21
|
- [What You Get](#what-you-get)
|
|
30
22
|
- [Installation](#installation)
|
|
31
23
|
- [Public API](#public-api)
|
|
@@ -39,6 +31,31 @@ That makes it easy to plug into your own connection layer.
|
|
|
39
31
|
- [CTEs](#ctes)
|
|
40
32
|
- [Custom Compile Expressions](#custom-compile-expressions)
|
|
41
33
|
- [What To Remember](#what-to-remember)
|
|
34
|
+
- [Feature Comparison](#feature-comparison)
|
|
35
|
+
|
|
36
|
+
## Motivation
|
|
37
|
+
|
|
38
|
+
SQL builders often look similar from the outside, but they make very different trade-offs in practice:
|
|
39
|
+
|
|
40
|
+
- some are template-driven and mainly render filter fragments
|
|
41
|
+
- some are lightweight CRUD helpers with a small API surface
|
|
42
|
+
- some are broad SQL toolkits with dialect systems and advanced composition features
|
|
43
|
+
- some keep SQL parameterized, while others render a finished SQL string directly
|
|
44
|
+
|
|
45
|
+
This README compares SQL Fusion with several other Python query builders so it is easier to see where the library fits and what it is intentionally optimized for.
|
|
46
|
+
|
|
47
|
+
### Why SQL Fusion?
|
|
48
|
+
|
|
49
|
+
SQL Fusion is built for the middle ground:
|
|
50
|
+
|
|
51
|
+
- it stays small and chainable instead of turning into a full ORM
|
|
52
|
+
- it keeps SQL parameterized by default, so the caller controls execution safely
|
|
53
|
+
- it gives you SQL-like syntax inside Python, with type hints and a clean separation of clauses, inspired by SQLAlchemy Core but without the heavy machinery of a full expression system
|
|
54
|
+
- it supports real SQL building blocks like joins, subqueries, CTEs, and grouping helpers without forcing a dialect-specific API
|
|
55
|
+
- it adds automatic alias management so common queries stay readable even as they grow
|
|
56
|
+
- it exposes `compile_expression()` for the cases where the final SQL needs a backend-specific rewrite
|
|
57
|
+
|
|
58
|
+
In short, the goal is to keep the ergonomics of a lightweight builder while still covering the parts of SQL that matter in real applications.
|
|
42
59
|
|
|
43
60
|
## What You Get
|
|
44
61
|
|
|
@@ -46,13 +63,21 @@ That makes it easy to plug into your own connection layer.
|
|
|
46
63
|
- automatic table aliases
|
|
47
64
|
- composable conditions with `AND`, `OR`, and `NOT`
|
|
48
65
|
- joins, subqueries, and CTEs
|
|
49
|
-
- ordering
|
|
66
|
+
- ordering and grouping with `GROUP BY`, `ROLLUP`, `CUBE`, and `GROUPING SETS`
|
|
50
67
|
- aggregate and custom SQL functions through `func`
|
|
51
68
|
- backend-specific SQL rewrites through compile expressions
|
|
52
69
|
|
|
53
70
|
## Installation
|
|
54
71
|
|
|
55
72
|
The project targets Python 3.14 or newer.
|
|
73
|
+
Install it from PyPI:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install sql_fusion
|
|
77
|
+
```
|
|
78
|
+
```bash
|
|
79
|
+
uv add sql_fusion
|
|
80
|
+
```
|
|
56
81
|
|
|
57
82
|
For local development:
|
|
58
83
|
|
|
@@ -69,17 +94,30 @@ pip install -e .
|
|
|
69
94
|
## Public API
|
|
70
95
|
|
|
71
96
|
```python
|
|
72
|
-
from sql_fusion import
|
|
97
|
+
from sql_fusion import (
|
|
98
|
+
Alias,
|
|
99
|
+
Column,
|
|
100
|
+
Table,
|
|
101
|
+
delete,
|
|
102
|
+
func,
|
|
103
|
+
insert,
|
|
104
|
+
select,
|
|
105
|
+
text,
|
|
106
|
+
update,
|
|
107
|
+
)
|
|
73
108
|
```
|
|
74
109
|
|
|
75
110
|
### Core Objects
|
|
76
111
|
|
|
77
112
|
- `Table` represents a real table or a subquery.
|
|
113
|
+
- `Column` is the reusable column object used by `Table` when you want to
|
|
114
|
+
predeclare columns.
|
|
78
115
|
- `select` creates a `SELECT` builder.
|
|
79
116
|
- `insert` creates an `INSERT` builder.
|
|
80
117
|
- `update` creates an `UPDATE` builder.
|
|
81
118
|
- `delete` creates a `DELETE` builder.
|
|
82
119
|
- `func` is a dynamic SQL function registry.
|
|
120
|
+
- `text_op` builds a condition with a raw SQL operator such as `@>`.
|
|
83
121
|
- `Alias` represents a reusable SQL alias for aggregate expressions and
|
|
84
122
|
`HAVING` conditions.
|
|
85
123
|
|
|
@@ -214,6 +252,26 @@ users = Table("users", alias="u")
|
|
|
214
252
|
`Table` can also wrap a subquery. In practice, you usually pass a query
|
|
215
253
|
builder directly to `from_()` or `join()`, and the library wraps it for you.
|
|
216
254
|
|
|
255
|
+
If you want explicit, hint-friendly columns on a table instance, pass them
|
|
256
|
+
when you create it:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from sql_fusion import Column, Table, select
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
users = Table(
|
|
263
|
+
"users",
|
|
264
|
+
Column("id"),
|
|
265
|
+
Column("name"),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
query = select(users.id, users.name).from_(users)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
This style keeps the column list declared in one place and is verified at
|
|
273
|
+
runtime when you access `users.id` / `users.name`.
|
|
274
|
+
|
|
217
275
|
### Conditions
|
|
218
276
|
|
|
219
277
|
Columns support the usual comparison operators:
|
|
@@ -231,6 +289,10 @@ They also support SQL helpers:
|
|
|
231
289
|
- `.ilike(pattern)`
|
|
232
290
|
- `.in_(values)`
|
|
233
291
|
- `.not_in(values)`
|
|
292
|
+
- `text(column, operator, value)` for backend-specific operators such as
|
|
293
|
+
PostgreSQL array containment (`@>`).
|
|
294
|
+
|
|
295
|
+
Use `|` for SQL `OR`. Python's `or` cannot be overloaded for SQL expressions.
|
|
234
296
|
|
|
235
297
|
Conditions can be combined with:
|
|
236
298
|
|
|
@@ -252,6 +314,19 @@ query = (
|
|
|
252
314
|
)
|
|
253
315
|
```
|
|
254
316
|
|
|
317
|
+
For PostgreSQL-style array containment, `text()` lets you pass the operator
|
|
318
|
+
symbol directly:
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
users = Table("users", Column("name"), Column("tags"))
|
|
322
|
+
|
|
323
|
+
query = (
|
|
324
|
+
select(users.name)
|
|
325
|
+
.from_(users)
|
|
326
|
+
.where(users.name == "bob" or text_op(users.tags, "@>", ["coffee"]))
|
|
327
|
+
)
|
|
328
|
+
```
|
|
329
|
+
|
|
255
330
|
### Join Example
|
|
256
331
|
|
|
257
332
|
```python
|
|
@@ -578,3 +653,31 @@ The library also exposes a few built-in compile-time helpers:
|
|
|
578
653
|
- query builders are chainable
|
|
579
654
|
- repeated calls to many methods merge rather than overwrite
|
|
580
655
|
- backend support still depends on the database you execute against
|
|
656
|
+
|
|
657
|
+
## Feature Comparison
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
| Project | Focus | SQL coverage | SQL injection protected | Automatic alias management | Advanced features | Dialect/output model | Takeaway |
|
|
661
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
662
|
+
| Py-QueryBuilder | Template-driven filter rendering | No direct CRUD builder; it renders a `WHERE` fragment into a Jinja template | Yes, via JinjaSQL qmark placeholders and a separate params list | No, subquery and join aliases are template-defined rather than auto-managed by the builder | Nested rule groups, operator mapping, field pruning | Jinja2 + JinjaSQL, SQL formatting | Best for UI-driven search forms, not for composing full statements |
|
|
663
|
+
| simple-query-builder-python | Small mutable CRUD helper | `SELECT`, `INSERT`, `UPDATE`, `DELETE` | Mostly yes, because execution uses `?` placeholders and a params tuple; `get_sql(with_values=True)` can inline values for display | No, subquery and join aliases are supplied manually in the input data | `JOIN`, `GROUP BY`, `HAVING`, `UNION`, `EXCEPT`, `INTERSECT`, `LIMIT`, `OFFSET` | SQLite-first, raw SQL string builder | Simple and approachable, but the SQL surface is modest |
|
|
664
|
+
| sqlquerybuilder | Django-ORM-style queryset wrapper | Basic read/write queries | No, it renders a ready SQL string with values embedded into the query text | No, subquery and join aliases are handled manually in query strings | Filters and excludes, joins, grouping, ordering, `extra()`, slicing, `with_nolock()` | SQLite-oriented, with SQL Server pagination branches in code | Convenient for ORM-like chaining, but not aimed at deep SQL composition |
|
|
665
|
+
| python-sql | Rich Pythonic SQL builder | `SELECT`, `INSERT`, `UPDATE`, `DELETE` | Yes, it keeps placeholders separate from args and can switch param styles via flavor | Partial, it can auto-alias tables and some subqueries, while join aliases are still often explicit | `JOIN`, subqueries, CTEs, `DISTINCT ON`, windows, `RETURNING`, `MERGE`, `UNION` / `INTERSECT` / `EXCEPT` | Dialect/flavor system with multiple param styles | Very broad SQL coverage and strong backend flexibility |
|
|
666
|
+
| PyPika | Mature fluent query builder | `SELECT`, `INSERT`, `UPDATE`, `DELETE` | No by default, it renders literal SQL strings with values injected into the output | Partial, it auto-aliases some subqueries and duplicate joins, but most table and join aliases are explicit | `JOIN`, subqueries, CTEs, set operations, analytics/window helpers, DDL support | Dialect-aware with vendor-specific extensions | One of the broadest and most extensible builders in the set |
|
|
667
|
+
| SQLFactory | General-purpose SQL builder | `SELECT`, `INSERT`, `UPDATE`, `DELETE` | Yes, it emits placeholders and keeps args separately | No, subquery and join aliases are mostly explicit and part of the statement shape | `JOIN`, subselects, CTEs, window functions, set operations, `INSERT ... SELECT`, MySQL-style duplicate-key handling | MySQL / SQLite / PostgreSQL / Oracle / custom dialects, async execution helpers | Full-featured and explicit, with a heavier API than lightweight builders |
|
|
668
|
+
| SQL Fusion | Lightweight chainable builder | `SELECT`, `INSERT`, `UPDATE`, `DELETE` | Yes, it returns `(sql, params)` and leaves binding to the caller | Yes, it auto-assigns stable table aliases and reuses them for subqueries and joins | `JOIN` variants including `CROSS`, `SEMI`, `ANTI`, subqueries, recursive CTEs, `ROLLUP`, `CUBE`, `GROUPING SETS`, functions, comments, `EXPLAIN` / `ANALYZE`, `DELETE RETURNING` | Backend-agnostic, `compile_expression()` hook for rewrites | Best when you want a compact, composable builder with post-processing hooks and no execution layer |
|
|
669
|
+
|
|
670
|
+
## Syntax Comparison
|
|
671
|
+
|
|
672
|
+
The examples below are representative shapes, not copy-paste snippets for every library. Where a library exposes a `Table`
|
|
673
|
+
object, the snippet uses it.
|
|
674
|
+
|
|
675
|
+
| Project | Typical syntax shape |
|
|
676
|
+
| --- | --- |
|
|
677
|
+
| SQL Fusion | `users = Table("users"); orders = Table("orders"); select(users.id, users.name).from_(users).join(orders, users.id == orders.user_id).where(users.active == True).compile()` |
|
|
678
|
+
| PyPika | `users = Table("users"); orders = Table("orders"); Query.from_(users).join(orders).on(users.id == orders.user_id).select(users.id, users.name).where(users.active == True).get_sql()` |
|
|
679
|
+
| python-sql | `user = Table("users"); tuple(user.select(user.name, where=user.active == True))` |
|
|
680
|
+
| SQLFactory | `users = Table("users"); orders = Table("orders"); Select(users.id, users.name, table=users, join=[Join(orders, Eq("users.id", "orders.user_id"))]).where(Eq("users.active", True))` |
|
|
681
|
+
| simple-query-builder-python | `qb.select("users").where([["active", "=", True]]).join("orders", on=[["users.id", "=", "orders.user_id"]]).all()` |
|
|
682
|
+
| sqlquerybuilder | `Queryset("users").filter(active=True).join("orders", on="users.id=orders.user_id")` |
|
|
683
|
+
| Py-QueryBuilder | `QueryBuilder("app.users", filters).render("query.sql", query)` |
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sql_fusion"
|
|
3
|
-
version = "1.
|
|
4
|
-
description = "
|
|
3
|
+
version = "1.1.0"
|
|
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
7
|
requires-python = ">=3.14"
|
|
8
8
|
dependencies = []
|
|
9
9
|
|
|
10
|
+
[project.urls]
|
|
11
|
+
Homepage = "https://github.com/Mastermind-U/sql_fusion"
|
|
12
|
+
Documentation = "https://github.com/Mastermind-U/sql_fusion/blob/main/README.md"
|
|
13
|
+
Repository = "https://github.com/Mastermind-U/sql_fusion.git"
|
|
14
|
+
Source = "https://github.com/Mastermind-U/sql_fusion"
|
|
15
|
+
Issues = "https://github.com/Mastermind-U/sql_fusion/issues"
|
|
16
|
+
|
|
10
17
|
[project.scripts]
|
|
11
|
-
|
|
18
|
+
sql_fusion = "sql_fusion:main"
|
|
12
19
|
|
|
13
20
|
[build-system]
|
|
14
21
|
requires = ["uv_build>=0.11.1,<0.12.0"]
|
|
@@ -125,6 +132,7 @@ ignore-variadic-names = true
|
|
|
125
132
|
|
|
126
133
|
[tool.ruff.lint.per-file-ignores]
|
|
127
134
|
"tests/*.py" = ["S101", "PLR2004"]
|
|
135
|
+
"examples/*.py" = ["S101", "T201"]
|
|
128
136
|
|
|
129
137
|
[tool.ruff.lint.mccabe]
|
|
130
138
|
max-complexity = 15
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .composite_table import Alias, Table, func
|
|
1
|
+
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
|
|
@@ -6,10 +6,12 @@ from .query.update import update
|
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"Alias",
|
|
9
|
+
"Column",
|
|
9
10
|
"Table",
|
|
10
11
|
"delete",
|
|
11
12
|
"func",
|
|
12
13
|
"insert",
|
|
13
14
|
"select",
|
|
15
|
+
"text_op",
|
|
14
16
|
"update",
|
|
15
17
|
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
1
2
|
from copy import copy
|
|
2
3
|
from typing import Any, Callable, Self
|
|
3
4
|
|
|
@@ -13,12 +14,14 @@ from sql_fusion.operators import (
|
|
|
13
14
|
LikeOperator,
|
|
14
15
|
NotEqualOperator,
|
|
15
16
|
NotInOperator,
|
|
17
|
+
TextOperator,
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
CompileExpression = Callable[
|
|
19
21
|
[str, tuple[Any, ...]],
|
|
20
22
|
tuple[str, tuple[Any, ...]],
|
|
21
23
|
]
|
|
24
|
+
OperatorFactory = Callable[[str], AbstractOperator]
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
class AliasRegistry:
|
|
@@ -303,7 +306,7 @@ class AbstractQuery:
|
|
|
303
306
|
class ComparableExpression:
|
|
304
307
|
def _cond(
|
|
305
308
|
self,
|
|
306
|
-
operator: type[AbstractOperator],
|
|
309
|
+
operator: type[AbstractOperator] | OperatorFactory,
|
|
307
310
|
other: object,
|
|
308
311
|
) -> Condition:
|
|
309
312
|
return Condition(column=self, operator=operator, value=other)
|
|
@@ -412,7 +415,9 @@ class Condition:
|
|
|
412
415
|
def __init__( # noqa: PLR0913
|
|
413
416
|
self,
|
|
414
417
|
column: ComparableExpression | FunctionCall | None = None,
|
|
415
|
-
operator:
|
|
418
|
+
operator: (
|
|
419
|
+
type[AbstractOperator] | AbstractOperator | OperatorFactory | None
|
|
420
|
+
) = None,
|
|
416
421
|
value: object | None = None,
|
|
417
422
|
is_and: bool = True,
|
|
418
423
|
left: Condition | None = None,
|
|
@@ -420,7 +425,9 @@ class Condition:
|
|
|
420
425
|
negated: bool = False,
|
|
421
426
|
) -> None:
|
|
422
427
|
self.column: ComparableExpression | FunctionCall | None = column
|
|
423
|
-
self.operator:
|
|
428
|
+
self.operator: (
|
|
429
|
+
type[AbstractOperator] | AbstractOperator | OperatorFactory | None
|
|
430
|
+
) = operator
|
|
424
431
|
self.value: object | None = value
|
|
425
432
|
self.is_and: bool = is_and
|
|
426
433
|
self.left: Condition | None = left
|
|
@@ -436,6 +443,15 @@ class Condition:
|
|
|
436
443
|
return value.to_sql(alias_registry)
|
|
437
444
|
return value.get_ref(alias_registry), tuple()
|
|
438
445
|
|
|
446
|
+
@staticmethod
|
|
447
|
+
def _resolve_operator(
|
|
448
|
+
operator: type[AbstractOperator] | AbstractOperator | OperatorFactory,
|
|
449
|
+
col_ref: str,
|
|
450
|
+
) -> AbstractOperator:
|
|
451
|
+
if isinstance(operator, AbstractOperator):
|
|
452
|
+
return operator
|
|
453
|
+
return operator(col_ref)
|
|
454
|
+
|
|
439
455
|
def __and__(self, other: Condition) -> Condition:
|
|
440
456
|
return Condition(is_and=True, left=self, right=other)
|
|
441
457
|
|
|
@@ -475,28 +491,30 @@ class Condition:
|
|
|
475
491
|
self.column,
|
|
476
492
|
alias_registry,
|
|
477
493
|
)
|
|
478
|
-
|
|
479
|
-
if
|
|
494
|
+
operator_spec = self.operator
|
|
495
|
+
if operator_spec is None:
|
|
480
496
|
return apply_negation(col_ref, col_params)
|
|
481
497
|
|
|
498
|
+
operator = self._resolve_operator(operator_spec, col_ref)
|
|
499
|
+
|
|
482
500
|
if isinstance(self.value, (ComparableExpression, FunctionCall)):
|
|
483
501
|
value_sql, value_params = self._render_expression(
|
|
484
502
|
self.value,
|
|
485
503
|
alias_registry,
|
|
486
504
|
)
|
|
487
|
-
sql, op_params =
|
|
505
|
+
sql, op_params = operator.to_sql_ref(value_sql)
|
|
488
506
|
return apply_negation(sql, col_params + value_params + op_params)
|
|
489
507
|
|
|
490
508
|
if isinstance(self.value, AbstractQuery):
|
|
491
509
|
subquery_sql, subquery_params = self.value.build_query(
|
|
492
510
|
alias_registry,
|
|
493
511
|
)
|
|
512
|
+
sql, op_params = operator.to_sql_ref(subquery_sql)
|
|
494
513
|
return apply_negation(
|
|
495
|
-
|
|
496
|
-
col_params + subquery_params,
|
|
514
|
+
sql, col_params + subquery_params + op_params
|
|
497
515
|
)
|
|
498
516
|
|
|
499
|
-
sql, op_params =
|
|
517
|
+
sql, op_params = operator.to_sql(self.value)
|
|
500
518
|
return apply_negation(sql, col_params + op_params)
|
|
501
519
|
|
|
502
520
|
|
|
@@ -624,6 +642,18 @@ class FunctionRegistry:
|
|
|
624
642
|
func = FunctionRegistry()
|
|
625
643
|
|
|
626
644
|
|
|
645
|
+
def text_op(
|
|
646
|
+
column: ComparableExpression | FunctionCall,
|
|
647
|
+
operator: str,
|
|
648
|
+
value: object,
|
|
649
|
+
) -> Condition:
|
|
650
|
+
return Condition(
|
|
651
|
+
column=column,
|
|
652
|
+
operator=lambda col_ref: TextOperator(col_ref, operator),
|
|
653
|
+
value=value,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
|
|
627
657
|
class Column(ComparableExpression):
|
|
628
658
|
def __init__(self, name: str) -> None:
|
|
629
659
|
self.name: str = name
|
|
@@ -652,15 +682,22 @@ class Table:
|
|
|
652
682
|
def __init__(
|
|
653
683
|
self,
|
|
654
684
|
name: str | AbstractQuery,
|
|
685
|
+
*columns: Column,
|
|
655
686
|
) -> None:
|
|
656
687
|
self._table_name: str = ""
|
|
657
688
|
self._subquery: AbstractQuery | None = None
|
|
689
|
+
self.columns: dict[str, Column] = {}
|
|
658
690
|
|
|
659
691
|
if isinstance(name, AbstractQuery):
|
|
660
692
|
self._subquery = name
|
|
661
693
|
else:
|
|
662
694
|
self._table_name = name
|
|
663
695
|
|
|
696
|
+
if columns:
|
|
697
|
+
for col in columns:
|
|
698
|
+
col._attach_table(self) # pyright: ignore[reportPrivateUsage]
|
|
699
|
+
self.columns[col.name] = col
|
|
700
|
+
|
|
664
701
|
def get_name(self) -> str:
|
|
665
702
|
if self._subquery is not None:
|
|
666
703
|
raise ValueError("Table is a subquery, no name available")
|
|
@@ -678,12 +715,22 @@ class Table:
|
|
|
678
715
|
|
|
679
716
|
return f'"{self._table_name}"', tuple()
|
|
680
717
|
|
|
718
|
+
def __dir__(self) -> Iterable[str]:
|
|
719
|
+
default_dir = super().__dir__()
|
|
720
|
+
cols = list(self.columns.keys())
|
|
721
|
+
cols.extend(default_dir)
|
|
722
|
+
return cols
|
|
723
|
+
|
|
681
724
|
def __getattr__(self, column_name: str) -> Column:
|
|
682
725
|
if column_name.startswith("_"):
|
|
683
726
|
raise AttributeError(
|
|
684
727
|
f"'{type(self).__name__}' "
|
|
685
728
|
f"object has no attribute '{column_name}'",
|
|
686
729
|
)
|
|
730
|
+
|
|
731
|
+
if self.columns:
|
|
732
|
+
return self.columns[column_name]
|
|
733
|
+
|
|
687
734
|
column = Column(column_name)
|
|
688
735
|
column._attach_table(self) # pyright: ignore[reportPrivateUsage]
|
|
689
736
|
return column
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
OPERATORS: dict[str, type[AbstractOperator]] = {}
|
|
1
|
+
from typing import Any
|
|
4
2
|
|
|
5
3
|
|
|
6
4
|
class AbstractOperator:
|
|
@@ -16,18 +14,6 @@ class AbstractOperator:
|
|
|
16
14
|
raise NotImplementedError()
|
|
17
15
|
|
|
18
16
|
|
|
19
|
-
def register_operator(
|
|
20
|
-
symbol: str,
|
|
21
|
-
) -> Callable[[type[AbstractOperator]], type[AbstractOperator]]:
|
|
22
|
-
def decorator(cls: type[AbstractOperator]) -> type[AbstractOperator]:
|
|
23
|
-
OPERATORS[symbol] = cls
|
|
24
|
-
cls.sql_symbol = symbol
|
|
25
|
-
return cls
|
|
26
|
-
|
|
27
|
-
return decorator
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@register_operator("=")
|
|
31
17
|
class EqualOperator(AbstractOperator):
|
|
32
18
|
def to_sql(self, value: Any) -> tuple[str, tuple[Any, ...]]:
|
|
33
19
|
return f"{self._col_ref} = ?", (value,)
|
|
@@ -36,7 +22,6 @@ class EqualOperator(AbstractOperator):
|
|
|
36
22
|
return f"{self._col_ref} = {value_ref}", tuple()
|
|
37
23
|
|
|
38
24
|
|
|
39
|
-
@register_operator("!=")
|
|
40
25
|
class NotEqualOperator(AbstractOperator):
|
|
41
26
|
def to_sql(self, value: Any) -> tuple[str, tuple[Any, ...]]:
|
|
42
27
|
return f"{self._col_ref} != ?", (value,)
|
|
@@ -45,7 +30,6 @@ class NotEqualOperator(AbstractOperator):
|
|
|
45
30
|
return f"{self._col_ref} != {value_ref}", tuple()
|
|
46
31
|
|
|
47
32
|
|
|
48
|
-
@register_operator("<")
|
|
49
33
|
class LessThanOperator(AbstractOperator):
|
|
50
34
|
def to_sql(self, value: Any) -> tuple[str, tuple[Any, ...]]:
|
|
51
35
|
return f"{self._col_ref} < ?", (value,)
|
|
@@ -54,7 +38,6 @@ class LessThanOperator(AbstractOperator):
|
|
|
54
38
|
return f"{self._col_ref} < {value_ref}", tuple()
|
|
55
39
|
|
|
56
40
|
|
|
57
|
-
@register_operator(">")
|
|
58
41
|
class GreaterThanOperator(AbstractOperator):
|
|
59
42
|
def to_sql(self, value: Any) -> tuple[str, tuple[Any, ...]]:
|
|
60
43
|
return f"{self._col_ref} > ?", (value,)
|
|
@@ -63,7 +46,6 @@ class GreaterThanOperator(AbstractOperator):
|
|
|
63
46
|
return f"{self._col_ref} > {value_ref}", tuple()
|
|
64
47
|
|
|
65
48
|
|
|
66
|
-
@register_operator("<=")
|
|
67
49
|
class LessThanOrEqualOperator(AbstractOperator):
|
|
68
50
|
def to_sql(self, value: Any) -> tuple[str, tuple[Any, ...]]:
|
|
69
51
|
return f"{self._col_ref} <= ?", (value,)
|
|
@@ -72,7 +54,6 @@ class LessThanOrEqualOperator(AbstractOperator):
|
|
|
72
54
|
return f"{self._col_ref} <= {value_ref}", tuple()
|
|
73
55
|
|
|
74
56
|
|
|
75
|
-
@register_operator(">=")
|
|
76
57
|
class GreaterThanOrEqualOperator(AbstractOperator):
|
|
77
58
|
def to_sql(self, value: Any) -> tuple[str, tuple[Any, ...]]:
|
|
78
59
|
return f"{self._col_ref} >= ?", (value,)
|
|
@@ -81,7 +62,6 @@ class GreaterThanOrEqualOperator(AbstractOperator):
|
|
|
81
62
|
return f"{self._col_ref} >= {value_ref}", tuple()
|
|
82
63
|
|
|
83
64
|
|
|
84
|
-
@register_operator("LIKE")
|
|
85
65
|
class LikeOperator(AbstractOperator):
|
|
86
66
|
def to_sql(self, value: Any) -> tuple[str, tuple[Any, ...]]:
|
|
87
67
|
return f"{self._col_ref} LIKE ?", (value,)
|
|
@@ -90,7 +70,6 @@ class LikeOperator(AbstractOperator):
|
|
|
90
70
|
return f"{self._col_ref} LIKE {value_ref}", tuple()
|
|
91
71
|
|
|
92
72
|
|
|
93
|
-
@register_operator("ILIKE")
|
|
94
73
|
class IlikeOperator(AbstractOperator):
|
|
95
74
|
def to_sql(self, value: Any) -> tuple[str, tuple[Any, ...]]:
|
|
96
75
|
return f"{self._col_ref} ILIKE ?", (value,)
|
|
@@ -99,7 +78,6 @@ class IlikeOperator(AbstractOperator):
|
|
|
99
78
|
return f"{self._col_ref} ILIKE {value_ref}", tuple()
|
|
100
79
|
|
|
101
80
|
|
|
102
|
-
@register_operator("IN")
|
|
103
81
|
class InOperator(AbstractOperator):
|
|
104
82
|
def to_sql(self, value: Any) -> tuple[str, tuple[Any, ...]]:
|
|
105
83
|
placeholders: str = ", ".join("?" * len(value))
|
|
@@ -109,7 +87,6 @@ class InOperator(AbstractOperator):
|
|
|
109
87
|
return f"{self._col_ref} IN ({value_ref})", tuple()
|
|
110
88
|
|
|
111
89
|
|
|
112
|
-
@register_operator("NOT IN")
|
|
113
90
|
class NotInOperator(AbstractOperator):
|
|
114
91
|
def to_sql(self, value: Any) -> tuple[str, tuple[Any, ...]]:
|
|
115
92
|
placeholders: str = ", ".join("?" * len(value))
|
|
@@ -117,3 +94,15 @@ class NotInOperator(AbstractOperator):
|
|
|
117
94
|
|
|
118
95
|
def to_sql_ref(self, value_ref: str) -> tuple[str, tuple[Any, ...]]:
|
|
119
96
|
return f"{self._col_ref} NOT IN ({value_ref})", tuple()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TextOperator(AbstractOperator):
|
|
100
|
+
def __init__(self, col_ref: str, sql_symbol: str) -> None:
|
|
101
|
+
super().__init__(col_ref)
|
|
102
|
+
self.sql_symbol: str = sql_symbol
|
|
103
|
+
|
|
104
|
+
def to_sql(self, value: Any) -> tuple[str, tuple[Any, ...]]:
|
|
105
|
+
return f"{self._col_ref} {self.sql_symbol} ?", (value,)
|
|
106
|
+
|
|
107
|
+
def to_sql_ref(self, value_ref: str) -> tuple[str, tuple[Any, ...]]:
|
|
108
|
+
return f"{self._col_ref} {self.sql_symbol} {value_ref}", tuple()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|