fix: deduplicate prosody table before creating unique index

The pre-upgrade hook failed with "could not create unique index
prosody_unique_index" because the prosody table contained duplicate
rows (same host, user, store, key). Without the unique index,
mod_storage_sql falls back to SELECT-then-INSERT instead of ON
CONFLICT upsert, which races under concurrent writes and inserts
duplicates — most commonly in the fcmtoken map store.

Add a DELETE that removes duplicate rows (keeping the last-written
row per group via ctid ordering) immediately before the CREATE UNIQUE
INDEX in both prosody-13-new-deployment.sql and
prosody-13-migration-once.sql. The DELETE is a no-op when no
duplicates exist, preserving idempotency.

Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/8>
This commit is contained in:
2026-07-16 06:10:01 +00:00
parent a4f17e2506
commit dde2e6a0e5
2 changed files with 44 additions and 0 deletions
@@ -44,8 +44,30 @@ end $$;
-- exist") and sets has_upsert_index=false — disabling ON CONFLICT upserts and
-- falling back to slower SELECT-then-INSERT/UPDATE. Create it here so the
-- index exists after migration and the warning is gone on next startup.
--
-- Without the unique index, mod_storage_sql falls back to SELECT-then-INSERT
-- instead of ON CONFLICT upsert, which can create duplicate rows under
-- concurrent writes (common in the fcmtoken map store). Deduplicate before
-- creating the index so it does not fail on pre-existing duplicates.
-- Idempotent: safe to re-run.
-- ---------------------------------------------------------------------------
-- Remove duplicate rows, keeping only the last-written row per
-- (host, user, store, key). ctid is the physical row identifier; the
-- highest ctid is the most recently inserted row. No-op if no duplicates.
delete from prosody
where ctid in (
select ctid from (
select ctid,
row_number() over (
partition by host, "user", store, key
order by ctid desc
) as rn
from prosody
) t
where rn > 1
);
create unique index if not exists prosody_unique_index
on prosody ("host", "user", "store", "key");
@@ -50,7 +50,29 @@ create extension if not exists pgcrypto;
-- creates this itself via create_table(); re-creating it here with IF NOT
-- EXISTS is a harmless no-op in that case and fixes deployments where the
-- table was created by an older Prosody version without the unique index.
--
-- Without the unique index, mod_storage_sql falls back to SELECT-then-INSERT
-- instead of ON CONFLICT upsert, which can create duplicate rows under
-- concurrent writes (common in the fcmtoken map store). Deduplicate before
-- creating the index so it does not fail on pre-existing duplicates.
-- ---------------------------------------------------------------------------
-- Remove duplicate rows, keeping only the last-written row per
-- (host, user, store, key). ctid is the physical row identifier; the
-- highest ctid is the most recently inserted row. No-op if no duplicates.
delete from prosody
where ctid in (
select ctid from (
select ctid,
row_number() over (
partition by host, "user", store, key
order by ctid desc
) as rn
from prosody
) t
where rn > 1
);
create unique index if not exists prosody_unique_index
on prosody ("host", "user", "store", "key");