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 operation | Generated operation(s) | Parameter type | Meaning |
|---|---|---|---|
eq, ne | eq, ne | serde_json::Value | Exact JSON value equality/inequality. Object key order is ignored; array order matters. |
contains | contains | serde_json::Value | Recursive JSON containment. Object key/value subsets and unordered array-element subsets are supported. |
has_key | has_key | String | The declared path must resolve to an object containing this key. |
has_any_keys, has_all_keys | same | Vec<String> | The declared path must resolve to an object containing any/all supplied keys. |
numeric_compare | gt, gte, lt, lte | serde_json::Number | Numeric comparison; non-number or missing paths do not match. |
string_match | like, starts_with, ends_with, string_contains | String | String 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>, andserde_json::Value. uuidfeature:uuid::Uuid.chronofeature:chrono::NaiveDate,NaiveTime,NaiveDateTime,DateTime<chrono::Utc>, andDateTime<chrono::FixedOffset>.timefeature:time::Date,Time,PrimitiveDateTime, andOffsetDateTime.rust_decimalfeature:rust_decimal::Decimal.bigdecimalfeature:bigdecimal::BigDecimal.ipnetworkfeature:std::net::IpAddrandipnetwork::IpNetwork.ipnetfeature:std::net::IpAddrandipnet::IpNet.mac_addressfeature:mac_address::MacAddress.bit-vecfeature: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.