Migration Script Creator
Category: Database October 15, 2025
Create safe database or data migration scripts with rollback capability and validation.
DatabaseMigrationDataSQLSafety
# Migration Script Creator
Create safe, reversible database migration scripts with proper validation, error handling, and rollback capability.
## Migration Structure
### 1. Up Migration (Forward)
- Schema changes
- Data transformations
- Index creation
- Constraint additions
### 2. Down Migration (Rollback)
- Reverse all changes
- Restore previous state
- Handle data loss warnings
- Clean up artifacts
### 3. Safety Checks
- Backup verification
- Dry run mode
- Data validation
- Dependency checks
- Lock prevention
## Example Migration
```sql
-- Migration: 001_add_user_roles.sql
-- Description: Add roles table and link to users
-- Author: DevTeam
-- Date: 2025-10-15
-- ============================================
-- UP MIGRATION
-- ============================================
BEGIN;
-- Safety checks
DO $$
BEGIN
-- Check if migration already applied
IF EXISTS (SELECT 1 FROM schema_migrations WHERE version = '001') THEN
RAISE EXCEPTION 'Migration 001 already applied';
END IF;
-- Check prerequisites
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'users') THEN
RAISE EXCEPTION 'Users table not found. Run migration 000 first.';
END IF;
END$$;
-- Create roles table
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Add role_id to users table
ALTER TABLE users
ADD COLUMN role_id INTEGER REFERENCES roles(id);
-- Insert default roles
INSERT INTO roles (name, description) VALUES
('admin', 'Administrator with full access'),
('user', 'Regular user with standard access'),
('guest', 'Guest with limited access')
ON CONFLICT (name) DO NOTHING;
-- Set default role for existing users
UPDATE users
SET role_id = (SELECT id FROM roles WHERE name = 'user')
WHERE role_id IS NULL;
-- Make role_id required
ALTER TABLE users
ALTER COLUMN role_id SET NOT NULL;
-- Create indexes
CREATE INDEX idx_users_role_id ON users(role_id);
CREATE INDEX idx_roles_name ON roles(name);
-- Record migration
INSERT INTO schema_migrations (version, name, applied_at)
VALUES ('001', 'add_user_roles', CURRENT_TIMESTAMP);
COMMIT;
-- ============================================
-- DOWN MIGRATION
-- ============================================
BEGIN;
-- Remove indexes
DROP INDEX IF EXISTS idx_users_role_id;
DROP INDEX IF EXISTS idx_roles_name;
-- Remove foreign key and column
ALTER TABLE users DROP COLUMN IF EXISTS role_id;
-- Drop roles table
DROP TABLE IF EXISTS roles;
-- Remove migration record
DELETE FROM schema_migrations WHERE version = '001';
COMMIT;
Migration Best Practices
Pre-Migration Checklist
- Backup database created
- Migration tested on staging
- Rollback script tested
- Downtime window communicated
- Team notified
- Monitoring in place
During Migration
- Use transactions where possible
- Add timeouts to prevent locks
- Log all operations
- Monitor database performance
- Keep team updated
Post-Migration
- Verify data integrity
- Check application functionality
- Monitor for errors
- Update documentation
- Notify team of completion
Data Migration Example
-- Migration: 002_normalize_addresses.sql
-- Description: Split address field into components
BEGIN;
-- Add new columns
ALTER TABLE users
ADD COLUMN street VARCHAR(255),
ADD COLUMN city VARCHAR(100),
ADD COLUMN state VARCHAR(50),
ADD COLUMN zip_code VARCHAR(20),
ADD COLUMN country VARCHAR(100);
-- Migrate data (example parsing logic)
UPDATE users
SET
street = split_part(address, ',', 1),
city = trim(split_part(address, ',', 2)),
state = trim(split_part(address, ',', 3)),
zip_code = trim(split_part(address, ',', 4)),
country = 'USA'
WHERE address IS NOT NULL;
-- Validation
DO $$
DECLARE
invalid_count INTEGER;
BEGIN
SELECT COUNT(*) INTO invalid_count
FROM users
WHERE address IS NOT NULL
AND (street IS NULL OR city IS NULL);
IF invalid_count > 0 THEN
RAISE WARNING '% users have invalid address data', invalid_count;
END IF;
END$$;
-- After validation, can drop old column
-- ALTER TABLE users DROP COLUMN address;
COMMIT;
TypeScript Migration Runner
interface Migration {
version: string;
name: string;
up: () => Promise<void>;
down: () => Promise<void>;
}
class MigrationRunner {
async runMigration(migration: Migration, direction: 'up' | 'down') {
const client = await pool.connect();
try {
await client.query('BEGIN');
console.log(`Running migration ${migration.version} (${direction})...`);
if (direction === 'up') {
await migration.up();
await this.recordMigration(client, migration);
} else {
await migration.down();
await this.removeMigration(client, migration);
}
await client.query('COMMIT');
console.log(`Migration ${migration.version} completed successfully`);
} catch (error) {
await client.query('ROLLBACK');
console.error(`Migration ${migration.version} failed:`, error);
throw error;
} finally {
client.release();
}
}
async getAppliedMigrations(): Promise<string[]> {
const result = await pool.query(
'SELECT version FROM schema_migrations ORDER BY applied_at'
);
return result.rows.map(row => row.version);
}
}
Safety Features
Backup Before Migration
# PostgreSQL
pg_dump -Fc mydb > backup_before_migration_$(date +%Y%m%d_%H%M%S).dump
# MySQL
mysqldump --single-transaction mydb > backup_$(date +%Y%m%d_%H%M%S).sql
Dry Run Mode
if (process.env.DRY_RUN === 'true') {
console.log('DRY RUN MODE: Changes will not be committed');
// Run migration but always rollback
await client.query('ROLLBACK');
}
Lock Timeout
-- Prevent long-running locks
SET lock_timeout = '5s';
SET statement_timeout = '30s';