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

Database Setup

Opening a database connection in Toasty has two steps: register your models, then connect to a database. Db::builder() handles both.

let mut db = toasty::Db::builder()
    .models(toasty::models!(User, Post))
    .connect("sqlite::memory:")
    .await?;

Registering models

The models! macro builds a ModelSet — the collection of model definitions Toasty uses to generate the database schema. It accepts three forms, which can be combined freely:

toasty::models!(
    // All models from the current crate
    crate::*,
    // All models from an external crate
    third_party_models::*,
    // Individual models
    User,
    other_module::Post,
)

crate::* finds all #[derive(Model)] and #[derive(Embed)] types in your crate at compile time. This is the simplest option when all your models live in one crate.

You don’t need to list every model. Registering a model also registers any models reachable through its fields — BelongsTo, HasMany, HasOne, and embedded types are all discovered by traversing the model’s fields. For example, if User has a Vec<Post> or Deferred<Vec<Post>> field and Post has a User or Deferred<User> field, toasty::models!(User) registers both User and Post.

Connection URLs

The connection URL determines which database driver Toasty uses. Each driver requires its corresponding feature flag in Cargo.toml.

SchemeDatabaseFeature flag
sqliteSQLitesqlite
tursoTurso (SQLite-compatible, async-native)turso
postgresql or postgresPostgreSQLpostgresql
mysqlMySQLmysql
dynamodbDynamoDBdynamodb

Examples:

// In-memory SQLite
.connect("sqlite::memory:")

// SQLite file
.connect("sqlite:./path/to/db.sqlite")

// PostgreSQL
.connect("postgresql://user:pass@localhost:5432/mydb")

// MySQL
.connect("mysql://user:pass@localhost:3306/mydb")

// DynamoDB (uses AWS config from environment)
.connect("dynamodb://us-east-1")

For per-backend details — URL query parameters, TLS, type mapping, and per-driver behavior — see the Database Backends chapters.

Using a driver directly

If you need more control over the driver configuration, construct the driver yourself and pass it to build() instead of connect():

let driver = toasty_driver_sqlite::Sqlite::in_memory();
let mut db = toasty::Db::builder()
    .models(toasty::models!(User))
    .build(driver)
    .await?;

Connection pool

Db owns a connection pool. Each query checks out a connection from the pool for the duration of the call and returns it when finished. The pool defaults work for typical applications; the builder exposes knobs for tuning size, timeouts, and broken-connection recovery.

use std::time::Duration;

let mut db = toasty::Db::builder()
    .models(toasty::models!(crate::*))
    .max_pool_size(32)
    .pool_wait_timeout(Some(Duration::from_secs(5)))
    .pool_create_timeout(Some(Duration::from_secs(10)))
    .connect("postgresql://user:pass@localhost/mydb")
    .await?;
Builder methodDefaultPurpose
max_pool_size(n)num_cpus * 2Cap on simultaneous open connections. Drivers may enforce a lower cap (e.g., in-memory SQLite is single-connection).
pool_wait_timeout(Some(d))NoneMaximum time Db waits for a free connection before returning an error. None waits indefinitely.
pool_create_timeout(Some(d))NoneMaximum time to spend opening a new connection.
pool_health_check_interval(Some(d))Some(60s)How often the background sweep pings an idle connection to detect a silently-broken backend. None disables the sweep.
pool_pre_ping(true)falsePing every connection before handing it to the caller. Adds one round-trip per checkout in exchange for guaranteeing the caller sees a live connection.

Recovering from a backend restart

A database restart, a load-balancer-closed socket, or a backend session timeout leaves the pool holding TCP sockets that look open but reject the next query. Toasty handles this two ways:

  • Background sweep. Every pool_health_check_interval, the pool pings one idle connection. If the ping fails, the pool drops the failing connection and eagerly pings the rest of the idle slots so a single bad result drains every dead connection in one pass.
  • Reactive sweep. When a user query observes a connection-lost error, the same eager sweep runs immediately. A backend restart typically costs one failed user query rather than one per pooled connection.

Enable pool_pre_ping(true) if even one failed query is unacceptable — for example, a public API behind a flaky network or an idempotent worker without retry. The cost is one extra round-trip per checkout.

Table name prefix

To prefix all generated table names (useful when multiple services share a database), call table_name_prefix() on the builder:

let mut db = toasty::Db::builder()
    .models(toasty::models!(crate::*))
    .table_name_prefix("myapp_")
    .connect("sqlite::memory:")
    .await?;