Files
buzz/scripts/reconcile-schema-after-pgschema.sql
Logan Johnson cd6b27483f Compute the member/bot label at read time and retire the roster trigger
`bot` has the same permissions as `member`; it only labels a key with a
recorded agent owner. Instead of storing that label and converting rows
when an owner is recorded, store `member` for both ordinary roles and
compute the label wherever a role is read: rosters, member lists, role
reads, DM participants and the bot list. An agent owner is recorded once
and never changes, so a key's label changes at most once.

- Writers store `member` for an ordinary role; reads show `bot` exactly
  when users.agent_owner_pubkey is set for the key in that community.
- The app-side roster fence treats `member` and `bot` as one role, so an
  owner recorded between roster capture and insert does not refuse the
  publication.
- After recording an owner, a background task lists the agent's active
  ordinary channels and republishes their rosters.
- BUZZ_RECONCILE_BOT_ROSTERS runs a one-shot startup repair of rosters
  that show a stale label (for example, ones older relays published).
  Nothing scans at every startup.
- Migration 0074 drops the 0032 roster trigger, which compared roster
  labels with the stored role exactly. Every relay must run #8611 first.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-10-26 16:40:10 -04:00

276 lines
12 KiB
SQL

-- Reconcile schema details that pgschema does not preserve.
--
-- pgschema reconciles DDL, but it does not execute seed DML or preserve every
-- table storage parameter from schema/schema.sql. It also currently emits
-- partition children as standalone CREATE TABLE statements. Every pgschema
-- apply caller must run this idempotent script so fresh bootstraps converge on
-- the same live database contract as migration-managed databases.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'events'::regclass
AND inhrelid = 'events_p_past'::regclass
) THEN
-- pgschema may copy parent triggers onto standalone children. Drop
-- those copies before ATTACH; PostgreSQL recreates inherited parent
-- triggers while attaching and rejects same-named child triggers.
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p_past;
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p_past;
ALTER TABLE events ATTACH PARTITION events_p_past
FOR VALUES FROM (MINVALUE) TO ('2026-01-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'events'::regclass
AND inhrelid = 'events_p2026_01'::regclass
) THEN
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_01;
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_01;
ALTER TABLE events ATTACH PARTITION events_p2026_01
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'events'::regclass
AND inhrelid = 'events_p2026_02'::regclass
) THEN
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_02;
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_02;
ALTER TABLE events ATTACH PARTITION events_p2026_02
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'events'::regclass
AND inhrelid = 'events_p2026_03'::regclass
) THEN
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_03;
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_03;
ALTER TABLE events ATTACH PARTITION events_p2026_03
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'events'::regclass
AND inhrelid = 'events_p2026_04'::regclass
) THEN
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_04;
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_04;
ALTER TABLE events ATTACH PARTITION events_p2026_04
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'events'::regclass
AND inhrelid = 'events_p2026_05'::regclass
) THEN
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_05;
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_05;
ALTER TABLE events ATTACH PARTITION events_p2026_05
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'events'::regclass
AND inhrelid = 'events_p2026_06'::regclass
) THEN
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p2026_06;
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p2026_06;
ALTER TABLE events ATTACH PARTITION events_p2026_06
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'events'::regclass
AND inhrelid = 'events_p_future'::regclass
) THEN
DROP TRIGGER IF EXISTS events_created_at_floor ON events_p_future;
DROP TRIGGER IF EXISTS community_write_fence_events ON events_p_future;
ALTER TABLE events ATTACH PARTITION events_p_future
FOR VALUES FROM ('2026-07-01') TO (MAXVALUE);
END IF;
-- When pgschema creates partition children as standalone tables, it also
-- preserves the parent's identity column on delivery_log children. PostgreSQL
-- rejects attaching a child table that has its own identity column, so each
-- delivery_log attach path drops that standalone identity first. Raw
-- schema-created partitions are already attached, so these branches do not
-- run against inherited partition columns.
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'delivery_log'::regclass
AND inhrelid = 'delivery_log_p_past'::regclass
) THEN
ALTER TABLE delivery_log_p_past ALTER COLUMN id DROP IDENTITY IF EXISTS;
DROP TRIGGER IF EXISTS community_write_fence_delivery_log ON delivery_log_p_past;
ALTER TABLE delivery_log ATTACH PARTITION delivery_log_p_past
FOR VALUES FROM (MINVALUE) TO ('2026-03-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'delivery_log'::regclass
AND inhrelid = 'delivery_log_p2026_03'::regclass
) THEN
ALTER TABLE delivery_log_p2026_03 ALTER COLUMN id DROP IDENTITY IF EXISTS;
DROP TRIGGER IF EXISTS community_write_fence_delivery_log ON delivery_log_p2026_03;
ALTER TABLE delivery_log ATTACH PARTITION delivery_log_p2026_03
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'delivery_log'::regclass
AND inhrelid = 'delivery_log_p2026_04'::regclass
) THEN
ALTER TABLE delivery_log_p2026_04 ALTER COLUMN id DROP IDENTITY IF EXISTS;
DROP TRIGGER IF EXISTS community_write_fence_delivery_log ON delivery_log_p2026_04;
ALTER TABLE delivery_log ATTACH PARTITION delivery_log_p2026_04
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'delivery_log'::regclass
AND inhrelid = 'delivery_log_p2026_05'::regclass
) THEN
ALTER TABLE delivery_log_p2026_05 ALTER COLUMN id DROP IDENTITY IF EXISTS;
DROP TRIGGER IF EXISTS community_write_fence_delivery_log ON delivery_log_p2026_05;
ALTER TABLE delivery_log ATTACH PARTITION delivery_log_p2026_05
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'delivery_log'::regclass
AND inhrelid = 'delivery_log_p2026_06'::regclass
) THEN
ALTER TABLE delivery_log_p2026_06 ALTER COLUMN id DROP IDENTITY IF EXISTS;
DROP TRIGGER IF EXISTS community_write_fence_delivery_log ON delivery_log_p2026_06;
ALTER TABLE delivery_log ATTACH PARTITION delivery_log_p2026_06
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhparent = 'delivery_log'::regclass
AND inhrelid = 'delivery_log_p_future'::regclass
) THEN
ALTER TABLE delivery_log_p_future ALTER COLUMN id DROP IDENTITY IF EXISTS;
DROP TRIGGER IF EXISTS community_write_fence_delivery_log ON delivery_log_p_future;
ALTER TABLE delivery_log ATTACH PARTITION delivery_log_p_future
FOR VALUES FROM ('2026-07-01') TO (MAXVALUE);
END IF;
END $$;
-- pgschema moves the partition key to the front of a partitioned table's
-- primary key when it creates the table, so events gets
-- PRIMARY KEY (created_at, community_id, id) instead of the declared
-- (community_id, created_at, id). Every later plan then drops and re-adds
-- events_pkey and each declared child's key, which fails once the children are
-- attached because their keys are inherited. Rebuild the key on the parent in
-- the declared order; PostgreSQL recreates each partition's key with it.
-- delivery_log already declares its partition key first. This block is a
-- verbatim copy of scripts/repair-events-pkey.sql; keep the two identical.
DO $$
DECLARE
declared CONSTANT text := 'PRIMARY KEY (community_id, created_at, id)';
BEGIN
-- Also runs before pgschema apply, when events may not exist yet.
IF to_regclass('events') IS NULL THEN
RETURN;
END IF;
IF (
SELECT pg_get_constraintdef(oid) FROM pg_constraint
WHERE conrelid = 'events'::regclass AND contype = 'p'
) IS DISTINCT FROM declared THEN
-- The rebuild locks events and every partition ACCESS EXCLUSIVE. Fail
-- fast instead of queueing behind a relay that still uses them.
BEGIN
PERFORM set_config('lock_timeout', '10s', true);
ALTER TABLE events DROP CONSTRAINT IF EXISTS events_pkey;
ALTER TABLE events
ADD CONSTRAINT events_pkey PRIMARY KEY (community_id, created_at, id);
EXCEPTION WHEN lock_not_available THEN
RAISE EXCEPTION 'timed out locking events to rebuild events_pkey'
USING HINT = 'Stop any relay using this database, then rerun.';
END;
END IF;
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE contype = 'p'
AND conrelid IN (
SELECT 'events'::regclass
UNION ALL
SELECT inhrelid FROM pg_inherits WHERE inhparent = 'events'::regclass
)
AND pg_get_constraintdef(oid) <> declared
) THEN
RAISE EXCEPTION 'events and its partitions must have %', declared;
END IF;
END $$;
-- pgschema drops multi-column CHECK constraints. Restore the direct-action
-- shape CHECK from schema/schema.sql (and migration 0055) verbatim.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'relay_admin_actions'::regclass
AND conname = 'relay_admin_actions_direct_shape'
) THEN
ALTER TABLE relay_admin_actions
ADD CONSTRAINT relay_admin_actions_direct_shape CHECK (
report_id IS NOT NULL
OR (action = 'ban' AND enforcement_target_pubkey IS NOT NULL
AND timeout_secs IS NULL AND timeout_until IS NULL)
OR (action = 'timeout' AND enforcement_target_pubkey IS NOT NULL
AND timeout_secs IS NOT NULL AND timeout_until IS NOT NULL)
OR (action = 'delete' AND enforcement_target_event_id IS NOT NULL
AND enforcement_target_pubkey IS NOT NULL
AND timeout_secs IS NULL AND timeout_until IS NULL)
);
END IF;
END $$;
-- pgschema reconciles DDL but does not apply seed DML or table storage
-- parameters from schema/schema.sql. Restore those parts of the desired-state
-- contract explicitly and fail the bootstrap if the live catalog disagrees.
ALTER TABLE replica_heartbeat SET (vacuum_truncate = false);
INSERT INTO replica_heartbeat (id) VALUES (1)
ON CONFLICT (id) DO NOTHING;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_class AS relation
JOIN pg_namespace AS namespace ON namespace.oid = relation.relnamespace
WHERE namespace.nspname = current_schema()
AND relation.relname = 'replica_heartbeat'
AND COALESCE(
relation.reloptions @> ARRAY['vacuum_truncate=false']::text[],
false
)
) THEN
RAISE EXCEPTION 'replica_heartbeat must disable vacuum truncation after pgschema apply';
END IF;
IF (SELECT count(*) FROM replica_heartbeat WHERE id = 1) <> 1 THEN
RAISE EXCEPTION 'replica_heartbeat must contain its singleton row after pgschema apply';
END IF;
END $$;