Models
Models are structs with #[derive(Model)]. The derive generates typed field
constants, static metadata, and persistence helpers. Queries are lazy
QuerySets; see QuerySets.
use siderite::prelude::*;
#[derive(Model, Serialize, Deserialize, Schema, Debug, Clone)]#[model(table = "users", ordering = ["name"])]pub struct User { #[field(primary_key, auto)] pub id: i64, #[field(max_length = 100, index)] pub name: String, #[field(max_length = 255, unique)] pub email: String, pub age: Option<i32>, #[field(auto_now_add)] pub created_at: DateTime<Utc>, pub team: Option<ForeignKey<Team>>,}
let user = User::objects(&db).get(User::id.eq(1)).await?;let team = user.fetch_team(&db).await?; // Option<Arc<Team>>One struct can derive Model, Validate, Schema, Serialize, Deserialize.
Validate and Schema ignore ORM keys; Model ignores validation keys
except max_length, max_digits, and decimal_places, which become
column metadata. Unknown keys are compile errors.
#[model(...)]
Section titled “#[model(...)]”| Key | Meaning |
|---|---|
table = "users" |
Table name. Default: snake_case(TypeName) (BlogPost → blog_post). No app prefix, no pluralisation |
ordering = ["name", "-created_at"] |
Default ordering. - means descending. Entries are field names; an unknown name is a compile error |
indexes(idx(columns = ["a", "b"], unique)) |
Named multi-column indexes. Relation fields become {field}_id |
unique_together(["a", "b"], ..) |
Unique constraints, named {table}_{columns}_uniq |
checks(age_positive = "age >= 0") |
Named CHECK constraints. The SQL is written by the author |
managed = false |
Migrations skip the table |
many_to_many(tags(Tag, through_table = "..", source_column = "..", target_column = "..", related_name = "..")) |
Many-to-many. Defaults: through_table = {table}_{name}, source_column = {snake(Model)}_id, target_column = {snake(Target)}_id. Set both columns for a self-referential relation. through = ThroughModel uses an explicit through model and then through_table must be omitted |
#[field(...)] ORM keys
Section titled “#[field(...)] ORM keys”| Key | Meaning |
|---|---|
primary_key |
Exactly one field |
auto |
Database-generated integer primary key (i16, i32, i64). A model is unsaved while the key equals its default. A primary key without auto is never unsaved |
unique, index |
Constraint and single-column index |
column = "name" |
Column name. Default: the field name, or {field}_id for relations |
db_default = 7 | true | "text" |
Server-side default |
auto_now_add, auto_now |
DbDefault::Now at insert, and refresh on every save |
on_delete = "cascade" | "protect" | "set_null" | "set_default" | "do_nothing" |
Relations only. Default: cascade. set_null requires Option<ForeignKey<..>> |
related_name = ".." |
Reverse accessor on the target |
skip |
Not a column. Loaded as Default::default() |
max_length, max_digits, decimal_places |
Column metadata |
#[field(default = ..)] is the validation default (a Rust expression).
Timestamps use auto_now_add / auto_now, Django’s names.
Generated API
Section titled “Generated API”User::nameis a typedField<User, String>constant per column.Book::authoris aField<Book, ForeignKey<Author>>overauthor_id.- Lookups exist only on suitable types, so
User::age.icontains(..)does not compile. Why: Typed field constants. - Foreign keys get
book.fetch_author(&db)returningArc<Author>, orOption<Arc<Author>>for a nullable key. It is not namedauthorbecause an associated constant and a method cannot share a name. - Foreign keys are indexed unless they are unique. A
OneToOneis unique. related_nameon a foreign key addsauthor.books(&db), aQuerySet<Book>. For aOneToOne, it adds an asyncprofile(&db)returningOption<Profile>. The target model must be in the same crate (Rust’s orphan rule). Across crates, filter:Book::objects(&db).filter(Book::author.eq(id)).- Each many-to-many adds
book.tags(&db), aManyToManyManager. Withrelated_name, it also adds a reverse queryset (a correlatedEXISTSover the join table). - Generic structs are not supported.
ForeignKey<T>implementsValidate,Schema, andDumpby delegating toT::Pk. OpenAPI shows the key type.
Persistence
Section titled “Persistence”An auto primary key that still holds its default (0) means the object
is unsaved, so save() INSERTs. For a manual primary key, save() tries
an UPDATE and falls back to an INSERT, as Django does.
let mut user = User { id: 0, name: "Ann".into(), /* .. */ };user.save(&db).await?; // INSERT, fills user.iduser.name = "Annabel".into();user.save(&db).await?; // UPDATEuser.delete(&db).await?;user.refresh(&db).await?; // reload columnsQuerySet::create, get_or_create, update_or_create, bulk_create, and
bulk_update are on the queryset. Bulk writes send no signals. See
QuerySets and Signals.
Column types
Section titled “Column types”DbType maps a Rust type to SQL: integers and floats, bool, String,
Vec<u8>, Decimal, Uuid, NaiveDate, NaiveTime, DateTime<Utc>,
TimeDelta, IpAddr, JSON, and Option<T>. Canonical storage forms per
backend are in Backends.