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 3d323bf55f
commit fe686b7c8d
9 changed files with 134 additions and 39 deletions

View File

@@ -7,27 +7,24 @@ use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* RFC-WS-6 v1.1 §3 Q8 addendum per-schema default CrowdType for
* RFC-WS-6.md v1.1 §3 Q9 addendum per-schema default CrowdType for
* Person provisioning. Replaces the silent oldest() heuristic from
* WS-6 session 2 with explicit configuration; RequiresDefaultCrowdType
* publish guard ensures event_registration schemas declare it.
*
* No backfill: pre-launch the table is empty. Dev seeders populate
* the column when reseeding (FormBuilderDevSeeder).
*
* The FK constraint is added in a follow-up migration
* (2026_04_28_100000_restore_default_crowd_type_id_foreign_key) once
* the test database moved off SQLite to MySQL the original session
* 2.5 skipped it due to an SQLite-only "rebuild on FK add" cascade
* quirk that does not apply on MySQL.
*/
return new class extends Migration
{
public function up(): void
{
// Plain nullable ULID column. NO database-level foreign key —
// SQLite's rebuild-on-FK-add cascade-deletes form_fields rows
// (form_fields.form_schema_id has cascadeOnDelete on
// form_schemas), which corrupts running migration tests.
// Application-level integrity: FormSchema::defaultCrowdType()
// belongsTo loads from CrowdType; CrowdTypeObserver could add
// a soft-handle on delete if needed in production. The new
// RequiresDefaultCrowdType publish guard plus the runtime
// failsafe in PersonProvisioner are the load-bearing checks.
Schema::table('form_schemas', function (Blueprint $table): void {
$table->ulid('default_crowd_type_id')
->nullable()

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* RFC-WS-6.md v1.1 §3 Q9 addendum restore the FK on
* form_schemas.default_crowd_type_id that session 2.5 had to omit
* because of an SQLite "rebuild on FK add" cascade-delete quirk.
*
* Crewli has dropped SQLite entirely (CLAUDE.md: MySQL-only). On
* MySQL 8.0 the quirk does not apply, and database-level referential
* integrity is restored to match every other FK in this table.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('form_schemas', function (Blueprint $table): void {
$table->foreign('default_crowd_type_id')
->references('id')
->on('crowd_types')
->nullOnDelete();
});
}
public function down(): void
{
Schema::table('form_schemas', function (Blueprint $table): void {
$table->dropForeign(['default_crowd_type_id']);
});
}
};