sqlstratum 0.1.0__tar.gz → 0.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.
- {sqlstratum-0.1.0/sqlstratum.egg-info → sqlstratum-0.2.0}/PKG-INFO +70 -6
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/README.md +66 -5
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/pyproject.toml +12 -2
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/sqlstratum/ast.py +1 -1
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/sqlstratum/dsl.py +3 -3
- sqlstratum-0.1.0/sqlstratum/hydrate.py → sqlstratum-0.2.0/sqlstratum/hydrate/__init__.py +3 -3
- sqlstratum-0.2.0/sqlstratum/hydrate/pydantic.py +52 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/sqlstratum/runner.py +2 -2
- {sqlstratum-0.1.0 → sqlstratum-0.2.0/sqlstratum.egg-info}/PKG-INFO +70 -6
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/sqlstratum.egg-info/SOURCES.txt +3 -1
- sqlstratum-0.2.0/sqlstratum.egg-info/requires.txt +8 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/tests/test_hydration.py +3 -3
- sqlstratum-0.2.0/tests/test_pydantic_hydration.py +79 -0
- sqlstratum-0.1.0/sqlstratum.egg-info/requires.txt +0 -4
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/LICENSE +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/setup.cfg +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/sqlstratum/__init__.py +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/sqlstratum/compile.py +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/sqlstratum/expr.py +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/sqlstratum/meta.py +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/sqlstratum/types.py +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/sqlstratum.egg-info/dependency_links.txt +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/sqlstratum.egg-info/top_level.txt +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/tests/test_compile_aggregate.py +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/tests/test_compile_dml.py +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/tests/test_compile_join.py +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/tests/test_compile_select.py +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/tests/test_runner_debug_logging.py +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/tests/test_sqlite_integration.py +0 -0
- {sqlstratum-0.1.0 → sqlstratum-0.2.0}/tests/test_transactions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlstratum
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Lightweight, source-first SQL AST + compiler + runner.
|
|
5
5
|
Author-email: Antonio Ognio <aognio@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -47,13 +47,16 @@ Description-Content-Type: text/markdown
|
|
|
47
47
|
License-File: LICENSE
|
|
48
48
|
Provides-Extra: dev
|
|
49
49
|
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
50
|
+
Requires-Dist: poethepoet>=0.30.0; extra == "dev"
|
|
50
51
|
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
52
|
+
Provides-Extra: pydantic
|
|
53
|
+
Requires-Dist: pydantic>=2; extra == "pydantic"
|
|
51
54
|
Dynamic: license-file
|
|
52
55
|
|
|
53
56
|
# SQLStratum
|
|
54
57
|
|
|
55
58
|
<p align="center">
|
|
56
|
-
<img src="assets/images/SQLStratum-Logo-500x500-transparent.png" alt="SQLStratum logo" />
|
|
59
|
+
<img src="https://raw.githubusercontent.com/aognio/sqlstratum/main/assets/images/SQLStratum-Logo-500x500-transparent.png" alt="SQLStratum logo" />
|
|
57
60
|
</p>
|
|
58
61
|
|
|
59
62
|
SQLStratum is a modern, typed, deterministic SQL query builder and compiler for Python with a
|
|
@@ -101,7 +104,7 @@ q = (
|
|
|
101
104
|
SELECT(users.c.id, users.c.email)
|
|
102
105
|
.FROM(users)
|
|
103
106
|
.WHERE(users.c.active.is_true())
|
|
104
|
-
.
|
|
107
|
+
.hydrate(dict)
|
|
105
108
|
)
|
|
106
109
|
|
|
107
110
|
rows = runner.fetch_all(q)
|
|
@@ -110,7 +113,7 @@ print(rows)
|
|
|
110
113
|
|
|
111
114
|
## Why `Table` objects?
|
|
112
115
|
SQLStratum’s `Table` objects are the schema anchor for the typed, deterministic query builder. They
|
|
113
|
-
|
|
116
|
+
provide column metadata and a stable namespace for column access, which enables predictable SQL
|
|
114
117
|
generation and safe parameter binding. They also support explicit aliasing to avoid ambiguous column
|
|
115
118
|
names in joins.
|
|
116
119
|
|
|
@@ -118,7 +121,7 @@ names in joins.
|
|
|
118
121
|
- AST: immutable query nodes in `sqlstratum/ast.py`
|
|
119
122
|
- Compiler: SQL + params generation in `sqlstratum/compile.py`
|
|
120
123
|
- Runner: SQLite execution and transactions in `sqlstratum/runner.py`
|
|
121
|
-
- Hydration: projection rules and targets in `sqlstratum/hydrate
|
|
124
|
+
- Hydration: projection rules and targets in `sqlstratum/hydrate/`
|
|
122
125
|
|
|
123
126
|
## SQL Debugging
|
|
124
127
|
SQLStratum can log executed SQL statements (compiled SQL + parameters + duration), but logging is
|
|
@@ -154,6 +157,33 @@ SQL: <compiled sql> | params={<sorted params>} | duration_ms=<...>
|
|
|
154
157
|
Architectural intent: logging happens at the Runner boundary (after execution). AST building and
|
|
155
158
|
compilation remain deterministic and side-effect free, preserving separation of concerns.
|
|
156
159
|
|
|
160
|
+
## Pydantic Hydration (Optional)
|
|
161
|
+
SQLStratum does not depend on Pydantic, but it provides an optional hydration adapter for Pydantic
|
|
162
|
+
v2 models.
|
|
163
|
+
|
|
164
|
+
Install:
|
|
165
|
+
```
|
|
166
|
+
pip install sqlstratum[pydantic]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
```python
|
|
171
|
+
from pydantic import BaseModel
|
|
172
|
+
from sqlstratum.hydrate.pydantic import hydrate_model, using_pydantic
|
|
173
|
+
|
|
174
|
+
class User(BaseModel):
|
|
175
|
+
id: int
|
|
176
|
+
email: str
|
|
177
|
+
|
|
178
|
+
row = {"id": "1", "email": "a@b.com"}
|
|
179
|
+
user = hydrate_model(User, row)
|
|
180
|
+
|
|
181
|
+
q = using_pydantic(
|
|
182
|
+
SELECT(users.c.id, users.c.email).FROM(users).WHERE(users.c.id == 1)
|
|
183
|
+
).hydrate(User)
|
|
184
|
+
user_row = runner.fetch_one(q)
|
|
185
|
+
```
|
|
186
|
+
|
|
157
187
|
## Logo Inspiration
|
|
158
188
|
|
|
159
189
|
Vinicunca (Rainbow Mountain) in Peru’s Cusco Region — a high-altitude day hike from
|
|
@@ -161,7 +191,7 @@ Cusco at roughly 5,036 m (16,500 ft). See [Vinicunca](https://en.wikipedia.org/w
|
|
|
161
191
|
background.
|
|
162
192
|
|
|
163
193
|
## Versioning / Roadmap
|
|
164
|
-
Current version: `0.
|
|
194
|
+
Current version: `0.2.0`.
|
|
165
195
|
Design notes and current limitations are tracked in `NOTES.md`. Roadmap planning is intentionally
|
|
166
196
|
minimal at this stage and will evolve with real usage.
|
|
167
197
|
|
|
@@ -177,3 +207,37 @@ MIT License.
|
|
|
177
207
|
|
|
178
208
|
## Contributing
|
|
179
209
|
PRs are welcome. Please read `CONTRIBUTING.md` for the workflow and expectations.
|
|
210
|
+
|
|
211
|
+
## Documentation
|
|
212
|
+
Install docs dependencies:
|
|
213
|
+
```bash
|
|
214
|
+
python -m pip install -r docs/requirements.txt
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Run the local docs server:
|
|
218
|
+
```bash
|
|
219
|
+
mkdocs serve
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Build the static site:
|
|
223
|
+
```bash
|
|
224
|
+
mkdocs build --clean
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Read the Docs will build documentation automatically once the repository is imported.
|
|
228
|
+
|
|
229
|
+
## Release Automation
|
|
230
|
+
Install dev dependencies:
|
|
231
|
+
```bash
|
|
232
|
+
python -m pip install -e ".[dev]"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Run the full release pipeline:
|
|
236
|
+
```bash
|
|
237
|
+
poe release
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
This runs, in order:
|
|
241
|
+
- `python -m build --no-isolation`
|
|
242
|
+
- `python -m twine check dist/*`
|
|
243
|
+
- `python -m twine upload dist/*`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SQLStratum
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="assets/images/SQLStratum-Logo-500x500-transparent.png" alt="SQLStratum logo" />
|
|
4
|
+
<img src="https://raw.githubusercontent.com/aognio/sqlstratum/main/assets/images/SQLStratum-Logo-500x500-transparent.png" alt="SQLStratum logo" />
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
SQLStratum is a modern, typed, deterministic SQL query builder and compiler for Python with a
|
|
@@ -49,7 +49,7 @@ q = (
|
|
|
49
49
|
SELECT(users.c.id, users.c.email)
|
|
50
50
|
.FROM(users)
|
|
51
51
|
.WHERE(users.c.active.is_true())
|
|
52
|
-
.
|
|
52
|
+
.hydrate(dict)
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
rows = runner.fetch_all(q)
|
|
@@ -58,7 +58,7 @@ print(rows)
|
|
|
58
58
|
|
|
59
59
|
## Why `Table` objects?
|
|
60
60
|
SQLStratum’s `Table` objects are the schema anchor for the typed, deterministic query builder. They
|
|
61
|
-
|
|
61
|
+
provide column metadata and a stable namespace for column access, which enables predictable SQL
|
|
62
62
|
generation and safe parameter binding. They also support explicit aliasing to avoid ambiguous column
|
|
63
63
|
names in joins.
|
|
64
64
|
|
|
@@ -66,7 +66,7 @@ names in joins.
|
|
|
66
66
|
- AST: immutable query nodes in `sqlstratum/ast.py`
|
|
67
67
|
- Compiler: SQL + params generation in `sqlstratum/compile.py`
|
|
68
68
|
- Runner: SQLite execution and transactions in `sqlstratum/runner.py`
|
|
69
|
-
- Hydration: projection rules and targets in `sqlstratum/hydrate
|
|
69
|
+
- Hydration: projection rules and targets in `sqlstratum/hydrate/`
|
|
70
70
|
|
|
71
71
|
## SQL Debugging
|
|
72
72
|
SQLStratum can log executed SQL statements (compiled SQL + parameters + duration), but logging is
|
|
@@ -102,6 +102,33 @@ SQL: <compiled sql> | params={<sorted params>} | duration_ms=<...>
|
|
|
102
102
|
Architectural intent: logging happens at the Runner boundary (after execution). AST building and
|
|
103
103
|
compilation remain deterministic and side-effect free, preserving separation of concerns.
|
|
104
104
|
|
|
105
|
+
## Pydantic Hydration (Optional)
|
|
106
|
+
SQLStratum does not depend on Pydantic, but it provides an optional hydration adapter for Pydantic
|
|
107
|
+
v2 models.
|
|
108
|
+
|
|
109
|
+
Install:
|
|
110
|
+
```
|
|
111
|
+
pip install sqlstratum[pydantic]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
```python
|
|
116
|
+
from pydantic import BaseModel
|
|
117
|
+
from sqlstratum.hydrate.pydantic import hydrate_model, using_pydantic
|
|
118
|
+
|
|
119
|
+
class User(BaseModel):
|
|
120
|
+
id: int
|
|
121
|
+
email: str
|
|
122
|
+
|
|
123
|
+
row = {"id": "1", "email": "a@b.com"}
|
|
124
|
+
user = hydrate_model(User, row)
|
|
125
|
+
|
|
126
|
+
q = using_pydantic(
|
|
127
|
+
SELECT(users.c.id, users.c.email).FROM(users).WHERE(users.c.id == 1)
|
|
128
|
+
).hydrate(User)
|
|
129
|
+
user_row = runner.fetch_one(q)
|
|
130
|
+
```
|
|
131
|
+
|
|
105
132
|
## Logo Inspiration
|
|
106
133
|
|
|
107
134
|
Vinicunca (Rainbow Mountain) in Peru’s Cusco Region — a high-altitude day hike from
|
|
@@ -109,7 +136,7 @@ Cusco at roughly 5,036 m (16,500 ft). See [Vinicunca](https://en.wikipedia.org/w
|
|
|
109
136
|
background.
|
|
110
137
|
|
|
111
138
|
## Versioning / Roadmap
|
|
112
|
-
Current version: `0.
|
|
139
|
+
Current version: `0.2.0`.
|
|
113
140
|
Design notes and current limitations are tracked in `NOTES.md`. Roadmap planning is intentionally
|
|
114
141
|
minimal at this stage and will evolve with real usage.
|
|
115
142
|
|
|
@@ -125,3 +152,37 @@ MIT License.
|
|
|
125
152
|
|
|
126
153
|
## Contributing
|
|
127
154
|
PRs are welcome. Please read `CONTRIBUTING.md` for the workflow and expectations.
|
|
155
|
+
|
|
156
|
+
## Documentation
|
|
157
|
+
Install docs dependencies:
|
|
158
|
+
```bash
|
|
159
|
+
python -m pip install -r docs/requirements.txt
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Run the local docs server:
|
|
163
|
+
```bash
|
|
164
|
+
mkdocs serve
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Build the static site:
|
|
168
|
+
```bash
|
|
169
|
+
mkdocs build --clean
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Read the Docs will build documentation automatically once the repository is imported.
|
|
173
|
+
|
|
174
|
+
## Release Automation
|
|
175
|
+
Install dev dependencies:
|
|
176
|
+
```bash
|
|
177
|
+
python -m pip install -e ".[dev]"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Run the full release pipeline:
|
|
181
|
+
```bash
|
|
182
|
+
poe release
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
This runs, in order:
|
|
186
|
+
- `python -m build --no-isolation`
|
|
187
|
+
- `python -m twine check dist/*`
|
|
188
|
+
- `python -m twine upload dist/*`
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlstratum"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Lightweight, source-first SQL AST + compiler + runner."
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -37,8 +37,18 @@ Issues = "https://github.com/aognio/sqlstratum/issues"
|
|
|
37
37
|
[project.optional-dependencies]
|
|
38
38
|
dev = [
|
|
39
39
|
"build>=1.2.0",
|
|
40
|
+
"poethepoet>=0.30.0",
|
|
40
41
|
"twine>=5.0.0",
|
|
41
42
|
]
|
|
43
|
+
pydantic = [
|
|
44
|
+
"pydantic>=2",
|
|
45
|
+
]
|
|
42
46
|
|
|
43
47
|
[tool.setuptools]
|
|
44
|
-
packages = ["sqlstratum"]
|
|
48
|
+
packages = ["sqlstratum", "sqlstratum.hydrate"]
|
|
49
|
+
|
|
50
|
+
[tool.poe.tasks]
|
|
51
|
+
release-build.cmd = "python -m build --no-isolation"
|
|
52
|
+
release-check.cmd = "python -m twine check dist/*"
|
|
53
|
+
release-upload.cmd = "python -m twine upload dist/*"
|
|
54
|
+
release.sequence = ["release-build", "release-check", "release-upload"]
|
|
@@ -22,7 +22,7 @@ def SELECT(*projections: Expression) -> SelectQuery:
|
|
|
22
22
|
limit=None,
|
|
23
23
|
offset=None,
|
|
24
24
|
distinct=False,
|
|
25
|
-
|
|
25
|
+
hydration=None,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
|
|
@@ -100,7 +100,7 @@ def _as(self: SelectQuery, alias: str) -> Subquery:
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
def _hydrate(self: SelectQuery, target: HydrationTarget) -> SelectQuery:
|
|
103
|
-
return replace(self,
|
|
103
|
+
return replace(self, hydration=target)
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
SelectQuery.FROM = _from # type: ignore[attr-defined]
|
|
@@ -114,7 +114,7 @@ SelectQuery.LIMIT = _limit # type: ignore[attr-defined]
|
|
|
114
114
|
SelectQuery.OFFSET = _offset # type: ignore[attr-defined]
|
|
115
115
|
SelectQuery.DISTINCT = _distinct # type: ignore[attr-defined]
|
|
116
116
|
SelectQuery.AS = _as # type: ignore[attr-defined]
|
|
117
|
-
SelectQuery.
|
|
117
|
+
SelectQuery.hydrate = _hydrate # type: ignore[attr-defined]
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
class InsertBuilder:
|
|
@@ -4,9 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
from dataclasses import is_dataclass
|
|
5
5
|
from typing import Any, Dict, Iterable, List, Mapping, Sequence
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from
|
|
7
|
+
from ..expr import AliasExpr, Function
|
|
8
|
+
from ..meta import Column
|
|
9
|
+
from ..types import HydrationTarget
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class HydrationError(ValueError):
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Optional Pydantic v2 hydration adapters."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import importlib
|
|
5
|
+
from typing import Any, Mapping, Protocol, TypeVar
|
|
6
|
+
|
|
7
|
+
_INSTALL_MESSAGE = "Install with: pip install sqlstratum[pydantic]"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _PydanticModel(Protocol):
|
|
11
|
+
@classmethod
|
|
12
|
+
def model_validate(cls, obj: Any) -> Any: # pragma: no cover - protocol signature
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
TModel = TypeVar("TModel", bound=_PydanticModel)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _import_pydantic():
|
|
20
|
+
return importlib.import_module("pydantic")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_pydantic_available() -> bool:
|
|
24
|
+
try:
|
|
25
|
+
_import_pydantic()
|
|
26
|
+
return True
|
|
27
|
+
except Exception:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def hydrate_model(model_cls: type[TModel], data: Mapping[str, Any]) -> TModel:
|
|
32
|
+
try:
|
|
33
|
+
_import_pydantic()
|
|
34
|
+
except Exception as exc:
|
|
35
|
+
raise RuntimeError(_INSTALL_MESSAGE) from exc
|
|
36
|
+
return model_cls.model_validate(dict(data))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def hydrate_models(model_cls: type[TModel], rows: list[Mapping[str, Any]]) -> list[TModel]:
|
|
40
|
+
return [hydrate_model(model_cls, row) for row in rows]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _PydanticHydrateWrapper:
|
|
44
|
+
def __init__(self, query: Any) -> None:
|
|
45
|
+
self._query = query
|
|
46
|
+
|
|
47
|
+
def hydrate(self, model_cls: type[TModel]) -> Any:
|
|
48
|
+
return self._query.hydrate(lambda m: hydrate_model(model_cls, m))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def using_pydantic(query: Any) -> _PydanticHydrateWrapper:
|
|
52
|
+
return _PydanticHydrateWrapper(query)
|
|
@@ -87,7 +87,7 @@ class Runner:
|
|
|
87
87
|
rows = cur.fetchall()
|
|
88
88
|
if log_enabled:
|
|
89
89
|
_debug_log(compiled, (time.perf_counter() - start) * 1000)
|
|
90
|
-
return hydrate_rows(rows, query.projections, query.
|
|
90
|
+
return hydrate_rows(rows, query.projections, query.hydration or dict)
|
|
91
91
|
|
|
92
92
|
def fetch_one(self, query: ast.SelectQuery) -> Optional[Any]:
|
|
93
93
|
compiled = compile(query)
|
|
@@ -100,7 +100,7 @@ class Runner:
|
|
|
100
100
|
_debug_log(compiled, (time.perf_counter() - start) * 1000)
|
|
101
101
|
if row is None:
|
|
102
102
|
return None
|
|
103
|
-
return hydrate_rows([row], query.projections, query.
|
|
103
|
+
return hydrate_rows([row], query.projections, query.hydration or dict)[0]
|
|
104
104
|
|
|
105
105
|
def scalar(self, query: ast.SelectQuery) -> Optional[Any]:
|
|
106
106
|
compiled = compile(query)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlstratum
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Lightweight, source-first SQL AST + compiler + runner.
|
|
5
5
|
Author-email: Antonio Ognio <aognio@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -47,13 +47,16 @@ Description-Content-Type: text/markdown
|
|
|
47
47
|
License-File: LICENSE
|
|
48
48
|
Provides-Extra: dev
|
|
49
49
|
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
50
|
+
Requires-Dist: poethepoet>=0.30.0; extra == "dev"
|
|
50
51
|
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
52
|
+
Provides-Extra: pydantic
|
|
53
|
+
Requires-Dist: pydantic>=2; extra == "pydantic"
|
|
51
54
|
Dynamic: license-file
|
|
52
55
|
|
|
53
56
|
# SQLStratum
|
|
54
57
|
|
|
55
58
|
<p align="center">
|
|
56
|
-
<img src="assets/images/SQLStratum-Logo-500x500-transparent.png" alt="SQLStratum logo" />
|
|
59
|
+
<img src="https://raw.githubusercontent.com/aognio/sqlstratum/main/assets/images/SQLStratum-Logo-500x500-transparent.png" alt="SQLStratum logo" />
|
|
57
60
|
</p>
|
|
58
61
|
|
|
59
62
|
SQLStratum is a modern, typed, deterministic SQL query builder and compiler for Python with a
|
|
@@ -101,7 +104,7 @@ q = (
|
|
|
101
104
|
SELECT(users.c.id, users.c.email)
|
|
102
105
|
.FROM(users)
|
|
103
106
|
.WHERE(users.c.active.is_true())
|
|
104
|
-
.
|
|
107
|
+
.hydrate(dict)
|
|
105
108
|
)
|
|
106
109
|
|
|
107
110
|
rows = runner.fetch_all(q)
|
|
@@ -110,7 +113,7 @@ print(rows)
|
|
|
110
113
|
|
|
111
114
|
## Why `Table` objects?
|
|
112
115
|
SQLStratum’s `Table` objects are the schema anchor for the typed, deterministic query builder. They
|
|
113
|
-
|
|
116
|
+
provide column metadata and a stable namespace for column access, which enables predictable SQL
|
|
114
117
|
generation and safe parameter binding. They also support explicit aliasing to avoid ambiguous column
|
|
115
118
|
names in joins.
|
|
116
119
|
|
|
@@ -118,7 +121,7 @@ names in joins.
|
|
|
118
121
|
- AST: immutable query nodes in `sqlstratum/ast.py`
|
|
119
122
|
- Compiler: SQL + params generation in `sqlstratum/compile.py`
|
|
120
123
|
- Runner: SQLite execution and transactions in `sqlstratum/runner.py`
|
|
121
|
-
- Hydration: projection rules and targets in `sqlstratum/hydrate
|
|
124
|
+
- Hydration: projection rules and targets in `sqlstratum/hydrate/`
|
|
122
125
|
|
|
123
126
|
## SQL Debugging
|
|
124
127
|
SQLStratum can log executed SQL statements (compiled SQL + parameters + duration), but logging is
|
|
@@ -154,6 +157,33 @@ SQL: <compiled sql> | params={<sorted params>} | duration_ms=<...>
|
|
|
154
157
|
Architectural intent: logging happens at the Runner boundary (after execution). AST building and
|
|
155
158
|
compilation remain deterministic and side-effect free, preserving separation of concerns.
|
|
156
159
|
|
|
160
|
+
## Pydantic Hydration (Optional)
|
|
161
|
+
SQLStratum does not depend on Pydantic, but it provides an optional hydration adapter for Pydantic
|
|
162
|
+
v2 models.
|
|
163
|
+
|
|
164
|
+
Install:
|
|
165
|
+
```
|
|
166
|
+
pip install sqlstratum[pydantic]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
```python
|
|
171
|
+
from pydantic import BaseModel
|
|
172
|
+
from sqlstratum.hydrate.pydantic import hydrate_model, using_pydantic
|
|
173
|
+
|
|
174
|
+
class User(BaseModel):
|
|
175
|
+
id: int
|
|
176
|
+
email: str
|
|
177
|
+
|
|
178
|
+
row = {"id": "1", "email": "a@b.com"}
|
|
179
|
+
user = hydrate_model(User, row)
|
|
180
|
+
|
|
181
|
+
q = using_pydantic(
|
|
182
|
+
SELECT(users.c.id, users.c.email).FROM(users).WHERE(users.c.id == 1)
|
|
183
|
+
).hydrate(User)
|
|
184
|
+
user_row = runner.fetch_one(q)
|
|
185
|
+
```
|
|
186
|
+
|
|
157
187
|
## Logo Inspiration
|
|
158
188
|
|
|
159
189
|
Vinicunca (Rainbow Mountain) in Peru’s Cusco Region — a high-altitude day hike from
|
|
@@ -161,7 +191,7 @@ Cusco at roughly 5,036 m (16,500 ft). See [Vinicunca](https://en.wikipedia.org/w
|
|
|
161
191
|
background.
|
|
162
192
|
|
|
163
193
|
## Versioning / Roadmap
|
|
164
|
-
Current version: `0.
|
|
194
|
+
Current version: `0.2.0`.
|
|
165
195
|
Design notes and current limitations are tracked in `NOTES.md`. Roadmap planning is intentionally
|
|
166
196
|
minimal at this stage and will evolve with real usage.
|
|
167
197
|
|
|
@@ -177,3 +207,37 @@ MIT License.
|
|
|
177
207
|
|
|
178
208
|
## Contributing
|
|
179
209
|
PRs are welcome. Please read `CONTRIBUTING.md` for the workflow and expectations.
|
|
210
|
+
|
|
211
|
+
## Documentation
|
|
212
|
+
Install docs dependencies:
|
|
213
|
+
```bash
|
|
214
|
+
python -m pip install -r docs/requirements.txt
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Run the local docs server:
|
|
218
|
+
```bash
|
|
219
|
+
mkdocs serve
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Build the static site:
|
|
223
|
+
```bash
|
|
224
|
+
mkdocs build --clean
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Read the Docs will build documentation automatically once the repository is imported.
|
|
228
|
+
|
|
229
|
+
## Release Automation
|
|
230
|
+
Install dev dependencies:
|
|
231
|
+
```bash
|
|
232
|
+
python -m pip install -e ".[dev]"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Run the full release pipeline:
|
|
236
|
+
```bash
|
|
237
|
+
poe release
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
This runs, in order:
|
|
241
|
+
- `python -m build --no-isolation`
|
|
242
|
+
- `python -m twine check dist/*`
|
|
243
|
+
- `python -m twine upload dist/*`
|
|
@@ -6,7 +6,6 @@ sqlstratum/ast.py
|
|
|
6
6
|
sqlstratum/compile.py
|
|
7
7
|
sqlstratum/dsl.py
|
|
8
8
|
sqlstratum/expr.py
|
|
9
|
-
sqlstratum/hydrate.py
|
|
10
9
|
sqlstratum/meta.py
|
|
11
10
|
sqlstratum/runner.py
|
|
12
11
|
sqlstratum/types.py
|
|
@@ -15,11 +14,14 @@ sqlstratum.egg-info/SOURCES.txt
|
|
|
15
14
|
sqlstratum.egg-info/dependency_links.txt
|
|
16
15
|
sqlstratum.egg-info/requires.txt
|
|
17
16
|
sqlstratum.egg-info/top_level.txt
|
|
17
|
+
sqlstratum/hydrate/__init__.py
|
|
18
|
+
sqlstratum/hydrate/pydantic.py
|
|
18
19
|
tests/test_compile_aggregate.py
|
|
19
20
|
tests/test_compile_dml.py
|
|
20
21
|
tests/test_compile_join.py
|
|
21
22
|
tests/test_compile_select.py
|
|
22
23
|
tests/test_hydration.py
|
|
24
|
+
tests/test_pydantic_hydration.py
|
|
23
25
|
tests/test_runner_debug_logging.py
|
|
24
26
|
tests/test_sqlite_integration.py
|
|
25
27
|
tests/test_transactions.py
|
|
@@ -30,7 +30,7 @@ class TestHydration(unittest.TestCase):
|
|
|
30
30
|
self.conn.close()
|
|
31
31
|
|
|
32
32
|
def test_dict_and_json(self):
|
|
33
|
-
q = SELECT(users.c.id, users.c.email).FROM(users).WHERE(users.c.id == 1).
|
|
33
|
+
q = SELECT(users.c.id, users.c.email).FROM(users).WHERE(users.c.id == 1).hydrate(dict)
|
|
34
34
|
rows = self.runner.fetch_all(q)
|
|
35
35
|
self.assertEqual(rows, [{"id": 1, "email": "a@b.com"}])
|
|
36
36
|
json.dumps(rows)
|
|
@@ -41,12 +41,12 @@ class TestHydration(unittest.TestCase):
|
|
|
41
41
|
id: int
|
|
42
42
|
email: str
|
|
43
43
|
|
|
44
|
-
q = SELECT(users.c.id, users.c.email).FROM(users).WHERE(users.c.id == 1).
|
|
44
|
+
q = SELECT(users.c.id, users.c.email).FROM(users).WHERE(users.c.id == 1).hydrate(User)
|
|
45
45
|
row = self.runner.fetch_one(q)
|
|
46
46
|
self.assertEqual(row, User(id=1, email="a@b.com"))
|
|
47
47
|
|
|
48
48
|
def test_callable(self):
|
|
49
|
-
q = SELECT(users.c.id, users.c.email).FROM(users).WHERE(users.c.id == 1).
|
|
49
|
+
q = SELECT(users.c.id, users.c.email).FROM(users).WHERE(users.c.id == 1).hydrate(
|
|
50
50
|
lambda m: f"{m['id']}:{m['email']}"
|
|
51
51
|
)
|
|
52
52
|
row = self.runner.fetch_one(q)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest import mock
|
|
4
|
+
|
|
5
|
+
from sqlstratum.hydrate import pydantic as pydantic_hydrate
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestPydanticHydration(unittest.TestCase):
|
|
9
|
+
def test_missing_pydantic_raises(self):
|
|
10
|
+
class DummyModel:
|
|
11
|
+
@classmethod
|
|
12
|
+
def model_validate(cls, obj):
|
|
13
|
+
return obj
|
|
14
|
+
|
|
15
|
+
with mock.patch(
|
|
16
|
+
"sqlstratum.hydrate.pydantic._import_pydantic",
|
|
17
|
+
side_effect=ImportError("no pydantic"),
|
|
18
|
+
):
|
|
19
|
+
with self.assertRaises(RuntimeError) as cm:
|
|
20
|
+
pydantic_hydrate.hydrate_model(DummyModel, {"id": 1})
|
|
21
|
+
self.assertIn("pip install sqlstratum[pydantic]", str(cm.exception))
|
|
22
|
+
|
|
23
|
+
def test_is_pydantic_available_false_when_missing(self):
|
|
24
|
+
with mock.patch(
|
|
25
|
+
"sqlstratum.hydrate.pydantic._import_pydantic",
|
|
26
|
+
side_effect=ImportError("no pydantic"),
|
|
27
|
+
):
|
|
28
|
+
self.assertFalse(pydantic_hydrate.is_pydantic_available())
|
|
29
|
+
|
|
30
|
+
def test_hydrate_model_and_models(self):
|
|
31
|
+
try:
|
|
32
|
+
pydantic = importlib.import_module("pydantic")
|
|
33
|
+
except Exception:
|
|
34
|
+
self.skipTest("Pydantic not installed")
|
|
35
|
+
|
|
36
|
+
class User(pydantic.BaseModel):
|
|
37
|
+
id: int
|
|
38
|
+
email: str
|
|
39
|
+
|
|
40
|
+
row = {"id": "1", "email": "a@b.com"}
|
|
41
|
+
user = pydantic_hydrate.hydrate_model(User, row)
|
|
42
|
+
self.assertIsInstance(user, User)
|
|
43
|
+
self.assertEqual(user.id, 1)
|
|
44
|
+
self.assertEqual(user.email, "a@b.com")
|
|
45
|
+
|
|
46
|
+
users = pydantic_hydrate.hydrate_models(User, [row])
|
|
47
|
+
self.assertEqual(users, [user])
|
|
48
|
+
|
|
49
|
+
def test_using_pydantic_wrapper(self):
|
|
50
|
+
try:
|
|
51
|
+
pydantic = importlib.import_module("pydantic")
|
|
52
|
+
except Exception:
|
|
53
|
+
self.skipTest("Pydantic not installed")
|
|
54
|
+
|
|
55
|
+
class User(pydantic.BaseModel):
|
|
56
|
+
id: int
|
|
57
|
+
email: str
|
|
58
|
+
|
|
59
|
+
class DummyQuery:
|
|
60
|
+
def __init__(self):
|
|
61
|
+
self.target = None
|
|
62
|
+
|
|
63
|
+
def hydrate(self, target):
|
|
64
|
+
self.target = target
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
query = DummyQuery()
|
|
68
|
+
wrapped = pydantic_hydrate.using_pydantic(query)
|
|
69
|
+
result = wrapped.hydrate(User)
|
|
70
|
+
self.assertIs(result, query)
|
|
71
|
+
self.assertIsNotNone(query.target)
|
|
72
|
+
hydrated = query.target({"id": "2", "email": "b@c.com"})
|
|
73
|
+
self.assertIsInstance(hydrated, User)
|
|
74
|
+
self.assertEqual(hydrated.id, 2)
|
|
75
|
+
self.assertEqual(hydrated.email, "b@c.com")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|