# 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.