fix(form-builder): restore FK on form_schemas.default_crowd_type_id (WS-6)

The original session 2.5 migration had to omit this FK due to an
SQLite-only "rebuild on FK add" cascade-delete quirk. Now that the
test infrastructure has moved to MySQL (Task 1 of this session), the
quirk does not apply and the FK is restored to match every other FK
in this table.

Changes:
- New migration `2026_04_28_100000_restore_default_crowd_type_id_foreign_key`
  adds a FOREIGN KEY (default_crowd_type_id) REFERENCES crowd_types(id)
  ON DELETE SET NULL. Deleting a CrowdType nulls the column on dependent
  schemas instead of cascading the schema delete.
- Original migration's comment block rewritten — the SQLite-quirk
  rationale was demonstrably misleading; replaced with a forward-looking
  pointer to the FK-restore migration.
- PersonProvisioner::resolveCrowdTypeId() docblock updated: the runtime
  failsafe is now defense in depth alongside the DB-level FK + publish
  guard, not the sole load-bearing check.

New test (`DefaultCrowdTypeForeignKeyTest`) exercises both the
ON-DELETE-SET-NULL cascade and the existence of the FK in
information_schema.REFERENTIAL_CONSTRAINTS — the second assertion would
have been impossible on SQLite, which is exactly the point.

Migration step counts in 5 backfill tests bumped +1 because the FK-
restore migration sits at the top of the migration stack:
  - FormFieldBindingMigrationTest:           17→18, 15→16
  - ConditionalLogicBackfillTest:             6→7
  - FormFieldConfigBackfillAndDropTest:      12→13
  - FormFieldOptionsBackfillTest:             2→3
  - FormFieldValidationRuleBackfillTest:     15→16

All 1388 tests pass on MySQL (1386 prior + 2 new FK tests). Larastan
baseline unchanged.

Refs: RFC-WS-6.md v1.1 §3 Q9 addendum, WS-6 session 2.5 deviation #1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 12:56:52 +02:00
parent 4cbb7f9497
commit ae06b16d9b
9 changed files with 134 additions and 39 deletions

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\FormBuilder\Schema;
use App\Models\CrowdType;
use App\Models\FormBuilder\FormSchema;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
/**
* RFC-WS-6.md v1.1 §3 Q9 addendum the FK on
* form_schemas.default_crowd_type_id is restored once the test
* infrastructure moved off SQLite (session 2.6). Asserts both the
* DB-level constraint exists AND the nullOnDelete cascade behaviour
* holds, so a future migration that quietly drops the FK is caught
* here, not in production.
*/
final class DefaultCrowdTypeForeignKeyTest extends TestCase
{
use RefreshDatabase;
public function test_deleting_crowd_type_nulls_default_on_form_schemas(): void
{
$crowdType = CrowdType::factory()->create();
$schema = FormSchema::factory()->create([
'organisation_id' => $crowdType->organisation_id,
'default_crowd_type_id' => $crowdType->id,
]);
$this->assertSame($crowdType->id, $schema->fresh()->default_crowd_type_id);
// Force-delete bypasses the SoftDeletes trait — only the
// DB-level FK can null the column on form_schemas.
$crowdType->forceDelete();
$this->assertNull($schema->fresh()->default_crowd_type_id);
}
public function test_database_level_foreign_key_constraint_exists(): void
{
$row = DB::selectOne(
'SELECT rc.CONSTRAINT_NAME, kcu.REFERENCED_TABLE_NAME, '
.'kcu.REFERENCED_COLUMN_NAME, rc.DELETE_RULE '
.'FROM information_schema.REFERENTIAL_CONSTRAINTS rc '
.'JOIN information_schema.KEY_COLUMN_USAGE kcu '
.' ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME '
.' AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA '
.'WHERE rc.CONSTRAINT_SCHEMA = DATABASE() '
." AND kcu.TABLE_NAME = 'form_schemas' "
." AND kcu.COLUMN_NAME = 'default_crowd_type_id'"
);
$this->assertNotNull($row, 'Expected FK on form_schemas.default_crowd_type_id');
$this->assertSame('crowd_types', $row->REFERENCED_TABLE_NAME);
$this->assertSame('id', $row->REFERENCED_COLUMN_NAME);
$this->assertSame('SET NULL', $row->DELETE_RULE);
}
}