owpengram-server/deploy/migrations/0128_star_gift_craft_readiness.up.sql

498 lines
21 KiB
PL/PgSQL

-- Official Android clients use a positive can_craft_at both as the Craft
-- capability marker and as the readiness boundary. Earlier zero-delay
-- upgrades persisted 0 while retaining a positive craft chance, so TDesktop
-- could Craft the gift but Android hid the entry entirely.
--
-- Block concurrent lifecycle/message writers while aggregate facts, message
-- snapshots and durable edit edges are repaired in this migration transaction.
LOCK TABLE public.peer_star_gifts, public.unique_star_gifts,
public.message_boxes, public.private_messages IN SHARE ROW EXCLUSIVE MODE;
DO $$
BEGIN
-- Craft capability remains an intrinsic collectible fact while ownership
-- moves between users and channels. Terminal/external states cannot Craft.
UPDATE public.unique_star_gifts unique_gift
SET craft_chance_permille = 0,
updated_at = now()
FROM public.peer_star_gifts saved_gift
WHERE saved_gift.unique_gift_id = unique_gift.id
AND unique_gift.craft_chance_permille > 0
AND (
saved_gift.lifecycle_status <> 'active'
OR unique_gift.owner_address <> ''
OR unique_gift.burned
OR unique_gift.crafted
);
UPDATE public.peer_star_gifts saved_gift
SET can_craft_at = 0
FROM public.unique_star_gifts unique_gift
WHERE unique_gift.id = saved_gift.unique_gift_id
AND unique_gift.craft_chance_permille = 0
AND saved_gift.can_craft_at <> 0;
IF EXISTS (
SELECT 1
FROM public.unique_star_gifts unique_gift
JOIN public.peer_star_gifts saved_gift
ON saved_gift.unique_gift_id = unique_gift.id
WHERE unique_gift.craft_chance_permille > 0
AND (
saved_gift.owner_peer_type NOT IN ('user', 'channel')
OR saved_gift.lifecycle_status <> 'active'
OR unique_gift.owner_address <> ''
OR unique_gift.burned
OR unique_gift.crafted
OR NOT EXISTS (
SELECT 1
FROM public.star_gift_collectible_models model
WHERE model.collectible_revision_id = unique_gift.collectible_revision_id
AND model.crafted
)
)
) THEN
RAISE EXCEPTION 'positive star gift craft chance has no valid owned aggregate';
END IF;
-- created_at is the stable persisted proxy for the original upgrade
-- transaction date on legacy rows. New writes use the exact request date.
UPDATE public.peer_star_gifts saved_gift
SET can_craft_at = GREATEST(
1,
LEAST(2147483647, FLOOR(EXTRACT(EPOCH FROM unique_gift.created_at))::bigint)::integer
)
FROM public.unique_star_gifts unique_gift
WHERE unique_gift.id = saved_gift.unique_gift_id
AND unique_gift.craft_chance_permille > 0
AND saved_gift.can_craft_at = 0;
IF EXISTS (
SELECT 1
FROM public.peer_star_gifts saved_gift
JOIN public.unique_star_gifts unique_gift
ON unique_gift.id = saved_gift.unique_gift_id
WHERE (unique_gift.craft_chance_permille > 0)
IS DISTINCT FROM (saved_gift.can_craft_at > 0)
) THEN
RAISE EXCEPTION 'star gift craft chance/readiness repair did not converge';
END IF;
END
$$;
CREATE TEMP TABLE star_gift_craft_message_repairs (
owner_user_id bigint NOT NULL,
box_id integer NOT NULL,
unique_gift_id bigint NOT NULL,
desired_craft_chance integer NOT NULL,
desired_can_craft_at integer NOT NULL,
PRIMARY KEY (owner_user_id, box_id)
) ON COMMIT DROP;
-- Adding capability is owner-scoped: repair only the current owner's
-- authoritative unique action (upgrade_msg_id) and the other visible box of
-- that same logical private message. Never add Craft back to an old owner's
-- historical transfer/resale action.
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM public.peer_star_gifts saved_gift
JOIN public.unique_star_gifts unique_gift
ON unique_gift.id = saved_gift.unique_gift_id
WHERE saved_gift.owner_peer_type = 'user'
AND saved_gift.lifecycle_status = 'active'
AND saved_gift.can_craft_at > 0
AND NOT EXISTS (
SELECT 1
FROM public.message_boxes owner_box
WHERE owner_box.owner_user_id = saved_gift.owner_peer_id
AND owner_box.box_id = saved_gift.upgrade_msg_id
AND NOT owner_box.deleted
AND owner_box.media #>> '{service_action,kind}' = 'star_gift_unique'
AND owner_box.media #>> '{service_action,star_gift_unique,gift,ID}' = unique_gift.id::text
)
) THEN
RAISE EXCEPTION 'craftable star gift is missing its current owner action';
END IF;
IF EXISTS (
SELECT 1
FROM public.message_boxes box
WHERE box.media #>> '{service_action,kind}' = 'star_gift_unique'
AND box.media #> '{service_action,star_gift_unique,can_craft_at}' IS NOT NULL
AND (
jsonb_typeof(box.media #> '{service_action,star_gift_unique,can_craft_at}') <> 'number'
OR COALESCE(box.media #>> '{service_action,star_gift_unique,can_craft_at}', '') !~ '^[0-9]+$'
OR (box.media #>> '{service_action,star_gift_unique,can_craft_at}')::numeric > 2147483647
)
) THEN
RAISE EXCEPTION 'star gift message has malformed can_craft_at';
END IF;
IF EXISTS (
SELECT 1
FROM public.message_boxes box
WHERE box.media #>> '{service_action,kind}' = 'star_gift_unique'
AND box.media #> '{service_action,star_gift_unique,gift,CraftChancePermille}' IS NOT NULL
AND (
jsonb_typeof(box.media #> '{service_action,star_gift_unique,gift,CraftChancePermille}') <> 'number'
OR COALESCE(box.media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}', '') !~ '^[0-9]+$'
OR (box.media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}')::numeric > 1000
)
) THEN
RAISE EXCEPTION 'star gift message has malformed craft chance';
END IF;
END
$$;
INSERT INTO star_gift_craft_message_repairs(
owner_user_id, box_id, unique_gift_id,
desired_craft_chance, desired_can_craft_at
)
SELECT visible_box.owner_user_id,
visible_box.box_id,
unique_gift.id,
unique_gift.craft_chance_permille,
saved_gift.can_craft_at
FROM public.peer_star_gifts saved_gift
JOIN public.unique_star_gifts unique_gift
ON unique_gift.id = saved_gift.unique_gift_id
JOIN public.message_boxes owner_box
ON owner_box.owner_user_id = saved_gift.owner_peer_id
AND owner_box.box_id = saved_gift.upgrade_msg_id
AND NOT owner_box.deleted
AND owner_box.media #>> '{service_action,star_gift_unique,gift,ID}' = unique_gift.id::text
JOIN public.message_boxes visible_box
ON visible_box.message_sender_id = owner_box.message_sender_id
AND visible_box.private_message_id = owner_box.private_message_id
AND NOT visible_box.deleted
WHERE saved_gift.owner_peer_type = 'user'
AND saved_gift.lifecycle_status = 'active'
AND saved_gift.can_craft_at > 0
AND (
COALESCE(NULLIF(visible_box.media #>> '{service_action,star_gift_unique,can_craft_at}', '')::integer, 0)
IS DISTINCT FROM saved_gift.can_craft_at
OR COALESCE(NULLIF(visible_box.media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}', '')::integer, 0)
IS DISTINCT FROM unique_gift.craft_chance_permille
);
-- Only the current owner's authoritative logical message may expose Craft.
-- Terminal gifts, channel-owned gifts (until channel Craft is implemented),
-- and old-owner historical actions must have both wire markers removed.
INSERT INTO star_gift_craft_message_repairs(
owner_user_id, box_id, unique_gift_id,
desired_craft_chance, desired_can_craft_at
)
SELECT box.owner_user_id, box.box_id, unique_gift.id, 0, 0
FROM public.message_boxes box
JOIN public.unique_star_gifts unique_gift
ON (box.media #>> '{service_action,star_gift_unique,gift,ID}') ~ '^[0-9]+$'
AND unique_gift.id = (box.media #>> '{service_action,star_gift_unique,gift,ID}')::bigint
JOIN public.peer_star_gifts saved_gift
ON saved_gift.unique_gift_id = unique_gift.id
WHERE NOT box.deleted
AND box.media #>> '{service_action,kind}' = 'star_gift_unique'
AND (
box.media #> '{service_action,star_gift_unique,can_craft_at}' IS NOT NULL
OR box.media #> '{service_action,star_gift_unique,gift,CraftChancePermille}' IS NOT NULL
)
AND NOT EXISTS (
SELECT 1
FROM public.message_boxes authority
WHERE saved_gift.owner_peer_type = 'user'
AND saved_gift.lifecycle_status = 'active'
AND saved_gift.can_craft_at > 0
AND unique_gift.craft_chance_permille > 0
AND authority.owner_user_id = saved_gift.owner_peer_id
AND authority.box_id = saved_gift.upgrade_msg_id
AND NOT authority.deleted
AND authority.media #>> '{service_action,kind}' = 'star_gift_unique'
AND authority.media #>> '{service_action,star_gift_unique,gift,ID}' = unique_gift.id::text
AND authority.message_sender_id = box.message_sender_id
AND authority.private_message_id = box.private_message_id
)
ON CONFLICT (owner_user_id, box_id) DO UPDATE
SET unique_gift_id = EXCLUDED.unique_gift_id,
desired_craft_chance = 0,
desired_can_craft_at = 0;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM star_gift_craft_message_repairs target
JOIN public.message_boxes box
ON box.owner_user_id = target.owner_user_id
AND box.box_id = target.box_id
WHERE box.deleted
OR box.media #>> '{service_action,kind}' <> 'star_gift_unique'
OR box.media #>> '{service_action,star_gift_unique,gift,ID}' <> target.unique_gift_id::text
) THEN
RAISE EXCEPTION 'craft readiness repair target is not the expected unique gift action';
END IF;
IF EXISTS (
SELECT 1
FROM star_gift_craft_message_repairs target
JOIN public.message_boxes box
ON box.owner_user_id = target.owner_user_id
AND box.box_id = target.box_id
WHERE NOT EXISTS (
SELECT 1
FROM public.private_messages private_message
WHERE private_message.sender_user_id = box.message_sender_id
AND private_message.id = box.private_message_id
AND private_message.media #>> '{service_action,kind}' = 'star_gift_unique'
AND private_message.media #>> '{service_action,star_gift_unique,gift,ID}' = target.unique_gift_id::text
)
) THEN
RAISE EXCEPTION 'craft readiness repair target has no matching private message';
END IF;
IF EXISTS (
SELECT 1
FROM star_gift_craft_message_repairs target
JOIN public.message_boxes box
ON box.owner_user_id = target.owner_user_id
AND box.box_id = target.box_id
GROUP BY box.message_sender_id, box.private_message_id
HAVING COUNT(DISTINCT (
target.unique_gift_id,
target.desired_craft_chance,
target.desired_can_craft_at
)) <> 1
) THEN
RAISE EXCEPTION 'craft readiness repair has conflicting logical message targets';
END IF;
END
$$;
DO $$
DECLARE
repair record;
next_pts integer;
event_date integer := LEAST(2147483647, EXTRACT(EPOCH FROM clock_timestamp())::bigint)::integer;
repaired_media jsonb;
repaired_private_media jsonb;
affected_rows bigint;
BEGIN
FOR repair IN
SELECT target.owner_user_id,
target.box_id,
target.unique_gift_id,
target.desired_craft_chance,
target.desired_can_craft_at,
box.peer_type,
box.peer_id,
box.message_sender_id,
box.private_message_id,
box.media
FROM star_gift_craft_message_repairs target
JOIN public.message_boxes box
ON box.owner_user_id = target.owner_user_id
AND box.box_id = target.box_id
AND NOT box.deleted
ORDER BY target.owner_user_id, target.box_id
FOR UPDATE OF box
LOOP
IF repair.desired_craft_chance > 0 THEN
repaired_media := jsonb_set(
jsonb_set(
repair.media,
'{service_action,star_gift_unique,gift,CraftChancePermille}',
to_jsonb(repair.desired_craft_chance),
true
),
'{service_action,star_gift_unique,can_craft_at}',
to_jsonb(repair.desired_can_craft_at),
true
);
ELSE
repaired_media := repair.media
#- '{service_action,star_gift_unique,can_craft_at}'
#- '{service_action,star_gift_unique,gift,CraftChancePermille}';
END IF;
IF repaired_media #>> '{service_action,kind}' <> 'star_gift_unique'
OR repaired_media #>> '{service_action,star_gift_unique,gift,ID}' <> repair.unique_gift_id::text
OR (
repair.desired_craft_chance > 0
AND (
repaired_media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}'
IS DISTINCT FROM repair.desired_craft_chance::text
OR repaired_media #>> '{service_action,star_gift_unique,can_craft_at}'
IS DISTINCT FROM repair.desired_can_craft_at::text
)
)
OR (
repair.desired_craft_chance = 0
AND (
repaired_media #> '{service_action,star_gift_unique,gift,CraftChancePermille}' IS NOT NULL
OR repaired_media #> '{service_action,star_gift_unique,can_craft_at}' IS NOT NULL
)
) THEN
RAISE EXCEPTION 'craft readiness repair cannot project message box for user %, box %',
repair.owner_user_id, repair.box_id;
END IF;
INSERT INTO public.user_update_watermarks (user_id, contiguous_pts)
VALUES (repair.owner_user_id, 0)
ON CONFLICT (user_id) DO NOTHING;
UPDATE public.user_update_watermarks
SET contiguous_pts = contiguous_pts + 1,
updated_at = now()
WHERE user_id = repair.owner_user_id
RETURNING contiguous_pts INTO next_pts;
UPDATE public.message_boxes
SET media = repaired_media,
pts = next_pts
WHERE owner_user_id = repair.owner_user_id
AND box_id = repair.box_id
AND NOT deleted;
GET DIAGNOSTICS affected_rows = ROW_COUNT;
IF affected_rows <> 1 THEN
RAISE EXCEPTION 'craft readiness repair lost user %, box %', repair.owner_user_id, repair.box_id;
END IF;
SELECT media
INTO repaired_private_media
FROM public.private_messages
WHERE sender_user_id = repair.message_sender_id
AND id = repair.private_message_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'craft readiness repair missing private message for user %, box %',
repair.owner_user_id, repair.box_id;
END IF;
IF repaired_private_media #>> '{service_action,kind}' <> 'star_gift_unique'
OR repaired_private_media #>> '{service_action,star_gift_unique,gift,ID}' <> repair.unique_gift_id::text THEN
RAISE EXCEPTION 'craft readiness repair found mismatched private message for user %, box %',
repair.owner_user_id, repair.box_id;
END IF;
IF repair.desired_craft_chance > 0 THEN
repaired_private_media := jsonb_set(
jsonb_set(
repaired_private_media,
'{service_action,star_gift_unique,gift,CraftChancePermille}',
to_jsonb(repair.desired_craft_chance),
true
),
'{service_action,star_gift_unique,can_craft_at}',
to_jsonb(repair.desired_can_craft_at),
true
);
IF repaired_private_media #>> '{service_action,star_gift_unique,can_craft_at}'
IS DISTINCT FROM repair.desired_can_craft_at::text
OR repaired_private_media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}'
IS DISTINCT FROM repair.desired_craft_chance::text THEN
RAISE EXCEPTION 'craft readiness repair cannot project private message for user %, box %',
repair.owner_user_id, repair.box_id;
END IF;
ELSE
repaired_private_media := repaired_private_media
#- '{service_action,star_gift_unique,can_craft_at}'
#- '{service_action,star_gift_unique,gift,CraftChancePermille}';
IF repaired_private_media #> '{service_action,star_gift_unique,can_craft_at}' IS NOT NULL
OR repaired_private_media #> '{service_action,star_gift_unique,gift,CraftChancePermille}' IS NOT NULL THEN
RAISE EXCEPTION 'craft readiness repair cannot clear private message for user %, box %',
repair.owner_user_id, repair.box_id;
END IF;
END IF;
UPDATE public.private_messages
SET media = repaired_private_media
WHERE sender_user_id = repair.message_sender_id
AND id = repair.private_message_id;
GET DIAGNOSTICS affected_rows = ROW_COUNT;
IF affected_rows <> 1 THEN
RAISE EXCEPTION 'craft readiness repair lost private message for user %, box %',
repair.owner_user_id, repair.box_id;
END IF;
INSERT INTO public.user_update_events (
user_id, pts, pts_count, date, event_type,
message_box_id, peer_type, peer_id
) VALUES (
repair.owner_user_id, next_pts, 1, event_date, 'edit_message',
repair.box_id, repair.peer_type, repair.peer_id
);
INSERT INTO public.dispatch_outbox (
target_user_id, pts, event_type,
exclude_auth_key_id, exclude_session_id
) VALUES (
repair.owner_user_id, next_pts, 'edit_message', 0, 0
);
END LOOP;
END
$$;
-- Extend the existing deferred unique/saved aggregate guard. Upgrade, Craft
-- and export update the two tables in separate statements, so commit-time
-- validation observes the final atomic state without a read fallback.
CREATE OR REPLACE FUNCTION public.telesrv_check_unique_star_gift_owner() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE
unique_id bigint;
gift_owner_type text;
gift_owner_id bigint;
gift_owner_address text;
gift_burned boolean;
gift_crafted boolean;
gift_craft_chance integer;
gift_revision_id bigint;
saved_status text;
saved_owner_type text;
saved_owner_id bigint;
saved_can_craft_at integer;
BEGIN
IF TG_TABLE_NAME = 'unique_star_gifts' THEN
unique_id := COALESCE(NEW.id, OLD.id);
ELSE
unique_id := COALESCE(NEW.unique_gift_id, OLD.unique_gift_id);
END IF;
IF unique_id IS NULL THEN RETURN NULL; END IF;
SELECT owner_peer_type, owner_peer_id, owner_address, burned, crafted,
craft_chance_permille, collectible_revision_id
INTO gift_owner_type, gift_owner_id, gift_owner_address, gift_burned, gift_crafted,
gift_craft_chance, gift_revision_id
FROM public.unique_star_gifts WHERE id=unique_id;
IF NOT FOUND THEN RETURN NULL; END IF;
SELECT lifecycle_status, owner_peer_type, owner_peer_id, can_craft_at
INTO saved_status, saved_owner_type, saved_owner_id, saved_can_craft_at
FROM public.peer_star_gifts WHERE unique_gift_id=unique_id;
IF NOT FOUND THEN RAISE EXCEPTION 'unique star gift missing saved aggregate'; END IF;
IF gift_burned THEN
IF saved_status <> 'burned' THEN RAISE EXCEPTION 'burned unique star gift has live saved aggregate'; END IF;
ELSIF gift_owner_address <> '' THEN
IF saved_status <> 'exported' THEN RAISE EXCEPTION 'exported unique star gift has non-exported saved aggregate'; END IF;
ELSIF saved_status <> 'active' OR gift_owner_type IS DISTINCT FROM saved_owner_type OR gift_owner_id IS DISTINCT FROM saved_owner_id THEN
RAISE EXCEPTION 'unique star gift owner mismatch';
END IF;
IF gift_craft_chance > 0 THEN
IF saved_can_craft_at <= 0
OR saved_status <> 'active'
OR saved_owner_type NOT IN ('user', 'channel')
OR gift_owner_address <> ''
OR gift_burned
OR gift_crafted THEN
RAISE EXCEPTION 'unique star gift craft capability has invalid aggregate state';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.star_gift_collectible_models model
WHERE model.collectible_revision_id = gift_revision_id
AND model.crafted
) THEN
RAISE EXCEPTION 'unique star gift craft chance has no crafted model';
END IF;
ELSIF saved_can_craft_at <> 0 THEN
RAISE EXCEPTION 'unique star gift readiness exists without craft chance';
END IF;
RETURN NULL;
END;
$$;