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:
@@ -156,10 +156,11 @@ final readonly class PersonProvisioner
|
||||
* declares its target CrowdType explicitly via `default_crowd_type_id`.
|
||||
*
|
||||
* RFC-WS-6 v1.1 §3 Q9 addendum (was: silent oldest() fallback in
|
||||
* session 2). The RequiresDefaultCrowdType publish guard prevents
|
||||
* misconfigured event_registration schemas from publishing; this
|
||||
* runtime throw is a failsafe for live-table edits between publish
|
||||
* and apply.
|
||||
* session 2). Defense in depth: the FK on form_schemas (added in
|
||||
* session 2.6 after the SQLite → MySQL test switch) gives DB-level
|
||||
* referential integrity; the RequiresDefaultCrowdType publish guard
|
||||
* blocks publish when null; this runtime throw is the failsafe for
|
||||
* live-table edits between publish and apply.
|
||||
*
|
||||
* @throws PersonProvisioningException
|
||||
*/
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -56,7 +56,7 @@ final class FormFieldBindingMigrationTest extends TestCase
|
||||
// validation-rules-backfill, create-validation-rules) +
|
||||
// 2 WS-6 migrations (action-failures, apply-status) +
|
||||
// 2 WS-5a migrations (drop-binding-cols, create-bindings) = 16.
|
||||
$this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 18])->assertSuccessful();
|
||||
$this->assertFalse(Schema::hasTable('form_field_bindings'));
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'binding'));
|
||||
$this->assertTrue(Schema::hasColumn('form_field_library', 'default_binding'));
|
||||
@@ -118,7 +118,7 @@ final class FormFieldBindingMigrationTest extends TestCase
|
||||
public function test_rollback_reconstructs_json_and_drops_table(): void
|
||||
{
|
||||
// Walk back the full WS-5d + WS-5c + WS-6 + WS-5b + WS-5a stack (16 migrations).
|
||||
$this->artisan('migrate:rollback', ['--step' => 17])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 18])->assertSuccessful();
|
||||
[$fieldAId] = $this->seedFieldsWithBindingJson();
|
||||
[$libAId] = $this->seedLibraryWithBindingJson();
|
||||
|
||||
@@ -133,7 +133,7 @@ final class FormFieldBindingMigrationTest extends TestCase
|
||||
// the pre-WS-5b state (conditional-logic, validation-rules, configs
|
||||
// and options tables gone, validation_rules + options JSON columns
|
||||
// reappear on source tables; binding contract intact).
|
||||
$this->artisan('migrate:rollback', ['--step' => 15])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 16])->assertSuccessful();
|
||||
$this->assertFalse(Schema::hasTable('form_field_options'));
|
||||
$this->assertFalse(Schema::hasTable('form_field_conditional_logic_groups'));
|
||||
$this->assertFalse(Schema::hasTable('form_field_conditional_logic_conditions'));
|
||||
|
||||
@@ -48,7 +48,7 @@ final class ConditionalLogicBackfillTest extends TestCase
|
||||
// create-options + WS-5c drop-cl-col + WS-5c backfill-cl
|
||||
// migrations to land in the conditional-logic JSON-era state with
|
||||
// no relational form_field_options table yet.
|
||||
$this->artisan('migrate:rollback', ['--step' => 6])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful();
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'conditional_logic'));
|
||||
|
||||
$fieldId = $this->seedFieldWithJson([
|
||||
@@ -169,7 +169,7 @@ final class ConditionalLogicBackfillTest extends TestCase
|
||||
]);
|
||||
|
||||
// Roll back only the backfill migration — writes the JSON back.
|
||||
$this->artisan('migrate:rollback', ['--step' => 6])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful();
|
||||
|
||||
$reconstructed = DB::table('form_fields')
|
||||
->where('id', $fieldId)
|
||||
@@ -198,7 +198,7 @@ final class ConditionalLogicBackfillTest extends TestCase
|
||||
|
||||
public function test_unknown_top_level_key_fails_migration(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 6])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithJson([
|
||||
'hide_when' => ['all' => [['field_slug' => 'x', 'operator' => 'equals', 'value' => 1]]],
|
||||
@@ -211,7 +211,7 @@ final class ConditionalLogicBackfillTest extends TestCase
|
||||
|
||||
public function test_unknown_comparison_operator_fails_migration(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 6])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 7])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithJson([
|
||||
'show_when' => ['all' => [['field_slug' => 'x', 'operator' => 'matches_regex', 'value' => 'y']]],
|
||||
|
||||
@@ -30,7 +30,7 @@ final class FormFieldConfigBackfillAndDropTest extends TestCase
|
||||
// Roll back 4 WS-5c migrations + 2 WS-6 migrations + 5 WS-5b
|
||||
// migrations = 11, to get the pre-WS-5b state where the JSON column
|
||||
// still exists on form_fields / form_field_library.
|
||||
$this->artisan('migrate:rollback', ['--step' => 12])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 13])->assertSuccessful();
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'validation_rules'));
|
||||
|
||||
$fieldId = $this->seedField([
|
||||
|
||||
@@ -46,7 +46,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
// Roll back only the backfill migration (latest WS-5d step).
|
||||
// Leaves the form_field_options table in place, JSON columns
|
||||
// present on the source tables, and snapshots in pre-WS-5d shape.
|
||||
$this->artisan('migrate:rollback', ['--step' => 2])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->assertTrue(Schema::hasTable('form_field_options'));
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'options'));
|
||||
|
||||
@@ -130,7 +130,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_rollback_reconstructs_json_columns_and_snapshots(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 2])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
[$selectId, $multiId, $libraryId] = $this->seedFieldsAndLibraryWithJson();
|
||||
$submissionId = $this->seedSubmissionWithSnapshot($selectId);
|
||||
|
||||
@@ -143,7 +143,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
// Step back over only the backfill migration → JSON columns repopulate
|
||||
// and snapshots revert to flat-string-array shape.
|
||||
$this->artisan('migrate:rollback', ['--step' => 2])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->assertSame(0, DB::table('form_field_options')->count());
|
||||
|
||||
$select = DB::table('form_fields')->where('id', $selectId)->first();
|
||||
@@ -162,7 +162,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_when_options_present_on_non_option_field_type(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 2])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->seedFieldWithOptions('TAG_PICKER', ['Veiligheid', 'Horeca']);
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
@@ -172,7 +172,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_when_options_contains_non_string_entry(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 2])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithOptionsRaw('SELECT', json_encode([
|
||||
['label' => 'A'],
|
||||
@@ -186,7 +186,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_when_options_is_object_shape(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 2])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithOptionsRaw('SELECT', json_encode([
|
||||
'XS' => 'Extra small',
|
||||
@@ -200,7 +200,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_on_translations_length_mismatch(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 2])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->seedFieldWithOptionsRaw('SELECT', json_encode(['XS', 'S', 'M']), json_encode([
|
||||
'de' => ['options' => ['Klein', 'Mittel']], // 2 vs 3
|
||||
]));
|
||||
@@ -212,7 +212,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_on_non_string_translation(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 2])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->seedFieldWithOptionsRaw('SELECT', json_encode(['XS', 'S']), json_encode([
|
||||
'de' => ['options' => ['Klein', 42]],
|
||||
]));
|
||||
@@ -224,7 +224,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_on_oversized_translation(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 2])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->seedFieldWithOptionsRaw('SELECT', json_encode(['XS']), json_encode([
|
||||
'de' => ['options' => [str_repeat('x', 256)]],
|
||||
]));
|
||||
@@ -236,7 +236,7 @@ final class FormFieldOptionsBackfillTest extends TestCase
|
||||
|
||||
public function test_fails_when_snapshot_options_present_on_non_option_field_type(): void
|
||||
{
|
||||
$this->artisan('migrate:rollback', ['--step' => 2])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 3])->assertSuccessful();
|
||||
$this->seedTemplateWithSnapshotRaw([
|
||||
'fields' => [[
|
||||
'id' => (string) Str::ulid(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
|
||||
// validation-rules-backfill + create-validation-rules) = 14.
|
||||
// Brings us to the pre-WS-5b state: validation_rules JSON column
|
||||
// present, no relational tables for WS-5b/c/d.
|
||||
$this->artisan('migrate:rollback', ['--step' => 15])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 16])->assertSuccessful();
|
||||
$this->assertFalse(Schema::hasTable('form_field_validation_rules'));
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'validation_rules'));
|
||||
|
||||
@@ -114,7 +114,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
|
||||
// (validation_rules JSON column present; no relational tables for
|
||||
// WS-5b). Step count: drop-cols + configs-backfill + create-configs
|
||||
// + validation-rules-backfill + create-validation-rules = 5.
|
||||
$this->artisan('migrate:rollback', ['--step' => 15])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 16])->assertSuccessful();
|
||||
|
||||
$fieldId = $this->seedFieldWithJson([
|
||||
'field_type' => 'TAG_PICKER',
|
||||
@@ -138,7 +138,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
|
||||
// (validation_rules JSON column present; no relational tables for
|
||||
// WS-5b). Step count: drop-cols + configs-backfill + create-configs
|
||||
// + validation-rules-backfill + create-validation-rules = 5.
|
||||
$this->artisan('migrate:rollback', ['--step' => 15])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 16])->assertSuccessful();
|
||||
|
||||
$fieldId = $this->seedFieldWithJson([
|
||||
'field_type' => 'TEXT',
|
||||
@@ -165,7 +165,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
|
||||
// (validation_rules JSON column present; no relational tables for
|
||||
// WS-5b). Step count: drop-cols + configs-backfill + create-configs
|
||||
// + validation-rules-backfill + create-validation-rules = 5.
|
||||
$this->artisan('migrate:rollback', ['--step' => 15])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 16])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithJson([
|
||||
'field_type' => 'TEXT',
|
||||
@@ -182,7 +182,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
|
||||
// (validation_rules JSON column present; no relational tables for
|
||||
// WS-5b). Step count: drop-cols + configs-backfill + create-configs
|
||||
// + validation-rules-backfill + create-validation-rules = 5.
|
||||
$this->artisan('migrate:rollback', ['--step' => 15])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 16])->assertSuccessful();
|
||||
|
||||
$this->seedFieldWithJson([
|
||||
'field_type' => 'BOOLEAN',
|
||||
@@ -201,7 +201,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
|
||||
// full-back-then-full-forward cycle — rolling back all WS-5b
|
||||
// migrations restores the pre-WS-5b state (columns present on
|
||||
// source tables; validation rules relational table gone).
|
||||
$this->artisan('migrate:rollback', ['--step' => 15])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 16])->assertSuccessful();
|
||||
[$numberId] = $this->seedFields();
|
||||
|
||||
$this->artisan('migrate')->assertSuccessful();
|
||||
@@ -216,7 +216,7 @@ final class FormFieldValidationRuleBackfillTest extends TestCase
|
||||
|
||||
// Roll back WS-5b fully → column reappears and carries canonical JSON
|
||||
// reconstructed from the relational rows.
|
||||
$this->artisan('migrate:rollback', ['--step' => 15])->assertSuccessful();
|
||||
$this->artisan('migrate:rollback', ['--step' => 16])->assertSuccessful();
|
||||
$this->assertTrue(Schema::hasColumn('form_fields', 'validation_rules'));
|
||||
|
||||
$field = DB::table('form_fields')->where('id', $numberId)->first();
|
||||
|
||||
Reference in New Issue
Block a user