From a65bf3d595098fb469acf2524b24198a300eccec Mon Sep 17 00:00:00 2001 From: Thorsten Bus Date: Sun, 3 May 2026 22:16:46 +0200 Subject: [PATCH] feat(db): add macro_assignments, service macro override tables, and guarded legacy data wipe --- ..._100300_create_macro_assignments_table.php | 26 ++++++++++++++ ...0_create_service_macro_override_tables.php | 36 +++++++++++++++++++ ...026_05_03_100500_wipe_legacy_song_data.php | 36 +++++++++++++++++++ .../Migrations/MacroAssignmentsTableTest.php | 25 +++++++++++++ .../ServiceMacroOverrideTablesTest.php | 35 ++++++++++++++++++ .../Migrations/WipeLegacySongDataTest.php | 16 +++++++++ 6 files changed, 174 insertions(+) create mode 100644 database/migrations/2026_05_03_100300_create_macro_assignments_table.php create mode 100644 database/migrations/2026_05_03_100400_create_service_macro_override_tables.php create mode 100644 database/migrations/2026_05_03_100500_wipe_legacy_song_data.php create mode 100644 tests/Feature/Migrations/MacroAssignmentsTableTest.php create mode 100644 tests/Feature/Migrations/ServiceMacroOverrideTablesTest.php create mode 100644 tests/Feature/Migrations/WipeLegacySongDataTest.php diff --git a/database/migrations/2026_05_03_100300_create_macro_assignments_table.php b/database/migrations/2026_05_03_100300_create_macro_assignments_table.php new file mode 100644 index 0000000..0620914 --- /dev/null +++ b/database/migrations/2026_05_03_100300_create_macro_assignments_table.php @@ -0,0 +1,26 @@ +id(); + $table->enum('part_type', ['information', 'moderation', 'sermon', 'song', 'agenda_item']); + $table->foreignId('macro_id')->constrained('macros')->restrictOnDelete(); + $table->enum('position', ['all_slides', 'first_slide', 'last_slide', 'by_label']); + $table->foreignId('label_id')->nullable()->constrained('labels')->restrictOnDelete(); + $table->unsignedInteger('order')->default(0); + $table->timestamps(); + $table->index(['part_type', 'order']); + }); + } + + public function down(): void + { + Schema::dropIfExists('macro_assignments'); + } +}; diff --git a/database/migrations/2026_05_03_100400_create_service_macro_override_tables.php b/database/migrations/2026_05_03_100400_create_service_macro_override_tables.php new file mode 100644 index 0000000..09b092d --- /dev/null +++ b/database/migrations/2026_05_03_100400_create_service_macro_override_tables.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('service_id')->constrained('services')->cascadeOnDelete(); + $table->enum('part_type', ['information', 'moderation', 'sermon', 'song', 'agenda_item']); + $table->timestamps(); + $table->unique(['service_id', 'part_type']); + }); + + Schema::create('service_macro_assignments', function (Blueprint $table) { + $table->id(); + $table->foreignId('service_id')->constrained('services')->cascadeOnDelete(); + $table->enum('part_type', ['information', 'moderation', 'sermon', 'song', 'agenda_item']); + $table->foreignId('macro_id')->constrained('macros')->restrictOnDelete(); + $table->enum('position', ['all_slides', 'first_slide', 'last_slide', 'by_label']); + $table->foreignId('label_id')->nullable()->constrained('labels')->restrictOnDelete(); + $table->unsignedInteger('order')->default(0); + $table->timestamps(); + $table->index(['service_id', 'part_type', 'order']); + }); + } + + public function down(): void + { + Schema::dropIfExists('service_macro_assignments'); + Schema::dropIfExists('service_macro_overrides'); + } +}; diff --git a/database/migrations/2026_05_03_100500_wipe_legacy_song_data.php b/database/migrations/2026_05_03_100500_wipe_legacy_song_data.php new file mode 100644 index 0000000..4116583 --- /dev/null +++ b/database/migrations/2026_05_03_100500_wipe_legacy_song_data.php @@ -0,0 +1,36 @@ +count() === 0) { + return; + } + + DB::statement('PRAGMA foreign_keys = OFF'); + + try { + DB::table('song_arrangement_groups')->delete(); + DB::table('song_arrangements')->delete(); + DB::table('song_slides')->delete(); + DB::table('song_groups')->delete(); + DB::table('service_songs')->delete(); + DB::table('songs')->delete(); + } finally { + DB::statement('PRAGMA foreign_keys = ON'); + } + } + + public function down(): void + { + // Data is unrecoverable — intentional no-op + } +}; diff --git a/tests/Feature/Migrations/MacroAssignmentsTableTest.php b/tests/Feature/Migrations/MacroAssignmentsTableTest.php new file mode 100644 index 0000000..0be7b73 --- /dev/null +++ b/tests/Feature/Migrations/MacroAssignmentsTableTest.php @@ -0,0 +1,25 @@ +toBeTrue(); + expect(Schema::hasColumns('macro_assignments', ['id', 'part_type', 'macro_id', 'position', 'label_id', 'order', 'created_at', 'updated_at']))->toBeTrue(); +}); + +test('macro_assignments restrictOnDelete prevents deleting referenced macro', function () { + $macroId = DB::table('macros')->insertGetId(['uuid' => 'AABB-1111-2222-3333-FFFFFFFFFFFF', 'name' => 'Test', 'created_at' => now(), 'updated_at' => now()]); + $labelId = DB::table('labels')->insertGetId(['name' => 'Copyright', 'created_at' => now(), 'updated_at' => now()]); + DB::table('macro_assignments')->insert(['part_type' => 'song', 'macro_id' => $macroId, 'position' => 'by_label', 'label_id' => $labelId, 'order' => 0, 'created_at' => now(), 'updated_at' => now()]); + + expect(fn () => DB::table('macros')->where('id', $macroId)->delete()) + ->toThrow(\Exception::class); +}); + +test('macro_assignments allows nullable label_id', function () { + $macroId = DB::table('macros')->insertGetId(['uuid' => 'CCBB-1111-2222-3333-FFFFFFFFFFFF', 'name' => 'Test2', 'created_at' => now(), 'updated_at' => now()]); + DB::table('macro_assignments')->insert(['part_type' => 'sermon', 'macro_id' => $macroId, 'position' => 'all_slides', 'label_id' => null, 'order' => 0, 'created_at' => now(), 'updated_at' => now()]); + + expect(DB::table('macro_assignments')->where('macro_id', $macroId)->value('label_id'))->toBeNull(); +}); diff --git a/tests/Feature/Migrations/ServiceMacroOverrideTablesTest.php b/tests/Feature/Migrations/ServiceMacroOverrideTablesTest.php new file mode 100644 index 0000000..8890487 --- /dev/null +++ b/tests/Feature/Migrations/ServiceMacroOverrideTablesTest.php @@ -0,0 +1,35 @@ +toBeTrue(); + expect(Schema::hasColumns('service_macro_overrides', ['id', 'service_id', 'part_type', 'created_at', 'updated_at']))->toBeTrue(); +}); + +test('service_macro_assignments table has expected columns', function () { + expect(Schema::hasTable('service_macro_assignments'))->toBeTrue(); + expect(Schema::hasColumns('service_macro_assignments', ['id', 'service_id', 'part_type', 'macro_id', 'position', 'label_id', 'order', 'created_at', 'updated_at']))->toBeTrue(); +}); + +test('service_macro_overrides unique constraint on service_id and part_type', function () { + $service = Service::factory()->create(); + DB::table('service_macro_overrides')->insert(['service_id' => $service->id, 'part_type' => 'song', 'created_at' => now(), 'updated_at' => now()]); + + expect(fn () => DB::table('service_macro_overrides')->insert(['service_id' => $service->id, 'part_type' => 'song', 'created_at' => now(), 'updated_at' => now()])) + ->toThrow(\Exception::class); +}); + +test('service delete cascades to overrides and assignments', function () { + $service = Service::factory()->create(); + $macroId = DB::table('macros')->insertGetId(['uuid' => 'CCDD-1111-2222-3333-FFFFFFFFFFFF', 'name' => 'Test', 'created_at' => now(), 'updated_at' => now()]); + DB::table('service_macro_overrides')->insert(['service_id' => $service->id, 'part_type' => 'sermon', 'created_at' => now(), 'updated_at' => now()]); + DB::table('service_macro_assignments')->insert(['service_id' => $service->id, 'part_type' => 'sermon', 'macro_id' => $macroId, 'position' => 'all_slides', 'order' => 0, 'created_at' => now(), 'updated_at' => now()]); + + $service->delete(); + + expect(DB::table('service_macro_overrides')->where('service_id', $service->id)->count())->toBe(0); + expect(DB::table('service_macro_assignments')->where('service_id', $service->id)->count())->toBe(0); +}); diff --git a/tests/Feature/Migrations/WipeLegacySongDataTest.php b/tests/Feature/Migrations/WipeLegacySongDataTest.php new file mode 100644 index 0000000..2383693 --- /dev/null +++ b/tests/Feature/Migrations/WipeLegacySongDataTest.php @@ -0,0 +1,16 @@ +count())->toBe(0); + expect(Schema::hasTable('songs'))->toBeTrue(); + expect(Schema::hasTable('song_groups'))->toBeTrue(); + expect(DB::table('song_groups')->count())->toBe(0); +}); + +test('wipe migration guard: song_groups exists but is empty after fresh install', function () { + expect(Schema::hasTable('song_groups'))->toBeTrue(); + expect(DB::table('song_groups')->count())->toBe(0); +});