131 lines
6.5 KiB
SQL
131 lines
6.5 KiB
SQL
-- Upgrades our existing broadcasts/broadcast_recipients tables (created by
|
|
-- 20260714003131_system_broadcasts.up.sql, already applied in production) to
|
|
-- upstream gramsrv's richer design: formatted entities, incremental
|
|
-- materialization of "all"-target campaigns instead of one giant upfront
|
|
-- INSERT, and a lease-based delivery worker safe against duplicate sends if
|
|
-- the worker restarts mid-cycle.
|
|
--
|
|
-- This is an ALTER-based migration on purpose: it must not DROP/CREATE these
|
|
-- tables, because production already has rows in them (including 'sent'
|
|
-- rows from the old code path that never recorded a private_message_id/
|
|
-- message_box_id/pts, since that tracking didn't exist yet). The CHECK
|
|
-- constraints below explicitly carve out that legacy shape as valid
|
|
-- alongside the new fully-tracked shape, so this migration applies cleanly
|
|
-- against live data without any hand-editing.
|
|
|
|
-- broadcasts: rename total_count -> target_count (upstream's name for the
|
|
-- same "how many recipients this campaign targets" figure) and widen it to
|
|
-- bigint to match. A rename is metadata-only, so existing values survive
|
|
-- untouched.
|
|
ALTER TABLE public.broadcasts
|
|
RENAME COLUMN total_count TO target_count;
|
|
ALTER TABLE public.broadcasts
|
|
ALTER COLUMN target_count TYPE bigint,
|
|
ALTER COLUMN target_count SET DEFAULT 0;
|
|
ALTER TABLE public.broadcasts
|
|
ADD CONSTRAINT broadcasts_target_count_check CHECK (target_count >= 0);
|
|
|
|
ALTER TABLE public.broadcasts
|
|
ADD COLUMN entities jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
ADD COLUMN snapshot_max_user_id bigint NOT NULL DEFAULT 0,
|
|
ADD COLUMN enumeration_cursor_user_id bigint NOT NULL DEFAULT 0,
|
|
ADD COLUMN enumeration_done boolean NOT NULL DEFAULT false,
|
|
ADD COLUMN materialized_count bigint NOT NULL DEFAULT 0 CHECK (materialized_count >= 0),
|
|
ADD COLUMN sent_count bigint NOT NULL DEFAULT 0 CHECK (sent_count >= 0),
|
|
ADD COLUMN failed_count bigint NOT NULL DEFAULT 0 CHECK (failed_count >= 0);
|
|
|
|
ALTER TABLE public.broadcasts
|
|
ADD CONSTRAINT broadcasts_entities_array_check CHECK (jsonb_typeof(entities) = 'array');
|
|
|
|
-- Backfill: every pre-existing broadcast was created by the old code path,
|
|
-- which inserted every recipient row upfront in one transaction -- so from
|
|
-- the new model's point of view enumeration is already complete and fully
|
|
-- materialized for every one of them.
|
|
UPDATE public.broadcasts
|
|
SET enumeration_done = true,
|
|
enumeration_cursor_user_id = snapshot_max_user_id,
|
|
materialized_count = target_count;
|
|
|
|
-- Backfill sent_count/failed_count from the actual recipient rows rather
|
|
-- than trusting target_count, so old broadcasts read correctly under the
|
|
-- new derived-elsewhere-no-more columns instead of showing zeros.
|
|
UPDATE public.broadcasts b
|
|
SET sent_count = counts.sent_count,
|
|
failed_count = counts.failed_count
|
|
FROM (
|
|
SELECT broadcast_id,
|
|
count(*) FILTER (WHERE status = 'sent')::bigint AS sent_count,
|
|
count(*) FILTER (WHERE status = 'failed')::bigint AS failed_count
|
|
FROM public.broadcast_recipients
|
|
GROUP BY broadcast_id
|
|
) AS counts
|
|
WHERE b.id = counts.broadcast_id;
|
|
|
|
ALTER TABLE public.broadcasts
|
|
ADD CONSTRAINT broadcasts_enumeration_cursor_check
|
|
CHECK (enumeration_cursor_user_id >= 0 AND enumeration_cursor_user_id <= snapshot_max_user_id),
|
|
ADD CONSTRAINT broadcasts_sent_failed_within_materialized_check
|
|
CHECK (sent_count + failed_count <= materialized_count);
|
|
|
|
CREATE INDEX broadcasts_enumeration_idx ON public.broadcasts (id)
|
|
WHERE target_mode = 'all' AND NOT enumeration_done;
|
|
|
|
-- broadcast_recipients: add the lease-based delivery columns. Defaults give
|
|
-- every existing row sane values (next_attempt_at = now(), no active lease,
|
|
-- no tracked delivery identifiers) with no data loss.
|
|
ALTER TABLE public.broadcast_recipients
|
|
ADD COLUMN next_attempt_at timestamptz NOT NULL DEFAULT now(),
|
|
ADD COLUMN lease_token varchar(64) NOT NULL DEFAULT '',
|
|
ADD COLUMN lease_until timestamptz,
|
|
ADD COLUMN private_message_id bigint NOT NULL DEFAULT 0,
|
|
ADD COLUMN message_box_id integer NOT NULL DEFAULT 0,
|
|
ADD COLUMN pts integer NOT NULL DEFAULT 0,
|
|
ADD COLUMN updated_at timestamptz NOT NULL DEFAULT now();
|
|
|
|
-- The original table had no CHECK on status at all, so widening the allowed
|
|
-- set to include 'processing' needs no data fixup -- every existing row is
|
|
-- already 'pending', 'sent', or 'failed'.
|
|
ALTER TABLE public.broadcast_recipients
|
|
ADD CONSTRAINT broadcast_recipients_status_check
|
|
CHECK (status IN ('pending', 'processing', 'sent', 'failed')),
|
|
ADD CONSTRAINT broadcast_recipients_attempts_check CHECK (attempts >= 0);
|
|
|
|
-- The landmine: upstream's CHECK requires every 'sent' row to carry a
|
|
-- positive private_message_id/message_box_id/pts. Production already has
|
|
-- 'sent' rows from before that tracking existed, all with those columns at
|
|
-- their just-added default of 0. Rather than reject that data (or worse,
|
|
-- silently corrupt it with fabricated ids), this CHECK treats
|
|
-- "sent with all three still 0" as a legitimate legacy/untracked case,
|
|
-- alongside the real "sent with all three populated" case. New code always
|
|
-- populates them on a genuine send, so only pre-migration rows will ever
|
|
-- take the legacy branch.
|
|
ALTER TABLE public.broadcast_recipients
|
|
ADD CONSTRAINT broadcast_recipients_sent_tracking_check
|
|
CHECK (
|
|
(status = 'sent' AND sent_at IS NOT NULL AND (
|
|
(private_message_id > 0 AND message_box_id > 0 AND pts > 0)
|
|
OR
|
|
(private_message_id = 0 AND message_box_id = 0 AND pts = 0)
|
|
))
|
|
OR
|
|
(status <> 'sent' AND private_message_id = 0 AND message_box_id = 0 AND pts = 0 AND sent_at IS NULL)
|
|
);
|
|
|
|
-- No legacy carve-out needed here: lease_token/lease_until default to
|
|
-- ''/NULL, and no pre-existing row is 'processing' (that status didn't
|
|
-- exist before this migration), so every existing row already satisfies the
|
|
-- "not processing => no lease" branch.
|
|
ALTER TABLE public.broadcast_recipients
|
|
ADD CONSTRAINT broadcast_recipients_lease_check
|
|
CHECK (
|
|
(status = 'processing' AND lease_token <> '' AND lease_until IS NOT NULL)
|
|
OR
|
|
(status <> 'processing' AND lease_token = '' AND lease_until IS NULL)
|
|
);
|
|
|
|
CREATE INDEX broadcast_recipients_pending_next_attempt_idx
|
|
ON public.broadcast_recipients (next_attempt_at, id)
|
|
WHERE status = 'pending';
|
|
CREATE INDEX broadcast_recipients_processing_idx
|
|
ON public.broadcast_recipients (lease_until, id)
|
|
WHERE status = 'processing';
|