sqlrules 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.
- {sqlrules-0.1.0 → sqlrules-0.2.0}/CHANGELOG.md +37 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/CONTRIBUTING.md +9 -5
- {sqlrules-0.1.0 → sqlrules-0.2.0}/PKG-INFO +8 -3
- {sqlrules-0.1.0 → sqlrules-0.2.0}/README.md +5 -2
- sqlrules-0.2.0/benchmarks/__init__.py +1 -0
- sqlrules-0.2.0/benchmarks/bench_compile.py +70 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/API.md +33 -7
- sqlrules-0.2.0/docs/ARCHITECTURE.md +33 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/COMPILER.md +19 -8
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/CONSTRAINTS.md +12 -1
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/DESIGN_DECISIONS.md +16 -16
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/DIALECT_SUPPORT.md +1 -1
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/ERRORS.md +35 -15
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/INTERNAL_API.md +13 -16
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/MILESTONES.md +5 -5
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/PERFORMANCE.md +18 -9
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/PLUGIN_SYSTEM.md +4 -3
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/ROADMAP.md +5 -3
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/SPEC.md +25 -4
- sqlrules-0.2.0/docs/TESTING.md +40 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/TRANSLATORS.md +13 -7
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/TYPE_SUPPORT.md +41 -25
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/index.md +1 -1
- {sqlrules-0.1.0 → sqlrules-0.2.0}/pyproject.toml +4 -1
- {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/__init__.py +12 -2
- sqlrules-0.2.0/src/sqlrules/cache.py +40 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/columns.py +3 -2
- sqlrules-0.2.0/src/sqlrules/compiler.py +141 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/constraints.py +111 -10
- {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/errors.py +1 -1
- {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/inspectors.py +1 -1
- sqlrules-0.2.0/src/sqlrules/ir.py +87 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/translators.py +35 -9
- {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_correctness.py +18 -8
- {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_errors.py +1 -1
- {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_v01_features.py +3 -4
- sqlrules-0.2.0/tests/test_v02_features.py +378 -0
- sqlrules-0.1.0/docs/ARCHITECTURE.md +0 -25
- sqlrules-0.1.0/docs/TESTING.md +0 -28
- sqlrules-0.1.0/src/sqlrules/compiler.py +0 -82
- sqlrules-0.1.0/src/sqlrules/ir.py +0 -28
- {sqlrules-0.1.0 → sqlrules-0.2.0}/.gitignore +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/LICENSE +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/CONTRIBUTING.md +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/NON_GOALS.md +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/PHILOSOPHY.md +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/VISION.md +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/py.typed +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/conftest.py +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_coverage_edges.py +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_helpers.py +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_literal_enum.py +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_numeric_constraints.py +0 -0
- {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_string_constraints.py +0 -0
|
@@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-07-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Two-phase compilation: `Compiler.compile_model()` (static IR) and
|
|
13
|
+
`Compiler.bind()` (column resolve + translate)
|
|
14
|
+
- In-process metadata cache for Phase-1 `ModelIR` (default on; disable with
|
|
15
|
+
`cache=False`)
|
|
16
|
+
- Structured diagnostics: public `Diagnostic` via `compiler.diagnostics` for
|
|
17
|
+
skipped constraints under `on_unsupported="warn"` / `"ignore"`
|
|
18
|
+
- Type support for `UUID` and `time` (range comparisons for `time`;
|
|
19
|
+
Literal/Enum for `UUID`)
|
|
20
|
+
- First-class `pattern` constraint IR (no portable core translator; register
|
|
21
|
+
a custom translator or use a future dialect plugin)
|
|
22
|
+
- IR types: `ModelIR`, `FieldIR`, `Diagnostic`
|
|
23
|
+
- Local benchmark suite: `python -m benchmarks.bench_compile`
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Unsupported-constraint error messages no longer hard-code a release version
|
|
28
|
+
- `CompilationContext` carries an optional diagnostics collector
|
|
29
|
+
- Unconstrained `UUID` / `time` fields are skipped (no longer type-rejected);
|
|
30
|
+
`timedelta` and containers still always raise
|
|
31
|
+
- Validation-only metadata flags (`strip_whitespace`, `allow_inf_nan`, …) are
|
|
32
|
+
ignored when extracting constraints
|
|
33
|
+
- `pattern` values from `re.Pattern` are normalized to strings in IR
|
|
34
|
+
- Invalid `column_map` entries raise instead of falling through to table columns
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- Reject `multiple_of` on `date` / `datetime` / `time` (previously emitted
|
|
39
|
+
nonsensical modulo SQL)
|
|
40
|
+
- Module-level `sqlrules.compile(..., on_unsupported="warn")` now attributes
|
|
41
|
+
warnings to the caller, not `compiler.py`
|
|
42
|
+
- `compile_model` clears stale diagnostics from prior binds
|
|
43
|
+
|
|
8
44
|
## [0.1.0] - 2026-07-10
|
|
9
45
|
|
|
10
46
|
### Added
|
|
@@ -48,4 +84,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
48
84
|
- Docs clarify that `on_unsupported` does not soften unsupported types;
|
|
49
85
|
plugin / two-phase compile designs are marked as future
|
|
50
86
|
|
|
87
|
+
[0.2.0]: https://github.com/eddiethedean/sqlrules/releases/tag/v0.2.0
|
|
51
88
|
[0.1.0]: https://github.com/eddiethedean/sqlrules/releases/tag/v0.1.0
|
|
@@ -23,11 +23,15 @@ pre-commit install
|
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
ruff check .
|
|
26
|
-
ruff format .
|
|
26
|
+
ruff format --check .
|
|
27
27
|
mypy src/sqlrules
|
|
28
28
|
pytest
|
|
29
|
+
python -m build && twine check dist/*
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
CI also verifies `pyproject.toml` / `__version__` sync and that the built
|
|
33
|
+
wheel imports cleanly.
|
|
34
|
+
|
|
31
35
|
## Pull requests
|
|
32
36
|
|
|
33
37
|
- Keep changes focused.
|
|
@@ -36,17 +40,17 @@ pytest
|
|
|
36
40
|
|
|
37
41
|
## Releasing
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
Releases are published to PyPI by pushing a version tag. Before tagging:
|
|
40
44
|
|
|
41
45
|
1. Confirm `pyproject.toml` and `sqlrules.__version__` match the changelog.
|
|
42
46
|
2. Run the full check suite (`ruff`, `mypy`, `pytest`).
|
|
43
47
|
3. Ensure CI is green on `main`.
|
|
44
48
|
|
|
45
|
-
Then create and push the tag (example for 0.
|
|
49
|
+
Then create and push the tag (example for 0.2.0):
|
|
46
50
|
|
|
47
51
|
```bash
|
|
48
|
-
git tag -a v0.
|
|
49
|
-
git push origin v0.
|
|
52
|
+
git tag -a v0.2.0 -m "sqlrules 0.2.0"
|
|
53
|
+
git push origin v0.2.0
|
|
50
54
|
```
|
|
51
55
|
|
|
52
56
|
The [release workflow](.github/workflows/release.yml) runs CI, builds the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlrules
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Compile constrained Pydantic models into SQLAlchemy WHERE-rule dictionaries.
|
|
5
5
|
Project-URL: Homepage, https://github.com/eddiethedean/sqlrules
|
|
6
6
|
Project-URL: Documentation, https://github.com/eddiethedean/sqlrules#readme
|
|
@@ -29,11 +29,13 @@ Requires-Dist: annotated-types>=0.6
|
|
|
29
29
|
Requires-Dist: pydantic>=2.0
|
|
30
30
|
Requires-Dist: sqlalchemy>=2.0
|
|
31
31
|
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
32
33
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
33
34
|
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
34
35
|
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
35
36
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
36
37
|
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
38
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
37
39
|
Description-Content-Type: text/markdown
|
|
38
40
|
|
|
39
41
|
# SQLRules
|
|
@@ -79,7 +81,7 @@ pip install sqlrules
|
|
|
79
81
|
|
|
80
82
|
Requires Python 3.10+, Pydantic v2, and SQLAlchemy 2.x.
|
|
81
83
|
|
|
82
|
-
## Supported constraints (0.
|
|
84
|
+
## Supported constraints (0.2)
|
|
83
85
|
|
|
84
86
|
| Constraint | SQLAlchemy expression |
|
|
85
87
|
|---|---|
|
|
@@ -89,6 +91,8 @@ Requires Python 3.10+, Pydantic v2, and SQLAlchemy 2.x.
|
|
|
89
91
|
| `Literal[...]` | `column.in_(...)` |
|
|
90
92
|
| `Enum` | `column.in_(...)` |
|
|
91
93
|
|
|
94
|
+
`pattern` is extracted into IR but has no portable core translator.
|
|
95
|
+
|
|
92
96
|
Unsupported constraints raise `UnsupportedConstraintError` by default.
|
|
93
97
|
Use `on_unsupported="warn"` or `"ignore"` to change that policy for unknown
|
|
94
98
|
constraint operators (unsupported types always raise).
|
|
@@ -96,7 +100,7 @@ constraint operators (unsupported types always raise).
|
|
|
96
100
|
## Public API
|
|
97
101
|
|
|
98
102
|
```python
|
|
99
|
-
sqlrules.compile(model, table, *, column_map=None, on_unsupported="raise")
|
|
103
|
+
sqlrules.compile(model, table, *, column_map=None, on_unsupported="raise", cache=True)
|
|
100
104
|
sqlrules.where(rules) # flatten all expressions
|
|
101
105
|
sqlrules.flatten(rules) # alias of where()
|
|
102
106
|
```
|
|
@@ -120,6 +124,7 @@ pip install -e ".[dev]"
|
|
|
120
124
|
pytest
|
|
121
125
|
ruff check .
|
|
122
126
|
mypy src/sqlrules
|
|
127
|
+
python -m benchmarks.bench_compile
|
|
123
128
|
```
|
|
124
129
|
|
|
125
130
|
## License
|
|
@@ -41,7 +41,7 @@ pip install sqlrules
|
|
|
41
41
|
|
|
42
42
|
Requires Python 3.10+, Pydantic v2, and SQLAlchemy 2.x.
|
|
43
43
|
|
|
44
|
-
## Supported constraints (0.
|
|
44
|
+
## Supported constraints (0.2)
|
|
45
45
|
|
|
46
46
|
| Constraint | SQLAlchemy expression |
|
|
47
47
|
|---|---|
|
|
@@ -51,6 +51,8 @@ Requires Python 3.10+, Pydantic v2, and SQLAlchemy 2.x.
|
|
|
51
51
|
| `Literal[...]` | `column.in_(...)` |
|
|
52
52
|
| `Enum` | `column.in_(...)` |
|
|
53
53
|
|
|
54
|
+
`pattern` is extracted into IR but has no portable core translator.
|
|
55
|
+
|
|
54
56
|
Unsupported constraints raise `UnsupportedConstraintError` by default.
|
|
55
57
|
Use `on_unsupported="warn"` or `"ignore"` to change that policy for unknown
|
|
56
58
|
constraint operators (unsupported types always raise).
|
|
@@ -58,7 +60,7 @@ constraint operators (unsupported types always raise).
|
|
|
58
60
|
## Public API
|
|
59
61
|
|
|
60
62
|
```python
|
|
61
|
-
sqlrules.compile(model, table, *, column_map=None, on_unsupported="raise")
|
|
63
|
+
sqlrules.compile(model, table, *, column_map=None, on_unsupported="raise", cache=True)
|
|
62
64
|
sqlrules.where(rules) # flatten all expressions
|
|
63
65
|
sqlrules.flatten(rules) # alias of where()
|
|
64
66
|
```
|
|
@@ -82,6 +84,7 @@ pip install -e ".[dev]"
|
|
|
82
84
|
pytest
|
|
83
85
|
ruff check .
|
|
84
86
|
mypy src/sqlrules
|
|
87
|
+
python -m benchmarks.bench_compile
|
|
85
88
|
```
|
|
86
89
|
|
|
87
90
|
## License
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Benchmark package for local performance checks (not a CI gate)."""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Compile benchmarks for small / medium / large models (cold vs warm cache).
|
|
2
|
+
|
|
3
|
+
Illustrative targets from docs/PERFORMANCE.md (not enforced):
|
|
4
|
+
Small (< 5 fields) < 0.5 ms
|
|
5
|
+
Medium (~25 fields) < 2 ms
|
|
6
|
+
Large (100+ fields) < 10 ms
|
|
7
|
+
|
|
8
|
+
Run::
|
|
9
|
+
|
|
10
|
+
python -m benchmarks.bench_compile
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import timeit
|
|
16
|
+
from typing import Annotated, Any
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, Field, create_model
|
|
19
|
+
from sqlalchemy import Column, Integer, MetaData, String, Table
|
|
20
|
+
|
|
21
|
+
import sqlrules
|
|
22
|
+
from sqlrules.cache import ModelIRCache
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_model(n_fields: int) -> type[BaseModel]:
|
|
26
|
+
fields: dict[str, Any] = {}
|
|
27
|
+
for i in range(n_fields):
|
|
28
|
+
if i % 2 == 0:
|
|
29
|
+
fields[f"n{i}"] = (Annotated[int, Field(ge=0, le=100)], ...)
|
|
30
|
+
else:
|
|
31
|
+
fields[f"s{i}"] = (Annotated[str, Field(min_length=1, max_length=50)], ...)
|
|
32
|
+
return create_model(f"Filter{n_fields}", **fields) # type: ignore[call-overload]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_table(n_fields: int) -> Table:
|
|
36
|
+
metadata = MetaData()
|
|
37
|
+
columns: list[Column[Any]] = []
|
|
38
|
+
for i in range(n_fields):
|
|
39
|
+
if i % 2 == 0:
|
|
40
|
+
columns.append(Column(f"n{i}", Integer))
|
|
41
|
+
else:
|
|
42
|
+
columns.append(Column(f"s{i}", String))
|
|
43
|
+
return Table(f"t{n_fields}", metadata, *columns)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _bench(label: str, model: type[BaseModel], table: Table, *, repeats: int = 50) -> None:
|
|
47
|
+
cache = ModelIRCache()
|
|
48
|
+
compiler = sqlrules.Compiler(cache=True, model_cache=cache)
|
|
49
|
+
|
|
50
|
+
# Cold: empty cache
|
|
51
|
+
cache.clear()
|
|
52
|
+
cold = timeit.timeit(lambda: compiler.compile(model, table), number=1)
|
|
53
|
+
|
|
54
|
+
# Warm: IR already cached
|
|
55
|
+
warm = min(timeit.repeat(lambda: compiler.compile(model, table), number=1, repeat=repeats))
|
|
56
|
+
|
|
57
|
+
print(f"{label:12} cold={cold * 1000:8.3f} ms warm={warm * 1000:8.3f} ms")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main() -> None:
|
|
61
|
+
print("SQLRules compile benchmarks (cache cold vs warm)")
|
|
62
|
+
print("-" * 56)
|
|
63
|
+
for size, n in (("small", 5), ("medium", 25), ("large", 100)):
|
|
64
|
+
model = _build_model(n)
|
|
65
|
+
table = _build_table(n)
|
|
66
|
+
_bench(f"{size} ({n})", model, table)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
@@ -11,6 +11,7 @@ sqlrules.compile(
|
|
|
11
11
|
*,
|
|
12
12
|
column_map=None,
|
|
13
13
|
on_unsupported="raise",
|
|
14
|
+
cache=True,
|
|
14
15
|
) -> dict[str, list[ColumnElement[bool]]]
|
|
15
16
|
```
|
|
16
17
|
|
|
@@ -20,8 +21,9 @@ Compile a constrained Pydantic model into a field-keyed rule dictionary.
|
|
|
20
21
|
|---|---|
|
|
21
22
|
| `model` | Pydantic `BaseModel` subclass |
|
|
22
23
|
| `table` | SQLAlchemy `Table`, alias, ORM class, or object with `.c` |
|
|
23
|
-
| `column_map` | Optional explicit `field_name` or alias → column mapping |
|
|
24
|
+
| `column_map` | Optional explicit `field_name` or alias → column mapping. If a key matches but the value is not a column, raises `MissingColumnError` (no fallthrough). |
|
|
24
25
|
| `on_unsupported` | `"raise"` (default), `"warn"`, or `"ignore"` for unknown **operators** only |
|
|
26
|
+
| `cache` | Cache Phase-1 model IR (default `True`) |
|
|
25
27
|
|
|
26
28
|
Rule dictionary keys are always the Python field names. String field aliases
|
|
27
29
|
(`alias`, `validation_alias`, `serialization_alias`) are used only for column
|
|
@@ -44,12 +46,32 @@ Flatten a rule dictionary into a single list of expressions suitable for
|
|
|
44
46
|
## `Compiler`
|
|
45
47
|
|
|
46
48
|
```python
|
|
47
|
-
compiler = sqlrules.Compiler(
|
|
49
|
+
compiler = sqlrules.Compiler(
|
|
50
|
+
on_unsupported="raise",
|
|
51
|
+
registry=None,
|
|
52
|
+
cache=True,
|
|
53
|
+
model_cache=None, # optional shared ModelIRCache; default is process-wide
|
|
54
|
+
)
|
|
48
55
|
rules = compiler.compile(model, table, column_map=None)
|
|
56
|
+
|
|
57
|
+
# Two-phase (advanced):
|
|
58
|
+
model_ir = compiler.compile_model(model) # also clears diagnostics
|
|
59
|
+
rules = compiler.bind(model_ir, table, column_map=None)
|
|
60
|
+
|
|
61
|
+
# Diagnostics from the last compile_model / compile / bind:
|
|
62
|
+
compiler.diagnostics
|
|
49
63
|
```
|
|
50
64
|
|
|
51
|
-
Reusable compiler instance with a fixed unsupported-constraint policy
|
|
52
|
-
optional custom translator registry.
|
|
65
|
+
Reusable compiler instance with a fixed unsupported-constraint policy,
|
|
66
|
+
optional custom translator registry, and optional Phase-1 metadata cache.
|
|
67
|
+
|
|
68
|
+
`compile_model` / `bind` separate static IR extraction from table binding so
|
|
69
|
+
the same `ModelIR` can be reused across tables.
|
|
70
|
+
|
|
71
|
+
**Concurrency:** do not call `compile` / `bind` / `compile_model` concurrently
|
|
72
|
+
on the same `Compiler` instance (diagnostics are per-instance and not locked).
|
|
73
|
+
The shared Phase-1 IR cache is thread-safe for concurrent reads/writes across
|
|
74
|
+
instances.
|
|
53
75
|
|
|
54
76
|
## Other exports
|
|
55
77
|
|
|
@@ -57,7 +79,11 @@ Also exported for advanced use and typing:
|
|
|
57
79
|
|
|
58
80
|
- `__version__`
|
|
59
81
|
- `SQLRulesWarning`
|
|
60
|
-
- `CompilationContext`, `Constraint`, `FieldDescriptor`
|
|
82
|
+
- `CompilationContext`, `Constraint`, `FieldDescriptor`, `FieldIR`, `ModelIR`,
|
|
83
|
+
`Diagnostic` (IR helpers)
|
|
84
|
+
|
|
85
|
+
`DiagnosticsCollector` and `ModelIRCache` are internal (import from
|
|
86
|
+
`sqlrules.ir` / `sqlrules.cache` only if you accept instability).
|
|
61
87
|
|
|
62
88
|
## Exceptions
|
|
63
89
|
|
|
@@ -67,9 +93,9 @@ All public exceptions inherit from `SQLRulesError`:
|
|
|
67
93
|
- `MissingColumnError`
|
|
68
94
|
- `UnsupportedConstraintError`
|
|
69
95
|
- `TranslatorError`
|
|
70
|
-
- `InvalidTranslatorError` (reserved; not raised in 0.
|
|
96
|
+
- `InvalidTranslatorError` (reserved; not raised in 0.2)
|
|
71
97
|
- `RegistryError`
|
|
72
98
|
- `ConfigurationError`
|
|
73
|
-
- `InternalCompilerError` (reserved; not raised in 0.
|
|
99
|
+
- `InternalCompilerError` (reserved; not raised in 0.2)
|
|
74
100
|
|
|
75
101
|
See [ERRORS.md](ERRORS.md) for details.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
Pydantic Model
|
|
5
|
+
↓
|
|
6
|
+
Metadata Reader (cached ModelIR)
|
|
7
|
+
↓
|
|
8
|
+
Constraint IR
|
|
9
|
+
↓
|
|
10
|
+
Column Binding
|
|
11
|
+
↓
|
|
12
|
+
SQLAlchemy Compiler
|
|
13
|
+
↓
|
|
14
|
+
Rule Dictionary (+ diagnostics)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
SQLRules is a pure compiler pipeline with two phases:
|
|
18
|
+
|
|
19
|
+
**Phase 1 — static model IR**
|
|
20
|
+
|
|
21
|
+
1. **Inspect** the Pydantic model and preserve field declaration order.
|
|
22
|
+
2. **Extract** supported constraints into a dialect-neutral IR.
|
|
23
|
+
3. **Cache** immutable `ModelIR` keyed by model class (optional).
|
|
24
|
+
|
|
25
|
+
**Phase 2 — bind and translate**
|
|
26
|
+
|
|
27
|
+
4. **Resolve** each constrained field to a SQLAlchemy column.
|
|
28
|
+
5. **Translate** each IR constraint into one SQLAlchemy expression.
|
|
29
|
+
6. **Assemble** expressions into `dict[str, list[ColumnElement[bool]]]`.
|
|
30
|
+
|
|
31
|
+
The compiler never connects to a database, executes SQL, or renders SQL
|
|
32
|
+
strings. See [COMPILER.md](COMPILER.md) and [INTERNAL_API.md](INTERNAL_API.md)
|
|
33
|
+
for stage-level detail.
|
|
@@ -37,13 +37,14 @@ Rule Dictionary
|
|
|
37
37
|
|
|
38
38
|
## Stage 1 -- Model Introspection
|
|
39
39
|
|
|
40
|
-
Inputs: - Pydantic BaseModel subclass
|
|
41
|
-
class, or column mapping
|
|
40
|
+
Inputs: - Pydantic BaseModel subclass
|
|
42
41
|
|
|
43
42
|
Responsibilities: - Validate the input model. - Enumerate model
|
|
44
43
|
fields. - Preserve declaration order.
|
|
45
44
|
|
|
46
|
-
Output: - Iterable of field definitions.
|
|
45
|
+
Output: - Iterable of field definitions (later cached as `ModelIR`).
|
|
46
|
+
|
|
47
|
+
Table / column binding happens in Phase 2 (`bind`), not Stage 1.
|
|
47
48
|
|
|
48
49
|
## Stage 2 -- Field Extraction
|
|
49
50
|
|
|
@@ -64,6 +65,9 @@ Read only constraints that have deterministic SQL equivalents.
|
|
|
64
65
|
Examples: - gt - ge - lt - le - multiple_of - min_length - max_length -
|
|
65
66
|
Literal - Enum
|
|
66
67
|
|
|
68
|
+
`pattern` is normalized into IR (`operator="pattern"`) but has no
|
|
69
|
+
portable core translator.
|
|
70
|
+
|
|
67
71
|
Constraints without SQL equivalents are delegated to the
|
|
68
72
|
unsupported-constraint policy.
|
|
69
73
|
|
|
@@ -130,16 +134,23 @@ Errors include:
|
|
|
130
134
|
|
|
131
135
|
## Caching
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
Phase-1 model IR is cached in-process by model class (default on).
|
|
138
|
+
Cached values are immutable field descriptors and constraints — never
|
|
139
|
+
SQLAlchemy column objects. Disable with `cache=False`.
|
|
140
|
+
|
|
141
|
+
Translation remains deterministic.
|
|
135
142
|
|
|
136
143
|
## Extension Points
|
|
137
144
|
|
|
145
|
+
Available today:
|
|
146
|
+
|
|
147
|
+
- Inject a custom `TranslatorRegistry` via `Compiler(registry=...)`
|
|
148
|
+
(including a `pattern` translator).
|
|
149
|
+
|
|
138
150
|
Future plugin hooks:
|
|
139
151
|
|
|
140
|
-
- Register
|
|
141
|
-
-
|
|
142
|
-
- Override built-in mappings.
|
|
152
|
+
- Register dialect-specific translators via a plugin API.
|
|
153
|
+
- Override built-in mappings through versioned plugins.
|
|
143
154
|
- Add new IR transforms.
|
|
144
155
|
|
|
145
156
|
## Design Principles
|
|
@@ -11,10 +11,21 @@
|
|
|
11
11
|
| `multiple_of` | `column % value == 0` |
|
|
12
12
|
| `Literal[...]` | `column.in_(...)` |
|
|
13
13
|
| `Enum` | `column.in_(member values)` |
|
|
14
|
+
| `pattern` | IR only in 0.2 (no portable core translator) |
|
|
14
15
|
|
|
15
16
|
Accepted input forms for the same semantics include `Field(...)`,
|
|
16
17
|
`annotated_types` primitives, `Interval`, `Len`, `conint`/`constr`, and
|
|
17
|
-
`StringConstraints` (length attributes
|
|
18
|
+
`StringConstraints` (length attributes; `pattern` becomes IR).
|
|
18
19
|
|
|
19
20
|
Constraints without a deterministic SQL equivalent are rejected by default.
|
|
20
21
|
See [TYPE_SUPPORT.md](TYPE_SUPPORT.md) and [ERRORS.md](ERRORS.md).
|
|
22
|
+
|
|
23
|
+
To translate `pattern`, register a custom translator:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from sqlrules.translators import default_registry
|
|
27
|
+
|
|
28
|
+
registry = default_registry()
|
|
29
|
+
registry.register("pattern", lambda c, col, ctx: col.op("~")(c.value))
|
|
30
|
+
compiler = sqlrules.Compiler(registry=registry)
|
|
31
|
+
```
|
|
@@ -235,8 +235,9 @@ SQL Server, or Oracle plugins.
|
|
|
235
235
|
|
|
236
236
|
## Decision
|
|
237
237
|
|
|
238
|
-
SQLRules 0.
|
|
239
|
-
to injecting a `TranslatorRegistry` into `Compiler(registry=...)
|
|
238
|
+
SQLRules 0.2 does **not** ship a plugin API. Customization today is limited
|
|
239
|
+
to injecting a `TranslatorRegistry` into `Compiler(registry=...)` (for
|
|
240
|
+
example a custom `pattern` translator).
|
|
240
241
|
|
|
241
242
|
A future release may add explicit plugin registration (for example
|
|
242
243
|
`Compiler(plugins=[...])`) without automatic discovery.
|
|
@@ -248,7 +249,7 @@ reproducibility, and keeps the compiler deterministic.
|
|
|
248
249
|
|
|
249
250
|
## Consequences
|
|
250
251
|
|
|
251
|
-
Do not treat plugin examples in design docs as available in 0.
|
|
252
|
+
Do not treat plugin examples in design docs as available in 0.2. See
|
|
252
253
|
[PLUGIN_SYSTEM.md](PLUGIN_SYSTEM.md) for the planned design.
|
|
253
254
|
|
|
254
255
|
------------------------------------------------------------------------
|
|
@@ -297,21 +298,19 @@ early.
|
|
|
297
298
|
|
|
298
299
|
------------------------------------------------------------------------
|
|
299
300
|
|
|
300
|
-
# Decision 12: Two-Phase Compilation
|
|
301
|
+
# Decision 12: Two-Phase Compilation
|
|
301
302
|
|
|
302
303
|
## Decision
|
|
303
304
|
|
|
304
|
-
SQLRules
|
|
305
|
-
binding. **0.1 compiles in a single pass** (inspect → extract → resolve →
|
|
306
|
-
translate).
|
|
305
|
+
SQLRules separates static model compilation from table binding.
|
|
307
306
|
|
|
308
|
-
##
|
|
307
|
+
## Phase 1: Static Compilation
|
|
309
308
|
|
|
310
309
|
- inspect Pydantic model
|
|
311
310
|
- extract constraints
|
|
312
|
-
- build
|
|
311
|
+
- build `ModelIR` (cached by default)
|
|
313
312
|
|
|
314
|
-
##
|
|
313
|
+
## Phase 2: Binding
|
|
315
314
|
|
|
316
315
|
- resolve SQLAlchemy columns
|
|
317
316
|
- translate IR to expressions
|
|
@@ -323,7 +322,8 @@ aliases, ORM models, or column maps.
|
|
|
323
322
|
|
|
324
323
|
## Consequences
|
|
325
324
|
|
|
326
|
-
|
|
325
|
+
`Compiler.compile_model` / `Compiler.bind` expose the phases.
|
|
326
|
+
`sqlrules.compile(...)` remains the one-shot public API.
|
|
327
327
|
|
|
328
328
|
------------------------------------------------------------------------
|
|
329
329
|
|
|
@@ -438,12 +438,12 @@ This may improve ergonomics while preserving dictionary output.
|
|
|
438
438
|
|
|
439
439
|
## Should Regex Be Core or Dialect Plugin?
|
|
440
440
|
|
|
441
|
-
|
|
441
|
+
**Decided for 0.2:**
|
|
442
442
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
-
|
|
446
|
-
|
|
443
|
+
- IR support in core (`Constraint(operator="pattern", ...)`)
|
|
444
|
+
- No portable core translator
|
|
445
|
+
- Translator support via custom `TranslatorRegistry` today, dialect
|
|
446
|
+
plugins in 0.3+
|
|
447
447
|
|
|
448
448
|
------------------------------------------------------------------------
|
|
449
449
|
|
|
@@ -31,7 +31,7 @@ compiler deterministic.
|
|
|
31
31
|
|
|
32
32
|
# Planned Dialects
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Core emits portable SQLAlchemy Core expressions. The table below describes
|
|
35
35
|
**intended** dialect posture, not a certified multi-backend test matrix.
|
|
36
36
|
|
|
37
37
|
Dialect Status Notes
|
|
@@ -13,6 +13,7 @@ Goals:
|
|
|
13
13
|
- Helpful error messages
|
|
14
14
|
- Deterministic behavior
|
|
15
15
|
- Configurable handling for unsupported **constraint operators**
|
|
16
|
+
- Structured diagnostics for skipped constraints
|
|
16
17
|
|
|
17
18
|
------------------------------------------------------------------------
|
|
18
19
|
|
|
@@ -24,10 +25,10 @@ SQLRulesError
|
|
|
24
25
|
├── MissingColumnError
|
|
25
26
|
├── UnsupportedConstraintError
|
|
26
27
|
├── TranslatorError
|
|
27
|
-
├── InvalidTranslatorError (reserved in 0.
|
|
28
|
+
├── InvalidTranslatorError (reserved in 0.2)
|
|
28
29
|
├── RegistryError
|
|
29
30
|
├── ConfigurationError
|
|
30
|
-
└── InternalCompilerError (reserved in 0.
|
|
31
|
+
└── InternalCompilerError (reserved in 0.2)
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
All public exceptions inherit from `SQLRulesError`.
|
|
@@ -66,15 +67,15 @@ an unsupported type, or an invalid operator/type combination.
|
|
|
66
67
|
|
|
67
68
|
Examples:
|
|
68
69
|
|
|
69
|
-
- `pattern
|
|
70
|
+
- `pattern` (no core translator), `max_digits`, `decimal_places`
|
|
70
71
|
- custom validators / `Strict`
|
|
71
|
-
- unsupported types (
|
|
72
|
+
- unsupported types (containers, `timedelta`)
|
|
72
73
|
- `multiple_of <= 0`
|
|
73
74
|
- length constraints on non-`str` fields
|
|
74
75
|
|
|
75
76
|
Example:
|
|
76
77
|
|
|
77
|
-
Field 'name': constraint 'pattern' is not supported by SQLRules
|
|
78
|
+
Field 'name': constraint 'pattern' is not supported by SQLRules.
|
|
78
79
|
Remove the constraint, or set on_unsupported='warn'/'ignore'.
|
|
79
80
|
|
|
80
81
|
------------------------------------------------------------------------
|
|
@@ -89,7 +90,7 @@ expression (unexpected SQLAlchemy errors wrapped by the registry).
|
|
|
89
90
|
# InvalidTranslatorError
|
|
90
91
|
|
|
91
92
|
Reserved for future validation of custom translators. Not raised by the
|
|
92
|
-
0.
|
|
93
|
+
0.2 compiler path.
|
|
93
94
|
|
|
94
95
|
------------------------------------------------------------------------
|
|
95
96
|
|
|
@@ -109,13 +110,13 @@ Missing operators raise `UnsupportedConstraintError`, not `RegistryError`.
|
|
|
109
110
|
|
|
110
111
|
Raised when compiler configuration is inconsistent.
|
|
111
112
|
|
|
112
|
-
In 0.
|
|
113
|
+
In 0.2 this is raised for an invalid `on_unsupported` mode only.
|
|
113
114
|
|
|
114
115
|
------------------------------------------------------------------------
|
|
115
116
|
|
|
116
117
|
# InternalCompilerError
|
|
117
118
|
|
|
118
|
-
Reserved for unexpected internal failures. Not raised by the 0.
|
|
119
|
+
Reserved for unexpected internal failures. Not raised by the 0.2
|
|
119
120
|
compiler path.
|
|
120
121
|
|
|
121
122
|
------------------------------------------------------------------------
|
|
@@ -131,16 +132,39 @@ Immediately raises `UnsupportedConstraintError`.
|
|
|
131
132
|
|
|
132
133
|
## warn
|
|
133
134
|
|
|
134
|
-
Emits a `SQLRulesWarning` and skips the
|
|
135
|
+
Emits a `SQLRulesWarning`, records a `Diagnostic`, and skips the
|
|
136
|
+
constraint.
|
|
135
137
|
|
|
136
138
|
## ignore
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
Records a `Diagnostic` and silently skips unsupported constraints.
|
|
139
141
|
|
|
140
142
|
Unsupported **types** always raise, regardless of `on_unsupported`.
|
|
141
143
|
|
|
142
144
|
------------------------------------------------------------------------
|
|
143
145
|
|
|
146
|
+
# Structured Diagnostics
|
|
147
|
+
|
|
148
|
+
After `compile` / `bind` / `compile_model`, inspect skipped constraints:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
compiler = sqlrules.Compiler(on_unsupported="warn")
|
|
152
|
+
rules = compiler.compile(Model, table)
|
|
153
|
+
for diag in compiler.diagnostics:
|
|
154
|
+
print(diag.severity, diag.field, diag.operator, diag.message)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`Diagnostic` fields: `severity` (`"warning"` | `"info"`), `field`,
|
|
158
|
+
`operator`, `value`, `message`.
|
|
159
|
+
|
|
160
|
+
`compile_model` clears diagnostics. Diagnostics are separate from
|
|
161
|
+
exceptions and do not change the rules dict return type.
|
|
162
|
+
|
|
163
|
+
Do not call `compile` / `bind` concurrently on the same `Compiler`
|
|
164
|
+
instance; diagnostics collection is not locked.
|
|
165
|
+
|
|
166
|
+
------------------------------------------------------------------------
|
|
167
|
+
|
|
144
168
|
# Error Message Guidelines
|
|
145
169
|
|
|
146
170
|
Every public exception should include:
|
|
@@ -150,17 +174,13 @@ Every public exception should include:
|
|
|
150
174
|
- offending value (when applicable)
|
|
151
175
|
- suggested resolution
|
|
152
176
|
|
|
153
|
-
Example:
|
|
154
|
-
|
|
155
|
-
Field 'age': constraint 'between' is not supported by SQLRules 0.1.
|
|
156
|
-
|
|
157
177
|
------------------------------------------------------------------------
|
|
158
178
|
|
|
159
179
|
# Logging
|
|
160
180
|
|
|
161
181
|
SQLRules does not log by default.
|
|
162
182
|
|
|
163
|
-
Applications decide how to handle exceptions and
|
|
183
|
+
Applications decide how to handle exceptions, warnings, and diagnostics.
|
|
164
184
|
|
|
165
185
|
------------------------------------------------------------------------
|
|
166
186
|
|