diff --git a/db-customization/README.md b/db-customization/README.md index bb5f09e..a578f57 100644 --- a/db-customization/README.md +++ b/db-customization/README.md @@ -108,12 +108,16 @@ select pg_get_triggerdef(oid) from pg_trigger where tgname = 'update_room_nick_j NO → already on 13.0 format → continue to 4 4. update_group_owners legacy RULE still present (alongside the TRIGGER)? - YES → UPGRADED FROM 0.11.6, 13.0 scripts already applied - The dead rule is harmless (fires on key='_affiliations' which never - matches). Optionally drop it: - drop rule if exists update_group_owners on prosody; - Re-run prosody-13-rules-triggers.sql for drift correction if needed. - NO → continue to 5 + YES → UPGRADED FROM 0.11.6, 13.0 scripts already applied + The dead rule MUST be dropped: PostgreSQL rejects + `INSERT ... ON CONFLICT ... DO UPDATE` on any table that has a + conditional (WHERE) rule, even one whose WHERE never matches, so + the dead `update_group_owners` rule blocks every kv upsert on the + `prosody` table (error: "INSERT with ON CONFLICT clause cannot be + used with table that has INSERT or UPDATE rules"). Re-running + `prosody-13-rules-triggers.sql` drops it and converts the other + conditional DO rules on `prosody` to triggers. + NO → continue to 5 5. room_nicknames view exists? YES → INCOMPLETE MIGRATION (legacy objects not cleaned up) @@ -131,7 +135,7 @@ select pg_get_triggerdef(oid) from pg_trigger where tgname = 'update_room_nick_j | `room_membership` uses `_affiliations` | n/a | **yes** | no | no | | `room_membership` uses per-row `key like '%@%'` | n/a | no | **yes** | **yes** | | `room_nicknames` view exists | absent | **yes** | no | no | -| `update_group_owners` is a RULE | absent | **yes** | yes (dead) | absent | +| `update_group_owners` is a RULE | absent | **yes** | yes (must drop) | absent | | `update_group_owners` is a TRIGGER | absent | no | **yes** | **yes** | | `update_room_nick_jid` fires on `_data` | n/a | no | **yes** | **yes** | | `update_room_nick_jid` fires on `_affiliations` | n/a | **yes** | no | no | diff --git a/db-customization/prosody-13-new-deployment.sql b/db-customization/prosody-13-new-deployment.sql index 018ffc8..9c38605 100644 --- a/db-customization/prosody-13-new-deployment.sql +++ b/db-customization/prosody-13-new-deployment.sql @@ -653,44 +653,115 @@ create or replace RULE upsert_groupchat AS -- RULES — on `prosody` (kv store) -- ============================================================================= --- vcard_muc room avatar cache (unchanged — vcard_muc store still uses key='') -create or replace rule cache_group_avatarids as - on insert to prosody where NEW.store='vcard_muc' and NEW.key='' and NEW.type='json' and NEW.host like 'conference.%' - do insert into group_avatarids (room, avatarid) select - NEW.user||'@'||NEW.host as room, - encode(digest(decode(split_part(split_part(split_part(NEW.value, 'BINVAL', 2), '__array":["', 2),'"',1), 'base64'), 'sha1'), 'hex') as groupavarid; +-- --------------------------------------------------------------------------- +-- NOTE: the vcard_muc / vcard / muc_remote side-effects used to be conditional +-- `DO` rules on `prosody`. They have been converted to AFTER INSERT (/OR +-- UPDATE) triggers because PostgreSQL rejects `INSERT ... ON CONFLICT ... +-- DO UPDATE` on any table that carries a conditional (WHERE) DO/DO ALSO rule +-- or a non-NOTHING DO INSTEAD rule (error: "INSERT with ON CONFLICT clause +-- cannot be used with table that has INSERT or UPDATE rules"). Prosody 13's +-- mod_storage_sql uses ON CONFLICT upserts against the `prosody` kv table +-- whenever `prosody_unique_index` exists (created above), so the rules broke +-- every kv upsert. Triggers do not block ON CONFLICT. The derived-table +-- INSTEAD upsert rules (upsert_group_avatarids, upsert_profile_update_queue, +-- upsert_remote_muc_names, insert_room_nick_jid_map) are unaffected — they +-- live on the derived tables and keep rewriting the inserts emitted here. +-- --------------------------------------------------------------------------- --- vcard profile-update queue (unchanged) -create or replace rule update_profile_queue_from_insert as - on insert to prosody where (NEW.store='vcard' and NEW.type='json' and NEW.key='') - do insert into profile_update_queue (username, timestamp) select - NEW.user || '@' || NEW.host as username, - extract(epoch from now())::integer as timestamp; - -create or replace rule update_profile_queue_from_update as - on update to prosody where (NEW.store='vcard' and NEW.type='json' and NEW.key='') - do insert into profile_update_queue (username, timestamp) select - NEW.user || '@' || NEW.host as username, - extract(epoch from now())::integer as timestamp; - --- muc_remote nickname map (unchanged — VNCtalk store) -create or replace rule update_muc_remote_name as - on insert to prosody where (NEW.store='muc_remote' and not (NEW.value ilike '%/%')) - do insert into remote_muc_names (username, remotemuc, displayname) select NEW.user||'@'||NEW.host as username, NEW.key as remotemuc, NEW.value as displayname; - -create or replace rule update_room_nick_jid_map_remote as - on insert to prosody where NEW.store='muc_remote' and (NEW.key like '%/%') and (NEW.value like '%@%') - do insert into room_nick_jid_map (room_name, user_jid, nickname, since) +-- vcard_muc room avatar cache (vcard_muc store still uses key='') +create or replace function fcache_group_avatarids() + returns trigger as $BODY$ +begin + insert into group_avatarids (room, avatarid) select - (split_part(NEW.key,'/',1)) as room_name, - NEW.value as user_jid, - NEW.key as nickname, - extract(epoch from now())::integer; + NEW.user || '@' || NEW.host as room, + encode(digest(decode(split_part(split_part(split_part(NEW.value, 'BINVAL', 2), '__array":["', 2), '"', 1), 'base64'), 'sha1'), 'hex') as avatarid; + return NEW; +end; +$BODY$ language plpgsql volatile; --- NOTE: the legacy `update_group_owners` rule (key='_affiliations') is GONE. --- group_owners is now maintained by the `fupdate_group_owners` trigger fired --- on the `_data` row (see trigger section below), because a rule conflicted --- with the existing `upsert_group_owners` INSTEAD rule. +drop trigger if exists cache_group_avatarids on prosody; +create trigger cache_group_avatarids after insert on prosody + for each row + when (NEW.store = 'vcard_muc' and NEW.key = '' and NEW.type = 'json' and NEW.host like 'conference.%') + execute procedure fcache_group_avatarids(); + +-- vcard profile-update queue. Was two rules (on insert + on update); merged +-- into one AFTER INSERT OR UPDATE trigger so the UPDATE branch of an ON +-- CONFLICT upsert is also covered (an AFTER INSERT trigger alone would miss +-- the conflict-update path, since ON CONFLICT DO UPDATE fires AFTER UPDATE +-- triggers, not AFTER INSERT, when the conflict is taken). +create or replace function fupdate_profile_queue() + returns trigger as $BODY$ +begin + insert into profile_update_queue (username, timestamp) + select + NEW.user || '@' || NEW.host as username, + extract(epoch from now())::integer as timestamp; + return NEW; +end; +$BODY$ language plpgsql volatile; + +drop trigger if exists update_profile_queue on prosody; +create trigger update_profile_queue after insert or update on prosody + for each row + when (NEW.store = 'vcard' and NEW.type = 'json' and NEW.key = '') + execute procedure fupdate_profile_queue(); + +-- muc_remote nickname map (VNCtalk store) +create or replace function fupdate_muc_remote_name() + returns trigger as $BODY$ +begin + insert into remote_muc_names (username, remotemuc, displayname) + select + NEW.user || '@' || NEW.host as username, + NEW.key as remotemuc, + NEW.value as displayname; + return NEW; +end; +$BODY$ language plpgsql volatile; + +drop trigger if exists update_muc_remote_name on prosody; +create trigger update_muc_remote_name after insert on prosody + for each row + when (NEW.store = 'muc_remote' and not (NEW.value ilike '%/%')) + execute procedure fupdate_muc_remote_name(); + +create or replace function fupdate_room_nick_jid_map_remote() + returns trigger as $BODY$ +begin + insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + split_part(NEW.key, '/', 1) as room_name, + NEW.value as user_jid, + NEW.key as nickname, + extract(epoch from now())::integer as since; + return NEW; +end; +$BODY$ language plpgsql volatile; + +drop trigger if exists update_room_nick_jid_map_remote on prosody; +create trigger update_room_nick_jid_map_remote after insert on prosody + for each row + when (NEW.store = 'muc_remote' and NEW.key like '%/%' and NEW.value like '%@%') + execute procedure fupdate_room_nick_jid_map_remote(); + +-- Drop any prior rule counterparts these triggers replace (no-op on a truly +-- fresh DB; cleans up a DB that previously ran an older script version). +drop rule if exists cache_group_avatarids on prosody; +drop rule if exists update_profile_queue_from_insert on prosody; +drop rule if exists update_profile_queue_from_update on prosody; +drop rule if exists update_muc_remote_name on prosody; +drop rule if exists update_room_nick_jid_map_remote on prosody; +-- Legacy 0.11.6 rule (fires on key='_affiliations', dead under 13.0.6). +-- PostgreSQL checks rule *existence* at plan time, so even this dead +-- conditional rule blocks ON CONFLICT — dropping it is mandatory. +drop rule if exists update_group_owners on prosody; + +-- NOTE: group_owners is maintained by the `fupdate_group_owners` trigger fired +-- on the `_data` row (see trigger section below); the legacy +-- `update_group_owners` rule conflicted with the `upsert_group_owners` +-- INSTEAD rule and is dropped above. -- ============================================================================= -- VIEWS (created early — several prosodyarchive rules below depend on them) diff --git a/db-customization/prosody-13-rules-triggers.sql b/db-customization/prosody-13-rules-triggers.sql index 8fe0dab..a768487 100644 --- a/db-customization/prosody-13-rules-triggers.sql +++ b/db-customization/prosody-13-rules-triggers.sql @@ -260,6 +260,122 @@ create trigger update_room_nick_jid after insert on prosody -- already created by `prosody-trigger-noowner.sql`; no change is needed and -- it is NOT recreated here to avoid drifting from the original definition. +-- =========================================================================== +-- TRIGGERS: replace conditional DO rules on `prosody` (ON CONFLICT compat) +-- =========================================================================== +-- +-- PostgreSQL rejects `INSERT ... ON CONFLICT ... DO UPDATE` on any table that +-- has a conditional (WHERE) DO/DO ALSO rule or a non-NOTHING DO INSTEAD rule +-- (error: "INSERT with ON CONFLICT clause cannot be used with table that has +-- INSERT or UPDATE rules"). Prosody 13's mod_storage_sql uses ON CONFLICT +-- upserts against the `prosody` kv table whenever `prosody_unique_index` +-- exists (created by the migration/new-deployment scripts). The historical +-- DO rules on `prosody` therefore broke every kv upsert (vcard, vcard_muc, +-- muc_remote, config, fcmtoken, ...). +-- +-- Fix: convert each conditional DO rule on `prosody` to an AFTER INSERT +-- (/OR UPDATE) row-level trigger. Triggers do not block ON CONFLICT. The +-- derived-table INSTEAD upsert rules (upsert_group_avatarids, +-- upsert_profile_update_queue, upsert_remote_muc_names, +-- insert_room_nick_jid_map) are unchanged — they live on the derived tables, +-- not on `prosody`, and continue to rewrite the inserts these triggers emit. +-- +-- This block is idempotent (drop if exists + create or replace) and is safe +-- to run on any 13.0.6 database, whether the rules are still present or were +-- already converted. +-- +-- Also drop the legacy 0.11.6 `update_group_owners` rule (fires on +-- `key='_affiliations'`, dead under 13.0.6). PostgreSQL checks rule +-- *existence* at plan time, so even a dead conditional rule blocks ON +-- CONFLICT — dropping it is mandatory, not optional. + +-- vcard_muc room avatar cache (was rule `cache_group_avatarids`) +create or replace function fcache_group_avatarids() + returns trigger as $BODY$ +begin + insert into group_avatarids (room, avatarid) + select + NEW.user || '@' || NEW.host as room, + encode(digest(decode(split_part(split_part(split_part(NEW.value, 'BINVAL', 2), '__array":["', 2), '"', 1), 'base64'), 'sha1'), 'hex') as avatarid; + return NEW; +end; +$BODY$ language plpgsql volatile; + +drop trigger if exists cache_group_avatarids on prosody; +create trigger cache_group_avatarids after insert on prosody + for each row + when (NEW.store = 'vcard_muc' and NEW.key = '' and NEW.type = 'json' and NEW.host like 'conference.%') + execute procedure fcache_group_avatarids(); + +-- vcard profile-update queue. Was two rules (on insert + on update); merged +-- into one AFTER INSERT OR UPDATE trigger so the UPDATE branch of an ON +-- CONFLICT upsert is also covered (ON CONFLICT DO UPDATE fires AFTER UPDATE +-- triggers, not AFTER INSERT, when the conflict is taken). +create or replace function fupdate_profile_queue() + returns trigger as $BODY$ +begin + insert into profile_update_queue (username, timestamp) + select + NEW.user || '@' || NEW.host as username, + extract(epoch from now())::integer as timestamp; + return NEW; +end; +$BODY$ language plpgsql volatile; + +drop trigger if exists update_profile_queue on prosody; +create trigger update_profile_queue after insert or update on prosody + for each row + when (NEW.store = 'vcard' and NEW.type = 'json' and NEW.key = '') + execute procedure fupdate_profile_queue(); + +-- muc_remote nickname map (was rule `update_muc_remote_name`) +create or replace function fupdate_muc_remote_name() + returns trigger as $BODY$ +begin + insert into remote_muc_names (username, remotemuc, displayname) + select + NEW.user || '@' || NEW.host as username, + NEW.key as remotemuc, + NEW.value as displayname; + return NEW; +end; +$BODY$ language plpgsql volatile; + +drop trigger if exists update_muc_remote_name on prosody; +create trigger update_muc_remote_name after insert on prosody + for each row + when (NEW.store = 'muc_remote' and not (NEW.value ilike '%/%')) + execute procedure fupdate_muc_remote_name(); + +-- room_nick_jid_map from muc_remote (was rule `update_room_nick_jid_map_remote`) +create or replace function fupdate_room_nick_jid_map_remote() + returns trigger as $BODY$ +begin + insert into room_nick_jid_map (room_name, user_jid, nickname, since) + select + split_part(NEW.key, '/', 1) as room_name, + NEW.value as user_jid, + NEW.key as nickname, + extract(epoch from now())::integer as since; + return NEW; +end; +$BODY$ language plpgsql volatile; + +drop trigger if exists update_room_nick_jid_map_remote on prosody; +create trigger update_room_nick_jid_map_remote after insert on prosody + for each row + when (NEW.store = 'muc_remote' and NEW.key like '%/%' and NEW.value like '%@%') + execute procedure fupdate_room_nick_jid_map_remote(); + +-- Drop the rule counterparts these triggers replace. +drop rule if exists cache_group_avatarids on prosody; +drop rule if exists update_profile_queue_from_insert on prosody; +drop rule if exists update_profile_queue_from_update on prosody; +drop rule if exists update_muc_remote_name on prosody; +drop rule if exists update_room_nick_jid_map_remote on prosody; +-- Legacy 0.11.6 rule (dead under 13.0.6 but blocks ON CONFLICT by existence). +drop rule if exists update_group_owners on prosody; + -- =========================================================================== -- VERIFICATION (optional, commented out) -- ===========================================================================