Defining Models

A Rango model is a regular Rust struct annotated with #[derive(Model)].

#![allow(unused)]
fn main() {
use rango::prelude::*;

#[derive(Model)]
pub struct Article {
    pub id: FieldUuid,
    pub title: FieldVarchar<1, 255>,
    pub body: FieldText,
    pub published: FieldBool,
    pub created_at: FieldDateTime,
}
}

A model is a normal struct, so it can derive other traits next to Model, for example #[derive(Model, Clone, Debug)]. A struct with a FieldPassword field prints <redacted> for it in Debug. FieldPassword::new, verify and from_hash are documented in Field types.

Table name

By default, the table name is the snake_case of the struct name. Tables are never prefixed.

Article → article

Override with #[model(table = "...")]:

#![allow(unused)]
fn main() {
#[derive(Model)]
#[model(table = "cms_articles")]
pub struct Article { ... }
}

Primary key

Declare a field named id — it becomes the primary key automatically:

#![allow(unused)]
fn main() {
pub id: FieldUuid,      // UUID primary key (recommended)
pub id: FieldAutoBigInt, // Integer generated by the database
}

FieldAutoInt / FieldAutoBigInt hold None until the row is inserted; the database assigns the id and insert returns the model with Some(id). Build new rows with FieldAutoBigInt::default(). bulk_create takes models that all have an id, or none. ForeignKey<M> and ManyToMany<M> follow the primary key type of the target model (FieldUuid, FieldInt, FieldAutoInt, FieldBigInt or FieldAutoBigInt). Text or composite keys cannot be the target of a relation.

Or use #[field(primary_key)] for a custom name:

#![allow(unused)]
fn main() {
#[field(primary_key)]
pub uuid: FieldUuid,
}

Unsupported field types

Fields use Rango's Field* types, not bare Rust types. The derive points at the offending type and names the replacement:

error: `String` is not a Rango field type: use `FieldText` or `FieldVarchar<MIN, MAX>`. ...
 --> src/models.rs:6:11
  |
6 |     name: String,
  |           ^^^^^^
Rust typeRango type
StringFieldText or FieldVarchar<MIN, MAX>
Vec<u8>FieldBytes
boolFieldBool
i16 / i32 / i64FieldSmallInt / FieldInt / FieldBigInt
f32 / f64FieldFloat / FieldDouble
UuidFieldUuid (ForeignKey<M> for a reference)
DateTime<Utc> / NaiveDate / NaiveTimeFieldDateTime / FieldDate / FieldTime
serde_json::ValueFieldJson

Wrapping any of them in Option<..> makes the column nullable.

Building a model value

There is no generated constructor: the fields are the API, so every field is written out and the compiler tells you which one is missing. Field types convert from their Rust equivalent, so .into() is the short form, and new(..) validates (FieldVarchar, FieldEmail, FieldUrl):

#![allow(unused)]
fn main() {
let article = Article {
    id: uuid::Uuid::new_v4().into(),
    title: FieldVarchar::new("Hello")?,
    body: "text".into(),
    published: true.into(),
    created_at: chrono::Utc::now().into(),
};
}

ForeignKey<M> and ManyToMany<M> are Clone / Debug (and ManyToMany is Default) whatever M is: M does not need to implement them. ForeignKey<M> is also Copy, Eq and Hash. ManyToMany::default() is an empty relation.

Nullable fields

Use Option<T> for nullable columns:

#![allow(unused)]
fn main() {
pub bio: Option<FieldText>,       // NULL allowed
pub score: Option<FieldInt>,      // NULL allowed
pub name: FieldVarchar<1, 100>,   // NOT NULL
}

Field attributes

#![allow(unused)]
fn main() {
#[field(unique)]                  // UNIQUE constraint
#[field(index)]                   // creates an index
#[field(column = "custom_name")]  // override column name
#[field(primary_key)]             // mark as primary key
#[field(auto_now_add)]            // set to now() on INSERT (FieldDateTime only)
#[field(auto_now)]                // update to now() on every UPDATE
#[field(default = "0")]           // SQL default value
}

Model attributes

#![allow(unused)]
fn main() {
#[model(table = "custom_name")]
#[model(managed = false)]          // exclude from migrations (external table)
#[model(ordering = [asc("name"), desc("created_at")])]
#[model(constraints = [
    CheckConstraint::new("age >= 0").name("chk_age"),
    UniqueConstraint::on(&["email", "tenant_id"]).name("uniq_email_tenant"),
])]
}