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. Resolve it in shadow mode, which runs the query for real, or move the result-dependent work into a job.
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();
}