Skip to content

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”.

  • An application registers backends under aliases ("default", "analytics") as Arc<dyn Backend> in a Databases registry (App::database / App::databases). A DatabaseRouter picks the alias per model. See Database routing.
  • Handlers receive a Db handle 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 a Db bound to the open transaction. Commit happens on Ok, rollback on Err or panic-unwind. Nested calls become savepoints only when TransactionSupport::Savepoints is declared.
  • QuerySet::using(&Db) rebinds a queryset to a handle you already hold. Databases::using::<M>(alias) bypasses the router. A queryset holds exactly one Db; combining querysets (or nesting a subquery) bound to different databases is QueryError::InvalidPlan before any I/O.
  1. The handler builds a QuerySet (.filter(..).order_by(..).limit(..)). This is pure data — no I/O.
  2. A terminal method (.all().await) compiles the queryset into an immutable QueryPlan.
  3. The backend checks plan.required_features() against BackendCapabilities.
  4. The dialect compiler emits SQL (or a MongoDB aggregation pipeline) plus bind parameters.
  5. The executor returns QueryResult { rows }. Typed decoding is Model::from_row.

See QueryPlan IR and Typed field constants.

  • Every backend returns QueryResult { rows: Vec<Row> }. A Row holds ordered (column, Value) pairs, where Value is a closed, backend-neutral enum.
  • Typed model decoding is Model::from_row, generated by #[derive(Model)]. It reads by column name and returns QueryError::Decode instead of panicking.
  • Dynamic projections (values(), annotations, aggregates) stay as Row. 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.

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 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)].

#[derive(Model)] generates, at compile time:

  • Typed field markers (Field<M, T> constants)
  • Static metadata: const META: ModelMeta with the table name, fields, indexes, constraints, and ordering. It is a &'static data structure with no runtime registration
  • Relation metadata: FK target, on_delete policy, 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 as T’s primary key
  • Migration metadata: the autodetector diffs ModelMeta snapshots, so migrations and the ORM share one source of truth