# Migrations and ORM Patterns

Database migration strategies, ORM-specific patterns, and safe schema evolution practices.

---

## Migration Strategies Overview

### Approach Comparison

| Strategy | Source of Truth | How It Works | Best For |
|----------|----------------|-------------|----------|
| **Declarative** (Prisma, Atlas) | Schema file | Tool diffs desired vs current state, generates SQL | Rapid development, smaller teams |
| **Imperative** (Knex, Alembic, raw SQL) | Migration files | Developer writes explicit up/down SQL | Fine-grained control, complex migrations |
| **Hybrid** (Prisma Migrate, Drizzle Kit) | Schema file + generated SQL | Tool generates SQL from schema diff, developer can customize | Balance of safety and speed |

---

## Prisma (TypeScript / JavaScript)

### Schema Definition

```prisma
// schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  @@map("users")
  @@index([email])
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String   @db.VarChar(255)
  body      String?  @db.Text
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int      @map("author_id")
  tags      Tag[]
  createdAt DateTime @default(now()) @map("created_at")

  @@map("posts")
  @@index([authorId])
  @@index([published, createdAt])
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]

  @@map("tags")
}
```

### Migration Workflow

```bash
# Development: generate and apply migration
npx prisma migrate dev --name add_user_posts

# Production: apply pending migrations (never use 'dev' in prod)
npx prisma migrate deploy

# Reset database (dev only)
npx prisma migrate reset

# Generate client after schema changes
npx prisma generate
```

### Best Practices

- Use `@map` and `@@map` to keep snake_case in DB, camelCase in code
- Add `@@index` on all foreign keys and frequently queried columns
- Use `@db.VarChar(n)` / `@db.Text` for explicit column types
- Never run `prisma migrate dev` in production
- Customize generated SQL for data migrations before applying

---

## Drizzle ORM (TypeScript)

### Schema Definition

```typescript
// schema.ts
import { pgTable, bigint, varchar, text, boolean, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';

// Reusable timestamp columns
const timestamps = {
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
};

export const users = pgTable('users', {
  id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 255 }),
  ...timestamps,
}, (table) => [
  uniqueIndex('uq_users_email').on(table.email),
]);

export const posts = pgTable('posts', {
  id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
  title: varchar('title', { length: 255 }).notNull(),
  body: text('body'),
  published: boolean('published').notNull().default(false),
  authorId: bigint('author_id', { mode: 'number' }).notNull().references(() => users.id),
  ...timestamps,
}, (table) => [
  index('idx_posts_author').on(table.authorId),
  index('idx_posts_published').on(table.published, table.createdAt),
]);
```

### Migration Workflow

```bash
# Generate migration SQL from schema changes
npx drizzle-kit generate

# Apply migrations
npx drizzle-kit migrate

# Push schema directly (dev only, no migration files)
npx drizzle-kit push

# View current schema diff
npx drizzle-kit check
```

### Drizzle Best Practices

- Define reusable column sets (timestamps, soft-delete) and spread them
- Use `{ mode: 'number' }` for bigint to get JavaScript numbers when safe
- Configure `casing: 'snake_case'` in drizzle config for automatic camelCase mapping
- Export relations separately for type-safe relational queries
- Use `drizzle-kit generate` (not push) for production workflows

---

## Alembic (Python / SQLAlchemy)

### Migration Workflow

```bash
# Generate migration from model changes
alembic revision --autogenerate -m "add user posts"

# Apply all pending migrations
alembic upgrade head

# Rollback one migration
alembic downgrade -1

# Show current revision
alembic current

# Show migration history
alembic history
```

### Best Practices

- Always review autogenerated migrations; they miss data migrations and some DDL
- Use `batch_alter_table` for SQLite (no ALTER TABLE support)
- Write explicit `downgrade()` functions for rollback safety
- Pin alembic version in the migration env to avoid drift

---

## Knex.js (JavaScript)

### Migration Workflow

```bash
# Create migration file
npx knex migrate:make add_user_posts

# Run pending migrations
npx knex migrate:latest

# Rollback last batch
npx knex migrate:rollback

# Run seeds
npx knex seed:run
```

### Migration File

```javascript
exports.up = function(knex) {
  return knex.schema
    .createTable('users', (table) => {
      table.bigIncrements('id').primary();
      table.string('email', 255).notNullable().unique();
      table.string('name', 255);
      table.timestamps(true, true); // created_at, updated_at
    })
    .createTable('posts', (table) => {
      table.bigIncrements('id').primary();
      table.string('title', 255).notNullable();
      table.text('body');
      table.boolean('published').notNullable().defaultTo(false);
      table.bigInteger('author_id').unsigned().notNullable()
        .references('id').inTable('users').onDelete('CASCADE');
      table.timestamps(true, true);
      table.index(['author_id']);
      table.index(['published', 'created_at']);
    });
};

exports.down = function(knex) {
  return knex.schema
    .dropTableIfExists('posts')
    .dropTableIfExists('users');
};
```

---

## Atlas (Database Schema-as-Code)

Atlas is a language-agnostic, declarative schema management tool that integrates with ORMs.

```bash
# Inspect current database schema
atlas schema inspect -u "postgres://localhost:5432/mydb"

# Apply desired schema (declarative)
atlas schema apply -u "postgres://localhost:5432/mydb" --to file://schema.hcl

# Generate versioned migration from ORM schema diff
atlas migrate diff --env prisma

# Lint migrations for safety issues
atlas migrate lint --env local
```

---

## Safe Migration Practices

### The Expand and Contract Pattern

Use for zero-downtime schema changes in production:

1. **Expand**: Add the new column/table alongside the old one
2. **Migrate data**: Copy/transform data from old to new
3. **Dual-write**: Application writes to both old and new
4. **Switch reads**: Application reads from new
5. **Contract**: Remove old column/table once fully migrated

```sql
-- Step 1: Expand - add new column
ALTER TABLE users ADD COLUMN full_name VARCHAR(500);

-- Step 2: Migrate data
UPDATE users SET full_name = first_name || ' ' || last_name;

-- Step 3-4: Deploy application code that reads/writes full_name
-- (application change, not SQL)

-- Step 5: Contract - remove old columns (after verification period)
ALTER TABLE users DROP COLUMN first_name;
ALTER TABLE users DROP COLUMN last_name;
```

### Migration Safety Checklist

- [ ] Migration is backward-compatible with current application code
- [ ] No table locks on large tables during peak hours
- [ ] Data migration tested on production-sized dataset (measure duration)
- [ ] Rollback migration written and tested
- [ ] Indexes created CONCURRENTLY where supported (PostgreSQL)
- [ ] No `NOT NULL` added to existing column without default value
- [ ] Foreign key constraints validated separately from creation (large tables)
- [ ] Migration runs in a transaction where supported
- [ ] Reviewed by at least one other engineer for production databases

### Dangerous Operations to Avoid

| Operation | Risk | Safe Alternative |
|-----------|------|-----------------|
| `DROP TABLE` | Data loss | Rename to `_deprecated_*`, drop after verification period |
| `ALTER COLUMN TYPE` | Table rewrite, locks | Add new column, migrate data, drop old |
| `ADD NOT NULL` (no default) | Fails on existing rows | Add column nullable, backfill, then add constraint |
| `CREATE INDEX` (blocking) | Table lock | `CREATE INDEX CONCURRENTLY` (PostgreSQL) |
| Renaming columns | Breaks running application | Expand-and-contract pattern |
