sqlstratum 0.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.
- sqlstratum-0.1.0/LICENSE +21 -0
- sqlstratum-0.1.0/PKG-INFO +179 -0
- sqlstratum-0.1.0/README.md +127 -0
- sqlstratum-0.1.0/pyproject.toml +44 -0
- sqlstratum-0.1.0/setup.cfg +4 -0
- sqlstratum-0.1.0/sqlstratum/__init__.py +32 -0
- sqlstratum-0.1.0/sqlstratum/ast.py +96 -0
- sqlstratum-0.1.0/sqlstratum/compile.py +156 -0
- sqlstratum-0.1.0/sqlstratum/dsl.py +150 -0
- sqlstratum-0.1.0/sqlstratum/expr.py +115 -0
- sqlstratum-0.1.0/sqlstratum/hydrate.py +55 -0
- sqlstratum-0.1.0/sqlstratum/meta.py +112 -0
- sqlstratum-0.1.0/sqlstratum/runner.py +141 -0
- sqlstratum-0.1.0/sqlstratum/types.py +44 -0
- sqlstratum-0.1.0/sqlstratum.egg-info/PKG-INFO +179 -0
- sqlstratum-0.1.0/sqlstratum.egg-info/SOURCES.txt +25 -0
- sqlstratum-0.1.0/sqlstratum.egg-info/dependency_links.txt +1 -0
- sqlstratum-0.1.0/sqlstratum.egg-info/requires.txt +4 -0
- sqlstratum-0.1.0/sqlstratum.egg-info/top_level.txt +1 -0
- sqlstratum-0.1.0/tests/test_compile_aggregate.py +56 -0
- sqlstratum-0.1.0/tests/test_compile_dml.py +45 -0
- sqlstratum-0.1.0/tests/test_compile_join.py +56 -0
- sqlstratum-0.1.0/tests/test_compile_select.py +69 -0
- sqlstratum-0.1.0/tests/test_hydration.py +70 -0
- sqlstratum-0.1.0/tests/test_runner_debug_logging.py +61 -0
- sqlstratum-0.1.0/tests/test_sqlite_integration.py +157 -0
- sqlstratum-0.1.0/tests/test_transactions.py +49 -0
sqlstratum-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antonio Ognio <aognio@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlstratum
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight, source-first SQL AST + compiler + runner.
|
|
5
|
+
Author-email: Antonio Ognio <aognio@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Antonio Ognio <aognio@gmail.com>
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/aognio/sqlstratum
|
|
29
|
+
Project-URL: Repository, https://github.com/aognio/sqlstratum
|
|
30
|
+
Project-URL: Issues, https://github.com/aognio/sqlstratum/issues
|
|
31
|
+
Keywords: sql,query-builder,sqlite,compiler,ast
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: Database
|
|
44
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
45
|
+
Requires-Python: >=3.8
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
License-File: LICENSE
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
50
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
|
|
53
|
+
# SQLStratum
|
|
54
|
+
|
|
55
|
+
<p align="center">
|
|
56
|
+
<img src="assets/images/SQLStratum-Logo-500x500-transparent.png" alt="SQLStratum logo" />
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
SQLStratum is a modern, typed, deterministic SQL query builder and compiler for Python with a
|
|
60
|
+
SQLite runner and a hydration pipeline. It exists to give applications and ORMs a reliable foundation
|
|
61
|
+
layer with composable SQL, predictable parameter binding, and explicit execution boundaries.
|
|
62
|
+
|
|
63
|
+
## Key Features
|
|
64
|
+
- Deterministic compilation: identical AST inputs produce identical SQL + params
|
|
65
|
+
- Typed, composable DSL for SELECT/INSERT/UPDATE/DELETE
|
|
66
|
+
- Safe parameter binding (no raw interpolation)
|
|
67
|
+
- Hydration targets for structured results
|
|
68
|
+
- SQLite-first execution via a small Runner API
|
|
69
|
+
- Testable compiled output and runtime behavior
|
|
70
|
+
|
|
71
|
+
## Non-Goals
|
|
72
|
+
- Not an ORM (no identity map, relationships, lazy loading)
|
|
73
|
+
- Not a migrations/DDL system
|
|
74
|
+
- Not a full database abstraction layer for every backend yet (SQLite first)
|
|
75
|
+
- Not a SQL string templating engine
|
|
76
|
+
|
|
77
|
+
SQLStratum focuses on queries. DDL statements such as `CREATE TABLE` or `ALTER TABLE` are intended to
|
|
78
|
+
live in a complementary library with similar design goals that is currently in the works.
|
|
79
|
+
|
|
80
|
+
## Quickstart
|
|
81
|
+
```python
|
|
82
|
+
import sqlite3
|
|
83
|
+
|
|
84
|
+
from sqlstratum import SELECT, INSERT, Table, col, Runner
|
|
85
|
+
|
|
86
|
+
users = Table(
|
|
87
|
+
"users",
|
|
88
|
+
col("id", int),
|
|
89
|
+
col("email", str),
|
|
90
|
+
col("active", int),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
conn = sqlite3.connect(":memory:")
|
|
94
|
+
runner = Runner(conn)
|
|
95
|
+
runner.exec_ddl("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, active INTEGER)")
|
|
96
|
+
|
|
97
|
+
runner.execute(INSERT(users).VALUES(email="a@b.com", active=1))
|
|
98
|
+
runner.execute(INSERT(users).VALUES(email="c@d.com", active=0))
|
|
99
|
+
|
|
100
|
+
q = (
|
|
101
|
+
SELECT(users.c.id, users.c.email)
|
|
102
|
+
.FROM(users)
|
|
103
|
+
.WHERE(users.c.active.is_true())
|
|
104
|
+
.HYDRATE(dict)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
rows = runner.fetch_all(q)
|
|
108
|
+
print(rows)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Why `Table` objects?
|
|
112
|
+
SQLStratum’s `Table` objects are the schema anchor for the typed, deterministic query builder. They
|
|
113
|
+
provides column metadata and a stable namespace for column access, which enables predictable SQL
|
|
114
|
+
generation and safe parameter binding. They also support explicit aliasing to avoid ambiguous column
|
|
115
|
+
names in joins.
|
|
116
|
+
|
|
117
|
+
## Project Structure
|
|
118
|
+
- AST: immutable query nodes in `sqlstratum/ast.py`
|
|
119
|
+
- Compiler: SQL + params generation in `sqlstratum/compile.py`
|
|
120
|
+
- Runner: SQLite execution and transactions in `sqlstratum/runner.py`
|
|
121
|
+
- Hydration: projection rules and targets in `sqlstratum/hydrate.py`
|
|
122
|
+
|
|
123
|
+
## SQL Debugging
|
|
124
|
+
SQLStratum can log executed SQL statements (compiled SQL + parameters + duration), but logging is
|
|
125
|
+
intentionally gated to avoid noisy output in production. Debug output requires two conditions:
|
|
126
|
+
- Environment variable gate: `SQLSTRATUM_DEBUG` must be truthy (`"1"`, `"true"`, `"yes"`,
|
|
127
|
+
case-insensitive).
|
|
128
|
+
- Logger gate: the `sqlstratum` logger must be DEBUG-enabled.
|
|
129
|
+
|
|
130
|
+
Why it does not work by default: Python logging defaults to WARNING level, so even if
|
|
131
|
+
`SQLSTRATUM_DEBUG=1` is set, DEBUG logs will not appear unless logging is configured.
|
|
132
|
+
|
|
133
|
+
To enable debugging in a development app:
|
|
134
|
+
|
|
135
|
+
Step 1 - set the environment variable:
|
|
136
|
+
```
|
|
137
|
+
SQLSTRATUM_DEBUG=1
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Step 2 - configure logging early in the app:
|
|
141
|
+
```python
|
|
142
|
+
import logging
|
|
143
|
+
|
|
144
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
145
|
+
# or
|
|
146
|
+
logging.getLogger("sqlstratum").setLevel(logging.DEBUG)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Output looks like:
|
|
150
|
+
```
|
|
151
|
+
SQL: <compiled sql> | params={<sorted params>} | duration_ms=<...>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Architectural intent: logging happens at the Runner boundary (after execution). AST building and
|
|
155
|
+
compilation remain deterministic and side-effect free, preserving separation of concerns.
|
|
156
|
+
|
|
157
|
+
## Logo Inspiration
|
|
158
|
+
|
|
159
|
+
Vinicunca (Rainbow Mountain) in Peru’s Cusco Region — a high-altitude day hike from
|
|
160
|
+
Cusco at roughly 5,036 m (16,500 ft). See [Vinicunca](https://en.wikipedia.org/wiki/Vinicunca) for
|
|
161
|
+
background.
|
|
162
|
+
|
|
163
|
+
## Versioning / Roadmap
|
|
164
|
+
Current version: `0.1.0`.
|
|
165
|
+
Design notes and current limitations are tracked in `NOTES.md`. Roadmap planning is intentionally
|
|
166
|
+
minimal at this stage and will evolve with real usage.
|
|
167
|
+
|
|
168
|
+
## Authorship
|
|
169
|
+
[Antonio Ognio](https://github.com/aognio/) is the maintainer and author of SQLStratum. ChatGPT is used for brainstorming,
|
|
170
|
+
architectural thinking, documentation drafting, and project management advisory. Codex (CLI/agentic
|
|
171
|
+
coding) is used to implement many code changes under Antonio's direction and review. The maintainer
|
|
172
|
+
reviews and curates changes; AI tools are assistants, not owners, and accountability remains with the
|
|
173
|
+
maintainer.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
MIT License.
|
|
177
|
+
|
|
178
|
+
## Contributing
|
|
179
|
+
PRs are welcome. Please read `CONTRIBUTING.md` for the workflow and expectations.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# SQLStratum
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/images/SQLStratum-Logo-500x500-transparent.png" alt="SQLStratum logo" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
SQLStratum is a modern, typed, deterministic SQL query builder and compiler for Python with a
|
|
8
|
+
SQLite runner and a hydration pipeline. It exists to give applications and ORMs a reliable foundation
|
|
9
|
+
layer with composable SQL, predictable parameter binding, and explicit execution boundaries.
|
|
10
|
+
|
|
11
|
+
## Key Features
|
|
12
|
+
- Deterministic compilation: identical AST inputs produce identical SQL + params
|
|
13
|
+
- Typed, composable DSL for SELECT/INSERT/UPDATE/DELETE
|
|
14
|
+
- Safe parameter binding (no raw interpolation)
|
|
15
|
+
- Hydration targets for structured results
|
|
16
|
+
- SQLite-first execution via a small Runner API
|
|
17
|
+
- Testable compiled output and runtime behavior
|
|
18
|
+
|
|
19
|
+
## Non-Goals
|
|
20
|
+
- Not an ORM (no identity map, relationships, lazy loading)
|
|
21
|
+
- Not a migrations/DDL system
|
|
22
|
+
- Not a full database abstraction layer for every backend yet (SQLite first)
|
|
23
|
+
- Not a SQL string templating engine
|
|
24
|
+
|
|
25
|
+
SQLStratum focuses on queries. DDL statements such as `CREATE TABLE` or `ALTER TABLE` are intended to
|
|
26
|
+
live in a complementary library with similar design goals that is currently in the works.
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
```python
|
|
30
|
+
import sqlite3
|
|
31
|
+
|
|
32
|
+
from sqlstratum import SELECT, INSERT, Table, col, Runner
|
|
33
|
+
|
|
34
|
+
users = Table(
|
|
35
|
+
"users",
|
|
36
|
+
col("id", int),
|
|
37
|
+
col("email", str),
|
|
38
|
+
col("active", int),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
conn = sqlite3.connect(":memory:")
|
|
42
|
+
runner = Runner(conn)
|
|
43
|
+
runner.exec_ddl("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, active INTEGER)")
|
|
44
|
+
|
|
45
|
+
runner.execute(INSERT(users).VALUES(email="a@b.com", active=1))
|
|
46
|
+
runner.execute(INSERT(users).VALUES(email="c@d.com", active=0))
|
|
47
|
+
|
|
48
|
+
q = (
|
|
49
|
+
SELECT(users.c.id, users.c.email)
|
|
50
|
+
.FROM(users)
|
|
51
|
+
.WHERE(users.c.active.is_true())
|
|
52
|
+
.HYDRATE(dict)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
rows = runner.fetch_all(q)
|
|
56
|
+
print(rows)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Why `Table` objects?
|
|
60
|
+
SQLStratum’s `Table` objects are the schema anchor for the typed, deterministic query builder. They
|
|
61
|
+
provides column metadata and a stable namespace for column access, which enables predictable SQL
|
|
62
|
+
generation and safe parameter binding. They also support explicit aliasing to avoid ambiguous column
|
|
63
|
+
names in joins.
|
|
64
|
+
|
|
65
|
+
## Project Structure
|
|
66
|
+
- AST: immutable query nodes in `sqlstratum/ast.py`
|
|
67
|
+
- Compiler: SQL + params generation in `sqlstratum/compile.py`
|
|
68
|
+
- Runner: SQLite execution and transactions in `sqlstratum/runner.py`
|
|
69
|
+
- Hydration: projection rules and targets in `sqlstratum/hydrate.py`
|
|
70
|
+
|
|
71
|
+
## SQL Debugging
|
|
72
|
+
SQLStratum can log executed SQL statements (compiled SQL + parameters + duration), but logging is
|
|
73
|
+
intentionally gated to avoid noisy output in production. Debug output requires two conditions:
|
|
74
|
+
- Environment variable gate: `SQLSTRATUM_DEBUG` must be truthy (`"1"`, `"true"`, `"yes"`,
|
|
75
|
+
case-insensitive).
|
|
76
|
+
- Logger gate: the `sqlstratum` logger must be DEBUG-enabled.
|
|
77
|
+
|
|
78
|
+
Why it does not work by default: Python logging defaults to WARNING level, so even if
|
|
79
|
+
`SQLSTRATUM_DEBUG=1` is set, DEBUG logs will not appear unless logging is configured.
|
|
80
|
+
|
|
81
|
+
To enable debugging in a development app:
|
|
82
|
+
|
|
83
|
+
Step 1 - set the environment variable:
|
|
84
|
+
```
|
|
85
|
+
SQLSTRATUM_DEBUG=1
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Step 2 - configure logging early in the app:
|
|
89
|
+
```python
|
|
90
|
+
import logging
|
|
91
|
+
|
|
92
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
93
|
+
# or
|
|
94
|
+
logging.getLogger("sqlstratum").setLevel(logging.DEBUG)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Output looks like:
|
|
98
|
+
```
|
|
99
|
+
SQL: <compiled sql> | params={<sorted params>} | duration_ms=<...>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Architectural intent: logging happens at the Runner boundary (after execution). AST building and
|
|
103
|
+
compilation remain deterministic and side-effect free, preserving separation of concerns.
|
|
104
|
+
|
|
105
|
+
## Logo Inspiration
|
|
106
|
+
|
|
107
|
+
Vinicunca (Rainbow Mountain) in Peru’s Cusco Region — a high-altitude day hike from
|
|
108
|
+
Cusco at roughly 5,036 m (16,500 ft). See [Vinicunca](https://en.wikipedia.org/wiki/Vinicunca) for
|
|
109
|
+
background.
|
|
110
|
+
|
|
111
|
+
## Versioning / Roadmap
|
|
112
|
+
Current version: `0.1.0`.
|
|
113
|
+
Design notes and current limitations are tracked in `NOTES.md`. Roadmap planning is intentionally
|
|
114
|
+
minimal at this stage and will evolve with real usage.
|
|
115
|
+
|
|
116
|
+
## Authorship
|
|
117
|
+
[Antonio Ognio](https://github.com/aognio/) is the maintainer and author of SQLStratum. ChatGPT is used for brainstorming,
|
|
118
|
+
architectural thinking, documentation drafting, and project management advisory. Codex (CLI/agentic
|
|
119
|
+
coding) is used to implement many code changes under Antonio's direction and review. The maintainer
|
|
120
|
+
reviews and curates changes; AI tools are assistants, not owners, and accountability remains with the
|
|
121
|
+
maintainer.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
MIT License.
|
|
125
|
+
|
|
126
|
+
## Contributing
|
|
127
|
+
PRs are welcome. Please read `CONTRIBUTING.md` for the workflow and expectations.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sqlstratum"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight, source-first SQL AST + compiler + runner."
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Antonio Ognio", email = "aognio@gmail.com" },
|
|
13
|
+
]
|
|
14
|
+
keywords = ["sql", "query-builder", "sqlite", "compiler", "ast"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Database",
|
|
28
|
+
"Topic :: Software Development :: Libraries",
|
|
29
|
+
]
|
|
30
|
+
requires-python = ">=3.8"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/aognio/sqlstratum"
|
|
34
|
+
Repository = "https://github.com/aognio/sqlstratum"
|
|
35
|
+
Issues = "https://github.com/aognio/sqlstratum/issues"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"build>=1.2.0",
|
|
40
|
+
"twine>=5.0.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools]
|
|
44
|
+
packages = ["sqlstratum"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""sqlstratum: minimal SQL AST + compiler + sqlite runner."""
|
|
2
|
+
from .dsl import SELECT, INSERT, UPDATE, DELETE, OR, AND, NOT
|
|
3
|
+
from .expr import COUNT, SUM, AVG, MIN, MAX
|
|
4
|
+
from .meta import Table, Column, col
|
|
5
|
+
from .compile import compile
|
|
6
|
+
from .runner import Runner
|
|
7
|
+
from .types import Expression, HydrationTarget, Hydrator, Predicate, Source
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"SELECT",
|
|
11
|
+
"INSERT",
|
|
12
|
+
"UPDATE",
|
|
13
|
+
"DELETE",
|
|
14
|
+
"OR",
|
|
15
|
+
"AND",
|
|
16
|
+
"NOT",
|
|
17
|
+
"COUNT",
|
|
18
|
+
"SUM",
|
|
19
|
+
"AVG",
|
|
20
|
+
"MIN",
|
|
21
|
+
"MAX",
|
|
22
|
+
"Table",
|
|
23
|
+
"Column",
|
|
24
|
+
"col",
|
|
25
|
+
"compile",
|
|
26
|
+
"Runner",
|
|
27
|
+
"Expression",
|
|
28
|
+
"HydrationTarget",
|
|
29
|
+
"Hydrator",
|
|
30
|
+
"Predicate",
|
|
31
|
+
"Source",
|
|
32
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""AST node definitions for sqlstratum."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, TypeVar
|
|
6
|
+
|
|
7
|
+
from .meta import Column
|
|
8
|
+
from .expr import OrderSpec
|
|
9
|
+
from .types import Expression, HydrationTarget, Predicate, Source
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Compiled:
|
|
14
|
+
sql: str
|
|
15
|
+
params: Dict[str, Any]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class SelectQuery:
|
|
20
|
+
projections: Tuple[Expression, ...]
|
|
21
|
+
from_: Optional[Source]
|
|
22
|
+
joins: Tuple["Join", ...]
|
|
23
|
+
where: Tuple[Predicate, ...]
|
|
24
|
+
group_by: Tuple[Expression, ...]
|
|
25
|
+
having: Tuple[Predicate, ...]
|
|
26
|
+
order_by: Tuple[OrderSpec, ...]
|
|
27
|
+
limit: Optional[int]
|
|
28
|
+
offset: Optional[int]
|
|
29
|
+
distinct: bool
|
|
30
|
+
hydrate: HydrationTarget
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Join:
|
|
35
|
+
kind: str # "INNER" or "LEFT"
|
|
36
|
+
source: Source
|
|
37
|
+
on: Predicate
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class InsertQuery:
|
|
42
|
+
table: Any
|
|
43
|
+
values: Tuple[Tuple[str, Any], ...]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class UpdateQuery:
|
|
48
|
+
table: Any
|
|
49
|
+
values: Tuple[Tuple[str, Any], ...]
|
|
50
|
+
where: Tuple[Predicate, ...]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class DeleteQuery:
|
|
55
|
+
table: Any
|
|
56
|
+
where: Tuple[Predicate, ...]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class Subquery:
|
|
61
|
+
query: SelectQuery
|
|
62
|
+
alias: str
|
|
63
|
+
c: Any = None
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
object.__setattr__(self, "c", _SubqueryColumnAccessor(self.alias))
|
|
67
|
+
|
|
68
|
+
def __getattr__(self, item: str) -> Column:
|
|
69
|
+
return getattr(self.c, item)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class _SubqueryTable:
|
|
74
|
+
name: str
|
|
75
|
+
alias: Optional[str] = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _SubqueryColumnAccessor:
|
|
79
|
+
def __init__(self, alias: str) -> None:
|
|
80
|
+
self._alias = alias
|
|
81
|
+
|
|
82
|
+
def __getattr__(self, item: str) -> Column:
|
|
83
|
+
return Column(name=item, py_type=object, table=_SubqueryTable(self._alias))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ExecutionResult:
|
|
88
|
+
rowcount: int
|
|
89
|
+
lastrowid: Optional[int]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
T = TypeVar("T")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def tupled(items: Iterable[T]) -> Tuple[T, ...]:
|
|
96
|
+
return tuple(items)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""SQLite compiler for sqlstratum AST."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Dict, Iterable, List
|
|
6
|
+
|
|
7
|
+
from . import ast
|
|
8
|
+
from .expr import (
|
|
9
|
+
AliasExpr,
|
|
10
|
+
BinaryPredicate,
|
|
11
|
+
Function,
|
|
12
|
+
Literal,
|
|
13
|
+
LogicalPredicate,
|
|
14
|
+
NotPredicate,
|
|
15
|
+
OrderSpec,
|
|
16
|
+
UnaryPredicate,
|
|
17
|
+
)
|
|
18
|
+
from .meta import Column, Table
|
|
19
|
+
from .types import Predicate
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Compiled(ast.Compiled):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def compile(query: Any, dialect: str = "sqlite") -> Compiled:
|
|
28
|
+
if dialect != "sqlite":
|
|
29
|
+
raise ValueError("Only sqlite dialect is supported in v0.1")
|
|
30
|
+
compiler = _Compiler()
|
|
31
|
+
sql = compiler.compile_query(query)
|
|
32
|
+
return Compiled(sql=sql, params=compiler.params)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _Compiler:
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self.params: Dict[str, Any] = {}
|
|
38
|
+
self._param_index = 0
|
|
39
|
+
|
|
40
|
+
def compile_query(self, query: Any) -> str:
|
|
41
|
+
if isinstance(query, ast.SelectQuery):
|
|
42
|
+
return self._compile_select(query)
|
|
43
|
+
if isinstance(query, ast.InsertQuery):
|
|
44
|
+
return self._compile_insert(query)
|
|
45
|
+
if isinstance(query, ast.UpdateQuery):
|
|
46
|
+
return self._compile_update(query)
|
|
47
|
+
if isinstance(query, ast.DeleteQuery):
|
|
48
|
+
return self._compile_delete(query)
|
|
49
|
+
raise TypeError(f"Unsupported query type: {type(query)}")
|
|
50
|
+
|
|
51
|
+
def _compile_select(self, query: ast.SelectQuery) -> str:
|
|
52
|
+
parts: List[str] = []
|
|
53
|
+
distinct = "DISTINCT " if query.distinct else ""
|
|
54
|
+
projections = ", ".join(self._compile_projection(p) for p in query.projections)
|
|
55
|
+
parts.append(f"SELECT {distinct}{projections}")
|
|
56
|
+
if query.from_ is not None:
|
|
57
|
+
parts.append("FROM " + self._compile_source(query.from_))
|
|
58
|
+
for join in query.joins:
|
|
59
|
+
join_sql = "JOIN" if join.kind == "INNER" else "LEFT JOIN"
|
|
60
|
+
parts.append(f"{join_sql} {self._compile_source(join.source)} ON {self._compile_predicate(join.on)}")
|
|
61
|
+
if query.where:
|
|
62
|
+
parts.append("WHERE " + self._compile_and_list(query.where))
|
|
63
|
+
if query.group_by:
|
|
64
|
+
parts.append("GROUP BY " + ", ".join(self._compile_expr(e) for e in query.group_by))
|
|
65
|
+
if query.having:
|
|
66
|
+
parts.append("HAVING " + self._compile_and_list(query.having))
|
|
67
|
+
if query.order_by:
|
|
68
|
+
parts.append("ORDER BY " + ", ".join(self._compile_order(o) for o in query.order_by))
|
|
69
|
+
if query.limit is not None:
|
|
70
|
+
parts.append("LIMIT " + self._bind(query.limit))
|
|
71
|
+
if query.offset is not None:
|
|
72
|
+
parts.append("OFFSET " + self._bind(query.offset))
|
|
73
|
+
return " ".join(parts)
|
|
74
|
+
|
|
75
|
+
def _compile_insert(self, query: ast.InsertQuery) -> str:
|
|
76
|
+
columns = ", ".join(self._quote(k) for k, _ in query.values)
|
|
77
|
+
values = ", ".join(self._bind(v) for _, v in query.values)
|
|
78
|
+
return f"INSERT INTO {self._compile_table(query.table)} ({columns}) VALUES ({values})"
|
|
79
|
+
|
|
80
|
+
def _compile_update(self, query: ast.UpdateQuery) -> str:
|
|
81
|
+
sets = ", ".join(f"{self._quote(k)} = {self._bind(v)}" for k, v in query.values)
|
|
82
|
+
parts = [f"UPDATE {self._compile_table(query.table)} SET {sets}"]
|
|
83
|
+
if query.where:
|
|
84
|
+
parts.append("WHERE " + self._compile_and_list(query.where))
|
|
85
|
+
return " ".join(parts)
|
|
86
|
+
|
|
87
|
+
def _compile_delete(self, query: ast.DeleteQuery) -> str:
|
|
88
|
+
parts = [f"DELETE FROM {self._compile_table(query.table)}"]
|
|
89
|
+
if query.where:
|
|
90
|
+
parts.append("WHERE " + self._compile_and_list(query.where))
|
|
91
|
+
return " ".join(parts)
|
|
92
|
+
|
|
93
|
+
def _compile_projection(self, expr: Any) -> str:
|
|
94
|
+
return self._compile_expr(expr)
|
|
95
|
+
|
|
96
|
+
def _compile_source(self, source: Any) -> str:
|
|
97
|
+
if isinstance(source, Table):
|
|
98
|
+
return self._compile_table(source)
|
|
99
|
+
if isinstance(source, ast.Subquery):
|
|
100
|
+
return f"({self._compile_select(source.query)}) AS {self._quote(source.alias)}"
|
|
101
|
+
raise TypeError(f"Unsupported source type: {type(source)}")
|
|
102
|
+
|
|
103
|
+
def _compile_table(self, table: Table) -> str:
|
|
104
|
+
if table.alias:
|
|
105
|
+
return f"{self._quote(table.name)} AS {self._quote(table.alias)}"
|
|
106
|
+
return self._quote(table.name)
|
|
107
|
+
|
|
108
|
+
def _compile_expr(self, expr: Any) -> str:
|
|
109
|
+
if isinstance(expr, Column):
|
|
110
|
+
return self._compile_column(expr)
|
|
111
|
+
if isinstance(expr, AliasExpr):
|
|
112
|
+
return f"{self._compile_expr(expr.expr)} AS {self._quote(expr.alias)}"
|
|
113
|
+
if isinstance(expr, Function):
|
|
114
|
+
args = ", ".join(self._compile_expr(a) for a in expr.args)
|
|
115
|
+
return f"{expr.name}({args})"
|
|
116
|
+
if isinstance(expr, OrderSpec):
|
|
117
|
+
return self._compile_order(expr)
|
|
118
|
+
if isinstance(expr, Literal):
|
|
119
|
+
return self._bind(expr.value)
|
|
120
|
+
if isinstance(expr, ast.Subquery):
|
|
121
|
+
return f"({self._compile_select(expr.query)})"
|
|
122
|
+
return self._compile_predicate(expr)
|
|
123
|
+
|
|
124
|
+
def _compile_predicate(self, pred: Predicate) -> str:
|
|
125
|
+
if isinstance(pred, BinaryPredicate):
|
|
126
|
+
return f"{self._compile_expr(pred.left)} {pred.op} {self._compile_expr(pred.right)}"
|
|
127
|
+
if isinstance(pred, UnaryPredicate):
|
|
128
|
+
return f"{self._compile_expr(pred.expr)} {pred.op}"
|
|
129
|
+
if isinstance(pred, LogicalPredicate):
|
|
130
|
+
inner = f" {pred.op} ".join(self._compile_predicate(p) for p in pred.predicates)
|
|
131
|
+
return f"({inner})"
|
|
132
|
+
if isinstance(pred, NotPredicate):
|
|
133
|
+
return f"NOT ({self._compile_predicate(pred.predicate)})"
|
|
134
|
+
raise TypeError(f"Unsupported predicate type: {type(pred)}")
|
|
135
|
+
|
|
136
|
+
def _compile_and_list(self, preds: Iterable[Predicate]) -> str:
|
|
137
|
+
parts = [self._compile_predicate(p) for p in preds]
|
|
138
|
+
return " AND ".join(parts)
|
|
139
|
+
|
|
140
|
+
def _compile_order(self, order: OrderSpec) -> str:
|
|
141
|
+
return f"{self._compile_expr(order.expr)} {order.direction}"
|
|
142
|
+
|
|
143
|
+
def _compile_column(self, col: Column) -> str:
|
|
144
|
+
if col.table.alias:
|
|
145
|
+
return f"{self._quote(col.table.alias)}.{self._quote(col.name)}"
|
|
146
|
+
return f"{self._quote(col.table.name)}.{self._quote(col.name)}"
|
|
147
|
+
|
|
148
|
+
def _quote(self, ident: str) -> str:
|
|
149
|
+
escaped = ident.replace('"', '""')
|
|
150
|
+
return f'"{escaped}"'
|
|
151
|
+
|
|
152
|
+
def _bind(self, value: Any) -> str:
|
|
153
|
+
name = f"p{self._param_index}"
|
|
154
|
+
self._param_index += 1
|
|
155
|
+
self.params[name] = value
|
|
156
|
+
return f":{name}"
|