Harden migration: transaction, chunking, lossy-down doc, data-preservation test
- Wrap the data copy in DB::transaction (FULLTEXT ALTER stays outside) - Switch to chunkById(500) so the migration scales - Document down() as irreversible for non-default-locale translations - Add test_existing_documents_data_is_copied_to_translations to cover the data copy itself (the only previously-untested behavior) - Drop unused Migrator import in DocumentMigrationTest - Also restore title index in down() so up() can be re-run cleanly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+12
-3
@@ -37,9 +37,12 @@ public function up(): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Migrate existing data
|
// 3. Migrate existing data
|
||||||
|
// Note: step 4 (ALTER TABLE … DROP INDEX) must remain OUTSIDE this
|
||||||
|
// transaction because MySQL's ALTER TABLE causes an implicit commit.
|
||||||
$defaultLocale = config('app.locale', 'en');
|
$defaultLocale = config('app.locale', 'en');
|
||||||
$now = now();
|
$now = now();
|
||||||
$rows = DB::table('documents')->get();
|
DB::transaction(function () use ($defaultLocale, $now) {
|
||||||
|
DB::table('documents')->orderBy('id')->chunkById(500, function ($rows) use ($defaultLocale, $now) {
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
DB::table('document_translations')->insert([
|
DB::table('document_translations')->insert([
|
||||||
'document_id' => $row->id,
|
'document_id' => $row->id,
|
||||||
@@ -54,6 +57,8 @@ public function up(): void
|
|||||||
]);
|
]);
|
||||||
DB::table('documents')->where('id', $row->id)->update(['default_locale' => $defaultLocale]);
|
DB::table('documents')->where('id', $row->id)->update(['default_locale' => $defaultLocale]);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 4. Drop the old FULLTEXT index on documents (MySQL only)
|
// 4. Drop the old FULLTEXT index on documents (MySQL only)
|
||||||
if (DB::connection()->getDriverName() === 'mysql') {
|
if (DB::connection()->getDriverName() === 'mysql') {
|
||||||
@@ -70,9 +75,13 @@ public function up(): void
|
|||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
// Re-add columns
|
// IRREVERSIBLE for non-default-locale translations: only the row matching
|
||||||
|
// each document's default_locale is restored to the legacy columns; any
|
||||||
|
// other-locale translations are dropped along with document_translations.
|
||||||
|
|
||||||
|
// Re-add columns (with the title index that up() expects to drop)
|
||||||
Schema::table('documents', function (Blueprint $table) {
|
Schema::table('documents', function (Blueprint $table) {
|
||||||
$table->string('title')->nullable()->after('default_locale');
|
$table->string('title')->nullable()->index()->after('default_locale');
|
||||||
$table->text('content')->nullable()->after('title');
|
$table->text('content')->nullable()->after('title');
|
||||||
$table->text('rendered_html')->nullable()->after('content');
|
$table->text('rendered_html')->nullable()->after('content');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migrator;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@@ -72,4 +71,44 @@ public function test_document_translations_unique_document_locale(): void
|
|||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_existing_documents_data_is_copied_to_translations(): void
|
||||||
|
{
|
||||||
|
// Roll the new migration back so the legacy columns exist again
|
||||||
|
\Illuminate\Support\Facades\Artisan::call('migrate:rollback', ['--step' => 1]);
|
||||||
|
$this->assertTrue(\Illuminate\Support\Facades\Schema::hasColumn('documents', 'title'));
|
||||||
|
|
||||||
|
// Seed a legacy document row directly
|
||||||
|
\Illuminate\Support\Facades\DB::table('documents')->insert([
|
||||||
|
'path' => 'Legacy.md',
|
||||||
|
'title' => 'Legacy Title',
|
||||||
|
'slug' => 'legacy-title',
|
||||||
|
'content' => '# Legacy body',
|
||||||
|
'rendered_html' => '<h1>Legacy body</h1>',
|
||||||
|
'file_size' => 0,
|
||||||
|
'file_hash' => str_repeat('0', 64),
|
||||||
|
'file_modified_at' => now(),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
$docId = \Illuminate\Support\Facades\DB::table('documents')->where('slug', 'legacy-title')->value('id');
|
||||||
|
|
||||||
|
// Re-run the migration
|
||||||
|
\Illuminate\Support\Facades\Artisan::call('migrate');
|
||||||
|
|
||||||
|
// Verify the data was copied to document_translations
|
||||||
|
$translation = \Illuminate\Support\Facades\DB::table('document_translations')
|
||||||
|
->where('document_id', $docId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($translation, 'Translation row should have been created from legacy data');
|
||||||
|
$this->assertSame('Legacy Title', $translation->title);
|
||||||
|
$this->assertSame('# Legacy body', $translation->content);
|
||||||
|
$this->assertSame('<h1>Legacy body</h1>', $translation->rendered_html);
|
||||||
|
$this->assertSame(config('app.locale', 'en'), $translation->locale);
|
||||||
|
|
||||||
|
// Verify documents.default_locale was set
|
||||||
|
$defaultLocale = \Illuminate\Support\Facades\DB::table('documents')->where('id', $docId)->value('default_locale');
|
||||||
|
$this->assertSame(config('app.locale', 'en'), $defaultLocale);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user