fix: convert conditional DO rules on prosody table to triggers
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, erroring with '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 these scripts), so the five inherited conditional DO rules on prosody (cache_group_avatarids, update_profile_queue_from_insert/_update, update_muc_remote_name, update_room_nick_jid_map_remote) broke every kv upsert (vcard, vcard_muc, muc_remote, config, fcmtoken, ...). Replace those rules with AFTER INSERT / AFTER INSERT OR UPDATE row-level triggers, which do not block ON CONFLICT. The two profile-queue rules merge into one AFTER INSERT OR UPDATE trigger so the UPDATE branch of an ON CONFLICT upsert is also covered (it fires AFTER UPDATE triggers, not AFTER INSERT, when the conflict is taken). Also drop the legacy 0.11.6 update_group_owners rule. It is logically dead under 13.0.6 (fires on key='_affiliations', which is never written) but PostgreSQL checks rule existence at plan time, so even a dead conditional rule blocks ON CONFLICT. Dropping it is mandatory, not optional as the README previously claimed. Conversion is added to both prosody-13-new-deployment.sql (fresh deployments, and the run_new_deployment branch of migrate.sh) and prosody-13-rules-triggers.sql (the run_rules_triggers branch for 0.11.6->13.0 upgrades), so every helm pre/post-upgrade hook path reaches the fix. Derived-table INSTEAD upsert rules are unchanged. Verified against postgres:16: both scripts apply cleanly, pg_rewrite for prosody returns 0 rows, ON CONFLICT upserts succeed, and triggers fire on both INSERT and conflict-UPDATE branches; reproduced the production error by re-adding the legacy rules, then confirmed the incremental script resolves it. Idempotent re-run safe. Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/10>
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
-- ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user