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.
Files changed (54) hide show
  1. {sqlrules-0.1.0 → sqlrules-0.2.0}/CHANGELOG.md +37 -0
  2. {sqlrules-0.1.0 → sqlrules-0.2.0}/CONTRIBUTING.md +9 -5
  3. {sqlrules-0.1.0 → sqlrules-0.2.0}/PKG-INFO +8 -3
  4. {sqlrules-0.1.0 → sqlrules-0.2.0}/README.md +5 -2
  5. sqlrules-0.2.0/benchmarks/__init__.py +1 -0
  6. sqlrules-0.2.0/benchmarks/bench_compile.py +70 -0
  7. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/API.md +33 -7
  8. sqlrules-0.2.0/docs/ARCHITECTURE.md +33 -0
  9. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/COMPILER.md +19 -8
  10. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/CONSTRAINTS.md +12 -1
  11. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/DESIGN_DECISIONS.md +16 -16
  12. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/DIALECT_SUPPORT.md +1 -1
  13. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/ERRORS.md +35 -15
  14. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/INTERNAL_API.md +13 -16
  15. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/MILESTONES.md +5 -5
  16. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/PERFORMANCE.md +18 -9
  17. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/PLUGIN_SYSTEM.md +4 -3
  18. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/ROADMAP.md +5 -3
  19. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/SPEC.md +25 -4
  20. sqlrules-0.2.0/docs/TESTING.md +40 -0
  21. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/TRANSLATORS.md +13 -7
  22. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/TYPE_SUPPORT.md +41 -25
  23. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/index.md +1 -1
  24. {sqlrules-0.1.0 → sqlrules-0.2.0}/pyproject.toml +4 -1
  25. {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/__init__.py +12 -2
  26. sqlrules-0.2.0/src/sqlrules/cache.py +40 -0
  27. {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/columns.py +3 -2
  28. sqlrules-0.2.0/src/sqlrules/compiler.py +141 -0
  29. {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/constraints.py +111 -10
  30. {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/errors.py +1 -1
  31. {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/inspectors.py +1 -1
  32. sqlrules-0.2.0/src/sqlrules/ir.py +87 -0
  33. {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/translators.py +35 -9
  34. {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_correctness.py +18 -8
  35. {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_errors.py +1 -1
  36. {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_v01_features.py +3 -4
  37. sqlrules-0.2.0/tests/test_v02_features.py +378 -0
  38. sqlrules-0.1.0/docs/ARCHITECTURE.md +0 -25
  39. sqlrules-0.1.0/docs/TESTING.md +0 -28
  40. sqlrules-0.1.0/src/sqlrules/compiler.py +0 -82
  41. sqlrules-0.1.0/src/sqlrules/ir.py +0 -28
  42. {sqlrules-0.1.0 → sqlrules-0.2.0}/.gitignore +0 -0
  43. {sqlrules-0.1.0 → sqlrules-0.2.0}/LICENSE +0 -0
  44. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/CONTRIBUTING.md +0 -0
  45. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/NON_GOALS.md +0 -0
  46. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/PHILOSOPHY.md +0 -0
  47. {sqlrules-0.1.0 → sqlrules-0.2.0}/docs/VISION.md +0 -0
  48. {sqlrules-0.1.0 → sqlrules-0.2.0}/src/sqlrules/py.typed +0 -0
  49. {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/conftest.py +0 -0
  50. {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_coverage_edges.py +0 -0
  51. {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_helpers.py +0 -0
  52. {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_literal_enum.py +0 -0
  53. {sqlrules-0.1.0 → sqlrules-0.2.0}/tests/test_numeric_constraints.py +0 -0
  54. {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
- 0.1.x releases are published to PyPI by pushing a version tag. Before tagging:
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.1.0):
49
+ Then create and push the tag (example for 0.2.0):
46
50
 
47
51
  ```bash
48
- git tag -a v0.1.0 -m "sqlrules 0.1.0"
49
- git push origin v0.1.0
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.1.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.1)
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.1)
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(on_unsupported="raise", registry=None)
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 and
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` (IR helpers)
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.1)
96
+ - `InvalidTranslatorError` (reserved; not raised in 0.2)
71
97
  - `RegistryError`
72
98
  - `ConfigurationError`
73
- - `InternalCompilerError` (reserved; not raised in 0.1)
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 - 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,16 +134,23 @@ 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
 
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 custom translators.
141
- - Register dialect-specific translators.
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 only).
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.1 does **not** ship a plugin API. Customization today is limited
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.1. See
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 (Future)
301
+ # Decision 12: Two-Phase Compilation
301
302
 
302
303
  ## Decision
303
304
 
304
- SQLRules should eventually separate static model compilation from table
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
- ## Planned Phase 1: Static Compilation
307
+ ## Phase 1: Static Compilation
309
308
 
310
309
  - inspect Pydantic model
311
310
  - extract constraints
312
- - build IR
311
+ - build `ModelIR` (cached by default)
313
312
 
314
- ## Planned Phase 2: Binding
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
- This supports better performance and cleaner architecture.
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
- Regex support varies by database.
441
+ **Decided for 0.2:**
442
442
 
443
- Likely answer:
444
-
445
- - IR support in core
446
- - translator support in dialect plugins
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
- 0.1 emits portable SQLAlchemy Core expressions. The table below describes
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.1)
28
+ ├── InvalidTranslatorError (reserved in 0.2)
28
29
  ├── RegistryError
29
30
  ├── ConfigurationError
30
- └── InternalCompilerError (reserved in 0.1)
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`, `max_digits`, `decimal_places`
70
+ - `pattern` (no core translator), `max_digits`, `decimal_places`
70
71
  - custom validators / `Strict`
71
- - unsupported types (`UUID`, containers, `time`, `timedelta`)
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 0.1.
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.1 compiler path.
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.1 this is raised for an invalid `on_unsupported` mode only.
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.1
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 constraint.
135
+ Emits a `SQLRulesWarning`, records a `Diagnostic`, and skips the
136
+ constraint.
135
137
 
136
138
  ## ignore
137
139
 
138
- Silently skips unsupported constraints.
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 warnings.
183
+ Applications decide how to handle exceptions, warnings, and diagnostics.
164
184
 
165
185
  ------------------------------------------------------------------------
166
186