CAP.PRESCAN.RESULT_DEPENDENT — Result-dependent migration
- Category: safety
- Level: 0
- Stability: preview
- Suites: lint
This migration's SQL depends on what a query answers. Under pretend mode every query returns an empty result, so a chunk() loop runs zero times, an if ($query->exists()) takes the other branch, and a backfill emits nothing at all — the captured SQL would be a strict subset of the real SQL, and the run would look clean because nothing failed.
The false-positive boundary is drawn on purpose: a predicate like count() is only a finding when the migration branches on it (a logged-only count changes no SQL), and a loop over an array written out in the file is not flagged because it runs the same number of times with or without a database. Move the result-dependent work into a job. Shadow mode holds the migration back as well: its throwaway database has the schema and none of the data, so the query would answer for an empty table and the captured SQL would be the same fragment pretend captures.
Flagged
public function up(): void
{
// Under pretend this loop runs zero times, so the UPDATE below
// is never captured and the run reports no findings at all.
DB::table('users')->whereNull('slug')->chunkById(500, function ($users): void {
foreach ($users as $user) {
DB::table('users')->where('id', $user->id)->update(['slug' => Str::slug($user->name)]);
}
});
}
Preferred
public function up(): void
{
// The schema change is what the migration owns; the backfill is a
// job the deploy runs afterwards. The migration now captures whole.
Schema::table('users', function (Blueprint $table): void {
$table->string('slug')->nullable();
});
BackfillUserSlugs::dispatch();
}