sqlrules 0.1.0__tar.gz → 0.3.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.3.0}/.gitignore +1 -0
- sqlrules-0.3.0/CHANGELOG.md +113 -0
- sqlrules-0.3.0/CONTRIBUTING.md +80 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/PKG-INFO +37 -5
- {sqlrules-0.1.0 → sqlrules-0.3.0}/README.md +34 -4
- sqlrules-0.3.0/benchmarks/__init__.py +1 -0
- sqlrules-0.3.0/benchmarks/bench_compile.py +70 -0
- sqlrules-0.3.0/docs/API.md +138 -0
- sqlrules-0.3.0/docs/ARCHITECTURE.md +33 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/COMPILER.md +22 -10
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/CONSTRAINTS.md +23 -1
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/DESIGN_DECISIONS.md +22 -20
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/DIALECT_SUPPORT.md +14 -10
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/ERRORS.md +51 -16
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/INTERNAL_API.md +21 -19
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/MILESTONES.md +6 -6
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/PERFORMANCE.md +18 -9
- sqlrules-0.3.0/docs/PLUGIN_SYSTEM.md +201 -0
- sqlrules-0.3.0/docs/ROADMAP.md +44 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/SPEC.md +39 -4
- sqlrules-0.3.0/docs/TESTING.md +51 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/TRANSLATORS.md +38 -20
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/TYPE_SUPPORT.md +41 -25
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/index.md +3 -3
- sqlrules-0.3.0/packages/sqlrules-postgresql/README.md +16 -0
- sqlrules-0.3.0/packages/sqlrules-postgresql/pyproject.toml +28 -0
- sqlrules-0.3.0/packages/sqlrules-postgresql/src/sqlrules_postgresql/__init__.py +28 -0
- sqlrules-0.3.0/packages/sqlrules-postgresql/src/sqlrules_postgresql/pattern.py +16 -0
- sqlrules-0.3.0/packages/sqlrules-postgresql/tests/test_postgres_plugin.py +33 -0
- sqlrules-0.3.0/packages/sqlrules-sqlite/README.md +19 -0
- sqlrules-0.3.0/packages/sqlrules-sqlite/pyproject.toml +28 -0
- sqlrules-0.3.0/packages/sqlrules-sqlite/src/sqlrules_sqlite/__init__.py +33 -0
- sqlrules-0.3.0/packages/sqlrules-sqlite/src/sqlrules_sqlite/pattern.py +19 -0
- sqlrules-0.3.0/packages/sqlrules-sqlite/tests/test_sqlite_plugin.py +33 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/pyproject.toml +5 -1
- {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/__init__.py +17 -2
- sqlrules-0.3.0/src/sqlrules/cache.py +40 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/columns.py +3 -2
- sqlrules-0.3.0/src/sqlrules/compiler.py +206 -0
- sqlrules-0.3.0/src/sqlrules/conformance.py +146 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/constraints.py +111 -10
- {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/errors.py +23 -7
- {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/inspectors.py +1 -1
- sqlrules-0.3.0/src/sqlrules/ir.py +92 -0
- sqlrules-0.3.0/src/sqlrules/plugins.py +57 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/translators.py +107 -14
- {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/conftest.py +5 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_correctness.py +18 -8
- {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_errors.py +1 -1
- sqlrules-0.3.0/tests/test_plugins.py +364 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_v01_features.py +3 -4
- sqlrules-0.3.0/tests/test_v02_features.py +380 -0
- sqlrules-0.1.0/CHANGELOG.md +0 -51
- sqlrules-0.1.0/CONTRIBUTING.md +0 -53
- sqlrules-0.1.0/docs/API.md +0 -75
- sqlrules-0.1.0/docs/ARCHITECTURE.md +0 -25
- sqlrules-0.1.0/docs/PLUGIN_SYSTEM.md +0 -251
- sqlrules-0.1.0/docs/ROADMAP.md +0 -33
- 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.3.0}/LICENSE +0 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/CONTRIBUTING.md +0 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/NON_GOALS.md +0 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/PHILOSOPHY.md +0 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/VISION.md +0 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/py.typed +0 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_coverage_edges.py +0 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_helpers.py +0 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_literal_enum.py +0 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_numeric_constraints.py +0 -0
- {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_string_constraints.py +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
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/).
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-07-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Versioned plugin API: `SQLRulesPlugin`, `PLUGIN_API_VERSION` (`"1"`),
|
|
13
|
+
and `PluginError`
|
|
14
|
+
- `Compiler(plugins=..., on_conflict=..., dialect=...)` for explicit
|
|
15
|
+
plugin registration (no auto-discovery)
|
|
16
|
+
- Expanded registry API: `register_constraint`, conflict policies
|
|
17
|
+
(`raise` / `replace` / `ignore`), `copy()`, `operators()`, and
|
|
18
|
+
`__contains__`
|
|
19
|
+
- `InvalidTranslatorError` raised for non-callable or wrong-arity
|
|
20
|
+
translators at registration time
|
|
21
|
+
- `Diagnostic.code` (e.g. `unsupported_constraint` on warn/ignore skips)
|
|
22
|
+
- Optional `CompilationContext.dialect` hint (never auto-detected)
|
|
23
|
+
- Plugin conformance helpers in `sqlrules.conformance`
|
|
24
|
+
- Official starter dialect packages:
|
|
25
|
+
- `sqlrules-postgresql` (`pattern` → `~`)
|
|
26
|
+
- `sqlrules-sqlite` (`pattern` → `REGEXP`)
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Package version bumped to 0.3.0
|
|
31
|
+
|
|
32
|
+
## [0.2.0] - 2026-07-10
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Two-phase compilation: `Compiler.compile_model()` (static IR) and
|
|
37
|
+
`Compiler.bind()` (column resolve + translate)
|
|
38
|
+
- In-process metadata cache for Phase-1 `ModelIR` (default on; disable with
|
|
39
|
+
`cache=False`)
|
|
40
|
+
- Structured diagnostics: public `Diagnostic` via `compiler.diagnostics` for
|
|
41
|
+
skipped constraints under `on_unsupported="warn"` / `"ignore"`
|
|
42
|
+
- Type support for `UUID` and `time` (range comparisons for `time`;
|
|
43
|
+
Literal/Enum for `UUID`)
|
|
44
|
+
- First-class `pattern` constraint IR (no portable core translator; register
|
|
45
|
+
a custom translator or use a future dialect plugin)
|
|
46
|
+
- IR types: `ModelIR`, `FieldIR`, `Diagnostic`
|
|
47
|
+
- Local benchmark suite: `python -m benchmarks.bench_compile`
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- Unsupported-constraint error messages no longer hard-code a release version
|
|
52
|
+
- `CompilationContext` carries an optional diagnostics collector
|
|
53
|
+
- Unconstrained `UUID` / `time` fields are skipped (no longer type-rejected);
|
|
54
|
+
`timedelta` and containers still always raise
|
|
55
|
+
- Validation-only metadata flags (`strip_whitespace`, `allow_inf_nan`, …) are
|
|
56
|
+
ignored when extracting constraints
|
|
57
|
+
- `pattern` values from `re.Pattern` are normalized to strings in IR
|
|
58
|
+
- Invalid `column_map` entries raise instead of falling through to table columns
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- Reject `multiple_of` on `date` / `datetime` / `time` (previously emitted
|
|
63
|
+
nonsensical modulo SQL)
|
|
64
|
+
- Module-level `sqlrules.compile(..., on_unsupported="warn")` now attributes
|
|
65
|
+
warnings to the caller, not `compiler.py`
|
|
66
|
+
- `compile_model` clears stale diagnostics from prior binds
|
|
67
|
+
|
|
68
|
+
## [0.1.0] - 2026-07-10
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- Public API: `compile()`, `where()`, `flatten()`, and `Compiler`
|
|
73
|
+
- Constraint support: `gt`, `ge`, `lt`, `le`, `multiple_of`, `min_length`,
|
|
74
|
+
`max_length`, `Literal`, and `Enum`
|
|
75
|
+
- Equivalent Pydantic forms: `Interval`, `conint`/`constr`, `Len`, and
|
|
76
|
+
`StringConstraints` (supported attributes only)
|
|
77
|
+
- Fail-fast handling for unsupported metadata (`pattern`, validators,
|
|
78
|
+
decimal precision) and unsupported types (containers, UUID, time,
|
|
79
|
+
timedelta)
|
|
80
|
+
- Field alias resolution when binding SQLAlchemy columns (`alias`,
|
|
81
|
+
`validation_alias`, and `serialization_alias` string forms)
|
|
82
|
+
- Unsupported-constraint policies: `raise` (default), `warn`, and `ignore`
|
|
83
|
+
for unknown **operators** (unsupported types always raise)
|
|
84
|
+
- Exception hierarchy rooted at `SQLRulesError`
|
|
85
|
+
- Column resolution for SQLAlchemy `Table` objects, ORM attributes, and
|
|
86
|
+
explicit `column_map`
|
|
87
|
+
- Documentation for the compiler, translators, errors, types, and roadmap
|
|
88
|
+
|
|
89
|
+
### Fixed
|
|
90
|
+
|
|
91
|
+
- Column resolution no longer treats non-column `Table` attributes (such as
|
|
92
|
+
`name` or `is_selectable`) as columns, which previously produced invalid
|
|
93
|
+
SQL or Python `bool` values in the rules dict
|
|
94
|
+
- `Annotated[T, ...] | None` / `Optional[Annotated[T, ...]]` now unwraps
|
|
95
|
+
nested metadata correctly
|
|
96
|
+
- String `validation_alias` / `serialization_alias` participate in column
|
|
97
|
+
binding
|
|
98
|
+
- `multiple_of` values `<= 0` are rejected
|
|
99
|
+
- Length constraints on non-`str` fields and numeric constraints on `str` /
|
|
100
|
+
`bool` fields are rejected
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
|
|
104
|
+
- Alias candidates are preferred over the Python field name when binding
|
|
105
|
+
columns
|
|
106
|
+
- Unconstrained fields are skipped and no longer require a matching column
|
|
107
|
+
- Packaging URLs point at `eddiethedean/sqlrules`
|
|
108
|
+
- Docs clarify that `on_unsupported` does not soften unsupported types;
|
|
109
|
+
plugin / two-phase compile designs are marked as future
|
|
110
|
+
|
|
111
|
+
[0.3.0]: https://github.com/eddiethedean/sqlrules/releases/tag/v0.3.0
|
|
112
|
+
[0.2.0]: https://github.com/eddiethedean/sqlrules/releases/tag/v0.2.0
|
|
113
|
+
[0.1.0]: https://github.com/eddiethedean/sqlrules/releases/tag/v0.1.0
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for helping improve SQLRules.
|
|
4
|
+
|
|
5
|
+
## Principles
|
|
6
|
+
|
|
7
|
+
- Keep the public API minimal.
|
|
8
|
+
- Every feature must map directly to SQLAlchemy expressions.
|
|
9
|
+
- Unsupported Pydantic features should not be approximated.
|
|
10
|
+
- Prefer fail-fast behavior over silent semantic changes.
|
|
11
|
+
- Update relevant docs before or alongside implementation.
|
|
12
|
+
|
|
13
|
+
## Development setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m venv .venv
|
|
17
|
+
source .venv/bin/activate
|
|
18
|
+
pip install -e ".[dev]"
|
|
19
|
+
pip install -e packages/sqlrules-postgresql -e packages/sqlrules-sqlite
|
|
20
|
+
pre-commit install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Checks
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ruff check .
|
|
27
|
+
ruff format --check .
|
|
28
|
+
mypy src/sqlrules
|
|
29
|
+
pytest tests packages/sqlrules-postgresql/tests packages/sqlrules-sqlite/tests
|
|
30
|
+
python -m build && twine check dist/*
|
|
31
|
+
python -m build packages/sqlrules-postgresql --outdir dist-plugins
|
|
32
|
+
python -m build packages/sqlrules-sqlite --outdir dist-plugins
|
|
33
|
+
twine check dist-plugins/*
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
CI also verifies `pyproject.toml` / `__version__` sync and that the built
|
|
37
|
+
wheel imports cleanly.
|
|
38
|
+
|
|
39
|
+
## Plugins
|
|
40
|
+
|
|
41
|
+
- Declare `name`, `api_version` (`PLUGIN_API_VERSION`), and `register(registry)`.
|
|
42
|
+
- Prefer `register_constraint(..., on_conflict=...)`.
|
|
43
|
+
- Run `sqlrules.conformance.run_basic_conformance(plugin)`.
|
|
44
|
+
- Official dialect packages live under `packages/` and should track the
|
|
45
|
+
plugin API major (`sqlrules>=0.3,<0.4` while `PLUGIN_API_VERSION == "1"`).
|
|
46
|
+
|
|
47
|
+
## Pull requests
|
|
48
|
+
|
|
49
|
+
- Keep changes focused.
|
|
50
|
+
- Add or update tests for every constraint or error path you touch.
|
|
51
|
+
- Match existing code style and documentation tone.
|
|
52
|
+
|
|
53
|
+
## Releasing
|
|
54
|
+
|
|
55
|
+
Releases are published to PyPI by pushing a version tag. Before tagging:
|
|
56
|
+
|
|
57
|
+
1. Confirm `pyproject.toml` and `sqlrules.__version__` match the changelog.
|
|
58
|
+
2. Run the full check suite (`ruff`, `mypy`, `pytest`).
|
|
59
|
+
3. Ensure CI is green on `main`.
|
|
60
|
+
|
|
61
|
+
Then create and push the tag (example for 0.3.0):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git tag -a v0.3.0 -m "sqlrules 0.3.0"
|
|
65
|
+
git push origin v0.3.0
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The [release workflow](.github/workflows/release.yml) runs CI, verifies the
|
|
69
|
+
tag matches `pyproject.toml` / `__version__`, builds the sdist/wheel, and
|
|
70
|
+
publishes **core** `sqlrules` with `PYPI_API_TOKEN`.
|
|
71
|
+
|
|
72
|
+
Dialect plugin packages under `packages/` are versioned independently and
|
|
73
|
+
are **not** published by the core release workflow. Publish them separately
|
|
74
|
+
after core `0.3.0` is on PyPI (they depend on `sqlrules>=0.3,<0.4`):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
python -m build packages/sqlrules-postgresql
|
|
78
|
+
python -m build packages/sqlrules-sqlite
|
|
79
|
+
twine upload dist-plugins/* # after building with --outdir dist-plugins
|
|
80
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlrules
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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,22 @@ pip install sqlrules
|
|
|
79
81
|
|
|
80
82
|
Requires Python 3.10+, Pydantic v2, and SQLAlchemy 2.x.
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
Optional dialect plugins:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install sqlrules-postgresql # pattern → PostgreSQL ~
|
|
88
|
+
pip install sqlrules-sqlite # pattern → SQLite REGEXP
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from sqlrules import Compiler
|
|
93
|
+
from sqlrules_postgresql import PostgresPlugin
|
|
94
|
+
|
|
95
|
+
compiler = Compiler(plugins=[PostgresPlugin()], dialect="postgresql")
|
|
96
|
+
rules = compiler.compile(UserFilter, users)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Supported constraints (0.3)
|
|
83
100
|
|
|
84
101
|
| Constraint | SQLAlchemy expression |
|
|
85
102
|
|---|---|
|
|
@@ -89,6 +106,10 @@ Requires Python 3.10+, Pydantic v2, and SQLAlchemy 2.x.
|
|
|
89
106
|
| `Literal[...]` | `column.in_(...)` |
|
|
90
107
|
| `Enum` | `column.in_(...)` |
|
|
91
108
|
|
|
109
|
+
`pattern` is extracted into IR but has no portable core translator.
|
|
110
|
+
Use a custom registry translator or a dialect plugin
|
|
111
|
+
(`sqlrules-postgresql`, `sqlrules-sqlite`).
|
|
112
|
+
|
|
92
113
|
Unsupported constraints raise `UnsupportedConstraintError` by default.
|
|
93
114
|
Use `on_unsupported="warn"` or `"ignore"` to change that policy for unknown
|
|
94
115
|
constraint operators (unsupported types always raise).
|
|
@@ -96,9 +117,17 @@ constraint operators (unsupported types always raise).
|
|
|
96
117
|
## Public API
|
|
97
118
|
|
|
98
119
|
```python
|
|
99
|
-
sqlrules.compile(model, table, *, column_map=None, on_unsupported="raise")
|
|
120
|
+
sqlrules.compile(model, table, *, column_map=None, on_unsupported="raise", cache=True)
|
|
100
121
|
sqlrules.where(rules) # flatten all expressions
|
|
101
122
|
sqlrules.flatten(rules) # alias of where()
|
|
123
|
+
|
|
124
|
+
compiler = sqlrules.Compiler(
|
|
125
|
+
plugins=[...], # optional SQLRulesPlugin instances
|
|
126
|
+
on_conflict="raise", # raise | replace | ignore
|
|
127
|
+
dialect=None, # optional hint for translators
|
|
128
|
+
on_unsupported="raise",
|
|
129
|
+
cache=True,
|
|
130
|
+
)
|
|
102
131
|
```
|
|
103
132
|
|
|
104
133
|
## Non-goals
|
|
@@ -111,15 +140,18 @@ constraints into SQLAlchemy expressions.
|
|
|
111
140
|
|
|
112
141
|
See [`docs/index.md`](docs/index.md) for the full documentation set,
|
|
113
142
|
including the [spec](docs/SPEC.md), [API](docs/API.md),
|
|
114
|
-
[
|
|
143
|
+
[plugin system](docs/PLUGIN_SYSTEM.md), [architecture](docs/ARCHITECTURE.md),
|
|
144
|
+
and [roadmap](docs/ROADMAP.md).
|
|
115
145
|
|
|
116
146
|
## Development
|
|
117
147
|
|
|
118
148
|
```bash
|
|
119
149
|
pip install -e ".[dev]"
|
|
120
|
-
|
|
150
|
+
pip install -e packages/sqlrules-postgresql -e packages/sqlrules-sqlite
|
|
151
|
+
pytest tests packages/sqlrules-postgresql/tests packages/sqlrules-sqlite/tests
|
|
121
152
|
ruff check .
|
|
122
153
|
mypy src/sqlrules
|
|
154
|
+
python -m benchmarks.bench_compile
|
|
123
155
|
```
|
|
124
156
|
|
|
125
157
|
## License
|
|
@@ -41,7 +41,22 @@ pip install sqlrules
|
|
|
41
41
|
|
|
42
42
|
Requires Python 3.10+, Pydantic v2, and SQLAlchemy 2.x.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Optional dialect plugins:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install sqlrules-postgresql # pattern → PostgreSQL ~
|
|
48
|
+
pip install sqlrules-sqlite # pattern → SQLite REGEXP
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from sqlrules import Compiler
|
|
53
|
+
from sqlrules_postgresql import PostgresPlugin
|
|
54
|
+
|
|
55
|
+
compiler = Compiler(plugins=[PostgresPlugin()], dialect="postgresql")
|
|
56
|
+
rules = compiler.compile(UserFilter, users)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Supported constraints (0.3)
|
|
45
60
|
|
|
46
61
|
| Constraint | SQLAlchemy expression |
|
|
47
62
|
|---|---|
|
|
@@ -51,6 +66,10 @@ Requires Python 3.10+, Pydantic v2, and SQLAlchemy 2.x.
|
|
|
51
66
|
| `Literal[...]` | `column.in_(...)` |
|
|
52
67
|
| `Enum` | `column.in_(...)` |
|
|
53
68
|
|
|
69
|
+
`pattern` is extracted into IR but has no portable core translator.
|
|
70
|
+
Use a custom registry translator or a dialect plugin
|
|
71
|
+
(`sqlrules-postgresql`, `sqlrules-sqlite`).
|
|
72
|
+
|
|
54
73
|
Unsupported constraints raise `UnsupportedConstraintError` by default.
|
|
55
74
|
Use `on_unsupported="warn"` or `"ignore"` to change that policy for unknown
|
|
56
75
|
constraint operators (unsupported types always raise).
|
|
@@ -58,9 +77,17 @@ constraint operators (unsupported types always raise).
|
|
|
58
77
|
## Public API
|
|
59
78
|
|
|
60
79
|
```python
|
|
61
|
-
sqlrules.compile(model, table, *, column_map=None, on_unsupported="raise")
|
|
80
|
+
sqlrules.compile(model, table, *, column_map=None, on_unsupported="raise", cache=True)
|
|
62
81
|
sqlrules.where(rules) # flatten all expressions
|
|
63
82
|
sqlrules.flatten(rules) # alias of where()
|
|
83
|
+
|
|
84
|
+
compiler = sqlrules.Compiler(
|
|
85
|
+
plugins=[...], # optional SQLRulesPlugin instances
|
|
86
|
+
on_conflict="raise", # raise | replace | ignore
|
|
87
|
+
dialect=None, # optional hint for translators
|
|
88
|
+
on_unsupported="raise",
|
|
89
|
+
cache=True,
|
|
90
|
+
)
|
|
64
91
|
```
|
|
65
92
|
|
|
66
93
|
## Non-goals
|
|
@@ -73,15 +100,18 @@ constraints into SQLAlchemy expressions.
|
|
|
73
100
|
|
|
74
101
|
See [`docs/index.md`](docs/index.md) for the full documentation set,
|
|
75
102
|
including the [spec](docs/SPEC.md), [API](docs/API.md),
|
|
76
|
-
[
|
|
103
|
+
[plugin system](docs/PLUGIN_SYSTEM.md), [architecture](docs/ARCHITECTURE.md),
|
|
104
|
+
and [roadmap](docs/ROADMAP.md).
|
|
77
105
|
|
|
78
106
|
## Development
|
|
79
107
|
|
|
80
108
|
```bash
|
|
81
109
|
pip install -e ".[dev]"
|
|
82
|
-
|
|
110
|
+
pip install -e packages/sqlrules-postgresql -e packages/sqlrules-sqlite
|
|
111
|
+
pytest tests packages/sqlrules-postgresql/tests packages/sqlrules-sqlite/tests
|
|
83
112
|
ruff check .
|
|
84
113
|
mypy src/sqlrules
|
|
114
|
+
python -m benchmarks.bench_compile
|
|
85
115
|
```
|
|
86
116
|
|
|
87
117
|
## 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()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Public API
|
|
2
|
+
|
|
3
|
+
SQLRules exposes a deliberately small public surface.
|
|
4
|
+
|
|
5
|
+
## `compile`
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
sqlrules.compile(
|
|
9
|
+
model,
|
|
10
|
+
table,
|
|
11
|
+
*,
|
|
12
|
+
column_map=None,
|
|
13
|
+
on_unsupported="raise",
|
|
14
|
+
cache=True,
|
|
15
|
+
) -> dict[str, list[ColumnElement[bool]]]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Compile a constrained Pydantic model into a field-keyed rule dictionary.
|
|
19
|
+
|
|
20
|
+
| Parameter | Description |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `model` | Pydantic `BaseModel` subclass |
|
|
23
|
+
| `table` | SQLAlchemy `Table`, alias, ORM class, or object with `.c` |
|
|
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). |
|
|
25
|
+
| `on_unsupported` | `"raise"` (default), `"warn"`, or `"ignore"` for unknown **operators** only |
|
|
26
|
+
| `cache` | Cache Phase-1 model IR (default `True`) |
|
|
27
|
+
|
|
28
|
+
Rule dictionary keys are always the Python field names. String field aliases
|
|
29
|
+
(`alias`, `validation_alias`, `serialization_alias`) are used only for column
|
|
30
|
+
binding. Unconstrained fields are omitted and do not require a column.
|
|
31
|
+
|
|
32
|
+
Unsupported types always raise, regardless of `on_unsupported`.
|
|
33
|
+
|
|
34
|
+
Module-level `compile` does not accept plugins; use `Compiler` for extensions.
|
|
35
|
+
|
|
36
|
+
## `where` / `flatten`
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
sqlrules.where(rules) -> list[ColumnElement[bool]]
|
|
40
|
+
sqlrules.flatten(rules) -> list[ColumnElement[bool]]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Flatten a rule dictionary into a single list of expressions suitable for
|
|
44
|
+
`Query.where(*expressions)` or `select(...).where(*expressions)`.
|
|
45
|
+
|
|
46
|
+
`where` and `flatten` are identical aliases.
|
|
47
|
+
|
|
48
|
+
## `Compiler`
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
compiler = sqlrules.Compiler(
|
|
52
|
+
on_unsupported="raise",
|
|
53
|
+
registry=None,
|
|
54
|
+
plugins=None,
|
|
55
|
+
on_conflict="raise",
|
|
56
|
+
dialect=None,
|
|
57
|
+
cache=True,
|
|
58
|
+
model_cache=None, # optional shared ModelIRCache; default is process-wide
|
|
59
|
+
)
|
|
60
|
+
rules = compiler.compile(model, table, column_map=None)
|
|
61
|
+
|
|
62
|
+
# Two-phase (advanced):
|
|
63
|
+
model_ir = compiler.compile_model(model) # also clears diagnostics
|
|
64
|
+
rules = compiler.bind(model_ir, table, column_map=None)
|
|
65
|
+
|
|
66
|
+
# Diagnostics from the last compile_model / compile / bind:
|
|
67
|
+
compiler.diagnostics
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Parameter | Description |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `plugins` | Optional sequence of `SQLRulesPlugin` objects registered at init |
|
|
73
|
+
| `on_conflict` | Default conflict policy for plugin `register()`: `"raise"`, `"replace"`, or `"ignore"` |
|
|
74
|
+
| `dialect` | Optional string hint stored on `CompilationContext` (never auto-detected) |
|
|
75
|
+
| `registry` | Optional base `TranslatorRegistry`; copied when `plugins` is non-empty |
|
|
76
|
+
|
|
77
|
+
Reusable compiler instance with a fixed unsupported-constraint policy,
|
|
78
|
+
optional plugins, optional custom translator registry, and optional Phase-1
|
|
79
|
+
metadata cache.
|
|
80
|
+
|
|
81
|
+
`compile_model` / `bind` separate static IR extraction from table binding so
|
|
82
|
+
the same `ModelIR` can be reused across tables.
|
|
83
|
+
|
|
84
|
+
**Concurrency:** do not call `compile` / `bind` / `compile_model` concurrently
|
|
85
|
+
on the same `Compiler` instance (diagnostics are per-instance and not locked).
|
|
86
|
+
The shared Phase-1 IR cache is thread-safe for concurrent reads/writes across
|
|
87
|
+
instances.
|
|
88
|
+
|
|
89
|
+
## Plugins
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from sqlrules import PLUGIN_API_VERSION, Compiler
|
|
93
|
+
|
|
94
|
+
class MyPlugin:
|
|
95
|
+
name = "my-plugin"
|
|
96
|
+
api_version = PLUGIN_API_VERSION
|
|
97
|
+
|
|
98
|
+
def register(self, registry):
|
|
99
|
+
registry.register_constraint(
|
|
100
|
+
"pattern",
|
|
101
|
+
lambda c, col, ctx: col.op("~")(c.value),
|
|
102
|
+
on_conflict="replace",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
compiler = Compiler(plugins=[MyPlugin()])
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
See [PLUGIN_SYSTEM.md](PLUGIN_SYSTEM.md). Conformance helpers live in
|
|
109
|
+
`sqlrules.conformance`.
|
|
110
|
+
|
|
111
|
+
## Other exports
|
|
112
|
+
|
|
113
|
+
Also exported for advanced use and typing:
|
|
114
|
+
|
|
115
|
+
- `__version__`
|
|
116
|
+
- `PLUGIN_API_VERSION`, `SQLRulesPlugin`
|
|
117
|
+
- `SQLRulesWarning`
|
|
118
|
+
- `CompilationContext`, `Constraint`, `FieldDescriptor`, `FieldIR`, `ModelIR`,
|
|
119
|
+
`Diagnostic` (IR helpers)
|
|
120
|
+
|
|
121
|
+
`DiagnosticsCollector` and `ModelIRCache` are internal (import from
|
|
122
|
+
`sqlrules.ir` / `sqlrules.cache` only if you accept instability).
|
|
123
|
+
|
|
124
|
+
## Exceptions
|
|
125
|
+
|
|
126
|
+
All public exceptions inherit from `SQLRulesError`:
|
|
127
|
+
|
|
128
|
+
- `InvalidModelError`
|
|
129
|
+
- `MissingColumnError`
|
|
130
|
+
- `UnsupportedConstraintError`
|
|
131
|
+
- `TranslatorError`
|
|
132
|
+
- `InvalidTranslatorError`
|
|
133
|
+
- `RegistryError`
|
|
134
|
+
- `ConfigurationError`
|
|
135
|
+
- `PluginError`
|
|
136
|
+
- `InternalCompilerError` (reserved)
|
|
137
|
+
|
|
138
|
+
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,17 +134,25 @@ 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
|
|
|
138
|
-
|
|
145
|
+
Available today:
|
|
146
|
+
|
|
147
|
+
- Inject a custom `TranslatorRegistry` via `Compiler(registry=...)`
|
|
148
|
+
- Register versioned plugins via `Compiler(plugins=[...])`
|
|
149
|
+
- Override operators with `register_constraint(..., on_conflict=...)`
|
|
150
|
+
- Optional `dialect=` hint on `CompilationContext`
|
|
151
|
+
|
|
152
|
+
Reserved for later:
|
|
139
153
|
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
- Override built-in mappings.
|
|
143
|
-
- Add new IR transforms.
|
|
154
|
+
- Compiler pass plugins
|
|
155
|
+
- Entry-point / auto-discovery
|
|
144
156
|
|
|
145
157
|
## Design Principles
|
|
146
158
|
|