Migrations
Migrations
Section titled “Migrations”Mantis uses Diesel for database migrations, providing versioned schema management.
Overview
Section titled “Overview”How Migrations Work
Section titled “How Migrations Work”Migrations are SQL files that:
- Apply changes in order (up migrations)
- Can be reversed if needed (down migrations)
- Track applied state in
__diesel_schema_migrationstable
Migration Location
Section titled “Migration Location”instinct/└── migrations/ ├── 00000000000000_diesel_initial_setup/ │ └── up.sql ├── 2025-12-01-004132_create_users/ │ ├── up.sql │ └── down.sql └── 2025-12-01-010016_create_tenants/ ├── up.sql └── down.sqlRunning Migrations
Section titled “Running Migrations”Using Diesel CLI
Section titled “Using Diesel CLI”The recommended approach:
# Install Diesel CLIcargo install diesel_cli --no-default-features --features postgres
# Run migrationscd instinctdiesel migration run
# Check statusdiesel migration listMigration Status
Section titled “Migration Status”Check Applied Migrations
Section titled “Check Applied Migrations”-- Connect to databasepsql -U mantis -d mantis
-- View migrationsSELECT version, run_onFROM __diesel_schema_migrationsORDER BY run_on DESC;Example output:
version | run_on----------------------+--------------------------- 2024010300000 | 2024-01-03 10:00:00.000000 2024010200000 | 2024-01-02 10:00:00.000000 2024010100000 | 2024-01-01 10:00:00.000000Pending Migrations
Section titled “Pending Migrations”# Check for pending migrationsdiesel migration pending
# List all migrations with statusdiesel migration listCreating Migrations
Section titled “Creating Migrations”Generate Migration
Section titled “Generate Migration”cd instinctdiesel migration generate create_new_tableCreates a directory named with a YYYY-MM-DD-HHMMSS timestamp prefix (14 digits with
hyphens) followed by the migration name:
migrations/└── 2026-06-22-163256_create_new_table/ ├── up.sql └── down.sqlWrite Up Migration
Section titled “Write Up Migration”-- migrations/2026-06-22-163256_create_new_table/up.sql
-- Note: UUIDs are generated by application code using crate::uuid::new_v8()-- and passed in at insert time. Do not use DEFAULT gen_random_uuid() for-- primary keys — application-generated UUIDv8 ensures sortable identifiers.CREATE TABLE new_table ( id UUID PRIMARY KEY, name VARCHAR(255) NOT NULL, tenant_id UUID NOT NULL REFERENCES tenants(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW());
CREATE INDEX idx_new_table_tenant ON new_table(tenant_id);Write Down Migration
Section titled “Write Down Migration”-- migrations/2026-06-22-163256_create_new_table/down.sql
DROP TABLE IF EXISTS new_table;Test Migration
Section titled “Test Migration”# Apply migrationdiesel migration run
# Verify it workspsql -U mantis -c "\dt new_table"
# Test rollbackdiesel migration revert
# Re-applydiesel migration runMigration Best Practices
Section titled “Migration Best Practices”1. Make Migrations Reversible
Section titled “1. Make Migrations Reversible”Always write down migrations:
-- up.sqlALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT false;
-- down.sqlALTER TABLE users DROP COLUMN email_verified;2. Use Transactions
Section titled “2. Use Transactions”Wrap complex migrations in transactions:
-- up.sqlBEGIN;
ALTER TABLE users ADD COLUMN status VARCHAR(20);UPDATE users SET status = 'active';ALTER TABLE users ALTER COLUMN status SET NOT NULL;
COMMIT;3. Avoid Data Loss
Section titled “3. Avoid Data Loss”Rename instead of dropping:
-- up.sql (safe)ALTER TABLE users RENAME COLUMN old_name TO new_name;
-- up.sql (risky - data loss)-- ALTER TABLE users DROP COLUMN old_name;-- ALTER TABLE users ADD COLUMN new_name VARCHAR(255);4. Add Indexes Concurrently
Section titled “4. Add Indexes Concurrently”For large tables, avoid locking:
-- up.sqlCREATE INDEX CONCURRENTLY idx_dh_status ON deployment_history(status);5. Handle NOT NULL Additions
Section titled “5. Handle NOT NULL Additions”Add NOT NULL constraints safely:
-- up.sql-- Step 1: Add nullable columnALTER TABLE users ADD COLUMN role_id UUID;
-- Step 2: Populate existing rowsUPDATE users SET role_id = (SELECT id FROM roles WHERE name = 'default');
-- Step 3: Add constraintALTER TABLE users ALTER COLUMN role_id SET NOT NULL;ALTER TABLE users ADD CONSTRAINT fk_users_role FOREIGN KEY (role_id) REFERENCES roles(id);Rolling Back Migrations
Section titled “Rolling Back Migrations”Revert Last Migration
Section titled “Revert Last Migration”diesel migration revertRevert Multiple Migrations
Section titled “Revert Multiple Migrations”# Revert the last 3 migrationsdiesel migration revert -n 3
# Revert all applied migrationsdiesel migration revert --allManual Rollback
Section titled “Manual Rollback”If automated rollback fails:
-- 1. Execute down.sql manually-- 2. Remove from tracking tableDELETE FROM __diesel_schema_migrationsWHERE version = '2024010400000';Troubleshooting
Section titled “Troubleshooting”Migration Failed
Section titled “Migration Failed”Error: migration 2024010400000 failedCaused by: duplicate key value violates unique constraintResolution:
- Check error message for specifics
- Fix the issue in your migration
- If partially applied, manually clean up
- Re-run migration
Schema Out of Sync
Section titled “Schema Out of Sync”# Regenerate schema file from database (run from within instinct/)diesel print-schema > src/database/schema.rsAlternatively, running diesel migration run automatically updates src/database/schema.rs per the diesel.toml configuration.
Locked Tables
Section titled “Locked Tables”-- Check for locksSELECT * FROM pg_locks WHERE NOT granted;
-- Identify blocking querySELECT pid, usename, query, stateFROM pg_stat_activityWHERE pid IN ( SELECT blocking_pid FROM pg_locks WHERE NOT granted);
-- Terminate blocking query (use with caution)SELECT pg_terminate_backend(<pid>);Connection Limit Reached
Section titled “Connection Limit Reached”Error: too many connections for database "mantis"Resolution:
- Check connection count
- Increase
max_connectionsin postgresql.conf - Use connection pooling
- Close idle connections
CI/CD Integration
Section titled “CI/CD Integration”Pre-Deployment Check
Section titled “Pre-Deployment Check”#!/bin/bash# Check for pending migrationsPENDING=$(diesel migration pending 2>&1)if [ -n "$PENDING" ]; then echo "Pending migrations found:" echo "$PENDING" exit 1fi
echo "All migrations are up to date"Deployment Pipeline
Section titled “Deployment Pipeline”jobs: deploy: steps: - name: Run migrations run: | cd instinct diesel migration run env: DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Deploy application run: | docker compose up -dKubernetes Job
Section titled “Kubernetes Job”apiVersion: batch/v1kind: Jobmetadata: name: mantis-migrationspec: template: spec: containers: - name: migration image: <registry>/mantis-mandible:latest command: ['mandible', '--migrate-only'] env: - name: DATABASE_URL valueFrom: secretKeyRef: name: mantis-database key: url restartPolicy: OnFailureMulti-Environment Management
Section titled “Multi-Environment Management”Environment-Specific Migrations
Section titled “Environment-Specific Migrations”# DevelopmentDATABASE_URL="postgres://localhost/mantis_dev" diesel migration run
# StagingDATABASE_URL="postgres://staging-db/mantis" diesel migration run
# ProductionDATABASE_URL="postgres://prod-db/mantis" diesel migration runMigration Testing
Section titled “Migration Testing”# Create test databasecreatedb mantis_migration_test
# Copy production schemapg_dump -s mantis_prod | psql mantis_migration_test
# Test new migrationsDATABASE_URL="postgres://localhost/mantis_migration_test" diesel migration run
# Verify successpsql mantis_migration_test -c "\dt"
# Clean updropdb mantis_migration_testData Migrations
Section titled “Data Migrations”Large Data Updates
Section titled “Large Data Updates”For large tables, batch updates:
-- up.sqlDO $$DECLARE batch_size INTEGER := 1000; affected_rows INTEGER;BEGIN LOOP UPDATE users SET status = 'active' WHERE status IS NULL AND id IN ( SELECT id FROM users WHERE status IS NULL LIMIT batch_size );
GET DIAGNOSTICS affected_rows = ROW_COUNT; EXIT WHEN affected_rows = 0;
PERFORM pg_sleep(0.1); -- Brief pause to reduce load END LOOP;END $$;Backfilling Data
Section titled “Backfilling Data”-- up.sql-- Add columnALTER TABLE deployment_history ADD COLUMN duration_seconds INTEGER;
-- Backfill from existing dataUPDATE deployment_historySET duration_seconds = EXTRACT(EPOCH FROM (completed_at - started_at))WHERE completed_at IS NOT NULL AND started_at IS NOT NULL;Next Steps
Section titled “Next Steps”- Connection Pooling - Optimize connections
- Maintenance - Database maintenance
- Backup & Recovery - Backup procedures
