# DDL Migrations: Batched Migration Pattern

**REQUIRED for tables exceeding 3,000 rows.**

For the full Table Recreation Pattern and verify & swap steps, see [overview.md](overview.md).

---

## Batch Size Rules

- **PREFER batches of 500-1,000 rows** for optimal performance
- Smaller batches reduce lock contention and enable better concurrency

---

## OFFSET-Based Batching

```sql
SELECT COUNT(*) as total FROM target_table;
-- Calculate: batches_needed = CEIL(total / 1000)

-- Batch 1
INSERT INTO target_table_new (id, col1, col2)
SELECT id, col1, col2 FROM target_table
ORDER BY id LIMIT 1000 OFFSET 0;

-- Batch 2
INSERT INTO target_table_new (id, col1, col2)
SELECT id, col1, col2 FROM target_table
ORDER BY id LIMIT 1000 OFFSET 1000;
-- Continue until all rows migrated...
```

---

## Cursor-Based Batching (Preferred for Large Tables)

Better performance than OFFSET for very large tables:

```sql
-- First batch
INSERT INTO target_table_new (id, col1, col2)
SELECT id, col1, col2 FROM target_table
ORDER BY id LIMIT 1000;

-- Get last processed ID
SELECT MAX(id) as last_id FROM target_table_new;

-- Subsequent batches
INSERT INTO target_table_new (id, col1, col2)
SELECT id, col1, col2 FROM target_table
WHERE id > 'last_processed_id'
ORDER BY id LIMIT 1000;
```

---

## Progress Tracking

```sql
SELECT (SELECT COUNT(*) FROM target_table_new) as migrated,
       (SELECT COUNT(*) FROM target_table) as total;
```

---

## Error Handling

### Pre-Migration Checks

1. **Verify table exists**

   ```sql
   SELECT table_name FROM information_schema.tables
   WHERE table_schema = 'public' AND table_name = 'target_table';
   ```

2. **Verify DDL permissions**

### Data Validation Errors

**MUST abort migration and report** when:

- Type conversion would fail
- Value truncation would occur
- NOT NULL constraint would be violated

```sql
-- Find problematic rows
SELECT id, problematic_column FROM target_table
WHERE problematic_column !~ '^-?[0-9]+$' LIMIT 100;
```

### Recovery from Failed Migration

```sql
-- Check table state
SELECT table_name FROM information_schema.tables
WHERE table_name IN ('target_table', 'target_table_new');
```

- **Both tables exist:** Original safe → `DROP TABLE IF EXISTS target_table_new` and restart
- **Only new table exists:** Verify count, then complete rename
