rowguard 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.
- rowguard-0.1.0/.github/workflows/benchmarks.yml +23 -0
- rowguard-0.1.0/.github/workflows/ci.yml +53 -0
- rowguard-0.1.0/.github/workflows/release.yml +62 -0
- rowguard-0.1.0/.gitignore +38 -0
- rowguard-0.1.0/.pre-commit-config.yaml +18 -0
- rowguard-0.1.0/API.md +177 -0
- rowguard-0.1.0/ARCHITECTURE.md +265 -0
- rowguard-0.1.0/CHANGELOG.md +41 -0
- rowguard-0.1.0/CURSOR_PROMPT.md +54 -0
- rowguard-0.1.0/LICENSE +5 -0
- rowguard-0.1.0/MANIFEST.md +28 -0
- rowguard-0.1.0/Makefile +21 -0
- rowguard-0.1.0/PKG-INFO +174 -0
- rowguard-0.1.0/README.md +129 -0
- rowguard-0.1.0/ROADMAP.md +193 -0
- rowguard-0.1.0/SPEC.md +325 -0
- rowguard-0.1.0/benchmarks/conftest.py +0 -0
- rowguard-0.1.0/benchmarks/test_validation_benchmark.py +17 -0
- rowguard-0.1.0/docs/architecture/ASYNC.md +210 -0
- rowguard-0.1.0/docs/architecture/CACHE.md +1429 -0
- rowguard-0.1.0/docs/architecture/DESIGN_DECISIONS.md +1241 -0
- rowguard-0.1.0/docs/architecture/DIALECT_SUPPORT.md +1387 -0
- rowguard-0.1.0/docs/architecture/EXECUTION_PIPELINE.md +249 -0
- rowguard-0.1.0/docs/architecture/FILTER_PUSHDOWN.md +918 -0
- rowguard-0.1.0/docs/architecture/INTERNAL_API.md +1388 -0
- rowguard-0.1.0/docs/architecture/PERFORMANCE.md +1345 -0
- rowguard-0.1.0/docs/architecture/PLUGIN_SYSTEM.md +1772 -0
- rowguard-0.1.0/docs/architecture/QUERY_COMPILATION.md +273 -0
- rowguard-0.1.0/docs/architecture/QUERY_ENGINE.md +247 -0
- rowguard-0.1.0/docs/architecture/RESULT_OBJECT.md +701 -0
- rowguard-0.1.0/docs/architecture/ROW_ADAPTER.md +726 -0
- rowguard-0.1.0/docs/architecture/SQLRULES_INTEGRATION.md +345 -0
- rowguard-0.1.0/docs/architecture/STREAMING.md +270 -0
- rowguard-0.1.0/docs/architecture/VALIDATION_ENGINE.md +296 -0
- rowguard-0.1.0/docs/developer/BENCHMARKS.md +822 -0
- rowguard-0.1.0/docs/developer/CONTRIBUTING.md +760 -0
- rowguard-0.1.0/docs/developer/MILESTONES.md +567 -0
- rowguard-0.1.0/docs/developer/TESTING.md +913 -0
- rowguard-0.1.0/docs/integrations/CORE.md +1169 -0
- rowguard-0.1.0/docs/integrations/ORM.md +1218 -0
- rowguard-0.1.0/docs/integrations/RAW_SQL.md +223 -0
- rowguard-0.1.0/docs/integrations/REFLECTION.md +263 -0
- rowguard-0.1.0/docs/integrations/SQLMODEL.md +1316 -0
- rowguard-0.1.0/docs/integrations/WHY_NOT_SQLMODEL.md +268 -0
- rowguard-0.1.0/docs/rejection/CALLBACKS.md +1160 -0
- rowguard-0.1.0/docs/rejection/DIAGNOSTICS.md +263 -0
- rowguard-0.1.0/docs/rejection/QUARANTINE.md +1395 -0
- rowguard-0.1.0/docs/rejection/REJECTION_HANDLING.md +436 -0
- rowguard-0.1.0/docs/rejection/REJECTION_POLICIES.md +277 -0
- rowguard-0.1.0/docs/validation/PARTIAL_VALIDATION.md +260 -0
- rowguard-0.1.0/docs/validation/PYDANTIC.md +1538 -0
- rowguard-0.1.0/docs/validation/TYPE_SUPPORT.md +1655 -0
- rowguard-0.1.0/docs/validation/VALIDATION_ERRORS.md +1193 -0
- rowguard-0.1.0/examples/basic.py +48 -0
- rowguard-0.1.0/pyproject.toml +105 -0
- rowguard-0.1.0/src/rowguard/__init__.py +31 -0
- rowguard-0.1.0/src/rowguard/adapters/__init__.py +6 -0
- rowguard-0.1.0/src/rowguard/adapters/base.py +15 -0
- rowguard-0.1.0/src/rowguard/adapters/sqlalchemy_row.py +46 -0
- rowguard-0.1.0/src/rowguard/api.py +130 -0
- rowguard-0.1.0/src/rowguard/cache.py +26 -0
- rowguard-0.1.0/src/rowguard/diagnostics.py +15 -0
- rowguard-0.1.0/src/rowguard/errors.py +59 -0
- rowguard-0.1.0/src/rowguard/execution/__init__.py +6 -0
- rowguard-0.1.0/src/rowguard/execution/processor.py +112 -0
- rowguard-0.1.0/src/rowguard/execution/streaming.py +14 -0
- rowguard-0.1.0/src/rowguard/execution/sync.py +152 -0
- rowguard-0.1.0/src/rowguard/integrations/__init__.py +11 -0
- rowguard-0.1.0/src/rowguard/integrations/sqlalchemy_core.py +28 -0
- rowguard-0.1.0/src/rowguard/integrations/sqlalchemy_orm.py +5 -0
- rowguard-0.1.0/src/rowguard/integrations/sqlmodel.py +5 -0
- rowguard-0.1.0/src/rowguard/integrations/sqlrules.py +68 -0
- rowguard-0.1.0/src/rowguard/planning/__init__.py +7 -0
- rowguard-0.1.0/src/rowguard/planning/compiler.py +116 -0
- rowguard-0.1.0/src/rowguard/planning/execution_plan.py +30 -0
- rowguard-0.1.0/src/rowguard/planning/request.py +24 -0
- rowguard-0.1.0/src/rowguard/plugins/__init__.py +0 -0
- rowguard-0.1.0/src/rowguard/plugins/registry.py +13 -0
- rowguard-0.1.0/src/rowguard/py.typed +0 -0
- rowguard-0.1.0/src/rowguard/rejection/__init__.py +12 -0
- rowguard-0.1.0/src/rowguard/rejection/base.py +14 -0
- rowguard-0.1.0/src/rowguard/rejection/policies.py +40 -0
- rowguard-0.1.0/src/rowguard/results/__init__.py +6 -0
- rowguard-0.1.0/src/rowguard/results/query_result.py +45 -0
- rowguard-0.1.0/src/rowguard/results/rejected_row.py +16 -0
- rowguard-0.1.0/src/rowguard/statistics.py +19 -0
- rowguard-0.1.0/src/rowguard/validation/__init__.py +6 -0
- rowguard-0.1.0/src/rowguard/validation/base.py +20 -0
- rowguard-0.1.0/src/rowguard/validation/pydantic.py +20 -0
- rowguard-0.1.0/tests/conftest.py +48 -0
- rowguard-0.1.0/tests/integration/test_core.py +177 -0
- rowguard-0.1.0/tests/typing/query_result_typing.py +12 -0
- rowguard-0.1.0/tests/unit/test_bugfix_regressions.py +211 -0
- rowguard-0.1.0/tests/unit/test_coverage_edges.py +155 -0
- rowguard-0.1.0/tests/unit/test_planner.py +85 -0
- rowguard-0.1.0/tests/unit/test_processor.py +59 -0
- rowguard-0.1.0/tests/unit/test_pydantic_validator.py +21 -0
- rowguard-0.1.0/tests/unit/test_query_result.py +58 -0
- rowguard-0.1.0/tests/unit/test_rejection_policies.py +70 -0
- rowguard-0.1.0/tests/unit/test_row_adapter.py +50 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Benchmarks
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: "0 6 * * 1"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
benchmark:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- run: |
|
|
17
|
+
python -m pip install --upgrade pip
|
|
18
|
+
pip install -e ".[dev]"
|
|
19
|
+
pytest benchmarks --benchmark-only --benchmark-json=benchmark.json
|
|
20
|
+
- uses: actions/upload-artifact@v4
|
|
21
|
+
with:
|
|
22
|
+
name: benchmark-results
|
|
23
|
+
path: benchmark.json
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install package
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install -e ".[dev,async]"
|
|
30
|
+
|
|
31
|
+
- name: Ruff
|
|
32
|
+
run: ruff check .
|
|
33
|
+
|
|
34
|
+
- name: Mypy
|
|
35
|
+
run: mypy src/rowguard
|
|
36
|
+
|
|
37
|
+
- name: Pytest
|
|
38
|
+
run: pytest --cov=rowguard --cov-report=term-missing
|
|
39
|
+
|
|
40
|
+
- name: Example smoke
|
|
41
|
+
run: python examples/basic.py
|
|
42
|
+
|
|
43
|
+
benchmark-smoke:
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
- uses: actions/setup-python@v5
|
|
48
|
+
with:
|
|
49
|
+
python-version: "3.12"
|
|
50
|
+
- run: |
|
|
51
|
+
python -m pip install --upgrade pip
|
|
52
|
+
pip install -e ".[dev]"
|
|
53
|
+
pytest benchmarks --benchmark-only
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
ci:
|
|
13
|
+
name: Commit checks
|
|
14
|
+
uses: ./.github/workflows/ci.yml
|
|
15
|
+
|
|
16
|
+
publish:
|
|
17
|
+
name: Publish to PyPI
|
|
18
|
+
needs: ci
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.12"
|
|
27
|
+
|
|
28
|
+
- name: Install build tools
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --upgrade pip
|
|
31
|
+
python -m pip install build twine
|
|
32
|
+
|
|
33
|
+
- name: Verify tag matches package version
|
|
34
|
+
run: |
|
|
35
|
+
python - <<'PY'
|
|
36
|
+
import os
|
|
37
|
+
import tomllib
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
tag = os.environ["GITHUB_REF_NAME"]
|
|
41
|
+
assert tag.startswith("v"), tag
|
|
42
|
+
expected = tag[1:]
|
|
43
|
+
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
|
44
|
+
init = Path("src/rowguard/__init__.py").read_text()
|
|
45
|
+
marker = '__version__ = "'
|
|
46
|
+
start = init.index(marker) + len(marker)
|
|
47
|
+
end = init.index('"', start)
|
|
48
|
+
init_version = init[start:end]
|
|
49
|
+
assert version == expected == init_version, (version, expected, init_version)
|
|
50
|
+
print(f"tag/version ok: {tag} -> {version}")
|
|
51
|
+
PY
|
|
52
|
+
|
|
53
|
+
- name: Build distributions
|
|
54
|
+
run: python -m build
|
|
55
|
+
|
|
56
|
+
- name: Twine check
|
|
57
|
+
run: twine check dist/*
|
|
58
|
+
|
|
59
|
+
- name: Publish to PyPI
|
|
60
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
61
|
+
with:
|
|
62
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Bytecode / caches
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
wheels/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
.eggs/
|
|
12
|
+
*.egg
|
|
13
|
+
|
|
14
|
+
# Virtual environments
|
|
15
|
+
.venv/
|
|
16
|
+
venv/
|
|
17
|
+
ENV/
|
|
18
|
+
|
|
19
|
+
# Test / type / lint caches
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
.coverage.*
|
|
25
|
+
htmlcov/
|
|
26
|
+
.hypothesis/
|
|
27
|
+
.benchmarks/
|
|
28
|
+
|
|
29
|
+
# Tooling
|
|
30
|
+
.pre-commit/
|
|
31
|
+
*.log
|
|
32
|
+
.DS_Store
|
|
33
|
+
|
|
34
|
+
# Editors
|
|
35
|
+
.idea/
|
|
36
|
+
.vscode/
|
|
37
|
+
*.swp
|
|
38
|
+
*~
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.9.9
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff
|
|
6
|
+
args: [--fix]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
|
|
9
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
10
|
+
rev: v1.15.0
|
|
11
|
+
hooks:
|
|
12
|
+
- id: mypy
|
|
13
|
+
additional_dependencies:
|
|
14
|
+
- pydantic>=2.7
|
|
15
|
+
- SQLAlchemy>=2.0
|
|
16
|
+
- sqlrules>=0.2.0
|
|
17
|
+
args: [--config-file=pyproject.toml]
|
|
18
|
+
pass_filenames: false
|
rowguard-0.1.0/API.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# API.md
|
|
2
|
+
|
|
3
|
+
# RowGuard Public API
|
|
4
|
+
|
|
5
|
+
## Philosophy
|
|
6
|
+
|
|
7
|
+
The public API is intentionally small. RowGuard should expose a few
|
|
8
|
+
powerful, composable entry points while keeping execution details
|
|
9
|
+
internal.
|
|
10
|
+
|
|
11
|
+
## Core Functions
|
|
12
|
+
|
|
13
|
+
### select()
|
|
14
|
+
|
|
15
|
+
Execute a SQL query, validate every returned row against a Pydantic
|
|
16
|
+
model, and return a `QueryResult`.
|
|
17
|
+
|
|
18
|
+
``` python
|
|
19
|
+
result = rowguard.select(
|
|
20
|
+
session=session,
|
|
21
|
+
table=users,
|
|
22
|
+
model=UserRead,
|
|
23
|
+
where=(),
|
|
24
|
+
field_map=None,
|
|
25
|
+
column_map=None,
|
|
26
|
+
parameters=None,
|
|
27
|
+
on_reject="raise",
|
|
28
|
+
use_sqlrules=True,
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
|
|
34
|
+
- `session` or `connection`: exactly one SQLAlchemy execution context
|
|
35
|
+
- `table`: SQLAlchemy Core `Table` or selectable
|
|
36
|
+
- `model`: Pydantic `BaseModel` subclass
|
|
37
|
+
- `where`: optional additional SQLAlchemy expressions (default `()`)
|
|
38
|
+
- `field_map`: optional model-field → result-key mapping
|
|
39
|
+
- `column_map`: optional model-field → SQLAlchemy column mapping for SQLRules
|
|
40
|
+
- `parameters`: optional bound parameters forwarded to SQLAlchemy
|
|
41
|
+
- `on_reject`: `raise`, `collect`, or `skip`
|
|
42
|
+
(`callback` / `quarantine` / `log` planned for later releases)
|
|
43
|
+
- `use_sqlrules`: enable SQLRules constraint pushdown (default `True`)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
|
|
47
|
+
``` python
|
|
48
|
+
QueryResult[UserRead]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
### stream()
|
|
54
|
+
|
|
55
|
+
Validate rows incrementally while streaming large result sets.
|
|
56
|
+
|
|
57
|
+
> **Status (0.1.0):** Not implemented. Raises `NotImplementedError`.
|
|
58
|
+
> Planned for 0.3.0.
|
|
59
|
+
|
|
60
|
+
``` python
|
|
61
|
+
for model in rowguard.stream(
|
|
62
|
+
session=session,
|
|
63
|
+
statement=stmt,
|
|
64
|
+
model=UserRead,
|
|
65
|
+
):
|
|
66
|
+
...
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
### execute()
|
|
72
|
+
|
|
73
|
+
Execute an existing SQLAlchemy statement instead of constructing one.
|
|
74
|
+
|
|
75
|
+
``` python
|
|
76
|
+
result = rowguard.execute(
|
|
77
|
+
session=session,
|
|
78
|
+
statement=stmt,
|
|
79
|
+
model=UserRead,
|
|
80
|
+
source=None,
|
|
81
|
+
on_reject="raise",
|
|
82
|
+
use_sqlrules=True,
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Optional `source=` supplies the selectable used for SQLRules pushdown when
|
|
87
|
+
`use_sqlrules=True`. When both `statement` and `source` are provided, RowGuard
|
|
88
|
+
emits a `sqlrules.pushdown_source_explicit` diagnostic.
|
|
89
|
+
|
|
90
|
+
------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
### validate_rows()
|
|
93
|
+
|
|
94
|
+
Validate an iterable of row mappings without executing SQL.
|
|
95
|
+
|
|
96
|
+
``` python
|
|
97
|
+
result = rowguard.validate_rows(
|
|
98
|
+
rows=rows,
|
|
99
|
+
model=UserRead,
|
|
100
|
+
field_map=None,
|
|
101
|
+
on_reject="raise",
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Useful for CSV readers, ETL pipelines, or custom data sources.
|
|
106
|
+
|
|
107
|
+
## QueryResult
|
|
108
|
+
|
|
109
|
+
``` python
|
|
110
|
+
result.models
|
|
111
|
+
result.rejected
|
|
112
|
+
result.statistics
|
|
113
|
+
result.statement
|
|
114
|
+
result.diagnostics
|
|
115
|
+
result.execution_time
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Convenience properties:
|
|
119
|
+
|
|
120
|
+
``` python
|
|
121
|
+
result.valid_count
|
|
122
|
+
result.rejected_count # retained rejections only (0 under skip)
|
|
123
|
+
result.has_rejections # True if any row was rejected (including skip)
|
|
124
|
+
result.is_clean
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`has_rejections` is based on `statistics.rows_rejected`, not on whether
|
|
128
|
+
rejected rows were retained. Under `skip`, `has_rejections` may be `True`
|
|
129
|
+
while `rejected` is empty.
|
|
130
|
+
|
|
131
|
+
`execution_time` is end-to-end wall time in seconds (statement fetch plus
|
|
132
|
+
validation for SQL paths; full processing for `validate_rows`).
|
|
133
|
+
|
|
134
|
+
## RejectedRow
|
|
135
|
+
|
|
136
|
+
Each rejected row exposes:
|
|
137
|
+
|
|
138
|
+
``` python
|
|
139
|
+
rejected.index
|
|
140
|
+
rejected.model
|
|
141
|
+
rejected.mapping
|
|
142
|
+
rejected.validation_error
|
|
143
|
+
rejected.adaptation_error
|
|
144
|
+
rejected.raw_row
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Statistics
|
|
148
|
+
|
|
149
|
+
`QueryStatistics` fields:
|
|
150
|
+
|
|
151
|
+
- `rows_read`
|
|
152
|
+
- `rows_validated` (rows that reached Pydantic validation)
|
|
153
|
+
- `rows_accepted`
|
|
154
|
+
- `rows_rejected`
|
|
155
|
+
- `execution_time_ns`
|
|
156
|
+
- `adaptation_time_ns`
|
|
157
|
+
- `validation_time_ns`
|
|
158
|
+
- `rejection_time_ns`
|
|
159
|
+
|
|
160
|
+
## Async API (Planned — 0.4.0)
|
|
161
|
+
|
|
162
|
+
> Not available in 0.1.0.
|
|
163
|
+
|
|
164
|
+
``` python
|
|
165
|
+
await rowguard.aselect(...)
|
|
166
|
+
await rowguard.aexecute(...)
|
|
167
|
+
async for model in rowguard.astream(...):
|
|
168
|
+
...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Design Guidelines
|
|
172
|
+
|
|
173
|
+
- Return immutable result objects where practical.
|
|
174
|
+
- Never silently discard invalid rows without counting them.
|
|
175
|
+
- Keep function names short and predictable.
|
|
176
|
+
- Prefer explicit configuration over implicit behavior.
|
|
177
|
+
- Build on SQLAlchemy and SQLRules rather than replacing them.
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# ARCHITECTURE.md
|
|
2
|
+
|
|
3
|
+
# RowGuard Architecture
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
RowGuard is a layered validation-first query engine built on top of
|
|
8
|
+
SQLAlchemy, Pydantic, and SQLRules.
|
|
9
|
+
|
|
10
|
+
Its architecture intentionally separates SQL compilation, query
|
|
11
|
+
execution, row adaptation, validation, rejection handling, and result
|
|
12
|
+
construction.
|
|
13
|
+
|
|
14
|
+
``` text
|
|
15
|
+
Application
|
|
16
|
+
│
|
|
17
|
+
▼
|
|
18
|
+
RowGuard Public API
|
|
19
|
+
│
|
|
20
|
+
┌───────────────┼────────────────┐
|
|
21
|
+
▼ ▼ ▼
|
|
22
|
+
Query Builder Validation Engine Streaming API
|
|
23
|
+
│ │ │
|
|
24
|
+
└───────────────┼────────────────┘
|
|
25
|
+
▼
|
|
26
|
+
Execution Engine
|
|
27
|
+
│
|
|
28
|
+
▼
|
|
29
|
+
SQLAlchemy Engine
|
|
30
|
+
│
|
|
31
|
+
▼
|
|
32
|
+
Database
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
# Architectural Principles
|
|
38
|
+
|
|
39
|
+
- Single responsibility per component
|
|
40
|
+
- SQLAlchemy-native
|
|
41
|
+
- Pydantic-native
|
|
42
|
+
- SQLRules for SQL pushdown only
|
|
43
|
+
- Immutable data flow where practical
|
|
44
|
+
- Explicit rejection handling
|
|
45
|
+
- Observable execution
|
|
46
|
+
|
|
47
|
+
------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
# Layered Design
|
|
50
|
+
|
|
51
|
+
## Layer 1 -- Public API
|
|
52
|
+
|
|
53
|
+
Responsibilities:
|
|
54
|
+
|
|
55
|
+
- Accept user requests
|
|
56
|
+
- Configure execution
|
|
57
|
+
- Return QueryResult
|
|
58
|
+
|
|
59
|
+
Representative API:
|
|
60
|
+
|
|
61
|
+
``` python
|
|
62
|
+
rowguard.select(...)
|
|
63
|
+
rowguard.stream(...)
|
|
64
|
+
rowguard.execute(...)
|
|
65
|
+
rowguard.validate_rows(...)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
## Layer 2 -- Query Builder
|
|
71
|
+
|
|
72
|
+
Responsibilities:
|
|
73
|
+
|
|
74
|
+
- Accept SQLAlchemy objects
|
|
75
|
+
- Invoke SQLRules
|
|
76
|
+
- Merge WHERE expressions
|
|
77
|
+
- Produce executable SQLAlchemy statements
|
|
78
|
+
|
|
79
|
+
Output:
|
|
80
|
+
|
|
81
|
+
``` text
|
|
82
|
+
SQLAlchemy Select
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
## Layer 3 -- Execution Engine
|
|
88
|
+
|
|
89
|
+
Responsibilities:
|
|
90
|
+
|
|
91
|
+
- Execute statements
|
|
92
|
+
- Iterate database rows
|
|
93
|
+
- Support sync and async execution
|
|
94
|
+
- Support streaming
|
|
95
|
+
|
|
96
|
+
The execution engine never performs validation itself.
|
|
97
|
+
|
|
98
|
+
------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
## Layer 4 -- Row Adapter
|
|
101
|
+
|
|
102
|
+
Responsibilities:
|
|
103
|
+
|
|
104
|
+
- Convert SQLAlchemy rows into mappings
|
|
105
|
+
- Preserve column names
|
|
106
|
+
- Apply aliases when configured
|
|
107
|
+
|
|
108
|
+
Output:
|
|
109
|
+
|
|
110
|
+
``` python
|
|
111
|
+
dict[str, object]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
## Layer 5 -- Validation Engine
|
|
117
|
+
|
|
118
|
+
Responsibilities:
|
|
119
|
+
|
|
120
|
+
- Call `model_validate()`
|
|
121
|
+
- Produce typed Pydantic models
|
|
122
|
+
- Capture ValidationError objects
|
|
123
|
+
- Forward failures to rejection handling
|
|
124
|
+
|
|
125
|
+
Validation remains entirely delegated to Pydantic.
|
|
126
|
+
|
|
127
|
+
------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
## Layer 6 -- Rejection Handling
|
|
130
|
+
|
|
131
|
+
Responsibilities:
|
|
132
|
+
|
|
133
|
+
- Raise
|
|
134
|
+
- Collect
|
|
135
|
+
- Skip
|
|
136
|
+
- Log
|
|
137
|
+
- Callback
|
|
138
|
+
- Quarantine
|
|
139
|
+
|
|
140
|
+
Every rejected row follows the configured policy.
|
|
141
|
+
|
|
142
|
+
------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
## Layer 7 -- Result Assembly
|
|
145
|
+
|
|
146
|
+
Produces a QueryResult containing:
|
|
147
|
+
|
|
148
|
+
- validated models
|
|
149
|
+
- rejected rows
|
|
150
|
+
- execution statistics
|
|
151
|
+
- diagnostics
|
|
152
|
+
- executed statement
|
|
153
|
+
|
|
154
|
+
------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
# SQLRules Integration
|
|
157
|
+
|
|
158
|
+
``` text
|
|
159
|
+
Pydantic Model
|
|
160
|
+
│
|
|
161
|
+
▼
|
|
162
|
+
SQLRules
|
|
163
|
+
│
|
|
164
|
+
▼
|
|
165
|
+
WHERE Expressions
|
|
166
|
+
│
|
|
167
|
+
▼
|
|
168
|
+
Query Builder
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
SQLRules is responsible only for SQL-safe constraint compilation.
|
|
172
|
+
|
|
173
|
+
RowGuard owns execution and validation.
|
|
174
|
+
|
|
175
|
+
------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
# Internal Components
|
|
178
|
+
|
|
179
|
+
``` text
|
|
180
|
+
Public API
|
|
181
|
+
│
|
|
182
|
+
Compiler Bridge
|
|
183
|
+
│
|
|
184
|
+
Query Builder
|
|
185
|
+
│
|
|
186
|
+
Execution Engine
|
|
187
|
+
│
|
|
188
|
+
Row Adapter
|
|
189
|
+
│
|
|
190
|
+
Validation Engine
|
|
191
|
+
│
|
|
192
|
+
Reject Handler
|
|
193
|
+
│
|
|
194
|
+
Result Assembler
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Each component should be independently testable.
|
|
198
|
+
|
|
199
|
+
------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
# Query Lifecycle
|
|
202
|
+
|
|
203
|
+
``` text
|
|
204
|
+
Receive Request
|
|
205
|
+
│
|
|
206
|
+
Build Statement
|
|
207
|
+
│
|
|
208
|
+
Compile SQLRules Filters
|
|
209
|
+
│
|
|
210
|
+
Execute SQL
|
|
211
|
+
│
|
|
212
|
+
Adapt Row
|
|
213
|
+
│
|
|
214
|
+
Validate
|
|
215
|
+
│
|
|
216
|
+
Accept or Reject
|
|
217
|
+
│
|
|
218
|
+
Return QueryResult
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
# Thread Safety
|
|
224
|
+
|
|
225
|
+
Compiler configuration and registries should be immutable after
|
|
226
|
+
initialization.
|
|
227
|
+
|
|
228
|
+
Execution state is scoped to each query.
|
|
229
|
+
|
|
230
|
+
------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
# Extension Points
|
|
233
|
+
|
|
234
|
+
Future plugins may provide:
|
|
235
|
+
|
|
236
|
+
- row adapters
|
|
237
|
+
- reject handlers
|
|
238
|
+
- diagnostics
|
|
239
|
+
- dialect helpers
|
|
240
|
+
- result exporters
|
|
241
|
+
|
|
242
|
+
------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
# Non-Goals
|
|
245
|
+
|
|
246
|
+
Architecture intentionally excludes:
|
|
247
|
+
|
|
248
|
+
- ORM mapping
|
|
249
|
+
- migrations
|
|
250
|
+
- SQL parsing
|
|
251
|
+
- schema reflection logic
|
|
252
|
+
- database drivers
|
|
253
|
+
|
|
254
|
+
------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
# Design Goals
|
|
257
|
+
|
|
258
|
+
The architecture should make it easy to:
|
|
259
|
+
|
|
260
|
+
- support legacy databases
|
|
261
|
+
- integrate SQLRules
|
|
262
|
+
- validate every row
|
|
263
|
+
- stream millions of rows
|
|
264
|
+
- add async support
|
|
265
|
+
- extend behavior without changing the core
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] — 2026-07-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Public sync API: `select()`, `execute()`, `validate_rows()`
|
|
13
|
+
- SQLAlchemy Core support for `Table` and existing `Select`
|
|
14
|
+
- Sync `Session` and `Connection` execution
|
|
15
|
+
- SQLRules pushdown integration (`use_sqlrules`, optional `column_map`)
|
|
16
|
+
- Pydantic v2 row validation via mapping adaptation
|
|
17
|
+
- Rejection policies: `raise`, `collect`, `skip`
|
|
18
|
+
- `QueryResult[T]`, `RejectedRow`, `QueryStatistics`, and diagnostics
|
|
19
|
+
- Optional `field_map` for result key remapping
|
|
20
|
+
- SQLite integration tests, unit tests, CI, and example
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- `field_map` no longer silently binds wrong keys or accepts defaults when mapped
|
|
25
|
+
source columns are missing (raises `RowAdaptationError`)
|
|
26
|
+
- SQLAlchemy results are closed after buffered execution
|
|
27
|
+
- Rows are processed incrementally so `raise` can abort without full prefetch
|
|
28
|
+
- Adaptation failures record timing and enrich `RowAdaptationError` with
|
|
29
|
+
`row_index` / `model` under the raise policy
|
|
30
|
+
- `execution_time` is end-to-end for SQL and `validate_rows` paths
|
|
31
|
+
- `rows_validated` counts only rows that reached Pydantic validation
|
|
32
|
+
- Explicit `statement` + `source` pushdown emits
|
|
33
|
+
`sqlrules.pushdown_source_explicit` diagnostic
|
|
34
|
+
- Require `sqlrules>=0.2.0` (needs `Compiler.diagnostics` for pushdown reporting)
|
|
35
|
+
|
|
36
|
+
### Deferred
|
|
37
|
+
|
|
38
|
+
- `stream()` (planned for 0.3.0)
|
|
39
|
+
- Async APIs (planned for 0.4.0)
|
|
40
|
+
- ORM / SQLModel integrations
|
|
41
|
+
- Callback and quarantine rejection policies
|