SPEC.md¶
RowGuard Specification¶
Current as of 0.6.0¶
Shipped surface:
Synchronous Core API:
select,execute,validate_rows,compile_plan,streamAsync Core API:
aselect,aexecute,astreamStreamResult[T]/AsyncStreamResult[T]with context-managed cleanupSQLAlchemy Core
Table/Selectwith sync or async session/connectionSQLAlchemy ORM mapped classes (projected + single-entity)
SQLModel table sources (
rowguard[sqlmodel])orm_validation,unloaded_attributes,attribute_map,source_identitySQLRules pushdown (
use_sqlrules, optionalcompiled_rules)Rejection policies:
raise,collect,skip,log,callback,quarantineCallback / quarantine async reject handlers on async APIs
Rejection thresholds (
max_rejections,max_rejection_rate)Staged immutable
ExecutionPlanand planning diagnosticsStreaming options:
yield_per,StreamObserver/BaseStreamObserver(observers remain sync callables)
Deferred (not available yet):
Nested relationship / graph validation — later
Plugin system / provider registries — 0.7.0
Reflection / raw
text()— 0.8.0
Overview¶
RowGuard is a validation-first query engine built on top of SQLAlchemy, Pydantic, and SQLRules.
Its purpose is to guarantee that every row returned from a database query is classified as either:
A valid Pydantic model.
A rejected row with structured diagnostics.
Unlike an ORM, RowGuard does not own your schema. It works with existing SQLAlchemy Core tables, ORM mapped classes, and SQLModel table models, and is designed to extend to reflected schemas and raw SQL in later releases.
Scope¶
RowGuard is responsible for:
Building SQLAlchemy queries
Integrating SQLRules WHERE expressions
Executing queries
Adapting rows to dictionaries
Validating rows with Pydantic
Handling rejected rows
Returning typed results
Producing diagnostics and statistics
RowGuard is not responsible for:
Owning ORM persistence or relationship graphs (0.5 does validate ORM / SQLModel reads)
Schema migrations
SQL generation beyond applying SQLRules /
where=expressionsDatabase drivers
Database connections
Core Pipeline¶
Pydantic Model
│
▼
SQLRules
Compile SQL-safe constraints
│
▼
SQLAlchemy Statement
│
▼
Database
│
▼
Row Adapter
│
▼
Pydantic Validation
│
├── Valid Models
└── Rejected Rows
Primary API¶
result = rowguard.select(
session=session,
table=users,
model=UserRead,
)
Additional APIs (0.2.0+):
rowguard.execute(...)
rowguard.validate_rows(...)
rowguard.compile_plan(...)
Streaming (0.3.0):
with rowguard.stream(...) as stream:
for model in stream:
...
QueryResult¶
Every query returns a QueryResult.
QueryResult[T]
Properties:
models
rejected
statistics
statement
diagnostics
execution_time (derived from
statistics.execution_time_ns)
RejectedRow¶
A rejected row contains:
row index
adapted mapping (when adaptation succeeded)
ValidationError and/or adaptation error
model type
optional raw row
Rejected rows are first-class objects.
Rejection Policies¶
Supported in 0.1.0+:
raise
collect
skip
Planned for later releases:
log
callback
quarantine
Policy selection is explicit.
SQLRules Integration¶
Before execution, RowGuard asks SQLRules to compile SQL-safe constraints.
Pydantic
↓
SQLRules
↓
WHERE expressions
These expressions are added to the SQLAlchemy statement before execution.
Validation still occurs after retrieval.
Validation¶
Validation uses:
Model.model_validate(mapping)
No custom validation engine is introduced.
Nested models, field validators, and model validators continue to work because Pydantic remains the source of truth.
Supported Inputs¶
Current (0.6.0):
SQLAlchemy Table
SQLAlchemy Select
SQLAlchemy ORM mapped classes
SQLModel table models (
rowguard[sqlmodel])SQLAlchemy Session / Connection
SQLAlchemy AsyncSession / AsyncConnection
Buffered (
select/execute/aselect/aexecute) and streaming (stream/astream) result modes
Future:
reflected metadata
raw
text()queries
Streaming¶
Shipped in 0.3.0. Large result sets are processed incrementally without retaining accepted models:
with rowguard.stream(
session=session,
table=users,
model=UserRead,
on_reject="skip",
) as stream:
for model in stream:
...
statistics = stream.statistics
StreamResult is separate from QueryResult. Async streaming shipped in
0.4.0 as astream / AsyncStreamResult with the same rejection and
lifecycle semantics.
Async¶
Shipped in 0.4.0:
await rowguard.aselect(...)
await rowguard.aexecute(...)
async with rowguard.astream(...) as stream:
async for model in stream:
...
Async behavior mirrors synchronous behavior for validation and rejection
policies. Await only DB I/O; Pydantic validation remains synchronous on the
event loop. See the docs site page Supported vs planned and
docs/architecture/ASYNC.md.
Statistics¶
Every QueryResult exposes QueryStatistics with:
rows_read
rows_validated
rows_accepted
rows_rejected
adaptation_time_ns
validation_time_ns
rejection_time_ns
execution_time_ns
QueryResult.execution_time is a convenience property over
execution_time_ns.
Diagnostics¶
Diagnostics may include:
planning codes (source resolved, pushdown applied/skipped, …)
rejected fields
validation summaries
SQL statement
rejection policy used
Error Model¶
Public exceptions derive from:
RowGuardError
Examples:
QueryExecutionError
RowValidationError
RowAdaptationError
PlanningError
ConfigurationError
RejectHandlerError
SQLModel Position¶
SQLModel and RowGuard solve different problems.
SQLModel models tables.
RowGuard validates query results.
The libraries should integrate naturally without overlapping responsibilities.
Performance Goals¶
Stream large datasets (0.3.0+)
Avoid duplicate validation
Reuse SQLRules compilation
Linear scaling with row count
Plugin Opportunities¶
Future extension points:
reject handlers
row adapters
result serializers
diagnostics
dialect helpers
Compatibility¶
Initial release:
Python 3.10+
Pydantic v2
SQLAlchemy 2.x
SQLRules 0.x
Design Principles¶
Validation-first
SQLAlchemy-native
Pydantic-native
Deterministic
Observable
Composable
Explicit rejection handling
Success Criteria¶
A successful RowGuard application can state:
Every returned model satisfies the Pydantic contract.
Every rejected row is accounted for.
SQL filtering and Pydantic validation work together.
Invalid data never silently reaches application code.