Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Models

#[derive(Model)] generates table metadata, row decoding, value conversion, and typed field proxies. It supports named-field structs and the built-in field types listed below. Models must be registered before a repository can be created; registration does not create or migrate tables.

use afastorm::Model;

#[derive(Debug, Model)]
#[orm(table = "users")]
struct User {
    #[orm(primary_key, auto_increment)]
    id: Option<i64>,
    #[orm(unique)]
    email: String,
    display_name: String,
    metadata: serde_json::Value,
    #[orm(soft_delete)]
    deleted_at: Option<String>,
    #[orm(version)]
    version: i64,
}

#[orm(table = "...")] overrides the inferred table name. Otherwise, the struct name is converted to lowercase snake case without pluralization (BlogPost becomes blog_post). Use #[orm(column = "...")] to override a column name. A model may have at most one primary-key, soft-delete, and version field. auto_increment requires a primary key.

Generated find_by methods

Mark a field with #[orm(find_by)] to generate a single-field repository lookup. A model-level find_by(...) declares a composite lookup; all listed values are combined with AND:

use afastorm::Model;

#[derive(Debug, Model)]
#[orm(find_by(tenant_id, username))]
pub struct User {
    #[orm(primary_key, auto_increment)]
    pub id: Option<i64>,
    #[orm(find_by)]
    pub email: String,
    pub tenant_id: i64,
    pub username: String,
}

The derive generates find_by_email from the field attribute and find_by_tenant_id_username from the model attribute. The model-level attribute takes Rust field identifiers (not database column names); a field renamed with #[orm(column = "...")] still uses its Rust field name here. Unknown field names and duplicate generated method names are compile errors. Repeat model-level find_by(...) attributes to generate additional composite lookup methods.

Generated methods are provided by the model-specific UserFindBy trait on Repository<User>. Import the trait wherever the methods are called:

use crate::models::{User, UserFindBy};

let user = orm.repo::<User>()?
    .find_by_tenant_id_username(7, "ada".to_owned())
    .await?;

Each method returns Result<Option<User>> and returns the first matching row without imposing an order. These methods do not require fields to be unique. For Option<T> model fields, the lookup argument is T; use first_where(User::field.is_null()) to search for SQL NULL.

Generated JSON lookups

Declare fixed JSON paths and the operations to expose with repeatable model-level find_json attributes:

#[derive(Debug, Model)]
#[orm(
    find_json(profile.settings.theme, eq, ne, string_match),
    find_json(profile.settings.age, numeric_compare),
    find_json(profile.settings, contains, has_key, has_any_keys, has_all_keys)
)]
struct User {
    #[orm(primary_key, auto_increment)]
    id: Option<i64>,
    profile: serde_json::Value,
}

The first path segment must name a model field of type serde_json::Value or Option<serde_json::Value>. Later segments are JSON object keys and must be Rust identifiers. Use Rust field names, not #[orm(column = "...")] database column names. A declared profile.settings.theme path generates operation-first methods such as find_json_eq_profile_settings_theme. Unknown/non-JSON fields, invalid paths, and duplicate generated names fail at derive time. The macro validates the Rust model declaration; it cannot inspect an external database schema at compile time. SQLite stores JSON as text and queries it with JSON1; PostgreSQL’s default model mapping is JSONB. Existing tables must have valid JSON/JSONB data in the mapped column.

Generated methods are provided by the model-specific UserFindJson trait, which callers import:

use crate::models::{User, UserFindJson};
use serde_json::json;

let exact = users.find_json_eq_profile_settings_theme(json!("dark")).await?;
let older = users
    .find_json_gt_profile_settings_age(serde_json::Number::from(18))
    .await?;
let has_key = users
    .find_json_has_key_profile_settings("theme".to_owned())
    .await?;

Every method returns Result<Option<User>>, has the same first-match/no-order behavior as first_where, and applies the repository’s soft-delete filter. The declared path selects the JSON value or object to test:

Declaration operationGenerated operation(s)Parameter typeMeaning
eq, neeq, neserde_json::ValueExact JSON value equality/inequality. Object key order is ignored; array order matters.
containscontainsserde_json::ValueRecursive JSON containment. Object key/value subsets and unordered array-element subsets are supported.
has_keyhas_keyStringThe declared path must resolve to an object containing this key.
has_any_keys, has_all_keyssameVec<String>The declared path must resolve to an object containing any/all supplied keys.
numeric_comparegt, gte, lt, lteserde_json::NumberNumeric comparison; non-number or missing paths do not match.
string_matchlike, starts_with, ends_with, string_containsStringString pattern or literal string matching.

Use json!(...) for equality and containment values, including JSON null. JSON null is a value and differs from SQL NULL; an SQL-null JSON column or a missing path does not match these lookups. For like, % and _ are SQL wildcards. The other string methods treat their argument as a literal and are case-sensitive; like follows the backend’s native LIKE behavior.

Built-in SQLite queries require JSON1 support. The built-in SQLite and PostgreSQL dialects implement these operations. A custom Dialect must override render_json_lookup; otherwise a JSON lookup fails with an explicit unsupported-dialect error.

Built-in model field types

The derive macro accepts these base types and Option<T> forms:

  • Always available: bool, i16, i32, i64, f32, f64, String, Vec<u8>, and serde_json::Value.
  • uuid feature: uuid::Uuid.
  • chrono feature: chrono::NaiveDate, NaiveTime, NaiveDateTime, DateTime<chrono::Utc>, and DateTime<chrono::FixedOffset>.
  • time feature: time::Date, Time, PrimitiveDateTime, and OffsetDateTime.
  • rust_decimal feature: rust_decimal::Decimal.
  • bigdecimal feature: bigdecimal::BigDecimal.
  • ipnetwork feature: std::net::IpAddr and ipnetwork::IpNetwork.
  • ipnet feature: std::net::IpAddr and ipnet::IpNet.
  • mac_address feature: mac_address::MacAddress.
  • bit-vec feature: bit_vec::BitVec.

The ipnetwork, ipnet, mac_address, and bit-vec features also enable the PostgreSQL driver. For backend-specific storage, null behavior, and supported operators, see SQLite Types and PostgreSQL Types.

sqlx::FromRow is not required for models: afastorm decodes derived model fields itself. It is still useful for result structs used directly with SQLx.

Custom Rust and database types

Arbitrary newtypes, type aliases with a different Rust type name, and custom SQLx types are not currently accepted as #[derive(Model)] fields. Implementing IntoValue, SQLx Type/Encode/ Decode, or PgArrayElement alone does not register a model field type or its schema mapping. PostgreSQL user-defined enum and composite model fields are not supported.

For a custom query result, use an SQLx FromRow result type with Orm::query_as when its decoding is supported by the selected driver. For a custom persistent model field, use one of the listed built-in types (for example, store an application newtype as String and convert at the application boundary), or implement Model manually. There is currently no derive codec attribute for custom model fields.

PostgreSQL arrays and ranges

With the postgres feature, PgArray<T> and PgRange<T> can be used as model fields and query values. Their supported element types and limitations are listed in PostgreSQL Types. They cannot be used as SQLite model fields.

Soft deletion and timestamps

#[orm(soft_delete)] requires an Option<T> field. Normal repository reads and counts filter out rows whose soft-delete column is non-NULL. delete marks a row deleted, restore clears the marker, and force_delete physically removes it.

The supported soft-delete and automatic timestamp types are bool, i32, i64, f32, f64, String, serde_json::Value, date/time types, and decimal types. i16, UUID, bytes, network, MAC, bit-vector, array, and range fields cannot be used with these attributes. The value is database-generated and backend-specific; see the backend type pages before choosing a storage type.

#[orm(auto_create)] fills a field on insertion. #[orm(auto_update)] fills it on insertion and each repository update. These attributes cannot be combined with primary-key, auto-increment, soft-delete, or version attributes.

Optimistic version locking

#[orm(version)] requires a non-optional i32 or i64. Inserts initialize it to 1; successful repository updates, deletes, and restores advance it. Repository save/update use the model’s current version by default. A stale write returns an optimistic-lock error.

Registration

use afastorm::ModelRegistry;

let registry = ModelRegistry::new()
    .register::<User>()
    .register::<Post>();
let orm = orm.with_models(registry);
let users = orm.repo::<User>()?;

The registry supplies metadata to repositories, schema snapshots, and integrations. Use migrations to create or evolve the database schema.