Files
rentall-app/backend/migrations
jackiettran 5d3c124d3e text changes
2026-01-21 19:20:07 -05:00
..
2025-11-24 18:11:39 -05:00
2025-11-24 18:11:39 -05:00
2026-01-07 21:55:41 -05:00

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 SequelizeMeta table to see which migrations have already run
  • Executes the up function in each pending migration file in order
  • Records each successful migration in the SequelizeMeta table
  • When to use: Deploy new schema changes to your database

npm run db:migrate:undo

Purpose: Rolls back the most recent migration.

  • Executes the down function 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 down function of every migration in reverse order
  • Clears the SequelizeMeta table
  • 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_NAME from 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 table
  • add-email-to-users - Adding a column
  • remove-legacy-field-from-items - Removing a column
  • add-index-on-users-email - Adding an index
  • change-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)

  1. Deploy 1: Update code to stop reading/writing the column
  2. Deploy 2: Run migration to remove column
  3. 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 up and down functions
  • 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 down migration (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" },
});