Architecture
siderite is an async-first Rust web framework. It takes API ergonomics from FastAPI, its validation model from Pydantic v2, and its ORM ergonomics from the Django ORM. It favours compilable, idiomatic Rust over Python look-alike syntax: where a Python feature has no natural Rust mapping, the Rust equivalent is documented instead.
Status: Phase 6 (production tooling) complete, pre-alpha.
The crate table and dependency rules are in Crate map. This page is the “why”.
Connection ownership
Section titled “Connection ownership”- An application registers backends under aliases (
"default","analytics") asArc<dyn Backend>in aDatabasesregistry (App::database/App::databases). ADatabaseRouterpicks the alias per model. See Database routing. - Handlers receive a
Dbhandle through dependency injection. It is a cheap clone of the pool handle, never a raw SQLx pool. - Transactions are scoped closures:
db.transaction(|tx| async move { … }).await. The closure receives aDbbound to the open transaction. Commit happens onOk, rollback onError panic-unwind. Nested calls become savepoints only whenTransactionSupport::Savepointsis declared. QuerySet::using(&Db)rebinds a queryset to a handle you already hold.Databases::using::<M>(alias)bypasses the router. A queryset holds exactly oneDb; combining querysets (or nesting a subquery) bound to different databases isQueryError::InvalidPlanbefore any I/O.
Query pipeline
Section titled “Query pipeline”- The handler builds a
QuerySet(.filter(..).order_by(..).limit(..)). This is pure data — no I/O. - A terminal method (
.all().await) compiles the queryset into an immutableQueryPlan. - The backend checks
plan.required_features()againstBackendCapabilities. - The dialect compiler emits SQL (or a MongoDB aggregation pipeline) plus bind parameters.
- The executor returns
QueryResult { rows }. Typed decoding isModel::from_row.
See QueryPlan IR and Typed field constants.
Query result decoding
Section titled “Query result decoding”- Every backend returns
QueryResult { rows: Vec<Row> }. ARowholds ordered(column, Value)pairs, whereValueis a closed, backend-neutral enum. - Typed model decoding is
Model::from_row, generated by#[derive(Model)]. It reads by column name and returnsQueryError::Decodeinstead of panicking. - Dynamic projections (
values(), annotations, aggregates) stay asRow.values_list::<(A, B)>()decodes into tuples through the same trait. - Bind parameters are always
Values. They are never interpolated into SQL and never logged by default.
Capability detection
Section titled “Capability detection”Unsupported features fail at the earliest possible point:
| When | Mechanism | Example |
|---|---|---|
| Compile time | Typed APIs; lookups exist only on suitable types | icontains on an i64 field |
| Query construction / compile | QueryPlan::required_features() checked against BackendCapabilities before any I/O |
select_for_update() on SQLite → BackendCapabilityError::RowLockingUnsupported |
| Execution | Server/version limits discovered at runtime | older SQLite without window functions |
Capabilities are richer than booleans where needed:
TransactionSupport { None, Flat, Savepoints } and
RowLocking { None, Basic, Full }.
Error architecture
Section titled “Error architecture”| Error | Crate | HTTP mapping |
|---|---|---|
ValidationError |
validation | 422 with errors extension |
QueryError::DoesNotExist |
orm | 404 |
QueryError::MultipleObjectsReturned |
orm | 500 |
BackendCapabilityError |
orm | 501 |
BackendError::Constraint |
orm | 409 (the detail is logged, not returned) |
OrmError::Signal |
orm | 500 (the text is logged, not returned) |
OrmError::UnknownDatabase |
orm | 500 |
other BackendError |
orm | 500 |
ApiError |
core | RFC 7807 application/problem+json |
Library code has no unwrap / expect: clippy unwrap_used and
expect_used are denied workspace-wide. Every crate declares
#![forbid(unsafe_code)].
Model metadata
Section titled “Model metadata”#[derive(Model)] generates, at compile time:
- Typed field markers (
Field<M, T>constants) - Static metadata:
const META: ModelMetawith the table name, fields, indexes, constraints, and ordering. It is a&'staticdata structure with no runtime registration - Relation metadata: FK target,
on_deletepolicy, related name. The target is referenced by type, so a typo in a model name is a compile error - Schema metadata: models can also
#[derive(Schema)];ForeignKey<T>describes itself asT’s primary key - Migration metadata: the autodetector diffs
ModelMetasnapshots, so migrations and the ORM share one source of truth