sql_fusion 1.0.1__tar.gz → 1.2.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.
@@ -1,3 +1,17 @@
1
+ Metadata-Version: 2.3
2
+ Name: sql_fusion
3
+ Version: 1.2.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,22 @@ 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, joins, subqueries, and CTEs
80
+ - set operations with `UNION`, `INTERSECT`, and `EXCEPT`
81
+ - ordering and grouping with `GROUP BY`, `ROLLUP`, `CUBE`, and `GROUPING SETS`
41
82
  - aggregate and custom SQL functions through `func`
42
83
  - backend-specific SQL rewrites through compile expressions
43
84
 
44
85
  ## Installation
45
86
 
46
87
  The project targets Python 3.14 or newer.
88
+ Install it from PyPI:
89
+
90
+ ```bash
91
+ pip install sql_fusion
92
+ ```
93
+ ```bash
94
+ uv add sql_fusion
95
+ ```
47
96
 
48
97
  For local development:
49
98
 
@@ -65,9 +114,13 @@ from sql_fusion import (
65
114
  Column,
66
115
  Table,
67
116
  delete,
117
+ except_,
68
118
  func,
69
119
  insert,
120
+ intersect,
70
121
  select,
122
+ union,
123
+ text_op,
71
124
  update,
72
125
  )
73
126
  ```
@@ -82,6 +135,7 @@ from sql_fusion import (
82
135
  - `update` creates an `UPDATE` builder.
83
136
  - `delete` creates a `DELETE` builder.
84
137
  - `func` is a dynamic SQL function registry.
138
+ - `text_op` builds a condition with a raw SQL operator such as `@>`.
85
139
  - `Alias` represents a reusable SQL alias for aggregate expressions and
86
140
  `HAVING` conditions.
87
141
 
@@ -253,6 +307,10 @@ They also support SQL helpers:
253
307
  - `.ilike(pattern)`
254
308
  - `.in_(values)`
255
309
  - `.not_in(values)`
310
+ - `text(column, operator, value)` for backend-specific operators such as
311
+ PostgreSQL array containment (`@>`).
312
+
313
+ Use `|` for SQL `OR`. Python's `or` cannot be overloaded for SQL expressions.
256
314
 
257
315
  Conditions can be combined with:
258
316
 
@@ -274,6 +332,19 @@ query = (
274
332
  )
275
333
  ```
276
334
 
335
+ For PostgreSQL-style array containment, `text()` lets you pass the operator
336
+ symbol directly:
337
+
338
+ ```python
339
+ users = Table("users", Column("name"), Column("tags"))
340
+
341
+ query = (
342
+ select(users.name)
343
+ .from_(users)
344
+ .where(users.name == "bob" or text_op(users.tags, "@>", ["coffee"]))
345
+ )
346
+ ```
347
+
277
348
  ### Join Example
278
349
 
279
350
  ```python
@@ -600,3 +671,31 @@ The library also exposes a few built-in compile-time helpers:
600
671
  - query builders are chainable
601
672
  - repeated calls to many methods merge rather than overwrite
602
673
  - backend support still depends on the database you execute against
674
+
675
+ ## Feature Comparison
676
+
677
+
678
+ | Project | Focus | SQL coverage | SQL injection protected | Automatic alias management | Advanced features | Dialect/output model | Takeaway |
679
+ | --- | --- | --- | --- | --- | --- | --- | --- |
680
+ | 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 |
681
+ | 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 |
682
+ | 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 |
683
+ | 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 |
684
+ | 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 |
685
+ | 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 |
686
+ | 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 |
687
+
688
+ ## Syntax Comparison
689
+
690
+ The examples below are representative shapes, not copy-paste snippets for every library. Where a library exposes a `Table`
691
+ object, the snippet uses it.
692
+
693
+ | Project | Typical syntax shape |
694
+ | --- | --- |
695
+ | 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()` |
696
+ | 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()` |
697
+ | python-sql | `user = Table("users"); tuple(user.select(user.name, where=user.active == True))` |
698
+ | 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))` |
699
+ | simple-query-builder-python | `qb.select("users").where([["active", "=", True]]).join("orders", on=[["users.id", "=", "orders.user_id"]]).all()` |
700
+ | sqlquerybuilder | `Queryset("users").filter(active=True).join("orders", on="users.id=orders.user_id")` |
701
+ | Py-QueryBuilder | `QueryBuilder("app.users", filters).render("query.sql", query)` |
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.3
2
- Name: sql_fusion
3
- Version: 1.0.1
4
- Summary: Add your description here
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_fuser
9
- Project-URL: Documentation, https://github.com/Mastermind-U/sql_fuser/blob/main/README.md
10
- Project-URL: Repository, https://github.com/Mastermind-U/sql_fuser.git
11
- Project-URL: Source, https://github.com/Mastermind-U/sql_fuser
12
- Project-URL: Issues, https://github.com/Mastermind-U/sql_fuser/issues
13
- Description-Content-Type: text/markdown
14
-
15
1
  # SQL Fusion
16
2
 
17
3
  SQL Fusion is a lightweight, chainable SQL query builder for Python with zero dependencies.
@@ -31,6 +17,7 @@ That makes it easy to plug into your own connection layer.
31
17
 
32
18
  ## Table of Contents
33
19
 
20
+ - [Motivation](#motivation)
34
21
  - [What You Get](#what-you-get)
35
22
  - [Installation](#installation)
36
23
  - [Public API](#public-api)
@@ -44,6 +31,31 @@ That makes it easy to plug into your own connection layer.
44
31
  - [CTEs](#ctes)
45
32
  - [Custom Compile Expressions](#custom-compile-expressions)
46
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.
47
59
 
48
60
  ## What You Get
49
61
 
@@ -51,13 +63,22 @@ That makes it easy to plug into your own connection layer.
51
63
  - automatic table aliases
52
64
  - composable conditions with `AND`, `OR`, and `NOT`
53
65
  - joins, subqueries, and CTEs
54
- - ordering, joins, subqueries, and CTEs
66
+ - set operations with `UNION`, `INTERSECT`, and `EXCEPT`
67
+ - ordering and grouping with `GROUP BY`, `ROLLUP`, `CUBE`, and `GROUPING SETS`
55
68
  - aggregate and custom SQL functions through `func`
56
69
  - backend-specific SQL rewrites through compile expressions
57
70
 
58
71
  ## Installation
59
72
 
60
73
  The project targets Python 3.14 or newer.
74
+ Install it from PyPI:
75
+
76
+ ```bash
77
+ pip install sql_fusion
78
+ ```
79
+ ```bash
80
+ uv add sql_fusion
81
+ ```
61
82
 
62
83
  For local development:
63
84
 
@@ -79,9 +100,13 @@ from sql_fusion import (
79
100
  Column,
80
101
  Table,
81
102
  delete,
103
+ except_,
82
104
  func,
83
105
  insert,
106
+ intersect,
84
107
  select,
108
+ union,
109
+ text_op,
85
110
  update,
86
111
  )
87
112
  ```
@@ -96,6 +121,7 @@ from sql_fusion import (
96
121
  - `update` creates an `UPDATE` builder.
97
122
  - `delete` creates a `DELETE` builder.
98
123
  - `func` is a dynamic SQL function registry.
124
+ - `text_op` builds a condition with a raw SQL operator such as `@>`.
99
125
  - `Alias` represents a reusable SQL alias for aggregate expressions and
100
126
  `HAVING` conditions.
101
127
 
@@ -267,6 +293,10 @@ They also support SQL helpers:
267
293
  - `.ilike(pattern)`
268
294
  - `.in_(values)`
269
295
  - `.not_in(values)`
296
+ - `text(column, operator, value)` for backend-specific operators such as
297
+ PostgreSQL array containment (`@>`).
298
+
299
+ Use `|` for SQL `OR`. Python's `or` cannot be overloaded for SQL expressions.
270
300
 
271
301
  Conditions can be combined with:
272
302
 
@@ -288,6 +318,19 @@ query = (
288
318
  )
289
319
  ```
290
320
 
321
+ For PostgreSQL-style array containment, `text()` lets you pass the operator
322
+ symbol directly:
323
+
324
+ ```python
325
+ users = Table("users", Column("name"), Column("tags"))
326
+
327
+ query = (
328
+ select(users.name)
329
+ .from_(users)
330
+ .where(users.name == "bob" or text_op(users.tags, "@>", ["coffee"]))
331
+ )
332
+ ```
333
+
291
334
  ### Join Example
292
335
 
293
336
  ```python
@@ -614,3 +657,31 @@ The library also exposes a few built-in compile-time helpers:
614
657
  - query builders are chainable
615
658
  - repeated calls to many methods merge rather than overwrite
616
659
  - backend support still depends on the database you execute against
660
+
661
+ ## Feature Comparison
662
+
663
+
664
+ | Project | Focus | SQL coverage | SQL injection protected | Automatic alias management | Advanced features | Dialect/output model | Takeaway |
665
+ | --- | --- | --- | --- | --- | --- | --- | --- |
666
+ | 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 |
667
+ | 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 |
668
+ | 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 |
669
+ | 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 |
670
+ | 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 |
671
+ | 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 |
672
+ | 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 |
673
+
674
+ ## Syntax Comparison
675
+
676
+ The examples below are representative shapes, not copy-paste snippets for every library. Where a library exposes a `Table`
677
+ object, the snippet uses it.
678
+
679
+ | Project | Typical syntax shape |
680
+ | --- | --- |
681
+ | 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()` |
682
+ | 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()` |
683
+ | python-sql | `user = Table("users"); tuple(user.select(user.name, where=user.active == True))` |
684
+ | 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))` |
685
+ | simple-query-builder-python | `qb.select("users").where([["active", "=", True]]).join("orders", on=[["users.id", "=", "orders.user_id"]]).all()` |
686
+ | sqlquerybuilder | `Queryset("users").filter(active=True).join("orders", on="users.id=orders.user_id")` |
687
+ | Py-QueryBuilder | `QueryBuilder("app.users", filters).render("query.sql", query)` |
@@ -1,21 +1,21 @@
1
1
  [project]
2
2
  name = "sql_fusion"
3
- version = "1.0.1"
4
- description = "Add your description here"
3
+ version = "1.2.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
10
  [project.urls]
11
- Homepage = "https://github.com/Mastermind-U/sql_fuser"
12
- Documentation = "https://github.com/Mastermind-U/sql_fuser/blob/main/README.md"
13
- Repository = "https://github.com/Mastermind-U/sql_fuser.git"
14
- Source = "https://github.com/Mastermind-U/sql_fuser"
15
- Issues = "https://github.com/Mastermind-U/sql_fuser/issues"
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
16
 
17
17
  [project.scripts]
18
- sql_fuser = "sql_fuser:main"
18
+ sql_fusion = "sql_fusion:main"
19
19
 
20
20
  [build-system]
21
21
  requires = ["uv_build>=0.11.1,<0.12.0"]
@@ -132,6 +132,7 @@ ignore-variadic-names = true
132
132
 
133
133
  [tool.ruff.lint.per-file-ignores]
134
134
  "tests/*.py" = ["S101", "PLR2004"]
135
+ "examples/*.py" = ["S101", "T201"]
135
136
 
136
137
  [tool.ruff.lint.mccabe]
137
138
  max-complexity = 15
@@ -1,7 +1,8 @@
1
- from .composite_table import Alias, Column, 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
5
+ from .query.sets import except_, intersect, union
5
6
  from .query.update import update
6
7
 
7
8
  __all__ = [
@@ -9,8 +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",
18
+ "text_op",
19
+ "union",
15
20
  "update",
16
21
  ]
@@ -14,12 +14,14 @@ from sql_fusion.operators import (
14
14
  LikeOperator,
15
15
  NotEqualOperator,
16
16
  NotInOperator,
17
+ TextOperator,
17
18
  )
18
19
 
19
20
  CompileExpression = Callable[
20
21
  [str, tuple[Any, ...]],
21
22
  tuple[str, tuple[Any, ...]],
22
23
  ]
24
+ OperatorFactory = Callable[[str], AbstractOperator]
23
25
 
24
26
 
25
27
  class AliasRegistry:
@@ -304,7 +306,7 @@ class AbstractQuery:
304
306
  class ComparableExpression:
305
307
  def _cond(
306
308
  self,
307
- operator: type[AbstractOperator],
309
+ operator: type[AbstractOperator] | OperatorFactory,
308
310
  other: object,
309
311
  ) -> Condition:
310
312
  return Condition(column=self, operator=operator, value=other)
@@ -413,7 +415,9 @@ class Condition:
413
415
  def __init__( # noqa: PLR0913
414
416
  self,
415
417
  column: ComparableExpression | FunctionCall | None = None,
416
- operator: type[AbstractOperator] | None = None,
418
+ operator: (
419
+ type[AbstractOperator] | AbstractOperator | OperatorFactory | None
420
+ ) = None,
417
421
  value: object | None = None,
418
422
  is_and: bool = True,
419
423
  left: Condition | None = None,
@@ -421,7 +425,9 @@ class Condition:
421
425
  negated: bool = False,
422
426
  ) -> None:
423
427
  self.column: ComparableExpression | FunctionCall | None = column
424
- self.operator: type[AbstractOperator] | None = operator
428
+ self.operator: (
429
+ type[AbstractOperator] | AbstractOperator | OperatorFactory | None
430
+ ) = operator
425
431
  self.value: object | None = value
426
432
  self.is_and: bool = is_and
427
433
  self.left: Condition | None = left
@@ -437,6 +443,15 @@ class Condition:
437
443
  return value.to_sql(alias_registry)
438
444
  return value.get_ref(alias_registry), tuple()
439
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
+
440
455
  def __and__(self, other: Condition) -> Condition:
441
456
  return Condition(is_and=True, left=self, right=other)
442
457
 
@@ -476,28 +491,31 @@ class Condition:
476
491
  self.column,
477
492
  alias_registry,
478
493
  )
479
- operator_class = self.operator
480
- if operator_class is None:
494
+ operator_spec = self.operator
495
+ if operator_spec is None:
481
496
  return apply_negation(col_ref, col_params)
482
497
 
498
+ operator = self._resolve_operator(operator_spec, col_ref)
499
+
483
500
  if isinstance(self.value, (ComparableExpression, FunctionCall)):
484
501
  value_sql, value_params = self._render_expression(
485
502
  self.value,
486
503
  alias_registry,
487
504
  )
488
- sql, op_params = operator_class(col_ref).to_sql_ref(value_sql)
505
+ sql, op_params = operator.to_sql_ref(value_sql)
489
506
  return apply_negation(sql, col_params + value_params + op_params)
490
507
 
491
508
  if isinstance(self.value, AbstractQuery):
492
509
  subquery_sql, subquery_params = self.value.build_query(
493
510
  alias_registry,
494
511
  )
512
+ sql, op_params = operator.to_sql_ref(subquery_sql)
495
513
  return apply_negation(
496
- f"{col_ref} {operator_class.sql_symbol} ({subquery_sql})",
497
- col_params + subquery_params,
514
+ sql,
515
+ col_params + subquery_params + op_params,
498
516
  )
499
517
 
500
- sql, op_params = operator_class(col_ref).to_sql(self.value)
518
+ sql, op_params = operator.to_sql(self.value)
501
519
  return apply_negation(sql, col_params + op_params)
502
520
 
503
521
 
@@ -625,6 +643,18 @@ class FunctionRegistry:
625
643
  func = FunctionRegistry()
626
644
 
627
645
 
646
+ def text_op(
647
+ column: ComparableExpression | FunctionCall,
648
+ operator: str,
649
+ value: object,
650
+ ) -> Condition:
651
+ return Condition(
652
+ column=column,
653
+ operator=lambda col_ref: TextOperator(col_ref, operator),
654
+ value=value,
655
+ )
656
+
657
+
628
658
  class Column(ComparableExpression):
629
659
  def __init__(self, name: str) -> None:
630
660
  self.name: str = name
@@ -1,6 +1,4 @@
1
- from typing import Any, Callable
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()
@@ -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