feat: sync collectible star gifts

This commit is contained in:
A 2026-07-16 12:38:53 +08:00
parent 47fcf0ea41
commit 5ecf4e912d
64 changed files with 7559 additions and 403 deletions

View file

@ -0,0 +1,17 @@
DROP TRIGGER IF EXISTS star_gift_catalog_changed ON public.star_gift_catalog;
DROP FUNCTION IF EXISTS public.telesrv_notify_star_gift_catalog_changed();
ALTER TABLE public.peer_star_gifts
DROP CONSTRAINT IF EXISTS peer_star_gifts_catalog_revision_fk,
DROP COLUMN IF EXISTS catalog_revision_id;
DROP INDEX IF EXISTS public.peer_star_gifts_gift_idx;
ALTER TABLE public.star_gift_catalog
DROP CONSTRAINT IF EXISTS star_gift_catalog_active_revision_fk;
DROP TABLE IF EXISTS public.star_gift_catalog_revisions;
DROP TABLE IF EXISTS public.star_gift_catalog;
DROP SEQUENCE IF EXISTS public.star_gift_catalog_revision_id_seq;
DROP SEQUENCE IF EXISTS public.star_gift_catalog_gift_id_seq;
DELETE FROM public.read_model_versions
WHERE model = 'star_gift_catalog' AND owner_user_id = 0 AND peer_type = '' AND peer_id = 0;

View file

@ -0,0 +1,81 @@
-- Durable, administrator-managed regular Star Gift catalog. The previous seven-item
-- animated_emoji-derived directory was development-only and is intentionally not migrated.
-- Existing received rows refer to those non-durable definitions, so clear them instead of
-- manufacturing a read-time compatibility fallback that could no longer reconstruct assets.
CREATE SEQUENCE public.star_gift_catalog_gift_id_seq AS bigint START WITH 9000000000000001;
CREATE SEQUENCE public.star_gift_catalog_revision_id_seq AS bigint START WITH 1;
CREATE TABLE public.star_gift_catalog (
gift_id bigint DEFAULT nextval('public.star_gift_catalog_gift_id_seq') NOT NULL,
active_revision_id bigint NOT NULL,
enabled boolean DEFAULT true NOT NULL,
sort_order integer DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT star_gift_catalog_pkey PRIMARY KEY (gift_id)
);
CREATE TABLE public.star_gift_catalog_revisions (
id bigint DEFAULT nextval('public.star_gift_catalog_revision_id_seq') NOT NULL,
gift_id bigint NOT NULL,
revision integer NOT NULL,
title text DEFAULT '' NOT NULL,
stars bigint NOT NULL,
convert_stars bigint NOT NULL,
document_id bigint NOT NULL,
animation_json jsonb NOT NULL,
animation_sha256 bytea NOT NULL,
source_name text DEFAULT '' NOT NULL,
source_format text NOT NULL,
width integer NOT NULL,
height integer NOT NULL,
frame_rate double precision DEFAULT 0 NOT NULL,
in_point double precision DEFAULT 0 NOT NULL,
out_point double precision DEFAULT 0 NOT NULL,
created_by text DEFAULT '' NOT NULL,
command_id text DEFAULT '' NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT star_gift_catalog_revisions_pkey PRIMARY KEY (id),
CONSTRAINT star_gift_catalog_revisions_gift_revision_uniq UNIQUE (gift_id, revision),
CONSTRAINT star_gift_catalog_revisions_document_uniq UNIQUE (document_id),
CONSTRAINT star_gift_catalog_revision_price_check CHECK (stars > 0 AND convert_stars >= 0 AND convert_stars <= stars),
CONSTRAINT star_gift_catalog_revision_shape_check CHECK (width = 512 AND height = 512 AND jsonb_typeof(animation_json) = 'object'),
CONSTRAINT star_gift_catalog_revision_source_check CHECK (source_format IN ('tgs', 'lottie')),
CONSTRAINT star_gift_catalog_revision_gift_fk FOREIGN KEY (gift_id)
REFERENCES public.star_gift_catalog(gift_id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
CONSTRAINT star_gift_catalog_revision_document_fk FOREIGN KEY (document_id)
REFERENCES public.documents(id) ON DELETE RESTRICT
);
ALTER TABLE public.star_gift_catalog
ADD CONSTRAINT star_gift_catalog_active_revision_fk FOREIGN KEY (active_revision_id)
REFERENCES public.star_gift_catalog_revisions(id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX star_gift_catalog_enabled_order_idx
ON public.star_gift_catalog (sort_order, gift_id) WHERE enabled;
CREATE INDEX star_gift_catalog_revisions_gift_idx
ON public.star_gift_catalog_revisions (gift_id, revision DESC);
DELETE FROM public.peer_star_gifts;
ALTER TABLE public.peer_star_gifts
ADD COLUMN catalog_revision_id bigint NOT NULL,
ADD CONSTRAINT peer_star_gifts_catalog_revision_fk FOREIGN KEY (catalog_revision_id)
REFERENCES public.star_gift_catalog_revisions(id) ON DELETE RESTRICT;
CREATE INDEX peer_star_gifts_catalog_revision_idx
ON public.peer_star_gifts (catalog_revision_id);
CREATE INDEX peer_star_gifts_gift_idx
ON public.peer_star_gifts (gift_id);
CREATE FUNCTION public.telesrv_notify_star_gift_catalog_changed() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
PERFORM public.telesrv_bump_read_model_version('star_gift_catalog', 0, '', 0);
RETURN NULL;
END;
$$;
CREATE TRIGGER star_gift_catalog_changed
AFTER INSERT OR UPDATE OR DELETE ON public.star_gift_catalog
FOR EACH STATEMENT EXECUTE FUNCTION public.telesrv_notify_star_gift_catalog_changed();

View file

@ -0,0 +1,31 @@
DROP TRIGGER IF EXISTS star_gift_collectible_backdrop_guard ON public.star_gift_collectible_backdrops;
DROP TRIGGER IF EXISTS star_gift_collectible_pattern_guard ON public.star_gift_collectible_patterns;
DROP TRIGGER IF EXISTS star_gift_collectible_model_guard ON public.star_gift_collectible_models;
DROP TRIGGER IF EXISTS star_gift_collectible_revision_guard ON public.star_gift_collectible_revisions;
DROP FUNCTION IF EXISTS public.telesrv_guard_collectible_attribute();
DROP FUNCTION IF EXISTS public.telesrv_guard_collectible_revision();
DROP TABLE IF EXISTS public.star_gift_collection_items;
DROP TABLE IF EXISTS public.star_gift_collections;
DROP TABLE IF EXISTS public.star_gift_upgrade_commands;
DROP INDEX IF EXISTS public.peer_star_gifts_unique_gift_uniq;
ALTER TABLE public.peer_star_gifts
DROP CONSTRAINT IF EXISTS peer_star_gifts_pinned_order_check,
DROP CONSTRAINT IF EXISTS peer_star_gifts_terminal_state_check,
DROP CONSTRAINT IF EXISTS peer_star_gifts_unique_gift_fk,
DROP COLUMN IF EXISTS pinned_order,
DROP COLUMN IF EXISTS upgrade_msg_id,
DROP COLUMN IF EXISTS unique_gift_id;
DROP TABLE IF EXISTS public.unique_star_gifts;
ALTER TABLE public.star_gift_catalog
DROP CONSTRAINT IF EXISTS star_gift_catalog_collectible_revision_fk,
DROP COLUMN IF EXISTS collectible_revision_id;
DROP TABLE IF EXISTS public.star_gift_collectible_backdrops;
DROP TABLE IF EXISTS public.star_gift_collectible_patterns;
DROP TABLE IF EXISTS public.star_gift_collectible_models;
DROP TABLE IF EXISTS public.star_gift_collectible_revisions;
DROP SEQUENCE IF EXISTS public.unique_star_gift_id_seq;

View file

@ -0,0 +1,238 @@
-- Collectible Star Gifts: immutable published attribute pools, unique gift instances and
-- peer-owned gift collections. Marketplace/transfer/auction/craft/TON state is intentionally
-- absent from this schema.
CREATE SEQUENCE public.unique_star_gift_id_seq AS bigint START WITH 9200000000000001;
CREATE TABLE public.star_gift_collectible_revisions (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
gift_id bigint NOT NULL REFERENCES public.star_gift_catalog(gift_id) ON DELETE RESTRICT,
revision integer NOT NULL,
upgrade_stars bigint NOT NULL,
supply_total integer NOT NULL,
issued integer DEFAULT 0 NOT NULL,
slug_prefix text NOT NULL,
status text DEFAULT 'draft' NOT NULL,
created_by text DEFAULT '' NOT NULL,
command_id text DEFAULT '' NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
published_at timestamp with time zone,
CONSTRAINT star_gift_collectible_revision_uniq UNIQUE (gift_id, revision),
CONSTRAINT star_gift_collectible_command_uniq UNIQUE (gift_id, command_id),
CONSTRAINT star_gift_collectible_price_check CHECK (upgrade_stars > 0),
CONSTRAINT star_gift_collectible_supply_check CHECK (supply_total > 0 AND issued >= 0 AND issued <= supply_total),
CONSTRAINT star_gift_collectible_slug_check CHECK (slug_prefix ~ '^[a-z0-9][a-z0-9-]{0,47}$'),
CONSTRAINT star_gift_collectible_status_check CHECK (status IN ('draft', 'published')),
CONSTRAINT star_gift_collectible_publish_check CHECK (
(status = 'draft' AND published_at IS NULL) OR
(status = 'published' AND published_at IS NOT NULL)
)
);
CREATE TABLE public.star_gift_collectible_models (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
collectible_revision_id bigint NOT NULL REFERENCES public.star_gift_collectible_revisions(id) ON DELETE CASCADE,
name text NOT NULL,
document_id bigint NOT NULL REFERENCES public.documents(id) ON DELETE RESTRICT,
animation_json jsonb NOT NULL,
animation_sha256 bytea NOT NULL,
source_name text DEFAULT '' NOT NULL,
source_format text NOT NULL,
width integer NOT NULL,
height integer NOT NULL,
frame_rate double precision DEFAULT 0 NOT NULL,
in_point double precision DEFAULT 0 NOT NULL,
out_point double precision DEFAULT 0 NOT NULL,
rarity_permille integer NOT NULL,
sort_order integer DEFAULT 0 NOT NULL,
CONSTRAINT star_gift_collectible_model_name_uniq UNIQUE (collectible_revision_id, name),
CONSTRAINT star_gift_collectible_model_id_revision_uniq UNIQUE (id, collectible_revision_id),
CONSTRAINT star_gift_collectible_model_rarity_check CHECK (rarity_permille BETWEEN 1 AND 1000),
CONSTRAINT star_gift_collectible_model_shape_check CHECK (width = 512 AND height = 512 AND jsonb_typeof(animation_json) = 'object'),
CONSTRAINT star_gift_collectible_model_source_check CHECK (source_format IN ('tgs', 'lottie'))
);
CREATE TABLE public.star_gift_collectible_patterns (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
collectible_revision_id bigint NOT NULL REFERENCES public.star_gift_collectible_revisions(id) ON DELETE CASCADE,
name text NOT NULL,
document_id bigint NOT NULL REFERENCES public.documents(id) ON DELETE RESTRICT,
animation_json jsonb NOT NULL,
animation_sha256 bytea NOT NULL,
source_name text DEFAULT '' NOT NULL,
source_format text NOT NULL,
width integer NOT NULL,
height integer NOT NULL,
frame_rate double precision DEFAULT 0 NOT NULL,
in_point double precision DEFAULT 0 NOT NULL,
out_point double precision DEFAULT 0 NOT NULL,
rarity_permille integer NOT NULL,
sort_order integer DEFAULT 0 NOT NULL,
CONSTRAINT star_gift_collectible_pattern_name_uniq UNIQUE (collectible_revision_id, name),
CONSTRAINT star_gift_collectible_pattern_id_revision_uniq UNIQUE (id, collectible_revision_id),
CONSTRAINT star_gift_collectible_pattern_rarity_check CHECK (rarity_permille BETWEEN 1 AND 1000),
CONSTRAINT star_gift_collectible_pattern_shape_check CHECK (width = 512 AND height = 512 AND jsonb_typeof(animation_json) = 'object'),
CONSTRAINT star_gift_collectible_pattern_source_check CHECK (source_format IN ('tgs', 'lottie'))
);
CREATE TABLE public.star_gift_collectible_backdrops (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
collectible_revision_id bigint NOT NULL REFERENCES public.star_gift_collectible_revisions(id) ON DELETE CASCADE,
name text NOT NULL,
backdrop_id integer NOT NULL,
center_color integer NOT NULL,
edge_color integer NOT NULL,
pattern_color integer NOT NULL,
text_color integer NOT NULL,
rarity_permille integer NOT NULL,
sort_order integer DEFAULT 0 NOT NULL,
CONSTRAINT star_gift_collectible_backdrop_name_uniq UNIQUE (collectible_revision_id, name),
CONSTRAINT star_gift_collectible_backdrop_display_uniq UNIQUE (collectible_revision_id, backdrop_id),
CONSTRAINT star_gift_collectible_backdrop_id_revision_uniq UNIQUE (id, collectible_revision_id),
CONSTRAINT star_gift_collectible_backdrop_rarity_check CHECK (rarity_permille BETWEEN 1 AND 1000),
CONSTRAINT star_gift_collectible_backdrop_color_check CHECK (
center_color BETWEEN 0 AND 16777215 AND edge_color BETWEEN 0 AND 16777215 AND
pattern_color BETWEEN 0 AND 16777215 AND text_color BETWEEN 0 AND 16777215
)
);
ALTER TABLE public.star_gift_catalog
ADD COLUMN collectible_revision_id bigint,
ADD CONSTRAINT star_gift_catalog_collectible_revision_fk FOREIGN KEY (collectible_revision_id)
REFERENCES public.star_gift_collectible_revisions(id) ON DELETE RESTRICT;
CREATE TABLE public.unique_star_gifts (
id bigint DEFAULT nextval('public.unique_star_gift_id_seq') PRIMARY KEY,
gift_id bigint NOT NULL REFERENCES public.star_gift_catalog(gift_id) ON DELETE RESTRICT,
collectible_revision_id bigint NOT NULL REFERENCES public.star_gift_collectible_revisions(id) ON DELETE RESTRICT,
source_saved_gift_id bigint NOT NULL REFERENCES public.peer_star_gifts(id) ON DELETE RESTRICT,
title text DEFAULT '' NOT NULL,
slug text NOT NULL,
num integer NOT NULL,
owner_peer_type text NOT NULL,
owner_peer_id bigint NOT NULL,
model_attribute_id bigint NOT NULL,
pattern_attribute_id bigint NOT NULL,
backdrop_attribute_id bigint NOT NULL,
keep_original_details boolean DEFAULT false NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT unique_star_gift_slug_uniq UNIQUE (slug),
CONSTRAINT unique_star_gift_number_uniq UNIQUE (gift_id, num),
CONSTRAINT unique_star_gift_source_uniq UNIQUE (source_saved_gift_id),
CONSTRAINT unique_star_gift_owner_check CHECK (owner_peer_type IN ('user', 'channel') AND owner_peer_id > 0),
CONSTRAINT unique_star_gift_num_check CHECK (num > 0),
CONSTRAINT unique_star_gift_model_fk FOREIGN KEY (model_attribute_id, collectible_revision_id)
REFERENCES public.star_gift_collectible_models(id, collectible_revision_id) ON DELETE RESTRICT,
CONSTRAINT unique_star_gift_pattern_fk FOREIGN KEY (pattern_attribute_id, collectible_revision_id)
REFERENCES public.star_gift_collectible_patterns(id, collectible_revision_id) ON DELETE RESTRICT,
CONSTRAINT unique_star_gift_backdrop_fk FOREIGN KEY (backdrop_attribute_id, collectible_revision_id)
REFERENCES public.star_gift_collectible_backdrops(id, collectible_revision_id) ON DELETE RESTRICT
);
ALTER TABLE public.peer_star_gifts
ADD COLUMN unique_gift_id bigint,
ADD COLUMN upgrade_msg_id integer DEFAULT 0 NOT NULL,
ADD COLUMN pinned_order integer DEFAULT 0 NOT NULL,
ADD CONSTRAINT peer_star_gifts_unique_gift_fk FOREIGN KEY (unique_gift_id)
REFERENCES public.unique_star_gifts(id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED,
ADD CONSTRAINT peer_star_gifts_terminal_state_check CHECK (NOT converted OR unique_gift_id IS NULL),
ADD CONSTRAINT peer_star_gifts_pinned_order_check CHECK (pinned_order >= 0);
CREATE UNIQUE INDEX peer_star_gifts_unique_gift_uniq ON public.peer_star_gifts(unique_gift_id)
WHERE unique_gift_id IS NOT NULL;
CREATE INDEX unique_star_gifts_owner_idx ON public.unique_star_gifts(owner_peer_type, owner_peer_id, id DESC);
CREATE TABLE public.star_gift_upgrade_commands (
user_id bigint NOT NULL,
command_key text NOT NULL,
source_saved_gift_id bigint NOT NULL REFERENCES public.peer_star_gifts(id) ON DELETE RESTRICT,
form_id bigint DEFAULT 0 NOT NULL,
unique_gift_id bigint NOT NULL REFERENCES public.unique_star_gifts(id) ON DELETE RESTRICT,
balance_after bigint NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT star_gift_upgrade_commands_pkey PRIMARY KEY (user_id, command_key),
CONSTRAINT star_gift_upgrade_command_source_uniq UNIQUE (source_saved_gift_id)
);
CREATE TABLE public.star_gift_collections (
collection_id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
owner_peer_type text NOT NULL,
owner_peer_id bigint NOT NULL,
title text NOT NULL,
sort_order integer DEFAULT 0 NOT NULL,
hash bigint DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT star_gift_collection_owner_check CHECK (owner_peer_type IN ('user', 'channel') AND owner_peer_id > 0),
CONSTRAINT star_gift_collection_title_check CHECK (char_length(title) BETWEEN 1 AND 12),
CONSTRAINT star_gift_collection_owner_id_uniq UNIQUE (owner_peer_type, owner_peer_id, collection_id)
);
CREATE INDEX star_gift_collections_owner_order_idx
ON public.star_gift_collections(owner_peer_type, owner_peer_id, sort_order, collection_id);
CREATE TABLE public.star_gift_collection_items (
collection_id integer NOT NULL REFERENCES public.star_gift_collections(collection_id) ON DELETE CASCADE,
saved_gift_id bigint NOT NULL REFERENCES public.peer_star_gifts(id) ON DELETE CASCADE,
sort_order integer DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT star_gift_collection_items_pkey PRIMARY KEY (collection_id, saved_gift_id)
);
CREATE INDEX star_gift_collection_items_order_idx
ON public.star_gift_collection_items(collection_id, sort_order, saved_gift_id);
CREATE INDEX star_gift_collection_items_saved_idx
ON public.star_gift_collection_items(saved_gift_id, collection_id);
-- Published definitions are immutable. The only permitted update is incrementing issued while
-- all definition columns remain byte-for-byte equal.
CREATE FUNCTION public.telesrv_guard_collectible_revision() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF OLD.status = 'published' THEN
IF TG_OP = 'DELETE' THEN
RAISE EXCEPTION 'published collectible revision is immutable';
END IF;
IF NEW.gift_id <> OLD.gift_id OR NEW.revision <> OLD.revision OR
NEW.upgrade_stars <> OLD.upgrade_stars OR NEW.supply_total <> OLD.supply_total OR
NEW.slug_prefix <> OLD.slug_prefix OR NEW.status <> OLD.status OR
NEW.created_by <> OLD.created_by OR NEW.command_id <> OLD.command_id OR
NEW.created_at <> OLD.created_at OR NEW.published_at <> OLD.published_at THEN
RAISE EXCEPTION 'published collectible revision is immutable';
END IF;
END IF;
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER star_gift_collectible_revision_guard
BEFORE UPDATE OR DELETE ON public.star_gift_collectible_revisions
FOR EACH ROW EXECUTE FUNCTION public.telesrv_guard_collectible_revision();
CREATE FUNCTION public.telesrv_guard_collectible_attribute() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
revision_status text;
BEGIN
SELECT status INTO revision_status FROM public.star_gift_collectible_revisions
WHERE id = COALESCE(OLD.collectible_revision_id, NEW.collectible_revision_id);
IF revision_status = 'published' THEN
RAISE EXCEPTION 'published collectible attributes are immutable';
END IF;
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER star_gift_collectible_model_guard BEFORE UPDATE OR DELETE ON public.star_gift_collectible_models
FOR EACH ROW EXECUTE FUNCTION public.telesrv_guard_collectible_attribute();
CREATE TRIGGER star_gift_collectible_pattern_guard BEFORE UPDATE OR DELETE ON public.star_gift_collectible_patterns
FOR EACH ROW EXECUTE FUNCTION public.telesrv_guard_collectible_attribute();
CREATE TRIGGER star_gift_collectible_backdrop_guard BEFORE UPDATE OR DELETE ON public.star_gift_collectible_backdrops
FOR EACH ROW EXECUTE FUNCTION public.telesrv_guard_collectible_attribute();

View file

@ -0,0 +1,3 @@
ALTER TABLE public.peer_star_gifts
DROP CONSTRAINT IF EXISTS peer_star_gifts_prepaid_upgrade_check,
DROP COLUMN IF EXISTS prepaid_upgrade_stars;

View file

@ -0,0 +1,15 @@
ALTER TABLE public.peer_star_gifts
ADD COLUMN IF NOT EXISTS prepaid_upgrade_stars bigint DEFAULT 0 NOT NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'public.peer_star_gifts'::regclass
AND conname = 'peer_star_gifts_prepaid_upgrade_check'
) THEN
ALTER TABLE public.peer_star_gifts
ADD CONSTRAINT peer_star_gifts_prepaid_upgrade_check CHECK (prepaid_upgrade_stars >= 0);
END IF;
END;
$$;

View file

@ -0,0 +1,21 @@
CREATE OR REPLACE FUNCTION public.telesrv_guard_collectible_revision() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF OLD.status = 'published' THEN
IF TG_OP = 'DELETE' THEN
RAISE EXCEPTION 'published collectible revision is immutable';
END IF;
IF NEW.gift_id <> OLD.gift_id OR NEW.revision <> OLD.revision OR
NEW.upgrade_stars <> OLD.upgrade_stars OR NEW.supply_total <> OLD.supply_total OR
NEW.slug_prefix <> OLD.slug_prefix OR NEW.status <> OLD.status OR
NEW.created_by <> OLD.created_by OR NEW.command_id <> OLD.command_id OR
NEW.created_at <> OLD.created_at OR NEW.published_at <> OLD.published_at THEN
RAISE EXCEPTION 'published collectible revision is immutable';
END IF;
END IF;
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$;

View file

@ -0,0 +1,28 @@
-- Tighten the published collectible invariant. Issuance is the only mutable
-- field and every committed upgrade advances it exactly once.
CREATE OR REPLACE FUNCTION public.telesrv_guard_collectible_revision() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
IF OLD.status = 'published' THEN
RAISE EXCEPTION 'published collectible revision is immutable';
END IF;
RETURN OLD;
END IF;
IF OLD.status = 'published' THEN
IF NEW.gift_id <> OLD.gift_id OR NEW.revision <> OLD.revision OR
NEW.upgrade_stars <> OLD.upgrade_stars OR NEW.supply_total <> OLD.supply_total OR
NEW.slug_prefix <> OLD.slug_prefix OR NEW.status <> OLD.status OR
NEW.created_by <> OLD.created_by OR NEW.command_id <> OLD.command_id OR
NEW.created_at <> OLD.created_at OR NEW.published_at <> OLD.published_at THEN
RAISE EXCEPTION 'published collectible revision is immutable';
END IF;
IF NEW.issued <> OLD.issued + 1 THEN
RAISE EXCEPTION 'published collectible issuance must advance exactly once';
END IF;
END IF;
RETURN NEW;
END;
$$;