Deferred Fields
A deferred field is a column Toasty omits from the default SELECT
list. Records returned from a query have the field unloaded; loading
the value requires either a follow-up .exec() call or a preload with
.include().
The pattern fits columns that are large, expensive to fetch, or rarely
read: a Document body, a binary blob, an audit-event JSON payload.
Without the deferred annotation, every list query reads every column
whether the caller needs it or not.
The API mirrors BelongsTo: a synchronous .get() reads an
already-loaded value, an async per-field accessor loads on demand, and
.include() preloads as part of the parent query.
Marking a field as deferred
Annotate the field with #[deferred] and wrap its type in Deferred<T>:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: u64,
title: String,
#[deferred]
body: toasty::Deferred<String>,
}
}
Both are required; using one without the other is a compile error. The attribute directs the macro to omit the field from the default projection and to generate the per-field load method. The wrapper type provides the unloaded-state runtime API.
A record from an ordinary query has body unloaded. Load it explicitly
with a follow-up read keyed on the primary key:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: u64,
title: String,
#[deferred]
body: toasty::Deferred<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let created = toasty::create!(Document {
title: "Hello",
body: "the long body",
}).exec(&mut db).await?;
let doc = Document::filter_by_id(created.id).get(&mut db).await?;
assert!(doc.body.is_unloaded());
// Issue a follow-up read for just the deferred column.
let body: String = doc.body().exec(&mut db).await?;
Ok(())
}
}
Or preload it with .include() so the value arrives on the record the
query returns — no second round-trip:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: u64,
title: String,
#[deferred]
body: toasty::Deferred<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let created = toasty::create!(Document {
title: "Hello",
body: "the long body",
}).exec(&mut db).await?;
let doc = Document::filter_by_id(created.id)
.include(Document::fields().body())
.get(&mut db)
.await?;
let body: &String = doc.body.get(); // synchronous, no query
Ok(())
}
}
#[deferred] is supported on primitive fields and on embedded types
(#[derive(Embed)] structs and enums). It does not compose with
#[belongs_to], #[has_many], or #[has_one] — relations are already
lazy.
A deferred embed value omits all of the embed’s columns from the default
projection. Loading is the same as for a primitive: call the per-field
accessor, or chain .include() to preload alongside the parent query.
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Embed)]
struct Metadata {
author: String,
notes: String,
}
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: u64,
title: String,
#[deferred]
metadata: toasty::Deferred<Metadata>,
}
}
#[deferred] is also valid on a primitive field inside an embedded
struct. The annotation defers just that column wherever the embed is
used; the embed’s other (eager) fields still load with the parent query.
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Embed)]
struct Metadata {
author: String,
#[deferred]
notes: toasty::Deferred<String>,
}
}
To load such a sub-field on a parent query, name it in .include():
let doc = Document::filter_by_id(id)
.include(Document::fields().metadata().notes())
.get(&mut db)
.await?;
When the user constructs an embed value directly (struct-literal
syntax), a deferred sub-field accepts the inner value via .into():
Metadata {
author: "Alice".to_string(),
notes: "the note".to_string().into(),
}
Loaded state on create vs query
The record returned by create! is loaded with the deferred value the
caller just wrote — .get() reads it without a round-trip. A
subsequent query against the same row returns a separate record with
the deferred field unloaded:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: u64,
title: String,
#[deferred]
body: toasty::Deferred<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let created = toasty::create!(Document {
title: "Hello",
body: "the long body",
})
.exec(&mut db)
.await?;
// Loaded — the value the caller passed in.
assert_eq!("the long body", created.body.get());
// A separate query returns the deferred field unloaded.
let doc = Document::filter_by_id(created.id).get(&mut db).await?;
assert_eq!("Hello", doc.title);
assert!(doc.body.is_unloaded());
Ok(())
}
}
Calling doc.body.get() in the unloaded state panics. .get() is the
synchronous accessor for a value already loaded into the record; on an
unloaded field there is nothing to return.
Loading on demand
The macro generates a per-field method that issues a single-row read
keyed on the model’s primary key. Call .exec() to fetch the value:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: u64,
title: String,
#[deferred]
body: toasty::Deferred<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let created = toasty::create!(Document {
title: "Hello",
body: "the long body",
}).exec(&mut db).await?;
let doc = Document::filter_by_id(created.id).get(&mut db).await?;
let body: String = doc.body().exec(&mut db).await?;
Ok(())
}
}
The return type of .exec() is the type within Deferred<T>. The
call does not mutate the in-memory record — doc.body.is_unloaded()
is still true afterward, and re-issuing the same load returns the
value again.
The .await makes the round-trip explicit. Code that needs the value
many times should preload with .include() instead — calling .exec()
in a loop over a Vec<Document> is N+1 by definition.
Preloading with .include()
.include() extends the parent query’s projection so deferred fields
are loaded onto the same record returned by the query:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: u64,
title: String,
#[deferred]
body: toasty::Deferred<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let created = toasty::create!(Document {
title: "Hello",
body: "the long body",
}).exec(&mut db).await?;
let doc = Document::filter_by_id(created.id)
.include(Document::fields().body())
.get(&mut db)
.await?;
let body: &String = doc.body.get(); // synchronous, no query
Ok(())
}
}
The .include() call adds the deferred column to the existing query —
no extra round-trip. Multiple .include() calls on the same query
coalesce, and they combine with relation .include()s:
let doc = Document::filter_by_id(id)
.include(Document::fields().body())
.include(Document::fields().summary())
.include(Document::fields().author()) // BelongsTo
.get(&mut db)
.await?;
Across a result set, .include() is the way to avoid N+1: a single
query loads the deferred fields for every record it returns.
Filtering and sorting
Filtering or sorting on a deferred field references the column in
WHERE or ORDER BY without loading the value — only .include()
adds the field to the SELECT list:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: u64,
title: String,
#[deferred]
body: toasty::Deferred<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let alpha = toasty::create!(Document {
title: "First",
body: "alpha body",
}).exec(&mut db).await?;
let docs = Document::filter_by_id(alpha.id)
.filter(Document::fields().body().eq("alpha body".to_string()))
.exec(&mut db)
.await?;
assert_eq!(1, docs.len());
assert!(docs[0].body.is_unloaded());
Ok(())
}
}
Updating
Updating a deferred field does not require it to be loaded. The
caller already supplies the value, so the field is loaded with the new
value after the update — no follow-up .exec() or .include() is
needed to read what was just written:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: u64,
title: String,
#[deferred]
body: toasty::Deferred<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let created = toasty::create!(Document {
title: "Hello",
body: "old body",
}).exec(&mut db).await?;
let mut doc = Document::filter_by_id(created.id).get(&mut db).await?;
assert!(doc.body.is_unloaded());
doc.update().body("new body".to_string()).exec(&mut db).await?;
// The field is loaded with the value just assigned.
assert_eq!("new body", doc.body.get());
Ok(())
}
}
Optional deferred fields
Deferred<T> where T is Option<U> makes the field nullable. The
column stores NULL when the value is None, and create! treats the
field as optional:
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: u64,
title: String,
#[deferred]
summary: toasty::Deferred<Option<String>>,
}
}
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: u64,
title: String,
#[deferred]
summary: toasty::Deferred<Option<String>>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// summary may be set or omitted at create time.
let with = toasty::create!(Document {
title: "With summary",
summary: "a brief summary",
}).exec(&mut db).await?;
let without = toasty::create!(Document {
title: "No summary",
}).exec(&mut db).await?;
let summary: Option<String> = with.summary().exec(&mut db).await?;
assert_eq!(Some("a brief summary".to_string()), summary);
let summary: Option<String> = without.summary().exec(&mut db).await?;
assert_eq!(None, summary);
Ok(())
}
}
A required Deferred<T> (where T is not Option<_>) is a required
argument to create!, just like any other non-nullable field —
create! fails to compile when it is missing.
Driver support
#[deferred] is supported on every driver. SQL backends shorten the
SELECT column list; DynamoDB shortens the ProjectionExpression.
Drivers do not need a capability flag for this feature.