# Database Migrations

This project uses Sequelize CLI for database migrations. Migrations provide version control for your database schema.

## Quick Reference

```bash
# 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:

```bash
# 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

```bash
# 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

```javascript
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)

```javascript
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

```javascript
// 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)

```javascript
// 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)

```javascript
// 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)

```javascript
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

```bash
npm run db:migrate:undo
```

### Undo Multiple Migrations

```bash
# Undo last 3 migrations
npx sequelize-cli db:migrate:undo --step 3
```

### Undo to Specific Migration

```bash
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:

```javascript
await queryInterface.removeIndex("users", "index_name");
await queryInterface.removeColumn("users", "column_name");
```

### Foreign Key Constraint Failures

Ensure data integrity before adding constraints:

```javascript
// 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" },
});
```
