name: migs description: Use when writing database migration scripts using the migs crate. Covers the migs! macro for registering SQL migrations, collect! for gathering them, and running them against Postgres via tokio-postgres.

migs — Compile-time SQL Migration Registry

migs registers SQL migration scripts at compile time using the inventory crate. Migrations are declared with the migs! macro and collected at runtime with collect!.

Macro Reference

migs! — Register a migration

Two forms: inline SQL or file path.

Inline SQL:

migs! {
    sql = r#"CREATE TABLE users (
        id UUID PRIMARY KEY,
        username TEXT UNIQUE NOT NULL,
        email TEXT,
        password_hash TEXT NOT NULL,
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );"#,
    scope = "init",
    order = 1
}

From file:

migs! {
    path = "migrations/001_init.sql",
    scope = "init",
    order = 1
}
ParameterRequiredDefaultDescription
sql or pathYesInline SQL string or path to .sql file (read at compile time)
scopeNo"init"Scope/category for grouping migrations (e.g., "init", "seed")
orderNoNoneExecution order within scope. Duplicate orders in the same scope cause a compile error.

collect! — Gather all registered migrations

Returns Vec<&'static Migs> containing every migration registered with migs!.

let all_migrations: Vec<&migs::Migs> = collect!();

Migs struct fields

FieldTypeDescription
scope&'static strScope/category
source&'static str"<inline>" for sql, or file path for path
content&'static strThe SQL content (with {{template}} placeholders if any)
orderOption<u32>Execution order within scope

Template Placeholders

SQL content supports {{placeholder}} syntax for runtime string substitution:

migs! {
    sql = r#"CREATE TABLE {{table_prefix}}_users (id UUID PRIMARY KEY);"#,
    scope = "init",
    order = 1
}

Replace {{...}} at runtime before executing:

let sql = migration.content.replace("{{table_prefix}}", "app");

Pattern: Running Migrations with tokio-postgres (Neon/Postgres)

This project uses Neon (serverless Postgres) via tokio-postgres. Migrations should be run using parameterized execute() calls on the Client.

use crate::db::Database;
use migs::{collect, migs};

migs! {
    sql = r#"CREATE TABLE IF NOT EXISTS users (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        username TEXT UNIQUE NOT NULL,
        email TEXT,
        password_hash TEXT NOT NULL,
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );"#,
    scope = "init",
    order = 1
}

migs! {
    sql = r#"CREATE TABLE IF NOT EXISTS audiences (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        slug TEXT UNIQUE NOT NULL,
        name TEXT NOT NULL,
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );"#,
    scope = "init",
    order = 2
}

migs! {
    sql = r#"CREATE TABLE IF NOT EXISTS articles (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        title TEXT NOT NULL,
        audience_id UUID NOT NULL REFERENCES audiences(id),
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );"#,
    scope = "init",
    order = 3
}

migs! {
    sql = r#"CREATE INDEX IF NOT EXISTS idx_articles_audience ON articles(audience_id);"#,
    scope = "init",
    order = 4
}

impl Database {
    pub async fn run_migrations(&self) -> Result<(), Box<dyn std::error::Error>> {
        let client = self.conn().await?;
        let mut migrations: Vec<_> = collect!();
        migrations.sort_by_key(|m| m.order.unwrap_or(u32::MAX));

        for migration in &migrations {
            log::info!(
                "Running migration: scope={}, order={:?}, source={}",
                migration.scope,
                migration.order,
                migration.source
            );
            client
                .execute(migration.content, &[])
                .await
                .map_err(|e| format!("Migration failed: {e}"))?;
        }

        Ok(())
    }
}

Key Rules

  • Postgres syntax, not SQLite: Use UUID, TIMESTAMPTZ, gen_random_uuid(), NOW(), REFERENCES, BOOLEAN — not TEXT PRIMARY KEY, TEXT NOT NULL for dates, or PRAGMA.
  • IF NOT EXISTS: Use CREATE TABLE IF NOT EXISTS and CREATE INDEX IF NOT EXISTS for idempotent migrations.
  • Order must be unique within a scope: Duplicate (scope, order) pairs cause a compile error.
  • Foreign keys reference with REFERENCES table(column): Postgres uses REFERENCES, not FOREIGN KEY inline (both work, but REFERENCES is more idiomatic).
  • No parameterized DDL: execute() for DDL statements (CREATE TABLE, CREATE INDEX) uses &[] as params since these are not parameterized queries.
  • Compile-time checked: File paths and syntax are validated at compile time. path = "nonexistent.sql" will fail to compile.

Migration Scope Conventions

ScopePurpose
"init"Schema creation — tables, indexes, constraints (default)
"seed"Seed data — initial rows, default accounts
"alter"Schema changes — ALTER TABLE, ADD COLUMN, etc.

Project Integration

  • Migrations are declared in src/db/migrations.rs (or alongside src/db/mod.rs).
  • Run migrations at app startup after establishing the DB connection.
  • The Database struct holds the tokio-postgres Client and exposes a run_migrations() method.

Dependency

Already in Cargo.toml:

migs = "0.1.7"