Markdown Files
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
}
| Parameter | Required | Default | Description |
|---|---|---|---|
sql or path | Yes | — | Inline SQL string or path to .sql file (read at compile time) |
scope | No | "init" | Scope/category for grouping migrations (e.g., "init", "seed") |
order | No | None | Execution 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
| Field | Type | Description |
|---|---|---|
scope | &'static str | Scope/category |
source | &'static str | "<inline>" for sql, or file path for path |
content | &'static str | The SQL content (with {{template}} placeholders if any) |
order | Option<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— notTEXT PRIMARY KEY,TEXT NOT NULLfor dates, orPRAGMA. IF NOT EXISTS: UseCREATE TABLE IF NOT EXISTSandCREATE INDEX IF NOT EXISTSfor 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 usesREFERENCES, notFOREIGN KEYinline (both work, butREFERENCESis 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
| Scope | Purpose |
|---|---|
"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 alongsidesrc/db/mod.rs). - Run migrations at app startup after establishing the DB connection.
- The
Databasestruct holds thetokio-postgresClientand exposes arun_migrations()method.
Dependency
Already in Cargo.toml:
migs = "0.1.7"