sqlrules 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -10,21 +10,31 @@ from sqlrules.errors import (
10
10
  TranslatorError,
11
11
  UnsupportedConstraintError,
12
12
  )
13
- from sqlrules.ir import CompilationContext, Constraint, FieldDescriptor
13
+ from sqlrules.ir import (
14
+ CompilationContext,
15
+ Constraint,
16
+ Diagnostic,
17
+ FieldDescriptor,
18
+ FieldIR,
19
+ ModelIR,
20
+ )
14
21
  from sqlrules.translators import SQLRulesWarning
15
22
 
16
- __version__ = "0.1.0"
23
+ __version__ = "0.2.0"
17
24
 
18
25
  __all__ = [
19
26
  "CompilationContext",
20
27
  "Compiler",
21
28
  "ConfigurationError",
22
29
  "Constraint",
30
+ "Diagnostic",
23
31
  "FieldDescriptor",
32
+ "FieldIR",
24
33
  "InternalCompilerError",
25
34
  "InvalidModelError",
26
35
  "InvalidTranslatorError",
27
36
  "MissingColumnError",
37
+ "ModelIR",
28
38
  "RegistryError",
29
39
  "SQLRulesError",
30
40
  "SQLRulesWarning",
sqlrules/cache.py ADDED
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from typing import Any
5
+ from weakref import WeakKeyDictionary
6
+
7
+ from sqlrules.ir import ModelIR
8
+
9
+
10
+ class ModelIRCache:
11
+ """Thread-safe in-process cache of Phase-1 ModelIR keyed by model class.
12
+
13
+ Cached values are immutable and may be shared across threads. Column
14
+ objects are never stored here.
15
+ """
16
+
17
+ def __init__(self) -> None:
18
+ self._lock = threading.RLock()
19
+ self._store: WeakKeyDictionary[type[Any], ModelIR] = WeakKeyDictionary()
20
+
21
+ def get(self, model: type[Any]) -> ModelIR | None:
22
+ with self._lock:
23
+ return self._store.get(model)
24
+
25
+ def put(self, model: type[Any], model_ir: ModelIR) -> ModelIR:
26
+ with self._lock:
27
+ self._store[model] = model_ir
28
+ return model_ir
29
+
30
+ def clear(self) -> None:
31
+ with self._lock:
32
+ self._store.clear()
33
+
34
+
35
+ # Shared default cache for Compiler instances with cache=True.
36
+ _default_cache = ModelIRCache()
37
+
38
+
39
+ def default_cache() -> ModelIRCache:
40
+ return _default_cache
sqlrules/columns.py CHANGED
@@ -55,8 +55,9 @@ def resolve_column(
55
55
  for candidate in candidates:
56
56
  if candidate in column_map:
57
57
  column = _as_column_element(column_map[candidate])
58
- if column is not None:
59
- return column
58
+ if column is None:
59
+ raise MissingColumnError(field=field_name)
60
+ return column
60
61
 
61
62
  columns = getattr(table, "c", None)
62
63
  if columns is not None:
sqlrules/compiler.py CHANGED
@@ -7,11 +7,19 @@ from typing import Any
7
7
  from pydantic import BaseModel
8
8
  from sqlalchemy.sql.elements import ColumnElement
9
9
 
10
+ from sqlrules.cache import ModelIRCache, default_cache
10
11
  from sqlrules.columns import resolve_column
11
12
  from sqlrules.constraints import ensure_supported_type, extract_constraints
12
13
  from sqlrules.errors import ConfigurationError
13
14
  from sqlrules.inspectors import inspect_model
14
- from sqlrules.ir import CompilationContext, OnUnsupported
15
+ from sqlrules.ir import (
16
+ CompilationContext,
17
+ Diagnostic,
18
+ DiagnosticsCollector,
19
+ FieldIR,
20
+ ModelIR,
21
+ OnUnsupported,
22
+ )
15
23
  from sqlrules.translators import TranslatorRegistry, default_registry
16
24
 
17
25
  RulesDict = dict[str, list[ColumnElement[bool]]]
@@ -23,46 +31,94 @@ class Compiler:
23
31
  *,
24
32
  registry: TranslatorRegistry | None = None,
25
33
  on_unsupported: OnUnsupported = "raise",
34
+ cache: bool = True,
35
+ model_cache: ModelIRCache | None = None,
26
36
  ) -> None:
27
37
  if on_unsupported not in {"raise", "warn", "ignore"}:
28
38
  raise ConfigurationError(option="on_unsupported", value=on_unsupported)
29
39
  self.registry = registry or default_registry()
30
- self.context = CompilationContext(on_unsupported=on_unsupported)
31
-
32
- def compile(
40
+ self.on_unsupported = on_unsupported
41
+ self.cache_enabled = cache
42
+ self._model_cache = model_cache if model_cache is not None else default_cache()
43
+ self._collector = DiagnosticsCollector()
44
+ self.context = CompilationContext(
45
+ on_unsupported=on_unsupported,
46
+ collector=self._collector,
47
+ )
48
+
49
+ @property
50
+ def diagnostics(self) -> tuple[Diagnostic, ...]:
51
+ """Diagnostics from the most recent ``compile`` / ``bind`` call."""
52
+ return self._collector.snapshot()
53
+
54
+ def compile_model(self, model: type[BaseModel]) -> ModelIR:
55
+ """Phase 1: inspect the model and extract constraint IR (no table binding)."""
56
+ self._collector.clear()
57
+ if self.cache_enabled:
58
+ cached = self._model_cache.get(model)
59
+ if cached is not None:
60
+ return cached
61
+
62
+ fields: list[FieldIR] = []
63
+ for descriptor in inspect_model(model):
64
+ ensure_supported_type(descriptor)
65
+ constraints = tuple(extract_constraints(descriptor))
66
+ fields.append(FieldIR(descriptor=descriptor, constraints=constraints))
67
+
68
+ model_ir = ModelIR(model=model, fields=tuple(fields))
69
+ if self.cache_enabled:
70
+ return self._model_cache.put(model, model_ir)
71
+ return model_ir
72
+
73
+ def bind(
33
74
  self,
34
- model: type[BaseModel],
75
+ model_ir: ModelIR,
35
76
  table: Any,
36
77
  *,
37
78
  column_map: Mapping[str, ColumnElement[Any]] | None = None,
38
79
  ) -> RulesDict:
39
- rules: RulesDict = {}
80
+ """Phase 2: resolve columns and translate IR into SQLAlchemy expressions."""
81
+ self._collector.clear()
82
+ # Refresh context in case on_unsupported was mutated (should not be).
83
+ self.context = CompilationContext(
84
+ on_unsupported=self.on_unsupported,
85
+ collector=self._collector,
86
+ )
40
87
 
41
- for field in inspect_model(model):
42
- ensure_supported_type(field)
43
- constraints = extract_constraints(field)
44
- if not constraints:
88
+ rules: RulesDict = {}
89
+ for field_ir in model_ir.fields:
90
+ if not field_ir.constraints:
45
91
  continue
46
92
 
93
+ descriptor = field_ir.descriptor
47
94
  column = resolve_column(
48
- field.name,
95
+ descriptor.name,
49
96
  table,
50
97
  column_map,
51
- alias=field.alias,
52
- aliases=field.aliases,
98
+ alias=descriptor.alias,
99
+ aliases=descriptor.aliases,
53
100
  )
54
101
 
55
102
  expressions: list[ColumnElement[bool]] = []
56
- for constraint in constraints:
103
+ for constraint in field_ir.constraints:
57
104
  expression = self.registry.translate(constraint, column, self.context)
58
105
  if expression is not None:
59
106
  expressions.append(expression)
60
107
 
61
108
  if expressions:
62
- rules[field.name] = expressions
109
+ rules[descriptor.name] = expressions
63
110
 
64
111
  return rules
65
112
 
113
+ def compile(
114
+ self,
115
+ model: type[BaseModel],
116
+ table: Any,
117
+ *,
118
+ column_map: Mapping[str, ColumnElement[Any]] | None = None,
119
+ ) -> RulesDict:
120
+ return self.bind(self.compile_model(model), table, column_map=column_map)
121
+
66
122
 
67
123
  def compile(
68
124
  model: type[BaseModel],
@@ -70,8 +126,11 @@ def compile(
70
126
  *,
71
127
  column_map: Mapping[str, ColumnElement[Any]] | None = None,
72
128
  on_unsupported: OnUnsupported = "raise",
129
+ cache: bool = True,
73
130
  ) -> RulesDict:
74
- return Compiler(on_unsupported=on_unsupported).compile(model, table, column_map=column_map)
131
+ return Compiler(on_unsupported=on_unsupported, cache=cache).compile(
132
+ model, table, column_map=column_map
133
+ )
75
134
 
76
135
 
77
136
  def flatten(rules: RulesDict) -> list[ColumnElement[bool]]:
sqlrules/constraints.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from datetime import date, datetime, time, timedelta
4
5
  from decimal import Decimal
5
6
  from enum import Enum
@@ -23,11 +24,14 @@ from pydantic.fields import FieldInfo
23
24
  from sqlrules.errors import UnsupportedConstraintError
24
25
  from sqlrules.ir import Constraint, FieldDescriptor
25
26
 
26
- _SUPPORTED_TYPES: frozenset[type[Any]] = frozenset({bool, int, float, Decimal, str, date, datetime})
27
- _UNSUPPORTED_TYPES: frozenset[type[Any]] = frozenset({UUID, time, timedelta})
27
+ _SUPPORTED_TYPES: frozenset[type[Any]] = frozenset(
28
+ {bool, int, float, Decimal, str, date, datetime, time, UUID}
29
+ )
30
+ _UNSUPPORTED_TYPES: frozenset[type[Any]] = frozenset({timedelta})
28
31
  _UNSUPPORTED_ORIGINS: frozenset[Any] = frozenset({list, dict, tuple, set, frozenset})
29
32
  _LENGTH_OPERATORS: frozenset[str] = frozenset({"min_length", "max_length"})
30
33
  _NUMERIC_OPERATORS: frozenset[str] = frozenset({"gt", "ge", "lt", "le", "multiple_of"})
34
+ _TEMPORAL_TYPES: frozenset[type[Any]] = frozenset({date, datetime, time})
31
35
  _VALIDATOR_TYPE_NAMES = frozenset(
32
36
  {
33
37
  "AfterValidator",
@@ -38,6 +42,33 @@ _VALIDATOR_TYPE_NAMES = frozenset(
38
42
  "Strict",
39
43
  }
40
44
  )
45
+ # Keys that map to IR constraint operators.
46
+ _CONSTRAINT_KEYS: frozenset[str] = frozenset(
47
+ {
48
+ "gt",
49
+ "ge",
50
+ "lt",
51
+ "le",
52
+ "multiple_of",
53
+ "min_length",
54
+ "max_length",
55
+ "pattern",
56
+ "max_digits",
57
+ "decimal_places",
58
+ }
59
+ )
60
+ # Pydantic / StringConstraints flags that are validation-only (not SQL operators).
61
+ _IGNORED_METADATA_KEYS: frozenset[str] = frozenset(
62
+ {
63
+ "strip_whitespace",
64
+ "to_upper",
65
+ "to_lower",
66
+ "strict",
67
+ "allow_inf_nan",
68
+ "ascii_only",
69
+ "coerce_numbers_to_str",
70
+ }
71
+ )
41
72
 
42
73
 
43
74
  def _unwrap_annotation(annotation: Any) -> tuple[Any, tuple[Any, ...]]:
@@ -96,7 +127,7 @@ def _concrete_type(field: FieldDescriptor) -> Any:
96
127
 
97
128
 
98
129
  def ensure_supported_type(field: FieldDescriptor) -> None:
99
- """Raise when a field annotation is outside the v0.1 support matrix."""
130
+ """Raise when a field annotation is outside the support matrix."""
100
131
  annotation = _concrete_type(field)
101
132
  origin = get_origin(annotation)
102
133
 
@@ -127,7 +158,7 @@ def ensure_supported_type(field: FieldDescriptor) -> None:
127
158
  field=field.name,
128
159
  operator=type_name,
129
160
  value=annotation,
130
- suggestion=f"Type {type_name!r} is not supported in SQLRules 0.1.",
161
+ suggestion=f"Type {type_name!r} is not supported by SQLRules.",
131
162
  )
132
163
 
133
164
  # Unions with multiple non-None members and other unknown annotations.
@@ -136,10 +167,41 @@ def ensure_supported_type(field: FieldDescriptor) -> None:
136
167
  field=field.name,
137
168
  operator="type",
138
169
  value=annotation,
139
- suggestion=f"Annotation {type_name} is outside the SQLRules 0.1 type support matrix.",
170
+ suggestion=f"Annotation {type_name} is outside the SQLRules type support matrix.",
171
+ )
172
+
173
+
174
+ def _normalize_pattern(field_name: str, pattern: Any) -> str:
175
+ if isinstance(pattern, str):
176
+ return pattern
177
+ if isinstance(pattern, re.Pattern):
178
+ return str(pattern.pattern)
179
+ raise UnsupportedConstraintError(
180
+ field=field_name,
181
+ operator="pattern",
182
+ value=pattern,
183
+ suggestion="pattern must be a str or re.Pattern.",
140
184
  )
141
185
 
142
186
 
187
+ def _constraints_from_mapping(field_name: str, data: dict[str, Any]) -> list[Constraint]:
188
+ """Map whitelisted metadata keys into IR; ignore validation-only flags."""
189
+ constraints: list[Constraint] = []
190
+ for key, value in data.items():
191
+ if value is None or key in _IGNORED_METADATA_KEYS:
192
+ continue
193
+ if key == "pattern":
194
+ constraints.append(
195
+ Constraint(field_name, "pattern", _normalize_pattern(field_name, value))
196
+ )
197
+ elif key in _CONSTRAINT_KEYS:
198
+ constraints.append(Constraint(field_name, key, value))
199
+ else:
200
+ # Unknown keys remain unsupported operators (fail-fast).
201
+ constraints.append(Constraint(field_name, key, value))
202
+ return constraints
203
+
204
+
143
205
  def _unsupported_constraints(field_name: str, item: Any) -> list[Constraint]:
144
206
  if isinstance(item, Predicate):
145
207
  return [Constraint(field_name, "predicate", item.func)]
@@ -154,11 +216,25 @@ def _unsupported_constraints(field_name: str, item: Any) -> list[Constraint]:
154
216
  )
155
217
  ]
156
218
 
219
+ # First-class pattern attribute (e.g. _PydanticGeneralMetadata(pattern=...)).
220
+ pattern = getattr(item, "pattern", None)
221
+ if pattern is not None and isinstance(pattern, (str, re.Pattern)):
222
+ constraints = _constraints_from_mapping(
223
+ field_name,
224
+ {
225
+ key: value
226
+ for key, value in getattr(item, "__dict__", {}).items()
227
+ if key != "pattern" and value is not None
228
+ },
229
+ )
230
+ constraints.append(
231
+ Constraint(field_name, "pattern", _normalize_pattern(field_name, pattern))
232
+ )
233
+ return constraints
234
+
157
235
  data = getattr(item, "__dict__", None)
158
236
  if isinstance(data, dict) and data:
159
- return [
160
- Constraint(field_name, key, value) for key, value in data.items() if value is not None
161
- ]
237
+ return _constraints_from_mapping(field_name, data)
162
238
 
163
239
  return [Constraint(field_name, type_name, item)]
164
240
 
@@ -191,12 +267,29 @@ def _reject_type_operator_mismatch(field: FieldDescriptor, constraint: Constrain
191
267
  return
192
268
 
193
269
  if annotation is bool and constraint.operator in _LENGTH_OPERATORS | _NUMERIC_OPERATORS:
270
+ raise UnsupportedConstraintError(
271
+ field=field.name,
272
+ operator=constraint.operator,
273
+ value=constraint.value,
274
+ suggestion=("Bool fields only support Literal/equality-style constraints in SQLRules."),
275
+ )
276
+
277
+ if annotation is UUID and constraint.operator in _LENGTH_OPERATORS | _NUMERIC_OPERATORS:
278
+ raise UnsupportedConstraintError(
279
+ field=field.name,
280
+ operator=constraint.operator,
281
+ value=constraint.value,
282
+ suggestion=("UUID fields only support Literal/Enum-style constraints in SQLRules."),
283
+ )
284
+
285
+ if annotation in _TEMPORAL_TYPES and constraint.operator == "multiple_of":
194
286
  raise UnsupportedConstraintError(
195
287
  field=field.name,
196
288
  operator=constraint.operator,
197
289
  value=constraint.value,
198
290
  suggestion=(
199
- "Bool fields only support Literal/equality-style constraints in SQLRules 0.1."
291
+ "multiple_of is not valid for date/datetime/time fields; "
292
+ "use gt/ge/lt/le range constraints."
200
293
  ),
201
294
  )
202
295
 
@@ -216,6 +309,14 @@ def _reject_type_operator_mismatch(field: FieldDescriptor, constraint: Constrain
216
309
  suggestion="Numeric comparison constraints are not valid for str fields.",
217
310
  )
218
311
 
312
+ if constraint.operator == "pattern" and annotation is not str:
313
+ raise UnsupportedConstraintError(
314
+ field=field.name,
315
+ operator=constraint.operator,
316
+ value=constraint.value,
317
+ suggestion="pattern constraints require a str field annotation.",
318
+ )
319
+
219
320
 
220
321
  def extract_constraints(field: FieldDescriptor) -> list[Constraint]:
221
322
  constraints: list[Constraint] = []
@@ -250,7 +351,7 @@ def extract_constraints(field: FieldDescriptor) -> list[Constraint]:
250
351
  constraints.append(Constraint(field.name, "enum", values))
251
352
 
252
353
  for constraint in constraints:
253
- if constraint.operator in _LENGTH_OPERATORS | _NUMERIC_OPERATORS:
354
+ if constraint.operator in _LENGTH_OPERATORS | _NUMERIC_OPERATORS | {"pattern"}:
254
355
  _reject_type_operator_mismatch(field, constraint)
255
356
 
256
357
  return constraints
sqlrules/errors.py CHANGED
@@ -39,7 +39,7 @@ class UnsupportedConstraintError(SQLRulesError):
39
39
 
40
40
  def __str__(self) -> str:
41
41
  message = (
42
- f"Field {self.field!r}: constraint {self.operator!r} is not supported by SQLRules 0.1."
42
+ f"Field {self.field!r}: constraint {self.operator!r} is not supported by SQLRules."
43
43
  )
44
44
  if self.suggestion:
45
45
  return f"{message} {self.suggestion}"
sqlrules/inspectors.py CHANGED
@@ -24,7 +24,7 @@ def _field_aliases(field: Any) -> tuple[str | None, tuple[str, ...]]:
24
24
 
25
25
  for attr in ("validation_alias", "serialization_alias"):
26
26
  value = getattr(field, attr, None)
27
- # AliasPath / AliasChoices are not used for column binding in 0.1.
27
+ # AliasPath / AliasChoices are not used for column binding.
28
28
  if isinstance(value, (AliasPath, AliasChoices)):
29
29
  continue
30
30
  candidate = _string_alias(value)
sqlrules/ir.py CHANGED
@@ -4,6 +4,7 @@ from dataclasses import dataclass, field
4
4
  from typing import Any, Literal
5
5
 
6
6
  OnUnsupported = Literal["raise", "warn", "ignore"]
7
+ DiagnosticSeverity = Literal["warning", "info"]
7
8
 
8
9
 
9
10
  @dataclass(frozen=True, slots=True)
@@ -22,7 +23,65 @@ class FieldDescriptor:
22
23
  aliases: tuple[str, ...] = ()
23
24
 
24
25
 
26
+ @dataclass(frozen=True, slots=True)
27
+ class Diagnostic:
28
+ severity: DiagnosticSeverity
29
+ field: str
30
+ operator: str
31
+ value: Any = None
32
+ message: str = ""
33
+
34
+
35
+ @dataclass(slots=True)
36
+ class DiagnosticsCollector:
37
+ """Mutable collector; snapshots are immutable tuples."""
38
+
39
+ _items: list[Diagnostic] = field(default_factory=list)
40
+
41
+ def add(self, diagnostic: Diagnostic) -> None:
42
+ self._items.append(diagnostic)
43
+
44
+ def clear(self) -> None:
45
+ self._items.clear()
46
+
47
+ def snapshot(self) -> tuple[Diagnostic, ...]:
48
+ return tuple(self._items)
49
+
50
+
25
51
  @dataclass(frozen=True, slots=True)
26
52
  class CompilationContext:
27
53
  on_unsupported: OnUnsupported = "raise"
28
- diagnostics: tuple[str, ...] = field(default_factory=tuple)
54
+ collector: DiagnosticsCollector | None = None
55
+
56
+ def record(
57
+ self,
58
+ *,
59
+ severity: DiagnosticSeverity,
60
+ field: str,
61
+ operator: str,
62
+ value: Any = None,
63
+ message: str = "",
64
+ ) -> None:
65
+ if self.collector is None:
66
+ return
67
+ self.collector.add(
68
+ Diagnostic(
69
+ severity=severity,
70
+ field=field,
71
+ operator=operator,
72
+ value=value,
73
+ message=message,
74
+ )
75
+ )
76
+
77
+
78
+ @dataclass(frozen=True, slots=True)
79
+ class FieldIR:
80
+ descriptor: FieldDescriptor
81
+ constraints: tuple[Constraint, ...]
82
+
83
+
84
+ @dataclass(frozen=True, slots=True)
85
+ class ModelIR:
86
+ model: type[Any]
87
+ fields: tuple[FieldIR, ...]
sqlrules/translators.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import operator
4
+ import sys
5
+ import types
4
6
  import warnings
5
7
  from collections.abc import Callable
6
8
  from typing import Any, cast
@@ -18,6 +20,19 @@ class SQLRulesWarning(UserWarning):
18
20
  """Warning emitted when an unsupported constraint is skipped."""
19
21
 
20
22
 
23
+ def _warning_stacklevel() -> int:
24
+ """Stacklevel that attributes warnings to the first frame outside sqlrules."""
25
+ frame: types.FrameType | None = sys._getframe(1)
26
+ level = 1
27
+ while frame is not None:
28
+ module = frame.f_globals.get("__name__", "")
29
+ if not (isinstance(module, str) and module.startswith("sqlrules")):
30
+ return level
31
+ frame = frame.f_back
32
+ level += 1
33
+ return 2
34
+
35
+
21
36
  def _binary(op: Callable[[Any, Any], Any]) -> Translator:
22
37
  def translate(
23
38
  constraint: Constraint,
@@ -104,6 +119,10 @@ class TranslatorRegistry:
104
119
  ) -> ColumnElement[bool] | None:
105
120
  translator = self.lookup(constraint.operator)
106
121
  if translator is None:
122
+ message = (
123
+ f"Field {constraint.field!r}: constraint {constraint.operator!r} "
124
+ "is not supported and will be skipped."
125
+ )
107
126
  if context.on_unsupported == "raise":
108
127
  raise UnsupportedConstraintError(
109
128
  field=constraint.field,
@@ -112,15 +131,21 @@ class TranslatorRegistry:
112
131
  suggestion=("Remove the constraint, or set on_unsupported='warn'/'ignore'."),
113
132
  )
114
133
  if context.on_unsupported == "warn":
115
- # sqlrules.compile → Compiler.compile → translate → warn
116
- # stacklevel=4 attributes the warning to the caller's code.
117
- warnings.warn(
118
- (
119
- f"Field {constraint.field!r}: constraint {constraint.operator!r} "
120
- "is not supported and will be skipped."
121
- ),
122
- SQLRulesWarning,
123
- stacklevel=4,
134
+ context.record(
135
+ severity="warning",
136
+ field=constraint.field,
137
+ operator=constraint.operator,
138
+ value=constraint.value,
139
+ message=message,
140
+ )
141
+ warnings.warn(message, SQLRulesWarning, stacklevel=_warning_stacklevel())
142
+ else:
143
+ context.record(
144
+ severity="info",
145
+ field=constraint.field,
146
+ operator=constraint.operator,
147
+ value=constraint.value,
148
+ message=message,
124
149
  )
125
150
  return None
126
151
 
@@ -147,4 +172,5 @@ def default_registry() -> TranslatorRegistry:
147
172
  registry.register("max_length", _max_length)
148
173
  registry.register("literal", _in_values)
149
174
  registry.register("enum", _in_values)
175
+ # pattern is extracted into IR but has no portable core translator.
150
176
  return registry
@@ -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
@@ -0,0 +1,14 @@
1
+ sqlrules/__init__.py,sha256=33UBpq4vkVgxLh6m9EU4RNM1gFRdnnZwXtYuZHJjvY4,982
2
+ sqlrules/cache.py,sha256=9NBDunUMvQR3bsx9QxIiO8L8JNbpI1tdLlFNapUZMj8,1051
3
+ sqlrules/columns.py,sha256=TcDhFG8wxgKC3-kwXhOBP27Ekrlfncm-WWrUkMLCplo,2428
4
+ sqlrules/compiler.py,sha256=UgtZfhCGFyt1bMu25koGaCkcd-Z9Lcer5KDF6JIe-bM,4621
5
+ sqlrules/constraints.py,sha256=YcI2TstNoW9GGR4TfoWBzMlmKKNPsqNjTdUr3Cd3rk8,12349
6
+ sqlrules/errors.py,sha256=SOfRXfhwb9wIyrPjukVP_alKEV83hlORyngN0NuCD_8,2533
7
+ sqlrules/inspectors.py,sha256=mEi7ixA1aQe7lyZtcFvLHVqVsAXt_tm60YNgJrYbrp8,1700
8
+ sqlrules/ir.py,sha256=Lq53l4ELGzrYZ0ZqM_fqeLau1kSrod91zjEUIO7_JE4,1945
9
+ sqlrules/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ sqlrules/translators.py,sha256=f21pBuDk6r5xKstnEgRiL8ZubFjo5QvbeHJDzOY10nU,5818
11
+ sqlrules-0.2.0.dist-info/METADATA,sha256=28Q8NuNDJLpREon9qGQruaR66_34QQtbTsy4Pr-2kIE,3988
12
+ sqlrules-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
13
+ sqlrules-0.2.0.dist-info/licenses/LICENSE,sha256=cLMlkCH6RhjWAkXBWR7LazV98TOG0WkPtCKB5zqqkAs,1078
14
+ sqlrules-0.2.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- sqlrules/__init__.py,sha256=51kBzKMVXNdUUgWNYzKcSFJfPA3ZlX2OiFUfZ7aljss,875
2
- sqlrules/columns.py,sha256=AKLzYvYkHC-jIMPXMDLj37kUUPU3hIHCDoZYhwa9064,2373
3
- sqlrules/compiler.py,sha256=iIPNGq5TN2cQ21HtrG6uRaYqksDfdvKE9h5KrnjHZu4,2529
4
- sqlrules/constraints.py,sha256=oSgNzzstRxyskeKddQTNo6Cbgc_tXnx8A4KYY7_a4po,8921
5
- sqlrules/errors.py,sha256=5R-tj_TROF_aWBcB-zB-e2crmAri3hhEH7bZlslyN5w,2537
6
- sqlrules/inspectors.py,sha256=h6TsUVyLTyNie0up3ZM1QLReYtKBlHBcwDbJxbS8gGs,1707
7
- sqlrules/ir.py,sha256=gdz9u2pnVM8WTjdUCTq1MdKA3U_NV3paVwqZqG1pc8Q,623
8
- sqlrules/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- sqlrules/translators.py,sha256=mKTcSPMZewqpjVRf58iGyZJlATTyYFZmklLz3qXNIjs,4955
10
- sqlrules-0.1.0.dist-info/METADATA,sha256=d9QCDHGEdUA_mr_mSFReh8wGcdaGV-3-Lj1Wdk7doZo,3788
11
- sqlrules-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
12
- sqlrules-0.1.0.dist-info/licenses/LICENSE,sha256=cLMlkCH6RhjWAkXBWR7LazV98TOG0WkPtCKB5zqqkAs,1078
13
- sqlrules-0.1.0.dist-info/RECORD,,