Migrations
The migration runner reads *.sql files from a directory in lexicographic order, executes any that have not already been applied, and records each run in a _schema_migrations table. No external tools or frameworks are required.
How it works
conn.migrate("migrations/")creates_schema_migrations(version TEXT PRIMARY KEY, applied_at TEXT)if it does not already exist.- It reads every
*.sqlfile in the directory sorted lexicographically by filename. - Files whose name is already in
_schema_migrationsare skipped. - Each unapplied file is executed in a
BEGIN/COMMITtransaction. If the file fails, the transaction is rolled back andmigratereturnsErrimmediately — subsequent files are not attempted.
File naming convention
Prefix files with a zero-padded sequence number so lexicographic order matches execution order:
migrations/ 001_create_users.sql 002_add_email_index.sql 003_create_posts.sql 004_add_posts_status_column.sqlThe full filename (without the directory prefix) is used as the version key, so renaming a file that has already been applied will cause it to run again. Never rename applied migration files.
Example migration files
-- migrations/001_create_users.sqlCREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL);-- migrations/002_add_email_index.sqlCREATE INDEX idx_users_email ON users (email);-- migrations/003_create_posts.sqlCREATE TABLE posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), title TEXT NOT NULL, body TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'draft', created_at TEXT NOT NULL);-- migrations/004_add_posts_status_column.sqlALTER TABLE posts ADD COLUMN published_at TEXT;Running migrations
Call migrate once at server startup, before the application begins accepting requests:
use rust_web_server::model::{DbConfig, DbConnection};
fn main() { let config = DbConfig::from_env().expect("database config"); let mut conn = DbConnection::open(&config).expect("database connection");
conn.migrate("migrations/").expect("migrations failed");
// start the server ...}If all migrations have already been applied, migrate is a no-op and returns immediately.
Checking migration status
conn.migration_status(dir) returns a Vec<MigrationStatus> — one entry per SQL file in the directory — without executing anything. Use it for health checks, admin endpoints, or CLI tooling.
use rust_web_server::model::MigrationStatus;
let statuses: Vec<MigrationStatus> = conn.migration_status("migrations/")?;
for s in &statuses { let state = if s.applied { "applied" } else { "pending" }; println!("{}: {}", s.version, state);}
// Check if any migrations are pendinglet pending = statuses.iter().any(|s| !s.applied);if pending { eprintln!("Warning: unapplied migrations exist");}MigrationStatus fields:
| Field | Type | Description |
|---|---|---|
version | String | Filename used as the version key |
applied | bool | true if the migration has been run |
Startup pattern
A typical server startup sequence:
use rust_web_server::model::{DbConfig, DbPool};use rust_web_server::app::App;use rust_web_server::server::Server;
fn main() { let db_config = DbConfig::from_env().expect("db config");
// Run migrations using a dedicated connection { let mut conn = rust_web_server::model::DbConnection::open(&db_config) .expect("migration connection"); conn.migrate("migrations/").expect("migrations"); }
// Create a pool for request handlers let pool = DbPool::new(&db_config, db_config.pool_size) .expect("connection pool");
let app = App::with_state(std::sync::Arc::new(pool)) // register routes ... ;
Server::new().run(app);}