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.
Files changed (72) hide show
  1. {sqlrules-0.1.0 → sqlrules-0.3.0}/.gitignore +1 -0
  2. sqlrules-0.3.0/CHANGELOG.md +113 -0
  3. sqlrules-0.3.0/CONTRIBUTING.md +80 -0
  4. {sqlrules-0.1.0 → sqlrules-0.3.0}/PKG-INFO +37 -5
  5. {sqlrules-0.1.0 → sqlrules-0.3.0}/README.md +34 -4
  6. sqlrules-0.3.0/benchmarks/__init__.py +1 -0
  7. sqlrules-0.3.0/benchmarks/bench_compile.py +70 -0
  8. sqlrules-0.3.0/docs/API.md +138 -0
  9. sqlrules-0.3.0/docs/ARCHITECTURE.md +33 -0
  10. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/COMPILER.md +22 -10
  11. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/CONSTRAINTS.md +23 -1
  12. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/DESIGN_DECISIONS.md +22 -20
  13. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/DIALECT_SUPPORT.md +14 -10
  14. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/ERRORS.md +51 -16
  15. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/INTERNAL_API.md +21 -19
  16. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/MILESTONES.md +6 -6
  17. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/PERFORMANCE.md +18 -9
  18. sqlrules-0.3.0/docs/PLUGIN_SYSTEM.md +201 -0
  19. sqlrules-0.3.0/docs/ROADMAP.md +44 -0
  20. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/SPEC.md +39 -4
  21. sqlrules-0.3.0/docs/TESTING.md +51 -0
  22. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/TRANSLATORS.md +38 -20
  23. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/TYPE_SUPPORT.md +41 -25
  24. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/index.md +3 -3
  25. sqlrules-0.3.0/packages/sqlrules-postgresql/README.md +16 -0
  26. sqlrules-0.3.0/packages/sqlrules-postgresql/pyproject.toml +28 -0
  27. sqlrules-0.3.0/packages/sqlrules-postgresql/src/sqlrules_postgresql/__init__.py +28 -0
  28. sqlrules-0.3.0/packages/sqlrules-postgresql/src/sqlrules_postgresql/pattern.py +16 -0
  29. sqlrules-0.3.0/packages/sqlrules-postgresql/tests/test_postgres_plugin.py +33 -0
  30. sqlrules-0.3.0/packages/sqlrules-sqlite/README.md +19 -0
  31. sqlrules-0.3.0/packages/sqlrules-sqlite/pyproject.toml +28 -0
  32. sqlrules-0.3.0/packages/sqlrules-sqlite/src/sqlrules_sqlite/__init__.py +33 -0
  33. sqlrules-0.3.0/packages/sqlrules-sqlite/src/sqlrules_sqlite/pattern.py +19 -0
  34. sqlrules-0.3.0/packages/sqlrules-sqlite/tests/test_sqlite_plugin.py +33 -0
  35. {sqlrules-0.1.0 → sqlrules-0.3.0}/pyproject.toml +5 -1
  36. {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/__init__.py +17 -2
  37. sqlrules-0.3.0/src/sqlrules/cache.py +40 -0
  38. {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/columns.py +3 -2
  39. sqlrules-0.3.0/src/sqlrules/compiler.py +206 -0
  40. sqlrules-0.3.0/src/sqlrules/conformance.py +146 -0
  41. {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/constraints.py +111 -10
  42. {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/errors.py +23 -7
  43. {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/inspectors.py +1 -1
  44. sqlrules-0.3.0/src/sqlrules/ir.py +92 -0
  45. sqlrules-0.3.0/src/sqlrules/plugins.py +57 -0
  46. {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/translators.py +107 -14
  47. {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/conftest.py +5 -0
  48. {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_correctness.py +18 -8
  49. {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_errors.py +1 -1
  50. sqlrules-0.3.0/tests/test_plugins.py +364 -0
  51. {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_v01_features.py +3 -4
  52. sqlrules-0.3.0/tests/test_v02_features.py +380 -0
  53. sqlrules-0.1.0/CHANGELOG.md +0 -51
  54. sqlrules-0.1.0/CONTRIBUTING.md +0 -53
  55. sqlrules-0.1.0/docs/API.md +0 -75
  56. sqlrules-0.1.0/docs/ARCHITECTURE.md +0 -25
  57. sqlrules-0.1.0/docs/PLUGIN_SYSTEM.md +0 -251
  58. sqlrules-0.1.0/docs/ROADMAP.md +0 -33
  59. sqlrules-0.1.0/docs/TESTING.md +0 -28
  60. sqlrules-0.1.0/src/sqlrules/compiler.py +0 -82
  61. sqlrules-0.1.0/src/sqlrules/ir.py +0 -28
  62. {sqlrules-0.1.0 → sqlrules-0.3.0}/LICENSE +0 -0
  63. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/CONTRIBUTING.md +0 -0
  64. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/NON_GOALS.md +0 -0
  65. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/PHILOSOPHY.md +0 -0
  66. {sqlrules-0.1.0 → sqlrules-0.3.0}/docs/VISION.md +0 -0
  67. {sqlrules-0.1.0 → sqlrules-0.3.0}/src/sqlrules/py.typed +0 -0
  68. {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_coverage_edges.py +0 -0
  69. {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_helpers.py +0 -0
  70. {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_literal_enum.py +0 -0
  71. {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_numeric_constraints.py +0 -0
  72. {sqlrules-0.1.0 → sqlrules-0.3.0}/tests/test_string_constraints.py +0 -0
@@ -10,6 +10,7 @@ __pycache__/
10
10
  .coverage.*
11
11
  htmlcov/
12
12
  dist/
13
+ dist-plugins/
13
14
  build/
14
15
  *.egg-info/
15
16
  .DS_Store
@@ -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.1.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
- ## Supported constraints (0.1)
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
- [architecture](docs/ARCHITECTURE.md), and [roadmap](docs/ROADMAP.md).
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
- pytest
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
- ## Supported constraints (0.1)
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
- [architecture](docs/ARCHITECTURE.md), and [roadmap](docs/ROADMAP.md).
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
- pytest
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 - SQLAlchemy Table, Alias, ORM
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
- Future versions may cache compiled metadata using the model type as the
134
- cache key. Translation remains deterministic.
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
- Future plugin hooks:
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
- - Register custom translators.
141
- - Register dialect-specific translators.
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