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 Portability

Laterite runs on Postgres, MySQL or SQLite, chosen by the connection URL. Migrations and queries are written once, through sea-query over sqlx::Any.

Write a migration

use laterite_core::strata::*;

One import brings the Migration trait, async_trait, the schema and query builders, Db, and the polyfills. lat make:migration <description> writes a new file with it.

One migration per file under src/migrations/, named m<NNNN>_<description>.rs, declaring pub struct Migration with a stable name, an up and an optional down. mod.rs lists them in apply order:

laterite_core::migration_set! {
    module_id: "acme.blog",
    m0001_create_posts,
    m0002_create_comments,
}

Append at the end. Never reorder or rename a shipped entry: applied migrations are tracked by (module_id, name).

Use portable types

You writeStored asHelper
boolinteger 0/1bool_col in schema; binds as bool; read with AnyRowExt::get_bool
DateTime<Utc>text, RFC 3339 at fixed precisionConvert at the query boundary
JSON object or arraytextserde_json at the boundary; no in-database JSON operators
Key, id or code columnvarchar(255)key_col; .text() only for unindexed prose
Any string readStringAnyRowExt::get_text, get_text_opt; never try_get::<String>
Unsigned integerThe next wider signedAutomatic
New row idi64query::insert_returning_id
Cast to stringchar or textExpr::col(c).cast_as(Alias::new(text_cast(db.backend)))
Insert, ignoring a duplicatequery::on_conflict_ignore(keys)
use laterite_core::{bool_col, AnyRowExt};

Table::create()
    .table(Article::Table)
    .col(bool_col(Article::Published).not_null().default(0));

let published: bool = row.get_bool("published")?;
let title: String = row.get_text("title")?;

Ids are bigint auto-increment; an insert never sets one. Canonicalise a user-facing unique key, lower-cased and trimmed, on write and on lookup: MySQL compares case-insensitively.

Write a query

use laterite_core::strata::*;

let stmt = Query::select()
    .column(Article::Title)
    .from(Article::Table)
    .and_where(Expr::col(Article::Published).eq(true))
    .to_owned();
let (sql, values) = build(db.backend, &stmt);
let rows = bind_values(sqlx::query(&sql), values).fetch_all(&db.pool).await?;

Never hand-write SQL with ? placeholders. db is a laterite_core::Db: the pool paired with its backend.

Differences the helpers do not hide

BackendBehaviour
SQLiteForeign keys are enforced only with PRAGMA foreign_keys = ON; sqlx sets it. Do not disable it.
MySQLvarchar needs a length: use key_col or .text(). DDL is not transactional: keep a migration to one change.