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

Vec<scalar> Fields

A Vec<scalar> field stores a homogeneous, ordered collection of scalar values in a single column — tags: Vec<String>, scores: Vec<i64>, weights: Vec<f64>. Toasty stores the collection directly; you do not wrap it in JSON by hand or manage a separate join table.

The element type must be a scalar: any primitive other than u8, plus String, Uuid, the decimal types, and the jiff date/time types. Vec<u8> keeps its existing meaning — a single binary blob, not a collection of one-byte integers.

Storage depends on the driver:

DriverRepresentation
PostgreSQLNative array column (text[], int8[], double precision[], …)
MySQLJSON column
SQLiteJSON-encoded text
DynamoDBList L attribute

All four built-in drivers support Vec<scalar> fields. A driver that does not will reject the model at schema build with an error naming the unsupported field rather than mis-storing it. The incremental update builders have narrower support — see Driver support.

Defining a scalar collection field

Declare the field as a Vec<T> for a scalar T. No attribute is needed:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Article {
    #[key]
    #[auto]
    id: u64,

    title: String,

    tags: Vec<String>,
    scores: Vec<i64>,
}
}

A Vec<scalar> field is always present — there is no unset state. A row with no elements holds an empty list, not NULL.

Creating records

The field accepts any value that converts into a list: a Vec<T>, an array literal [T; N], or a slice. The create! macro and the create builder take the same forms.

With the create! macro — an array literal works, no vec! needed:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Article {
    #[key]
    #[auto]
    id: u64,
    title: String,
    tags: Vec<String>,
    scores: Vec<i64>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let article = toasty::create!(Article {
    title: "Hello",
    tags: ["rust", "toasty"],
    scores: [1, 2, 3],
})
.exec(&mut db)
.await?;

// A `Vec` works the same way.
let tags = vec!["rust".to_string(), "toasty".to_string()];
let article = toasty::create!(Article {
    title: "Hello",
    tags: tags,
    scores: Vec::<i64>::new(),
})
.exec(&mut db)
.await?;
Ok(())
}
}

With the create builder, the per-field setter accepts the same forms:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Article {
    #[key]
    #[auto]
    id: u64,
    title: String,
    tags: Vec<String>,
    scores: Vec<i64>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let article = Article::create()
    .title("Hello")
    .tags(["rust", "toasty"])
    .scores(vec![1, 2, 3])
    .exec(&mut db)
    .await?;
Ok(())
}
}

The batch form of create! works as well:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Article {
    #[key]
    #[auto]
    id: u64,
    title: String,
    tags: Vec<String>,
    scores: Vec<i64>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
toasty::create!(Article::[
    { title: "First", tags: ["rust"], scores: [1] },
    { title: "Second", tags: ["toasty", "orm"], scores: [2, 3] },
])
.exec(&mut db)
.await?;
Ok(())
}
}

Querying

A path to a Vec<scalar> field exposes array predicates:

MethodMeaning
.contains(value)The array contains value.
.is_superset(values)The array contains every element of values.
.intersects(values)The array shares at least one element with values.
.len()The array’s length, as Expr<i64>.
.is_empty()The array is empty.
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Article {
    #[key]
    #[auto]
    id: u64,
    title: String,
    tags: Vec<String>,
    scores: Vec<i64>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Articles tagged "rust".
let tagged = Article::filter(Article::fields().tags().contains("rust"))
    .exec(&mut db)
    .await?;

// Articles tagged with both "rust" and "orm".
let both = Article::filter(
    Article::fields().tags().is_superset(["rust", "orm"]),
)
.exec(&mut db)
.await?;

// Articles sharing at least one tag with this set.
let related = Article::filter(
    Article::fields().tags().intersects(["rust", "toasty"]),
)
.exec(&mut db)
.await?;

// Articles with more than three tags.
let many = Article::filter(Article::fields().tags().len().gt(3))
    .exec(&mut db)
    .await?;

// Articles with no tags.
let untagged = Article::filter(Article::fields().tags().is_empty())
    .exec(&mut db)
    .await?;
Ok(())
}
}

.len() produces an Expr<i64> rather than a boolean, so pair it with a comparison (.gt(), .eq(), …) to form a predicate.

These predicates lower to PostgreSQL-specific operators (@>, &&, = ANY(col), cardinality). On document-backed drivers the engine substitutes equivalent JSON or list operations. A few carry backend-specific restrictions — see the per-database pages (for example, DynamoDB, where is_superset and intersects require a literal right-hand side).

Updating

Replacing the whole list

Passing a list to the field setter replaces the entire value:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Article {
    #[key]
    #[auto]
    id: u64,
    title: String,
    tags: Vec<String>,
    scores: Vec<i64>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let mut article = toasty::create!(Article {
    title: "Hello",
    tags: ["rust"],
    scores: [1],
}).exec(&mut db).await?;
article.update()
    .tags(["x", "y", "z"])
    .exec(&mut db)
    .await?;
Ok(())
}
}

toasty::stmt::set(value) is the explicit form of the same whole-value replacement, useful when building an assignment programmatically.

Incremental mutations

For changes relative to the stored value, the toasty::stmt module provides builders. Each produces one update statement and refreshes the in-memory field after .exec():

FunctionWhat it does
stmt::push(value)Append one element.
stmt::extend(iter)Append every element of an iterator, in order.
stmt::pop()Remove the last element.
stmt::remove(value)Remove every element equal to the value.
stmt::remove_at(idx)Remove the element at a 0-based index.
stmt::clear()Replace the field with an empty list.
stmt::apply([ops])Apply several of the above in order, in one statement.
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Article {
    #[key]
    #[auto]
    id: u64,
    title: String,
    tags: Vec<String>,
    scores: Vec<i64>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let mut article = toasty::create!(Article {
    title: "Hello",
    tags: ["rust"],
    scores: [1],
}).exec(&mut db).await?;
// Append one element.
article.update()
    .tags(toasty::stmt::push("toasty"))
    .exec(&mut db)
    .await?;

// Append several. `stmt::extend` of an empty iterator is a no-op.
article.update()
    .tags(toasty::stmt::extend(["orm", "async"]))
    .exec(&mut db)
    .await?;

// Remove the last element.
article.update()
    .tags(toasty::stmt::pop())
    .exec(&mut db)
    .await?;

// Remove every element equal to "orm".
article.update()
    .tags(toasty::stmt::remove("orm"))
    .exec(&mut db)
    .await?;

// Remove the element at index 0.
article.update()
    .tags(toasty::stmt::remove_at(0usize))
    .exec(&mut db)
    .await?;

// Remove every element.
article.update()
    .tags(toasty::stmt::clear())
    .exec(&mut db)
    .await?;

// Combine operations into one statement, applied in order.
article.update()
    .tags(toasty::stmt::apply([
        toasty::stmt::push("rust"),
        toasty::stmt::push("toasty"),
    ]))
    .exec(&mut db)
    .await?;
Ok(())
}
}

Each operation is atomic against the existing column value — the database applies it to whatever the row currently holds, not to the in-memory snapshot. Concurrent writers can still interleave between operations, but no single operation reads then writes in a way another writer can split.

pop on an empty list, remove of an absent value, and remove_at past the end of the list are all no-ops rather than errors. remove deletes every matching element, not just the first.

stmt::apply runs each operation in order, against the result of the previous one. An apply is valid only where every operation it contains is valid on the target backend.

After .exec(), the in-memory field reflects the new value.

Driver support

Defining a Vec<scalar> field, creating and reading rows, and the array query predicates work on every built-in driver. Whole-value replacement and the appending builders — set, push, extend, clear — also work everywhere.

The element-removal builders are narrower:

OperationPostgreSQLMySQLSQLiteDynamoDB
Define field, create, read
contains, len, is_empty
is_superset, intersectsliteral right-hand side only
Replace, set, push, extend, clear
pop, remove, remove_at

pop, remove, and remove_at currently require PostgreSQL, where they lower to array_remove and array slicing. On the other drivers they return an error. See the per-database pages for the storage and operator details specific to each backend.