diff --git a/database/migrations/2026_07_18_060001_add_ring_id_unique_index_to_devices.php b/database/migrations/2026_07_18_060001_add_ring_id_unique_index_to_devices.php index 3c62149..ad1cd4c 100644 --- a/database/migrations/2026_07_18_060001_add_ring_id_unique_index_to_devices.php +++ b/database/migrations/2026_07_18_060001_add_ring_id_unique_index_to_devices.php @@ -7,6 +7,23 @@ return new class extends Migration { public function up(): void { + // Reconcile any duplicates the pre-index race may already have created, otherwise the + // CREATE UNIQUE INDEX below would abort on existing data. Keep the earliest row per + // ring_id; the rest cascade-delete their entities/state (both FKs cascadeOnDelete). + // Portable subquery form (works on Postgres in prod and SQLite in tests). + DB::statement(<<<'SQL' + DELETE FROM devices + WHERE config->>'ring_id' IS NOT NULL + AND id NOT IN ( + SELECT min_id FROM ( + SELECT MIN(id) AS min_id + FROM devices + WHERE config->>'ring_id' IS NOT NULL + GROUP BY config->>'ring_id' + ) keep + ) + SQL); + // Concurrent first-sight Ring messages (retained info + motion on bridge startup) must not // create two rows for one device. A partial unique index on the JSON ring_id lets the // second concurrent insert fail so the ingest job can re-fetch the winner (race-safe).