# Safe vs Unsafe Migration Examples

This document provides examples of migrations that are safely reversible vs those that are not.

## ✅ Safe Rollback Examples (implement SafelyReversible)

### 1. Create New Table

```php
use App\Framework\Database\Migration\{Migration, SafelyReversible};

/**
 * Safe: New empty table can be dropped without data loss
 */
final readonly class CreateSessionsTable implements Migration, SafelyReversible
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->create('sessions', function ($table) {
            $table->string('id', 255)->primary();
            $table->text('data');
            $table->timestamp('created_at');
        });
        $schema->execute();
    }

    public function down(ConnectionInterface $connection): void
    {
        // Safe: Table is new, no existing data
        $schema = new Schema($connection);
        $schema->dropIfExists('sessions');
        $schema->execute();
    }
}
```

### 2. Add Nullable Column

```php
/**
 * Safe: Adding nullable column can be dropped without data loss
 */
final readonly class AddPhoneToUsersTable implements Migration, SafelyReversible
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->string('phone', 20)->nullable();
        });
        $schema->execute();
    }

    public function down(ConnectionInterface $connection): void
    {
        // Safe: Column is nullable and likely empty or not critical
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->dropColumn('phone');
        });
        $schema->execute();
    }
}
```

### 3. Create Index

```php
/**
 * Safe: Indexes don't affect data, only query performance
 */
final readonly class AddEmailIndexToUsers implements Migration, SafelyReversible
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->index('email', 'idx_users_email');
        });
        $schema->execute();
    }

    public function down(ConnectionInterface $connection): void
    {
        // Safe: Dropping index doesn't lose data
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->dropIndex('idx_users_email');
        });
        $schema->execute();
    }
}
```

### 4. Rename Column (Data Preserved)

```php
/**
 * Safe: Renaming preserves all data
 */
final readonly class RenameEmailColumnInUsers implements Migration, SafelyReversible
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->renameColumn('email', 'email_address');
        });
        $schema->execute();
    }

    public function down(ConnectionInterface $connection): void
    {
        // Safe: Rename back, all data intact
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->renameColumn('email_address', 'email');
        });
        $schema->execute();
    }
}
```

### 5. Add Foreign Key

```php
/**
 * Safe: Foreign keys are constraints, can be removed without data loss
 */
final readonly class AddUserIdForeignKey implements Migration, SafelyReversible
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->table('orders', function ($table) {
            $table->foreign('user_id')
                ->references('id')
                ->on('users')
                ->onDelete(ForeignKeyAction::CASCADE);
        });
        $schema->execute();
    }

    public function down(ConnectionInterface $connection): void
    {
        // Safe: Removing constraint doesn't delete data
        $schema = new Schema($connection);
        $schema->table('orders', function ($table) {
            $table->dropForeign('user_id');
        });
        $schema->execute();
    }
}
```

---

## ❌ Unsafe Rollback Examples (DO NOT implement SafelyReversible)

### 1. Drop Column with Data

```php
/**
 * UNSAFE: Dropping column with data - NOT reversible
 * DO NOT implement SafelyReversible!
 */
final readonly class RemoveLegacyIdColumn implements Migration
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->dropColumn('legacy_id');  // Data is LOST!
        });
        $schema->execute();
    }

    // NO down() method - data cannot be recovered
}
```

**Why Unsafe?** Once the column is dropped, all data in that column is permanently lost. You cannot restore it via rollback.

**Fix-Forward Instead:**
```php
// If you need to restore it, create a new migration:
final readonly class RestoreLegacyIdColumn implements Migration, SafelyReversible
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->integer('legacy_id')->nullable();
        });
        $schema->execute();

        // Note: Original data is gone, you'll need to repopulate
    }
}
```

### 2. Data Transformation

```php
/**
 * UNSAFE: Data format transformation - original format lost
 * DO NOT implement SafelyReversible!
 */
final readonly class MigratePreferencesToJsonb implements Migration
{
    public function up(ConnectionInterface $connection): void
    {
        // Transform multiple columns into single JSONB column
        $connection->execute("
            UPDATE users
            SET preferences = jsonb_build_object(
                'theme', theme_preference,
                'notifications', notification_preference,
                'language', language_preference
            )
        ");

        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->dropColumn('theme_preference', 'notification_preference', 'language_preference');
        });
        $schema->execute();
    }

    // NO down() - original column structure is gone
}
```

**Why Unsafe?** Once you've merged columns into JSONB and dropped the originals, you cannot perfectly restore the original schema structure.

### 3. Data Aggregation/Merge

```php
/**
 * UNSAFE: Merging tables loses original structure
 * DO NOT implement SafelyReversible!
 */
final readonly class MergeUserProfilesIntoUsers implements Migration
{
    public function up(ConnectionInterface $connection): void
    {
        // Merge user_profiles data into users table
        $connection->execute("
            UPDATE users u
            SET bio = up.bio,
                avatar = up.avatar,
                settings = up.settings
            FROM user_profiles up
            WHERE u.id = up.user_id
        ");

        $schema = new Schema($connection);
        $schema->dropIfExists('user_profiles');
        $schema->execute();
    }

    // NO down() - user_profiles table structure is lost
}
```

**Why Unsafe?** The original `user_profiles` table schema and any additional data it contained is lost forever.

### 4. Change Column Type (Data Loss Risk)

```php
/**
 * UNSAFE: Changing column type may lose precision/data
 * DO NOT implement SafelyReversible!
 */
final readonly class ChangeUserAgeToInteger implements Migration
{
    public function up(ConnectionInterface $connection): void
    {
        $schema = new Schema($connection);
        $schema->table('users', function ($table) {
            $table->changeColumn('age', 'integer');  // Was VARCHAR, now INT
        });
        $schema->execute();
    }

    // NO down() - original string format may have been lost during conversion
}
```

**Why Unsafe?** Converting from string to integer may lose data (e.g., "25 years" becomes 25, losing " years"). Converting back doesn't restore original format.

### 5. Truncate/Delete Data

```php
/**
 * UNSAFE: Deleting data permanently
 * DO NOT implement SafelyReversible!
 */
final readonly class CleanupOldSessions implements Migration
{
    public function up(ConnectionInterface $connection): void
    {
        $connection->execute("
            DELETE FROM sessions
            WHERE created_at < NOW() - INTERVAL '90 days'
        ");
    }

    // NO down() - deleted data is gone forever
}
```

**Why Unsafe?** Deleted data cannot be recovered via rollback.

---

## Decision Matrix: SafelyReversible or Not?

| Operation | SafelyReversible? | Reason |
|-----------|-------------------|--------|
| Create table | ✅ Yes | New table can be dropped |
| Drop table (with data) | ❌ No | Data is lost |
| Add nullable column | ✅ Yes | Column likely empty |
| Drop column (with data) | ❌ No | Data is lost |
| Add NOT NULL column | ⚠️ Maybe | Only if default value is safe |
| Change column type | ❌ No | Data format may be lost |
| Rename column | ✅ Yes | Data is preserved |
| Create index | ✅ Yes | Only affects performance |
| Drop index | ✅ Yes | Only affects performance |
| Add foreign key | ✅ Yes | Only adds constraint |
| Drop foreign key | ✅ Yes | Only removes constraint |
| Data transformation | ❌ No | Original format lost |
| Merge tables | ❌ No | Original structure lost |
| Delete data | ❌ No | Data is gone |
| Aggregate data | ❌ No | Original granularity lost |

---

## Best Practices

### 1. Default to Forward-Only

```php
// Default: Only implement Migration interface
final readonly class MyMigration implements Migration
{
    public function up(ConnectionInterface $connection): void
    {
        // Migration logic
    }

    // No down() unless CERTAIN it's safe
}
```

### 2. Document Why SafelyReversible

```php
/**
 * This migration is safely reversible because:
 * - Only creates a new empty table
 * - No existing data is affected
 * - Can be dropped without data loss
 */
final readonly class CreateCacheTable implements Migration, SafelyReversible
{
    // ...
}
```

### 3. Use Fix-Forward for Errors

Instead of rollback:
```bash
# Instead of:
php console.php db:rollback 1

# Do:
php console.php make:migration FixUserTableColumnName
```

### 4. Test Rollback in Development

If implementing SafelyReversible:
```bash
# Test the full cycle in dev
php console.php db:migrate
php console.php db:rollback 1
php console.php db:migrate  # Should work again
```

### 5. Production: Forward-Only Mindset

In production, even "safe" rollbacks should be avoided:
- Create fix-forward migration instead
- Test thoroughly in staging first
- Never rollback without backup
- Document why rollback was needed

---

## Summary

**Implement SafelyReversible when:**
- ✅ Creating new tables
- ✅ Adding nullable columns
- ✅ Creating/dropping indexes
- ✅ Renaming columns (data preserved)
- ✅ Adding/removing constraints

**DO NOT implement SafelyReversible when:**
- ❌ Dropping columns with data
- ❌ Transforming data formats
- ❌ Changing column types
- ❌ Merging/splitting tables
- ❌ Deleting data
- ❌ Any operation that loses information

**When in doubt:** Don't implement SafelyReversible. Use fix-forward migrations instead.
