<overview>
Zero-downtime migration patterns for EF Core. Use when a migration contains destructive operations (column rename, type change, column drop) that could cause downtime during deployment.

These patterns apply to production deployments where the application runs continuously. For development environments, standard migrations are sufficient.

> **Exception to "no raw SQL" rule:** `migrationBuilder.Sql(...)` is authorized ONLY for data backfill operations within zero-downtime migration patterns (e.g., `UPDATE ... SET NewCol = OldCol`). Never use it for DDL (CREATE/ALTER/DROP) — those must go through EF Core Fluent API or `SqlObjects/`.
</overview>

<when_to_use>
## When These Patterns Apply

**Destructive operations requiring zero-downtime strategy:**
- `RenameColumn` / `RenameTable`
- `DropColumn` / `DropTable`
- `AlterColumn` (type change, nullability change)

**Safe operations (no special handling needed):**
- `AddColumn` (nullable or with default value)
- `CreateTable`
- `CreateIndex`
- `AddForeignKey`
</when_to_use>

<column_rename>
## Pattern 1: Column Rename (5 phases)

A column rename cannot be done atomically without downtime. Use a phased approach across multiple deployments.

### Phase 1 - Add New Column (Migration 1)
```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<string>(
        name: "FullName",
        table: "auth_users",
        schema: "core",
        type: "nvarchar(256)",
        nullable: true); // Must be nullable initially

    // Backfill existing data
    migrationBuilder.Sql(
        "UPDATE core.auth_users SET FullName = DisplayName WHERE FullName IS NULL");
}

protected override void Down(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropColumn(name: "FullName", table: "auth_users", schema: "core");
}
```

### Phase 2 - Dual-Write Code (Application deployment)
```csharp
// Entity writes to BOTH columns during transition
public void UpdateName(string name)
{
    DisplayName = name;  // Old column
    FullName = name;     // New column
}
```

### Phase 3 - Backfill Remaining Data (Migration 2, optional)
```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    // Catch any rows missed during dual-write transition
    migrationBuilder.Sql(
        "UPDATE core.auth_users SET FullName = DisplayName WHERE FullName IS NULL");

    // Make new column non-nullable
    migrationBuilder.AlterColumn<string>(
        name: "FullName",
        table: "auth_users",
        schema: "core",
        type: "nvarchar(256)",
        nullable: false,
        defaultValue: "");
}
```

### Phase 4 - Switch Reads (Application deployment)
```csharp
// Entity now reads from new column only
public string Name => FullName;
```

### Phase 5 - Drop Old Column (Migration 3)
```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropColumn(name: "DisplayName", table: "auth_users", schema: "core");
}

protected override void Down(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<string>(
        name: "DisplayName",
        table: "auth_users",
        schema: "core",
        type: "nvarchar(256)",
        nullable: false,
        defaultValue: "");

    migrationBuilder.Sql(
        "UPDATE core.auth_users SET DisplayName = FullName");
}
```
</column_rename>

<column_type_change>
## Pattern 2: Column Type Change (4 phases)

Changing a column type (e.g., `int` to `string`, `string` to `decimal`) follows the same dual-column approach.

### Phase 1 - Add New Typed Column
```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<decimal>(
        name: "PriceDecimal",
        table: "extensions_products",
        schema: "extensions",
        type: "decimal(18,2)",
        nullable: true);

    migrationBuilder.Sql(
        "UPDATE extensions.extensions_products SET PriceDecimal = CAST(Price AS decimal(18,2))");
}
```

### Phase 2 - Dual-Write in Code
### Phase 3 - Make Non-Nullable + Switch Reads
### Phase 4 - Drop Old Column

Same phased pattern as column rename.
</column_type_change>

<large_table_operations>
## Pattern 3: Large Table Backfill

For tables with millions of rows, batch the backfill to avoid lock escalation.

```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<string>(
        name: "NormalizedEmail",
        table: "auth_users",
        schema: "core",
        type: "nvarchar(256)",
        nullable: true);

    // Batched backfill to avoid table locks
    migrationBuilder.Sql(@"
        DECLARE @BatchSize INT = 5000;
        DECLARE @RowsAffected INT = 1;

        WHILE @RowsAffected > 0
        BEGIN
            UPDATE TOP (@BatchSize) core.auth_users
            SET NormalizedEmail = UPPER(Email)
            WHERE NormalizedEmail IS NULL;

            SET @RowsAffected = @@ROWCOUNT;
        END
    ");
}
```

**Rules:**
- Batch size: 1,000-10,000 rows depending on table width
- Always include a `WHERE` clause to avoid re-processing
- Monitor transaction log growth during execution
- Run during low-traffic windows if possible
</large_table_operations>

<rollback_strategy>
## Rollback Strategies

### Migration Down() Method
Every migration MUST have a functional `Down()` method for rollback:

```csharp
protected override void Down(MigrationBuilder migrationBuilder)
{
    // Reverse the operation
    migrationBuilder.DropColumn(name: "NewColumn", table: "auth_users", schema: "core");
}
```

### Rollback Command
```bash
# Rollback to specific migration
dotnet ef database update {PreviousMigrationName} \
  --context $DBCONTEXT \
  --project $INFRA_PROJECT \
  --startup-project $STARTUP_PROJECT
```

### Pre-Deployment Checklist
- [ ] `Down()` method tested locally
- [ ] Database backup taken before deployment
- [ ] Application compatible with both old and new schema (dual-write phase)
- [ ] Rollback procedure documented in deployment notes
- [ ] Monitoring alerts configured for error spike
</rollback_strategy>

<decision_guide>
## Decision Guide

| Operation | Risk | Strategy |
|-----------|------|----------|
| Add nullable column | Low | Standard migration |
| Add column with default | Low | Standard migration |
| Create table | Low | Standard migration |
| Create index | Medium | `CREATE INDEX CONCURRENTLY` if supported |
| Rename column | High | 5-phase pattern |
| Change column type | High | 4-phase pattern |
| Drop column | High | Verify no reads, then drop |
| Drop table | High | Verify no references, then drop |
| Make column non-nullable | Medium | Ensure no NULL values exist first |

**Rule of thumb:** If the operation would cause a `SqlException` on running code, use a phased approach.
</decision_guide>

<sources>
- EF Core Migrations documentation
- Zero-Downtime Deployments with EF Core (Microsoft patterns)
- Blue-Green Deployment pattern (Martin Fowler)
</sources>
