- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
421 lines
11 KiB
Markdown
421 lines
11 KiB
Markdown
# 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.
|