🦀 Database Migrations in TuskLang for Rust
Database Migrations in TuskLang for Rust
TuskLang's Rust migration system provides a type-safe, version-controlled approach to database schema management with compile-time guarantees, async/await support, and zero-downtime deployment capabilities.
🚀 Why Rust Migrations?
Rust's type system and ownership model make it the perfect language for database migrations:
- Type Safety: Compile-time validation of schema changes - Version Control: Automatic tracking of migration history - Rollback Safety: Guaranteed rollback capabilities - Async/Await: Non-blocking migration execution - Zero-Downtime: Support for complex deployment strategies
Basic Migration Structure
use tusk_db::{Migration, Schema, ColumnType, IndexType, Result};
use async_trait::async_trait;
use chrono::{DateTime, Utc};// Basic migration with Rust traits
#[derive(Debug)]
struct CreateUsersTable;
#[async_trait]
impl Migration for CreateUsersTable {
fn name() -> &'static str {
"create_users_table"
}
fn version() -> &'static str {
"2024_01_15_000001"
}
fn description() -> &'static str {
"Create the users table with basic authentication fields"
}
async fn up(schema: &Schema) -> Result<()> {
schema.create_table("users", |table| {
table.id("id");
table.string("name", 255).not_null();
table.string("email", 255).unique().not_null();
table.string("password_hash", 255).not_null();
table.timestamp("email_verified_at").nullable();
table.boolean("is_active").default(true);
table.timestamp("created_at").default_current();
table.timestamp("updated_at").default_current();
// Indexes for performance
table.index(&["email"], IndexType::BTree);
table.index(&["created_at"], IndexType::BTree);
table.index(&["is_active", "created_at"], IndexType::BTree);
}).await?;
Ok(())
}
async fn down(schema: &Schema) -> Result<()> {
schema.drop_table_if_exists("users").await?;
Ok(())
}
}
Advanced Migration Features
use tusk_db::{MigrationBuilder, ForeignKey, Constraint};// Complex migration with relationships
#[derive(Debug)]
struct CreatePostsTable;
#[async_trait]
impl Migration for CreatePostsTable {
fn name() -> &'static str {
"create_posts_table"
}
fn version() -> &'static str {
"2024_01_15_000002"
}
fn description() -> &'static str {
"Create the posts table with user relationships"
}
async fn up(schema: &Schema) -> Result<()> {
schema.create_table("posts", |table| {
table.id("id");
table.string("title", 255).not_null();
table.text("content").not_null();
table.integer("user_id").unsigned().not_null();
table.boolean("published").default(false);
table.integer("view_count").default(0);
table.decimal("rating", 3, 2).nullable();
table.json("tags").nullable();
table.json("metadata").nullable();
table.timestamp("published_at").nullable();
table.timestamp("created_at").default_current();
table.timestamp("updated_at").default_current();
// Foreign key constraints
table.foreign_key("user_id", "users", "id")
.on_delete(ForeignKey::Cascade)
.on_update(ForeignKey::Cascade);
// Indexes
table.index(&["user_id"], IndexType::BTree);
table.index(&["published"], IndexType::BTree);
table.index(&["created_at"], IndexType::BTree);
table.index(&["user_id", "published"], IndexType::BTree);
// Unique constraints
table.unique(&["user_id", "title"]);
}).await?;
Ok(())
}
async fn down(schema: &Schema) -> Result<()> {
schema.drop_table_if_exists("posts").await?;
Ok(())
}
}
Migration Dependencies and Ordering
use tusk_db::{MigrationDependency, MigrationOrder};// Migration with dependencies
#[derive(Debug)]
struct CreateCommentsTable;
#[async_trait]
impl Migration for CreateCommentsTable {
fn name() -> &'static str {
"create_comments_table"
}
fn version() -> &'static str {
"2024_01_15_000003"
}
fn description() -> &'static str {
"Create the comments table with post and user relationships"
}
// Define dependencies
fn dependencies() -> &'static [&'static str] {
&["create_users_table", "create_posts_table"]
}
async fn up(schema: &Schema) -> Result<()> {
schema.create_table("comments", |table| {
table.id("id");
table.text("content").not_null();
table.integer("user_id").unsigned().not_null();
table.integer("post_id").unsigned().not_null();
table.integer("parent_id").unsigned().nullable();
table.boolean("is_approved").default(false);
table.timestamp("approved_at").nullable();
table.timestamp("created_at").default_current();
table.timestamp("updated_at").default_current();
// Foreign keys
table.foreign_key("user_id", "users", "id")
.on_delete(ForeignKey::Cascade);
table.foreign_key("post_id", "posts", "id")
.on_delete(ForeignKey::Cascade);
table.foreign_key("parent_id", "comments", "id")
.on_delete(ForeignKey::Cascade);
// Indexes
table.index(&["post_id"], IndexType::BTree);
table.index(&["user_id"], IndexType::BTree);
table.index(&["parent_id"], IndexType::BTree);
table.index(&["is_approved"], IndexType::BTree);
table.index(&["created_at"], IndexType::BTree);
}).await?;
Ok(())
}
async fn down(schema: &Schema) -> Result<()> {
schema.drop_table_if_exists("comments").await?;
Ok(())
}
}
Data Migrations and Seeding
use tusk_db::{DataMigration, Seeder};// Data migration for seeding initial data
#[derive(Debug)]
struct SeedInitialData;
#[async_trait]
impl DataMigration for SeedInitialData {
fn name() -> &'static str {
"seed_initial_data"
}
fn version() -> &'static str {
"2024_01_15_000004"
}
fn description() -> &'static str {
"Seed initial users and roles"
}
async fn up(db: &Database) -> Result<()> {
// Create admin user
let admin_id = db.insert(
"INSERT INTO users (name, email, password_hash, is_active, email_verified_at) VALUES (?, ?, ?, ?, ?)",
&[
"Admin User",
"admin@example.com",
&hash_password("admin123"),
&true,
&Utc::now(),
]
).await?;
// Create default roles
let admin_role_id = db.insert(
"INSERT INTO roles (name, description) VALUES (?, ?)",
&["admin", "Administrator with full access"]
).await?;
let user_role_id = db.insert(
"INSERT INTO roles (name, description) VALUES (?, ?)",
&["user", "Regular user with limited access"]
).await?;
// Assign admin role to admin user
db.insert(
"INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)",
&[&admin_id, &admin_role_id]
).await?;
Ok(())
}
async fn down(db: &Database) -> Result<()> {
// Remove seeded data
db.delete("DELETE FROM user_roles WHERE user_id IN (SELECT id FROM users WHERE email = ?)", &["admin@example.com"]).await?;
db.delete("DELETE FROM users WHERE email = ?", &["admin@example.com"]).await?;
db.delete("DELETE FROM roles WHERE name IN (?, ?)", &["admin", "user"]).await?;
Ok(())
}
}
Schema Modifications and Alterations
use tusk_db::{SchemaModification, ColumnModification};// Migration for modifying existing tables
#[derive(Debug)]
struct AddUserProfileFields;
#[async_trait]
impl Migration for AddUserProfileFields {
fn name() -> &'static str {
"add_user_profile_fields"
}
fn version() -> &'static str {
"2024_01_20_000001"
}
fn description() -> &'static str {
"Add profile fields to users table"
}
async fn up(schema: &Schema) -> Result<()> {
// Add new columns
schema.alter_table("users", |table| {
table.add_column("first_name", ColumnType::String(100)).nullable();
table.add_column("last_name", ColumnType::String(100)).nullable();
table.add_column("phone", ColumnType::String(20)).nullable();
table.add_column("date_of_birth", ColumnType::Date).nullable();
table.add_column("bio", ColumnType::Text).nullable();
table.add_column("avatar_url", ColumnType::String(500)).nullable();
table.add_column("website", ColumnType::String(500)).nullable();
table.add_column("location", ColumnType::String(255)).nullable();
table.add_column("timezone", ColumnType::String(50)).default("UTC");
table.add_column("language", ColumnType::String(10)).default("en");
}).await?;
// Add indexes for new columns
schema.alter_table("users", |table| {
table.add_index(&["first_name", "last_name"], IndexType::BTree);
table.add_index(&["phone"], IndexType::BTree);
table.add_index(&["location"], IndexType::BTree);
}).await?;
Ok(())
}
async fn down(schema: &Schema) -> Result<()> {
// Remove added columns
schema.alter_table("users", |table| {
table.drop_index("users_first_name_last_name_index");
table.drop_index("users_phone_index");
table.drop_index("users_location_index");
table.drop_column("first_name");
table.drop_column("last_name");
table.drop_column("phone");
table.drop_column("date_of_birth");
table.drop_column("bio");
table.drop_column("avatar_url");
table.drop_column("website");
table.drop_column("location");
table.drop_column("timezone");
table.drop_column("language");
}).await?;
Ok(())
}
}
Complex Schema Changes
use tusk_db::{SchemaChange, DataTransformation};// Migration for complex schema changes
#[derive(Debug)]
struct MigrateUserNames;
#[async_trait]
impl Migration for MigrateUserNames {
fn name() -> &'static str {
"migrate_user_names"
}
fn version() -> &'static str {
"2024_01_25_000001"
}
fn description() -> &'static str {
"Migrate from single name field to first_name and last_name"
}
async fn up(schema: &Schema) -> Result<()> {
// Add new columns
schema.alter_table("users", |table| {
table.add_column("first_name_temp", ColumnType::String(100)).nullable();
table.add_column("last_name_temp", ColumnType::String(100)).nullable();
}).await?;
// Migrate data
let users = schema.query("SELECT id, name FROM users").await?;
for user in users {
let name_parts: Vec<&str> = user["name"].as_str().unwrap().split_whitespace().collect();
let first_name = name_parts.first().unwrap_or(&"");
let last_name = if name_parts.len() > 1 {
name_parts[1..].join(" ")
} else {
String::new()
};
schema.update(
"UPDATE users SET first_name_temp = ?, last_name_temp = ? WHERE id = ?",
&[first_name, &last_name, &user["id"]]
).await?;
}
// Drop old column and rename new ones
schema.alter_table("users", |table| {
table.drop_column("name");
table.rename_column("first_name_temp", "first_name");
table.rename_column("last_name_temp", "last_name");
}).await?;
// Make columns not null
schema.alter_table("users", |table| {
table.modify_column("first_name", |col| {
col.not_null();
});
}).await?;
Ok(())
}
async fn down(schema: &Schema) -> Result<()> {
// Reverse the migration
schema.alter_table("users", |table| {
table.add_column("name_temp", ColumnType::String(255)).nullable();
}).await?;
// Combine first_name and last_name back to name
let users = schema.query("SELECT id, first_name, last_name FROM users").await?;
for user in users {
let full_name = format!("{} {}",
user["first_name"].as_str().unwrap_or(""),
user["last_name"].as_str().unwrap_or("")
).trim().to_string();
schema.update(
"UPDATE users SET name_temp = ? WHERE id = ?",
&[&full_name, &user["id"]]
).await?;
}
// Drop new columns and rename old one
schema.alter_table("users", |table| {
table.drop_column("first_name");
table.drop_column("last_name");
table.rename_column("name_temp", "name");
}).await?;
Ok(())
}
}
Migration Runner and Management
use tusk_db::{MigrationRunner, MigrationStatus, MigrationHistory};// Migration runner with Rust async patterns
async fn run_migrations() -> Result<()> {
let runner = @MigrationRunner::new().await?;
// Get pending migrations
let pending = runner.get_pending_migrations().await?;
if pending.is_empty() {
println!("No pending migrations");
return Ok(());
}
// Run migrations in order
for migration in pending {
println!("Running migration: {}", migration.name());
match runner.run_migration(migration).await {
Ok(_) => println!("✓ Migration completed successfully"),
Err(e) => {
println!("✗ Migration failed: {}", e);
return Err(e);
}
}
}
println!("All migrations completed successfully");
Ok(())
}
// Rollback migrations
async fn rollback_migrations(steps: usize) -> Result<()> {
let runner = @MigrationRunner::new().await?;
let history = runner.get_migration_history().await?;
let to_rollback = history.iter().rev().take(steps).collect::<Vec<_>>();
for migration in to_rollback {
println!("Rolling back migration: {}", migration.name());
match runner.rollback_migration(migration).await {
Ok(_) => println!("✓ Rollback completed successfully"),
Err(e) => {
println!("✗ Rollback failed: {}", e);
return Err(e);
}
}
}
println!("Rollback completed successfully");
Ok(())
}
// Migration status and history
async fn migration_status() -> Result<()> {
let runner = @MigrationRunner::new().await?;
let status = runner.get_status().await?;
let history = runner.get_migration_history().await?;
println!("Migration Status:");
println!(" Total migrations: {}", status.total);
println!(" Run migrations: {}", status.run);
println!(" Pending migrations: {}", status.pending);
println!(" Last migration: {}", status.last_migration.unwrap_or("None".to_string()));
println!("\nMigration History:");
for migration in history {
println!(" {} - {} ({})",
migration.version(),
migration.name(),
migration.run_at().unwrap_or_default()
);
}
Ok(())
}
Testing Migrations
use tusk_db::test_utils::{TestDatabase, TestMigration};// Test migration with test database
#[tokio::test]
async fn test_create_users_table() -> Result<()> {
let test_db = @TestDatabase::new().await?;
// Run migration
let migration = CreateUsersTable;
test_db.run_migration(&migration).await?;
// Verify table was created
let tables = test_db.get_tables().await?;
assert!(tables.contains(&"users".to_string()));
// Verify columns exist
let columns = test_db.get_table_columns("users").await?;
assert!(columns.iter().any(|col| col.name == "id"));
assert!(columns.iter().any(|col| col.name == "name"));
assert!(columns.iter().any(|col| col.name == "email"));
// Test rollback
test_db.rollback_migration(&migration).await?;
// Verify table was dropped
let tables_after_rollback = test_db.get_tables().await?;
assert!(!tables_after_rollback.contains(&"users".to_string()));
Ok(())
}
// Integration test for complex migrations
#[tokio::test]
async fn test_complete_migration_chain() -> Result<()> {
let test_db = @TestDatabase::new().await?;
let migrations = vec![
Box::new(CreateUsersTable),
Box::new(CreatePostsTable),
Box::new(CreateCommentsTable),
Box::new(SeedInitialData),
];
// Run all migrations
for migration in &migrations {
test_db.run_migration(migration.as_ref()).await?;
}
// Verify data integrity
let user_count = test_db.query_one::<i64>("SELECT COUNT(*) FROM users").await?;
assert_eq!(user_count, 1); // Admin user
let post_count = test_db.query_one::<i64>("SELECT COUNT(*) FROM posts").await?;
assert_eq!(post_count, 0); // No posts yet
// Test rollback in reverse order
for migration in migrations.iter().rev() {
test_db.rollback_migration(migration.as_ref()).await?;
}
// Verify all tables were dropped
let tables = test_db.get_tables().await?;
assert!(tables.is_empty());
Ok(())
}
Production Migration Strategies
use tusk_db::{ProductionMigration, ZeroDowntimeMigration};// Zero-downtime migration strategy
#[derive(Debug)]
struct AddUserIndexesZeroDowntime;
#[async_trait]
impl ZeroDowntimeMigration for AddUserIndexesZeroDowntime {
fn name() -> &'static str {
"add_user_indexes_zero_downtime"
}
fn version() -> &'static str {
"2024_02_01_000001"
}
async fn up(schema: &Schema) -> Result<()> {
// Add indexes concurrently to avoid locking
schema.create_index_concurrently("users", &["email"], IndexType::BTree).await?;
schema.create_index_concurrently("users", &["created_at"], IndexType::BTree).await?;
schema.create_index_concurrently("users", &["is_active", "created_at"], IndexType::BTree).await?;
Ok(())
}
async fn down(schema: &Schema) -> Result<()> {
// Drop indexes concurrently
schema.drop_index_concurrently("users", "users_email_index").await?;
schema.drop_index_concurrently("users", "users_created_at_index").await?;
schema.drop_index_concurrently("users", "users_is_active_created_at_index").await?;
Ok(())
}
// Check if migration can be run safely
async fn can_run_safely(schema: &Schema) -> Result<bool> {
// Check table size
let row_count = schema.query_one::<i64>("SELECT COUNT(*) FROM users").await?;
// Check if indexes already exist
let indexes = schema.get_table_indexes("users").await?;
let email_index_exists = indexes.iter().any(|idx| idx.name == "users_email_index");
Ok(row_count < 1_000_000 && !email_index_exists)
}
}
// Blue-green deployment migration
#[derive(Debug)]
struct BlueGreenMigration;
#[async_trait]
impl ProductionMigration for BlueGreenMigration {
fn name() -> &'static str {
"blue_green_migration"
}
fn version() -> &'static str {
"2024_02_01_000002"
}
async fn up(schema: &Schema) -> Result<()> {
// Create new table with new schema
schema.create_table("users_v2", |table| {
table.id("id");
table.string("name", 255).not_null();
table.string("email", 255).unique().not_null();
table.string("password_hash", 255).not_null();
table.boolean("is_active").default(true);
table.timestamp("created_at").default_current();
table.timestamp("updated_at").default_current();
// New fields
table.string("first_name", 100).nullable();
table.string("last_name", 100).nullable();
table.string("phone", 20).nullable();
}).await?;
// Copy data from old table to new table
schema.execute(
"INSERT INTO users_v2 (id, name, email, password_hash, is_active, created_at, updated_at)
SELECT id, name, email, password_hash, is_active, created_at, updated_at
FROM users"
).await?;
// Rename tables
schema.rename_table("users", "users_old").await?;
schema.rename_table("users_v2", "users").await?;
Ok(())
}
async fn down(schema: &Schema) -> Result<()> {
// Rollback by renaming tables back
schema.rename_table("users", "users_v2").await?;
schema.rename_table("users_old", "users").await?;
schema.drop_table_if_exists("users_v2").await?;
Ok(())
}
}
Migration Configuration and Environment
use tusk_db::{MigrationConfig, Environment};// Migration configuration for different environments
async fn configure_migrations() -> Result<MigrationConfig> {
let config = MigrationConfig {
// Database connection
database_url: @env("DATABASE_URL", "postgresql://localhost/myapp"),
// Migration settings
migrations_path: "migrations/",
migration_table: "migrations",
// Environment-specific settings
environment: match @env("APP_ENV", "development").as_str() {
"production" => Environment::Production,
"staging" => Environment::Staging,
"testing" => Environment::Testing,
_ => Environment::Development,
},
// Production safety settings
require_confirmation: @env("REQUIRE_MIGRATION_CONFIRMATION", "true").parse().unwrap_or(true),
backup_before_migration: @env("BACKUP_BEFORE_MIGRATION", "true").parse().unwrap_or(true),
max_execution_time: @env("MIGRATION_TIMEOUT", "300").parse().unwrap_or(300),
// Rollback settings
allow_rollback_in_production: false,
max_rollback_steps: 1,
// Logging
verbose: @env("MIGRATION_VERBOSE", "false").parse().unwrap_or(false),
log_queries: @env("MIGRATION_LOG_QUERIES", "false").parse().unwrap_or(false),
};
Ok(config)
}
// Environment-specific migration runner
async fn run_environment_migrations() -> Result<()> {
let config = configure_migrations().await?;
let runner = @MigrationRunner::with_config(config).await?;
match config.environment {
Environment::Development => {
// Run all migrations without confirmation
runner.run_all_migrations().await?;
}
Environment::Staging => {
// Run with confirmation but allow rollback
if runner.confirm_migrations().await? {
runner.run_all_migrations().await?;
}
}
Environment::Production => {
// Strict production rules
if !runner.can_run_safely().await? {
return Err("Migrations cannot be run safely in production".into());
}
if runner.confirm_migrations().await? {
runner.run_all_migrations().await?;
}
}
Environment::Testing => {
// Run in test mode
runner.run_in_test_mode().await?;
}
}
Ok(())
}
Best Practices for Rust Migrations
1. Version Control: Always use semantic versioning for migrations 2. Dependencies: Define clear migration dependencies 3. Rollback Safety: Ensure all migrations can be rolled back 4. Data Integrity: Use transactions for data migrations 5. Performance: Use concurrent operations for large tables 6. Testing: Test migrations in all environments 7. Documentation: Document complex schema changes 8. Backup: Always backup before production migrations 9. Monitoring: Monitor migration performance and errors 10. Zero-Downtime: Use zero-downtime strategies for production
Related Topics
- database-overview-rust
- Database integration overview
- query-builder-rust
- Fluent query interface
- orm-models-rust
- Model definition and usage
- relationships-rust
- Model relationships
- database-transactions-rust
- Transaction handling
---
Ready to manage your database schema with type-safe, version-controlled migrations in Rust?