owpengram-server/deploy/migrations/20260901000024_broadcast_lease_delivery_and_entities.up.sql
2026-09-01 12:50:18 +03:00

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';