SQLRules Design Decisions¶
Purpose¶
This document records the major design decisions behind SQLRules so future development stays consistent, focused, and easy to reason about.
SQLRules exists to do one thing well:
Compile constrained Pydantic models into SQLAlchemy WHERE-rule dictionaries.
Decision 1: SQLRules is a Compiler, Not a Query Builder¶
Decision¶
SQLRules compiles model metadata into SQLAlchemy boolean expressions.
It does not build complete queries.
Rationale¶
SQLAlchemy already provides excellent query composition. SQLRules should produce reusable pieces that fit naturally into SQLAlchemy Core and ORM workflows.
Consequences¶
SQLRules returns expressions such as:
users.c.age >= 18
not full statements such as:
select(users).where(users.c.age >= 18)
Decision 2: Return a Dictionary Grouped by Field¶
Decision¶
The primary output format is:
dict[str, list[ColumnElement[bool]]]
Example:
{
"age": [
users.c.age >= 18,
users.c.age <= 65,
],
"name": [
func.length(users.c.name) >= 2,
],
}
Rationale¶
A dictionary preserves field-level meaning and lets users compose rules selectively.
Consequences¶
Users can apply all rules:
stmt = select(users).where(*sqlrules.where(rules))
or only some rules:
stmt = select(users).where(*rules["age"])
Decision 3: SQLAlchemy Expressions, Not SQL Strings¶
Decision¶
SQLRules returns SQLAlchemy expression objects.
Rationale¶
SQL strings are dialect-sensitive, harder to compose, and easier to misuse. SQLAlchemy expressions are safer, composable, and backend-aware.
Consequences¶
SQLRules never renders SQL strings directly.
Decision 4: Pydantic v2 First¶
Decision¶
SQLRules targets Pydantic v2 first.
Rationale¶
Pydantic v2 has a modern metadata model based heavily on Annotated and
constraint metadata. Supporting v1 would add complexity before the
package proves its core value.
Consequences¶
Initial compatibility target:
Python 3.10+
Pydantic v2
SQLAlchemy 2.x
Pydantic v1 support may be considered later as a compatibility plugin or separate adapter.
Decision 5: Fail Fast by Default¶
Decision¶
Unsupported constraints raise errors by default.
Rationale¶
Silently ignoring constraints could produce overly broad SQL filters and create correctness or security issues.
Consequences¶
Default mode:
on_unsupported="raise"
Optional modes:
warn
ignore
Decision 6: Only Deterministic Constraints Are Supported¶
Decision¶
SQLRules only supports constraints that have deterministic SQL equivalents.
Supported Examples¶
gt
ge
lt
le
min_length
max_length
multiple_of
Literal
Enum
Unsupported Examples¶
custom validators
model validators
computed fields
arbitrary Python predicates
Rationale¶
SQLRules should never pretend that arbitrary Python validation logic can be safely converted to SQL.
Decision 7: Optionality Does Not Generate Rules by Default¶
Decision¶
Optional[T] does not automatically produce IS NULL or IS NOT NULL
expressions.
Rationale¶
Optionality describes whether a value may be absent or null during validation. It does not always imply a SQL filtering rule.
Consequences¶
Future configuration may allow explicit nullability policies, but the default behavior remains no rule.
Decision 8: Keep the Intermediate Representation Dialect-Neutral¶
Decision¶
The IR describes semantic intent, not database-specific syntax.
Example:
Constraint(field="name", operator="min_length", value=2)
Rationale¶
Dialect-specific behavior belongs in translators, not in the compiler core.
Consequences¶
The same IR can be translated differently by SQLite, PostgreSQL, MySQL, SQL Server, or Oracle plugins.
Decision 9: Plugins Are Explicit¶
Decision¶
SQLRules 0.3 ships an explicit plugin API:
Compiler(plugins=[PostgresPlugin()], on_conflict="raise", dialect="postgresql")
There is no automatic discovery. Plugins declare api_version matching
PLUGIN_API_VERSION.
Rationale¶
Explicit plugin registration avoids hidden behavior, improves reproducibility, and keeps the compiler deterministic.
Consequences¶
See PLUGIN_SYSTEM.md. Official starter packages:
sqlrules-postgresql and sqlrules-sqlite.
Decision 10: No Database Connections¶
Decision¶
SQLRules never opens database connections or reflects database metadata.
Rationale¶
The package should be safe to use in any environment, including application startup, tests, CI, and code generation tools.
Consequences¶
Users provide SQLAlchemy tables, ORM classes, aliases, or explicit column maps.
Decision 11: Small Public API¶
Decision¶
The public API should remain minimal.
Initial API:
sqlrules.compile(...)
sqlrules.where(...)
sqlrules.flatten(...)
Rationale¶
A small API is easier to document, test, stabilize, and maintain.
Consequences¶
Advanced behavior should live behind compiler options, internal components, or plugins rather than expanding the top-level API too early.
Decision 12: Two-Phase Compilation¶
Decision¶
SQLRules separates static model compilation from table binding.
Phase 1: Static Compilation¶
inspect Pydantic model
extract constraints
build
ModelIR(cached by default)
Phase 2: Binding¶
resolve SQLAlchemy columns
translate IR to expressions
Rationale¶
Static model work can be cached and reused across multiple tables, aliases, ORM models, or column maps.
Consequences¶
Compiler.compile_model / Compiler.bind expose the phases.
sqlrules.compile(...) remains the one-shot public API.
Decision 13: Prefer Simple Translators¶
Decision¶
Basic constraints should use simple function-based translators where possible.
Example:
"gt": operator.gt
"ge": operator.ge
Rationale¶
Many translations are direct operator mappings and do not need complex class hierarchies.
Consequences¶
Dedicated translator classes should be reserved for more complex constraints such as:
min_length
max_length
pattern
Literal
Enum
dialect-specific behavior
Decision 14: Stable Errors Matter¶
Decision¶
SQLRules exceptions should be structured and stable.
Rationale¶
Clear errors make the package easier to use, test, and integrate into developer tools.
Consequences¶
All public exceptions inherit from:
SQLRulesError
Exceptions should include structured context such as:
field
constraint
value
translator
suggested fix
Decision 15: Documentation Drives Implementation¶
Decision¶
SQLRules planning documents are treated as implementation guidance.
Rationale¶
The package is intentionally small, so high-quality design docs can prevent scope creep and architectural drift.
Consequences¶
New features should update relevant docs before implementation.
Open Questions¶
Should SQLRules Support Pydantic v1?¶
Likely not in core initially.
Possible future path:
sqlrules-pydantic-v1adapter layer
compatibility mode
Should SQLRules Include a CompiledRules Object?¶
Potential API:
compiled = sqlrules.compile(UserFilter, users)
compiled.rules
compiled.where()
compiled.field("age")
This may improve ergonomics while preserving dictionary output.
Should Regex Be Core or Dialect Plugin?¶
Decided for 0.2 / 0.3:
IR support in core (
Constraint(operator="pattern", ...))No portable core translator
Translator support via
TranslatorRegistryand dialect plugins (sqlrules-postgresql,sqlrules-sqlite)
Should Nullability Rules Be Optional?¶
Potential future configuration:
null_policy="ignore" # default
null_policy="not_null"
null_policy="is_null"
This should not be part of the initial MVP unless a strong use case emerges.
Summary¶
SQLRules should stay small, explicit, deterministic, and composable.
The most important design boundary is this:
SQLRules converts supported Pydantic constraints into SQLAlchemy WHERE expressions. Nothing more.