7.4 KiB
Database Migrations
This project uses Sequelize CLI for database migrations. Migrations provide version control for your database schema.
Quick Reference
# Run pending migrations
npm run db:migrate
# Undo last migration
npm run db:migrate:undo
# Undo all migrations
npm run db:migrate:undo:all
# Check migration status
npm run db:migrate:status
# Test all migrations (up, down, up)
npm run test:migrations
Available Commands
npm run db:migrate
Purpose: Runs all pending migrations that haven't been applied yet.
- Checks the
SequelizeMetatable to see which migrations have already run - Executes the
upfunction in each pending migration file in order - Records each successful migration in the
SequelizeMetatable - When to use: Deploy new schema changes to your database
npm run db:migrate:undo
Purpose: Rolls back the most recent migration.
- Executes the
downfunction of the last applied migration - Removes that migration's entry from
SequelizeMeta - When to use: Quickly revert the last change if something went wrong
npm run db:migrate:undo:all
Purpose: Rolls back ALL migrations, returning to an empty database.
- Executes the
downfunction of every migration in reverse order - Clears the
SequelizeMetatable - When to use: Reset development database to start fresh, or in testing
npm run db:migrate:status
Purpose: Shows the status of all migrations.
- Lists which migrations have been executed (with timestamps)
- Shows which migrations are pending
- When to use: Check what's been applied before deploying or debugging
npm run db:create
Purpose: Creates the database specified in your config.
- Reads
DB_NAMEfrom environment variables - Creates a new PostgreSQL database with that name
- When to use: Initial setup on a new environment
npm run test:migrations
Purpose: Automated testing of migrations (to be implemented in Phase 4).
- Will create a fresh test database
- Run all migrations up, then down, then up again
- Verify migrations are reversible and idempotent
- When to use: In CI/CD pipeline before merging migration changes
Environment Configuration
All commands use the environment specified by NODE_ENV (dev, test, qa, prod) and load the corresponding .env file automatically.
Examples:
# Run migrations in development
NODE_ENV=dev npm run db:migrate
# Check status in QA environment
NODE_ENV=qa npm run db:migrate:status
# Run migrations in production
NODE_ENV=prod npm run db:migrate
Creating a New Migration
# Generate a new migration file
npx sequelize-cli migration:generate --name description-of-change
This creates a timestamped file in backend/migrations/:
20241125123456-description-of-change.js
Migration File Structure
module.exports = {
up: async (queryInterface, Sequelize) => {
// Schema changes to apply
},
down: async (queryInterface, Sequelize) => {
// How to revert the changes
},
};
Naming Conventions
Use descriptive names that indicate the action:
create-users- Creating a new tableadd-email-to-users- Adding a columnremove-legacy-field-from-items- Removing a columnadd-index-on-users-email- Adding an indexchange-status-enum-in-rentals- Modifying a column
Zero-Downtime Patterns
Adding a Column (Safe)
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn("users", "newField", {
type: Sequelize.STRING,
allowNull: true, // Must be nullable or have default
});
};
Adding a NOT NULL Column
// Step 1: Add as nullable
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn("users", "newField", {
type: Sequelize.STRING,
allowNull: true,
});
};
// Step 2: Backfill data (separate migration)
up: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(
`UPDATE users SET "newField" = 'default_value' WHERE "newField" IS NULL`
);
};
// Step 3: Add NOT NULL constraint (separate migration, after code deployed)
up: async (queryInterface, Sequelize) => {
await queryInterface.changeColumn("users", "newField", {
type: Sequelize.STRING,
allowNull: false,
});
};
Removing a Column (3-Step Process)
- Deploy 1: Update code to stop reading/writing the column
- Deploy 2: Run migration to remove column
- Deploy 3: Remove column references from model (cleanup)
// Migration in Deploy 2
up: async (queryInterface) => {
await queryInterface.removeColumn('users', 'oldField');
},
down: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'oldField', {
type: Sequelize.STRING,
allowNull: true
});
}
Renaming a Column (3-Step Process)
// Deploy 1: Add new column, copy data
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn("users", "newName", {
type: Sequelize.STRING,
});
await queryInterface.sequelize.query(
'UPDATE users SET "newName" = "oldName"'
);
};
// Deploy 2: Update code to use newName, keep oldName as fallback
// (no migration)
// Deploy 3: Remove old column
up: async (queryInterface) => {
await queryInterface.removeColumn("users", "oldName");
};
Creating Indexes (Use CONCURRENTLY)
up: async (queryInterface) => {
await queryInterface.addIndex("users", ["email"], {
unique: true,
concurrently: true, // Prevents table locking
name: "users_email_unique",
});
};
Testing Checklist
Before committing a migration:
- Migration has both
upanddownfunctions - Tested locally:
npm run db:migrate - Tested rollback:
npm run db:migrate:undo - Tested re-apply:
npm run db:migrate - Full test:
npm run test:migrations - Application starts and works correctly
- No data loss in
downmigration (where possible)
Deployment Checklist
Before deploying to production:
- Backup production database
- Test migration on copy of production data
- Review migration for safety (no destructive operations)
- Schedule during low-traffic window
- Have rollback plan ready
- Monitor logs after deployment
Rollback Procedures
Undo Last Migration
npm run db:migrate:undo
Undo Multiple Migrations
# Undo last 3 migrations
npx sequelize-cli db:migrate:undo --step 3
Undo to Specific Migration
npx sequelize-cli db:migrate:undo:all --to 20241124000005-create-rentals.js
Common Issues
"Migration file not found"
Ensure the migration filename in SequelizeMeta matches the file on disk.
"Column already exists"
The migration may have partially run. Check the schema and either:
- Manually fix the schema
- Mark migration as complete:
INSERT INTO "SequelizeMeta" VALUES ('filename.js')
"Cannot drop column - dependent objects"
Drop dependent indexes/constraints first:
await queryInterface.removeIndex("users", "index_name");
await queryInterface.removeColumn("users", "column_name");
Foreign Key Constraint Failures
Ensure data integrity before adding constraints:
// Clean orphaned records first
await queryInterface.sequelize.query(
'DELETE FROM rentals WHERE "itemId" NOT IN (SELECT id FROM items)'
);
// Then add constraint
await queryInterface.addConstraint("rentals", {
fields: ["itemId"],
type: "foreign key",
references: { table: "items", field: "id" },
});