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

Indexes and Unique Constraints

Toasty supports two field-level attributes for indexing: #[unique] and #[index]. Both create database indexes, but they differ in what gets generated.

Unique fields

Add #[unique] to a field to create a unique index. On databases that support unique constraints (SQLite, PostgreSQL, MySQL), the database enforces that no two records can have the same value for this field. Toasty applies the constraint on a best-effort basis — if the underlying database does not support unique indexes (e.g., DynamoDB on non-key attributes), the #[unique] attribute still generates the same query methods but uniqueness is not enforced at the storage level.

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

    name: String,

    #[unique]
    email: String,
}
}

This generates a unique index on the email column:

CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL
);
CREATE UNIQUE INDEX idx_users_email ON users (email);

Attempting to insert a duplicate value returns an error:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
User::create()
    .name("Alice")
    .email("alice@example.com")
    .exec(&mut db)
    .await?;

// This fails — email must be unique
let result = User::create()
    .name("Bob")
    .email("alice@example.com")
    .exec(&mut db)
    .await;

assert!(result.is_err());
Ok(())
}
}

Generated methods for unique fields

Because a unique field identifies at most one record, Toasty generates a get_by_* method that returns a single record:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Get a single user by email (errors if not found)
let user = User::get_by_email(&mut db, "alice@example.com").await?;
Ok(())
}
}

Toasty also generates filter_by_*, update_by_*, and delete_by_* methods:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Filter — returns a query builder
let user = User::filter_by_email("alice@example.com")
    .get(&mut db)
    .await?;

// Update by email
User::update_by_email("alice@example.com")
    .name("Alice Smith")
    .exec(&mut db)
    .await?;

// Delete by email
User::delete_by_email(&mut db, "alice@example.com").await?;
Ok(())
}
}

Indexed fields

Add #[index] to a field to tell Toasty that this field is a query target. On SQL databases, Toasty creates a database index on the column, which lets the database find matching rows without scanning the entire table. On DynamoDB, the attribute maps to a secondary index.

Unlike #[unique], #[index] does not enforce uniqueness — multiple records can share the same value.

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

    name: String,

    #[index]
    country: String,
}
}

This generates a non-unique index:

CREATE INDEX idx_users_country ON users (country);

Generated methods for indexed fields

Because an indexed field may match multiple records, the generated methods work with collections rather than single records:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[index]
    country: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// filter_by_country returns a query builder (may match many records)
let users = User::filter_by_country("US")
    .exec(&mut db)
    .await?;

// Update all records matching the index
User::update_by_country("US")
    .country("United States")
    .exec(&mut db)
    .await?;

// Delete all records matching the index
User::delete_by_country(&mut db, "US").await?;
Ok(())
}
}

Toasty also generates a get_by_* method for indexed fields. It returns the matching record directly, but errors if no record matches or if more than one record matches:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[index]
    country: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::get_by_country(&mut db, "US").await?;
Ok(())
}
}

Choosing between #[unique] and #[index]

Both attributes tell Toasty that a field is a query target and generate the same set of methods: get_by_*, filter_by_*, update_by_*, and delete_by_*.

The difference is in the constraint they express:

AttributeMeaningDatabase effect (SQL)
#[unique]Each record has a distinct valueCREATE UNIQUE INDEX — the database rejects duplicates
#[index]Multiple records may share a valueCREATE INDEX — no uniqueness enforcement

Use #[unique] for fields that identify a single record — email addresses, usernames, slugs. Use #[index] for fields you query frequently but that naturally repeat — country, status, category.

What gets generated

For a model with #[unique] on email and #[index] on country:

MethodDescription
User::get_by_email(&mut db, email)One record by unique field
User::filter_by_email(email)Query builder for unique field
User::update_by_email(email)Update builder for unique field
User::delete_by_email(&mut db, email)Delete by unique field
User::get_by_country(&mut db, country)One record by indexed field
User::filter_by_country(country)Query builder for indexed field
User::update_by_country(country)Update builder for indexed field
User::delete_by_country(&mut db, country)Delete by indexed field

These methods follow the same patterns as key-generated methods. See Querying Records, Updating Records, and Deleting Records for details on terminal methods and builders.