merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
|
|
@ -2648,7 +2648,9 @@ CREATE TABLE public.secret_chats (
|
|||
history_deleted boolean DEFAULT false NOT NULL,
|
||||
random_id integer NOT NULL,
|
||||
date integer NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT secret_chats_nonzero_id CHECK ((chat_id <> 0)),
|
||||
CONSTRAINT secret_chats_random_id_is_chat_id CHECK ((chat_id = random_id))
|
||||
);
|
||||
|
||||
|
||||
|
|
@ -4524,13 +4526,6 @@ CREATE INDEX upload_parts_object_key_idx ON public.upload_parts USING btree (obj
|
|||
CREATE UNIQUE INDEX uq_emq_dedup ON public.encrypted_message_queue USING btree (receiver_auth_key_id, chat_id, random_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: uq_secret_chats_admin_random; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX uq_secret_chats_admin_random ON public.secret_chats USING btree (admin_auth_key_id, random_id) WHERE (state <> 'discarded'::text);
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_channel_member_index_admined_public_idx; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE IF EXISTS broadcast_recipients;
|
||||
DROP TABLE IF EXISTS broadcasts;
|
||||
54
deploy/migrations/20260901000001_system_broadcasts.up.sql
Normal file
54
deploy/migrations/20260901000001_system_broadcasts.up.sql
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
CREATE TABLE broadcasts (
|
||||
id bigserial PRIMARY KEY,
|
||||
message text NOT NULL CHECK (message <> '' AND octet_length(message) <= 4096),
|
||||
target_mode varchar(16) NOT NULL CHECK (target_mode IN ('all', 'selected')),
|
||||
snapshot_max_user_id bigint NOT NULL DEFAULT 0,
|
||||
enumeration_cursor_user_id bigint NOT NULL DEFAULT 0,
|
||||
enumeration_done boolean NOT NULL DEFAULT false,
|
||||
target_count bigint NOT NULL DEFAULT 0 CHECK (target_count >= 0),
|
||||
materialized_count bigint NOT NULL DEFAULT 0 CHECK (materialized_count >= 0),
|
||||
sent_count bigint NOT NULL DEFAULT 0 CHECK (sent_count >= 0),
|
||||
failed_count bigint NOT NULL DEFAULT 0 CHECK (failed_count >= 0),
|
||||
created_by varchar(128) NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CHECK (enumeration_cursor_user_id >= 0 AND enumeration_cursor_user_id <= snapshot_max_user_id),
|
||||
CHECK (sent_count + failed_count <= materialized_count)
|
||||
);
|
||||
|
||||
CREATE TABLE broadcast_recipients (
|
||||
id bigserial PRIMARY KEY,
|
||||
broadcast_id bigint NOT NULL REFERENCES broadcasts(id) ON DELETE CASCADE,
|
||||
user_id bigint NOT NULL,
|
||||
status varchar(16) NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'processing', 'sent', 'failed')),
|
||||
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
||||
next_attempt_at timestamptz NOT NULL DEFAULT now(),
|
||||
lease_token varchar(64) NOT NULL DEFAULT '',
|
||||
lease_until timestamptz,
|
||||
last_error varchar(500) NOT NULL DEFAULT '',
|
||||
private_message_id bigint NOT NULL DEFAULT 0,
|
||||
message_box_id integer NOT NULL DEFAULT 0,
|
||||
pts integer NOT NULL DEFAULT 0,
|
||||
sent_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (broadcast_id, user_id),
|
||||
CHECK (
|
||||
(status = 'sent' AND private_message_id > 0 AND message_box_id > 0 AND pts > 0 AND sent_at IS NOT NULL)
|
||||
OR
|
||||
(status <> 'sent' AND private_message_id = 0 AND message_box_id = 0 AND pts = 0 AND sent_at IS NULL)
|
||||
),
|
||||
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 broadcasts_enumeration_idx ON broadcasts (id)
|
||||
WHERE target_mode = 'all' AND NOT enumeration_done;
|
||||
CREATE INDEX broadcast_recipients_pending_idx ON broadcast_recipients (next_attempt_at, id)
|
||||
WHERE status = 'pending';
|
||||
CREATE INDEX broadcast_recipients_processing_idx ON broadcast_recipients (lease_until, id)
|
||||
WHERE status = 'processing';
|
||||
CREATE INDEX broadcast_recipients_broadcast_idx ON broadcast_recipients (broadcast_id, id);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
DROP INDEX IF EXISTS public.channel_messages_public_forward_source_seek_idx;
|
||||
DROP INDEX IF EXISTS public.channel_members_stats_period_idx;
|
||||
DROP INDEX IF EXISTS public.channel_message_viewers_stats_date_idx;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
-- Bounded stats reads: event-time viewers, membership snapshots, and exact
|
||||
-- public-forward seek pagination by the durable MessageForward JSON shape.
|
||||
CREATE INDEX channel_message_viewers_stats_date_idx
|
||||
ON public.channel_message_viewers (channel_id, viewed_at, viewer_user_id, message_id);
|
||||
|
||||
CREATE INDEX channel_members_stats_period_idx
|
||||
ON public.channel_members (channel_id, joined_at, left_at, user_id);
|
||||
|
||||
CREATE INDEX channel_messages_public_forward_source_seek_idx
|
||||
ON public.channel_messages (
|
||||
(fwd_from #>> '{From,Type}'),
|
||||
(fwd_from #>> '{From,ID}'),
|
||||
(fwd_from #>> '{ChannelPost}'),
|
||||
message_date DESC,
|
||||
channel_id ASC,
|
||||
id DESC
|
||||
)
|
||||
WHERE NOT deleted AND fwd_from <> '{}'::jsonb;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE broadcasts
|
||||
DROP CONSTRAINT IF EXISTS broadcasts_entities_array_check,
|
||||
DROP COLUMN IF EXISTS entities;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
ALTER TABLE broadcasts
|
||||
ADD COLUMN entities jsonb NOT NULL DEFAULT '[]'::jsonb;
|
||||
|
||||
ALTER TABLE broadcasts
|
||||
ADD CONSTRAINT broadcasts_entities_array_check
|
||||
CHECK (jsonb_typeof(entities) = 'array');
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
CREATE OR REPLACE FUNCTION public.telesrv_notify_user_base_read_model() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
changed_id BIGINT;
|
||||
projection_changed BOOLEAN;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
changed_id := OLD.id;
|
||||
projection_changed := true;
|
||||
ELSE
|
||||
changed_id := NEW.id;
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
projection_changed := true;
|
||||
ELSE
|
||||
projection_changed :=
|
||||
OLD.access_hash IS DISTINCT FROM NEW.access_hash OR
|
||||
OLD.phone IS DISTINCT FROM NEW.phone OR
|
||||
OLD.first_name IS DISTINCT FROM NEW.first_name OR
|
||||
OLD.last_name IS DISTINCT FROM NEW.last_name OR
|
||||
OLD.username IS DISTINCT FROM NEW.username OR
|
||||
OLD.country_code IS DISTINCT FROM NEW.country_code OR
|
||||
OLD.verified IS DISTINCT FROM NEW.verified OR
|
||||
OLD.support IS DISTINCT FROM NEW.support OR
|
||||
OLD.about IS DISTINCT FROM NEW.about OR
|
||||
OLD.default_history_ttl_period IS DISTINCT FROM NEW.default_history_ttl_period OR
|
||||
OLD.is_bot IS DISTINCT FROM NEW.is_bot OR
|
||||
OLD.bot_info_version IS DISTINCT FROM NEW.bot_info_version OR
|
||||
OLD.premium_expires_at IS DISTINCT FROM NEW.premium_expires_at OR
|
||||
OLD.emoji_status_document_id IS DISTINCT FROM NEW.emoji_status_document_id OR
|
||||
OLD.emoji_status_until IS DISTINCT FROM NEW.emoji_status_until OR
|
||||
OLD.color_set IS DISTINCT FROM NEW.color_set OR
|
||||
OLD.color IS DISTINCT FROM NEW.color OR
|
||||
OLD.color_background_emoji_id IS DISTINCT FROM NEW.color_background_emoji_id OR
|
||||
OLD.profile_color_set IS DISTINCT FROM NEW.profile_color_set OR
|
||||
OLD.profile_color IS DISTINCT FROM NEW.profile_color OR
|
||||
OLD.profile_color_background_emoji_id IS DISTINCT FROM NEW.profile_color_background_emoji_id;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF projection_changed THEN
|
||||
PERFORM telesrv_bump_read_model_version('user_base', changed_id, 'user', changed_id);
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
PERFORM telesrv_bump_read_model_version('contact_account', changed_id, 'user', changed_id);
|
||||
END IF;
|
||||
PERFORM telesrv_bump_read_model_version('contact_account', c.user_id, 'user', c.user_id)
|
||||
FROM contacts c
|
||||
WHERE c.contact_user_id = changed_id;
|
||||
PERFORM telesrv_bump_private_dialog_light_for_user(changed_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_user_channel_participants_read_model() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
changed_id BIGINT;
|
||||
old_id BIGINT;
|
||||
projection_changed BOOLEAN;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
changed_id := OLD.id;
|
||||
projection_changed := true;
|
||||
ELSE
|
||||
changed_id := NEW.id;
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
projection_changed := true;
|
||||
PERFORM telesrv_bump_read_model_version('contact_account', changed_id, 'user', changed_id);
|
||||
ELSE
|
||||
old_id := OLD.id;
|
||||
projection_changed :=
|
||||
OLD.access_hash IS DISTINCT FROM NEW.access_hash OR
|
||||
OLD.phone IS DISTINCT FROM NEW.phone OR
|
||||
OLD.first_name IS DISTINCT FROM NEW.first_name OR
|
||||
OLD.last_name IS DISTINCT FROM NEW.last_name OR
|
||||
OLD.username IS DISTINCT FROM NEW.username OR
|
||||
OLD.country_code IS DISTINCT FROM NEW.country_code OR
|
||||
OLD.verified IS DISTINCT FROM NEW.verified OR
|
||||
OLD.support IS DISTINCT FROM NEW.support OR
|
||||
OLD.about IS DISTINCT FROM NEW.about OR
|
||||
OLD.default_history_ttl_period IS DISTINCT FROM NEW.default_history_ttl_period OR
|
||||
OLD.is_bot IS DISTINCT FROM NEW.is_bot OR
|
||||
OLD.bot_info_version IS DISTINCT FROM NEW.bot_info_version OR
|
||||
OLD.premium_expires_at IS DISTINCT FROM NEW.premium_expires_at OR
|
||||
OLD.emoji_status_document_id IS DISTINCT FROM NEW.emoji_status_document_id OR
|
||||
OLD.emoji_status_until IS DISTINCT FROM NEW.emoji_status_until OR
|
||||
OLD.color_set IS DISTINCT FROM NEW.color_set OR
|
||||
OLD.color IS DISTINCT FROM NEW.color OR
|
||||
OLD.color_background_emoji_id IS DISTINCT FROM NEW.color_background_emoji_id OR
|
||||
OLD.profile_color_set IS DISTINCT FROM NEW.profile_color_set OR
|
||||
OLD.profile_color IS DISTINCT FROM NEW.profile_color OR
|
||||
OLD.profile_color_background_emoji_id IS DISTINCT FROM NEW.profile_color_background_emoji_id;
|
||||
IF old_id IS DISTINCT FROM changed_id THEN
|
||||
PERFORM telesrv_bump_channel_participants_for_user(old_id);
|
||||
END IF;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF projection_changed THEN
|
||||
PERFORM telesrv_bump_channel_participants_for_user(changed_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS users_release_collectible_phone_on_soft_delete ON public.users;
|
||||
CREATE TRIGGER users_release_collectible_phone_on_soft_delete
|
||||
BEFORE UPDATE OF deleted_at ON public.users
|
||||
FOR EACH ROW WHEN (OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL)
|
||||
EXECUTE FUNCTION public.release_soft_deleted_user_collectible_phone();
|
||||
129
deploy/migrations/20260901000004_logical_account_deletion.up.sql
Normal file
129
deploy/migrations/20260901000004_logical_account_deletion.up.sql
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
-- Human account deletion is a logical user tombstone. The deleted user keeps
|
||||
-- all relationship/history rows, so the users UPDATE must not fan out through
|
||||
-- every reverse contact, dialog and channel membership. A dedicated event
|
||||
-- invalidates the base-user and RPC projection caches as one coarse boundary.
|
||||
|
||||
-- Collectible phone ownership is an account asset, not the editable users.phone
|
||||
-- identity field. Logical deletion preserves it; physical user deletion keeps
|
||||
-- the separate 0171 release trigger.
|
||||
DROP TRIGGER IF EXISTS users_release_collectible_phone_on_soft_delete ON public.users;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_user_base_read_model() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
changed_id BIGINT;
|
||||
projection_changed BOOLEAN;
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE'
|
||||
AND OLD.deleted_at IS NULL
|
||||
AND NEW.deleted_at IS NOT NULL THEN
|
||||
PERFORM telesrv_bump_read_model_version('user_deleted', NEW.id, 'user', NEW.id);
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
changed_id := OLD.id;
|
||||
projection_changed := true;
|
||||
ELSE
|
||||
changed_id := NEW.id;
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
projection_changed := true;
|
||||
ELSE
|
||||
projection_changed :=
|
||||
OLD.access_hash IS DISTINCT FROM NEW.access_hash OR
|
||||
OLD.phone IS DISTINCT FROM NEW.phone OR
|
||||
OLD.first_name IS DISTINCT FROM NEW.first_name OR
|
||||
OLD.last_name IS DISTINCT FROM NEW.last_name OR
|
||||
OLD.username IS DISTINCT FROM NEW.username OR
|
||||
OLD.country_code IS DISTINCT FROM NEW.country_code OR
|
||||
OLD.verified IS DISTINCT FROM NEW.verified OR
|
||||
OLD.support IS DISTINCT FROM NEW.support OR
|
||||
OLD.about IS DISTINCT FROM NEW.about OR
|
||||
OLD.default_history_ttl_period IS DISTINCT FROM NEW.default_history_ttl_period OR
|
||||
OLD.is_bot IS DISTINCT FROM NEW.is_bot OR
|
||||
OLD.bot_info_version IS DISTINCT FROM NEW.bot_info_version OR
|
||||
OLD.premium_expires_at IS DISTINCT FROM NEW.premium_expires_at OR
|
||||
OLD.emoji_status_document_id IS DISTINCT FROM NEW.emoji_status_document_id OR
|
||||
OLD.emoji_status_until IS DISTINCT FROM NEW.emoji_status_until OR
|
||||
OLD.color_set IS DISTINCT FROM NEW.color_set OR
|
||||
OLD.color IS DISTINCT FROM NEW.color OR
|
||||
OLD.color_background_emoji_id IS DISTINCT FROM NEW.color_background_emoji_id OR
|
||||
OLD.profile_color_set IS DISTINCT FROM NEW.profile_color_set OR
|
||||
OLD.profile_color IS DISTINCT FROM NEW.profile_color OR
|
||||
OLD.profile_color_background_emoji_id IS DISTINCT FROM NEW.profile_color_background_emoji_id;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF projection_changed THEN
|
||||
PERFORM telesrv_bump_read_model_version('user_base', changed_id, 'user', changed_id);
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
PERFORM telesrv_bump_read_model_version('contact_account', changed_id, 'user', changed_id);
|
||||
END IF;
|
||||
PERFORM telesrv_bump_read_model_version('contact_account', c.user_id, 'user', c.user_id)
|
||||
FROM contacts c
|
||||
WHERE c.contact_user_id = changed_id;
|
||||
PERFORM telesrv_bump_private_dialog_light_for_user(changed_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_user_channel_participants_read_model() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
changed_id BIGINT;
|
||||
old_id BIGINT;
|
||||
projection_changed BOOLEAN;
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE'
|
||||
AND OLD.deleted_at IS NULL
|
||||
AND NEW.deleted_at IS NOT NULL THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
changed_id := OLD.id;
|
||||
projection_changed := true;
|
||||
ELSE
|
||||
changed_id := NEW.id;
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
projection_changed := true;
|
||||
PERFORM telesrv_bump_read_model_version('contact_account', changed_id, 'user', changed_id);
|
||||
ELSE
|
||||
old_id := OLD.id;
|
||||
projection_changed :=
|
||||
OLD.access_hash IS DISTINCT FROM NEW.access_hash OR
|
||||
OLD.phone IS DISTINCT FROM NEW.phone OR
|
||||
OLD.first_name IS DISTINCT FROM NEW.first_name OR
|
||||
OLD.last_name IS DISTINCT FROM NEW.last_name OR
|
||||
OLD.username IS DISTINCT FROM NEW.username OR
|
||||
OLD.country_code IS DISTINCT FROM NEW.country_code OR
|
||||
OLD.verified IS DISTINCT FROM NEW.verified OR
|
||||
OLD.support IS DISTINCT FROM NEW.support OR
|
||||
OLD.about IS DISTINCT FROM NEW.about OR
|
||||
OLD.default_history_ttl_period IS DISTINCT FROM NEW.default_history_ttl_period OR
|
||||
OLD.is_bot IS DISTINCT FROM NEW.is_bot OR
|
||||
OLD.bot_info_version IS DISTINCT FROM NEW.bot_info_version OR
|
||||
OLD.premium_expires_at IS DISTINCT FROM NEW.premium_expires_at OR
|
||||
OLD.emoji_status_document_id IS DISTINCT FROM NEW.emoji_status_document_id OR
|
||||
OLD.emoji_status_until IS DISTINCT FROM NEW.emoji_status_until OR
|
||||
OLD.color_set IS DISTINCT FROM NEW.color_set OR
|
||||
OLD.color IS DISTINCT FROM NEW.color OR
|
||||
OLD.color_background_emoji_id IS DISTINCT FROM NEW.color_background_emoji_id OR
|
||||
OLD.profile_color_set IS DISTINCT FROM NEW.profile_color_set OR
|
||||
OLD.profile_color IS DISTINCT FROM NEW.profile_color OR
|
||||
OLD.profile_color_background_emoji_id IS DISTINCT FROM NEW.profile_color_background_emoji_id;
|
||||
IF old_id IS DISTINCT FROM changed_id THEN
|
||||
PERFORM telesrv_bump_channel_participants_for_user(old_id);
|
||||
END IF;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF projection_changed THEN
|
||||
PERFORM telesrv_bump_channel_participants_for_user(changed_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
DROP TRIGGER IF EXISTS channel_messages_suggested_post_lifecycle_wakeup ON public.channel_messages;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_wake_suggested_post_lifecycle_on_delete();
|
||||
|
||||
DROP TABLE IF EXISTS public.suggested_post_lifecycle_wakeups;
|
||||
DROP INDEX IF EXISTS public.suggested_post_approvals_published_message_idx;
|
||||
DROP INDEX IF EXISTS public.suggested_post_approvals_retry_idx;
|
||||
|
||||
ALTER TABLE public.suggested_post_approvals
|
||||
DROP CONSTRAINT IF EXISTS suggested_post_approvals_retry_shape_check,
|
||||
DROP COLUMN IF EXISTS last_lifecycle_error,
|
||||
DROP COLUMN IF EXISTS next_attempt_at,
|
||||
DROP COLUMN IF EXISTS lifecycle_attempts;
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
-- Isolate poisoned suggested-post lifecycle rows from the global due queue.
|
||||
-- A failed aggregate remains durable and retryable, but receives a bounded
|
||||
-- backoff so it cannot monopolize every one-second dispatcher pass.
|
||||
|
||||
ALTER TABLE public.suggested_post_approvals
|
||||
ADD COLUMN lifecycle_attempts integer NOT NULL DEFAULT 0,
|
||||
ADD COLUMN next_attempt_at integer NOT NULL DEFAULT 0,
|
||||
ADD COLUMN last_lifecycle_error text NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE public.suggested_post_approvals
|
||||
ADD CONSTRAINT suggested_post_approvals_retry_shape_check CHECK (
|
||||
lifecycle_attempts BETWEEN 0 AND 1000000 AND
|
||||
next_attempt_at >= 0 AND
|
||||
octet_length(last_lifecycle_error) <= 4096
|
||||
) NOT VALID;
|
||||
|
||||
ALTER TABLE public.suggested_post_approvals
|
||||
VALIDATE CONSTRAINT suggested_post_approvals_retry_shape_check;
|
||||
|
||||
-- next_attempt_at is the queue's next eligible time, not merely a retry flag.
|
||||
-- Healthy active rows point at their publish/settlement deadline; only an
|
||||
-- explicit message-deletion wakeup or an already-due row is <= worker now.
|
||||
UPDATE public.suggested_post_approvals
|
||||
SET next_attempt_at = CASE state
|
||||
WHEN 'scheduled' THEN schedule_date
|
||||
WHEN 'published' THEN settlement_due
|
||||
ELSE 0
|
||||
END
|
||||
WHERE state IN ('scheduled','published');
|
||||
|
||||
CREATE INDEX suggested_post_approvals_retry_idx
|
||||
ON public.suggested_post_approvals(next_attempt_at,monoforum_id,suggestion_message_id)
|
||||
WHERE state IN ('scheduled','published');
|
||||
|
||||
CREATE INDEX suggested_post_approvals_published_message_idx
|
||||
ON public.suggested_post_approvals(parent_channel_id,published_message_id)
|
||||
INCLUDE (monoforum_id,suggestion_message_id,next_attempt_at)
|
||||
WHERE state='published';
|
||||
|
||||
-- A separate wakeup table avoids a message-row -> approval-row trigger lock
|
||||
-- that would invert the worker's approval-row -> message-row order. It only
|
||||
-- contains active suggested-post aggregates and is drained by atomic claim.
|
||||
CREATE TABLE public.suggested_post_lifecycle_wakeups (
|
||||
monoforum_id bigint NOT NULL,
|
||||
suggestion_message_id integer NOT NULL,
|
||||
created_at integer NOT NULL,
|
||||
CONSTRAINT suggested_post_lifecycle_wakeups_pkey
|
||||
PRIMARY KEY (monoforum_id,suggestion_message_id),
|
||||
CONSTRAINT suggested_post_lifecycle_wakeups_shape_check
|
||||
CHECK (monoforum_id>0 AND suggestion_message_id>0 AND created_at>=0)
|
||||
);
|
||||
|
||||
CREATE INDEX suggested_post_lifecycle_wakeups_due_idx
|
||||
ON public.suggested_post_lifecycle_wakeups(created_at,monoforum_id,suggestion_message_id);
|
||||
|
||||
-- Preserve deletions that predate this migration without making the recurring
|
||||
-- worker rescan global channel tombstone history.
|
||||
INSERT INTO public.suggested_post_lifecycle_wakeups(monoforum_id,suggestion_message_id,created_at)
|
||||
SELECT a.monoforum_id,a.suggestion_message_id,0
|
||||
FROM public.suggested_post_approvals a
|
||||
WHERE (a.state='scheduled' AND EXISTS (
|
||||
SELECT 1 FROM public.channel_messages m
|
||||
WHERE m.channel_id=a.monoforum_id AND m.id=a.suggestion_message_id AND m.deleted))
|
||||
OR (a.state='published' AND EXISTS (
|
||||
SELECT 1 FROM public.channel_messages m
|
||||
WHERE m.channel_id=a.parent_channel_id AND m.id=a.published_message_id AND m.deleted))
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Deletion is a durable lifecycle wakeup. Drive the lookup from the exact
|
||||
-- message being tombstoned and only insert a small queue fact; never rescan
|
||||
-- global channel tombstones from the one-second worker.
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_wake_suggested_post_lifecycle_on_delete()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
INSERT INTO public.suggested_post_lifecycle_wakeups(monoforum_id,suggestion_message_id,created_at)
|
||||
SELECT monoforum_id,suggestion_message_id,EXTRACT(EPOCH FROM clock_timestamp())::integer
|
||||
FROM public.suggested_post_approvals
|
||||
WHERE state='scheduled' AND monoforum_id=NEW.channel_id AND suggestion_message_id=NEW.id
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO public.suggested_post_lifecycle_wakeups(monoforum_id,suggestion_message_id,created_at)
|
||||
SELECT monoforum_id,suggestion_message_id,EXTRACT(EPOCH FROM clock_timestamp())::integer
|
||||
FROM public.suggested_post_approvals
|
||||
WHERE state='published' AND parent_channel_id=NEW.channel_id AND published_message_id=NEW.id
|
||||
ON CONFLICT DO NOTHING;
|
||||
RETURN NEW;
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER channel_messages_suggested_post_lifecycle_wakeup
|
||||
AFTER UPDATE OF deleted ON public.channel_messages
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.deleted AND NOT OLD.deleted)
|
||||
EXECUTE FUNCTION public.telesrv_wake_suggested_post_lifecycle_on_delete();
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
DROP INDEX IF EXISTS public.sticker_sets_system_key_idx;
|
||||
|
||||
CREATE INDEX sticker_sets_system_key_idx
|
||||
ON public.sticker_sets USING btree (system_key)
|
||||
WHERE system_key <> ''::text;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
-- system_key is the constructor-routing identity for global sticker sets. More
|
||||
-- than one row makes messages.getStickerSet/inputStickerSet* nondeterministic
|
||||
-- and can alternate clients between different catalogs after a restart.
|
||||
|
||||
-- StatusPack is now imported from the official export. Preserve the historical
|
||||
-- synthesized set as an addressable short-name catalog, but only remove its
|
||||
-- routing key when another real default-status set already owns that key.
|
||||
UPDATE public.sticker_sets AS synthesized
|
||||
SET system_key = '', updated_at = now()
|
||||
WHERE synthesized.id = 7777000000000001
|
||||
AND synthesized.system_key = 'emoji_default_statuses'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM public.sticker_sets AS real_set
|
||||
WHERE real_set.system_key = synthesized.system_key
|
||||
AND real_set.id <> synthesized.id
|
||||
);
|
||||
|
||||
DROP INDEX IF EXISTS public.sticker_sets_system_key_idx;
|
||||
|
||||
-- Fail closed on any other duplicate instead of making the read path choose an
|
||||
-- arbitrary row. Empty keys are ordinary/non-system sets and are not unique.
|
||||
CREATE UNIQUE INDEX sticker_sets_system_key_idx
|
||||
ON public.sticker_sets USING btree (system_key)
|
||||
WHERE system_key <> ''::text;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE IF EXISTS welcome_messages;
|
||||
DROP TABLE IF EXISTS welcome_message_peers;
|
||||
31
deploy/migrations/20260901000007_welcome_messages.up.sql
Normal file
31
deploy/migrations/20260901000007_welcome_messages.up.sql
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
CREATE TABLE welcome_message_peers (
|
||||
channel_id bigint PRIMARY KEY REFERENCES channels(id) ON DELETE CASCADE,
|
||||
next_id integer NOT NULL DEFAULT 1,
|
||||
revision bigint NOT NULL DEFAULT 1,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT welcome_message_peers_shape CHECK (
|
||||
channel_id > 0 AND next_id > 0 AND next_id <= 2147483647 AND revision > 0
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE welcome_messages (
|
||||
channel_id bigint NOT NULL REFERENCES welcome_message_peers(channel_id) ON DELETE CASCADE,
|
||||
id integer NOT NULL,
|
||||
creator_user_id bigint NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
date integer NOT NULL,
|
||||
edit_date integer NOT NULL DEFAULT 0,
|
||||
random_id bigint NOT NULL,
|
||||
content jsonb NOT NULL,
|
||||
create_fingerprint bytea NOT NULL,
|
||||
version bigint NOT NULL DEFAULT 1,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (channel_id, id),
|
||||
UNIQUE (channel_id, creator_user_id, random_id),
|
||||
CONSTRAINT welcome_messages_shape CHECK (
|
||||
channel_id > 0 AND id > 0 AND creator_user_id > 0 AND date > 0 AND
|
||||
edit_date >= 0 AND (edit_date = 0 OR edit_date >= date) AND random_id <> 0 AND
|
||||
jsonb_typeof(content) = 'object' AND pg_column_size(content) <= 4194304 AND
|
||||
octet_length(create_fingerprint) = 32 AND version > 0
|
||||
)
|
||||
);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE IF EXISTS welcome_message_deliveries;
|
||||
DROP SEQUENCE IF EXISTS welcome_message_join_event_id_seq;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
CREATE SEQUENCE welcome_message_join_event_id_seq AS bigint;
|
||||
|
||||
CREATE TABLE welcome_message_deliveries (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
join_event_id bigint NOT NULL,
|
||||
channel_id bigint NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
target_user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
template_id integer NOT NULL,
|
||||
ephemeral_id integer GENERATED ALWAYS AS (((id - 1) % 2147483646 + 1)::integer) STORED,
|
||||
joined_at integer NOT NULL,
|
||||
content jsonb NOT NULL,
|
||||
attempt_count integer NOT NULL DEFAULT 0,
|
||||
next_attempt_at timestamptz NOT NULL DEFAULT now(),
|
||||
lease_owner text,
|
||||
lease_expires_at timestamptz,
|
||||
delivered_at timestamptz,
|
||||
last_error text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL DEFAULT (now() + interval '24 hours'),
|
||||
UNIQUE (join_event_id, template_id),
|
||||
UNIQUE (target_user_id, ephemeral_id),
|
||||
CONSTRAINT welcome_message_deliveries_shape CHECK (
|
||||
join_event_id > 0 AND channel_id > 0 AND target_user_id > 0 AND
|
||||
template_id > 0 AND ephemeral_id > 0 AND joined_at > 0 AND
|
||||
jsonb_typeof(content) = 'object' AND pg_column_size(content) <= 4194304 AND
|
||||
attempt_count >= 0 AND char_length(lease_owner) <= 128 AND
|
||||
char_length(last_error) <= 1024 AND expires_at > created_at AND
|
||||
expires_at <= created_at + interval '24 hours 1 second'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX welcome_message_deliveries_due_idx
|
||||
ON welcome_message_deliveries (next_attempt_at, id)
|
||||
WHERE delivered_at IS NULL;
|
||||
|
||||
CREATE INDEX welcome_message_deliveries_expiry_idx
|
||||
ON welcome_message_deliveries (expires_at, id);
|
||||
|
|
@ -0,0 +1 @@
|
|||
DROP INDEX IF EXISTS welcome_message_deliveries_target_idx;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
CREATE INDEX welcome_message_deliveries_target_idx
|
||||
ON welcome_message_deliveries (channel_id, target_user_id, id);
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
DROP TRIGGER dispatch_outbox_insert_user_head ON dispatch_outbox;
|
||||
DROP FUNCTION dispatch_outbox_insert_user_heads();
|
||||
|
||||
CREATE OR REPLACE FUNCTION dispatch_outbox_maintain_user_head()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
removed_head bigint;
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
INSERT INTO dispatch_outbox_user_heads (
|
||||
target_user_id, head_id, head_pts, status, next_attempt_at, updated_at
|
||||
) VALUES (
|
||||
NEW.target_user_id, NEW.id, NEW.pts, NEW.status, NEW.next_attempt_at, NEW.updated_at
|
||||
)
|
||||
ON CONFLICT (target_user_id) DO UPDATE
|
||||
SET head_id = EXCLUDED.head_id,
|
||||
head_pts = EXCLUDED.head_pts,
|
||||
status = EXCLUDED.status,
|
||||
next_attempt_at = EXCLUDED.next_attempt_at,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
WHERE (EXCLUDED.head_pts, EXCLUDED.head_id) <
|
||||
(dispatch_outbox_user_heads.head_pts, dispatch_outbox_user_heads.head_id);
|
||||
RETURN NULL;
|
||||
ELSIF TG_OP = 'UPDATE' THEN
|
||||
UPDATE dispatch_outbox_user_heads
|
||||
SET status = NEW.status,
|
||||
next_attempt_at = NEW.next_attempt_at,
|
||||
updated_at = NEW.updated_at
|
||||
WHERE target_user_id = NEW.target_user_id
|
||||
AND head_id = NEW.id;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
DELETE FROM dispatch_outbox_user_heads
|
||||
WHERE target_user_id = OLD.target_user_id
|
||||
AND head_id = OLD.id
|
||||
RETURNING head_id INTO removed_head;
|
||||
|
||||
IF removed_head IS NOT NULL THEN
|
||||
INSERT INTO dispatch_outbox_user_heads (
|
||||
target_user_id, head_id, head_pts, status, next_attempt_at, updated_at
|
||||
)
|
||||
SELECT target_user_id, id, pts, status, next_attempt_at, updated_at
|
||||
FROM dispatch_outbox
|
||||
WHERE target_user_id = OLD.target_user_id
|
||||
ORDER BY pts ASC, id ASC
|
||||
LIMIT 1
|
||||
ON CONFLICT (target_user_id) DO UPDATE
|
||||
SET head_id = EXCLUDED.head_id,
|
||||
head_pts = EXCLUDED.head_pts,
|
||||
status = EXCLUDED.status,
|
||||
next_attempt_at = EXCLUDED.next_attempt_at,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
WHERE (EXCLUDED.head_pts, EXCLUDED.head_id) <
|
||||
(dispatch_outbox_user_heads.head_pts, dispatch_outbox_user_heads.head_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER dispatch_outbox_insert_user_head
|
||||
AFTER INSERT ON dispatch_outbox
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION dispatch_outbox_maintain_user_head();
|
||||
|
||||
DROP FUNCTION dispatch_outbox_lane_advisory_key(bigint);
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
-- Producers hold a shared per-lane fence until commit. A consumer that may
|
||||
-- remove the last head takes the exclusive form in a preceding statement;
|
||||
-- after that fence is acquired, its DELETE sees every earlier append or every
|
||||
-- later append observes the missing marker and installs a new one.
|
||||
CREATE FUNCTION dispatch_outbox_lane_advisory_key(target_user_id bigint)
|
||||
RETURNS bigint
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
PARALLEL SAFE
|
||||
AS $$
|
||||
SELECT hashtextextended('telesrv:dispatch-outbox-lane:' || target_user_id::text, 0)
|
||||
$$;
|
||||
|
||||
DROP TRIGGER dispatch_outbox_insert_user_head ON dispatch_outbox;
|
||||
|
||||
CREATE FUNCTION dispatch_outbox_insert_user_heads()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM pg_advisory_xact_lock_shared(
|
||||
dispatch_outbox_lane_advisory_key(streams.target_user_id)
|
||||
)
|
||||
FROM (
|
||||
SELECT DISTINCT target_user_id
|
||||
FROM new_rows
|
||||
ORDER BY target_user_id
|
||||
) AS streams;
|
||||
|
||||
-- The anti-join avoids touching a committed mutable head. ON CONFLICT is
|
||||
-- retained only for concurrent producers racing to create an empty lane.
|
||||
WITH candidates AS MATERIALIZED (
|
||||
SELECT DISTINCT ON (target_user_id)
|
||||
target_user_id, id AS head_id, pts AS head_pts,
|
||||
status, next_attempt_at, updated_at
|
||||
FROM new_rows
|
||||
ORDER BY target_user_id, pts, id
|
||||
)
|
||||
INSERT INTO dispatch_outbox_user_heads (
|
||||
target_user_id, head_id, head_pts, status, next_attempt_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
c.target_user_id, c.head_id, c.head_pts,
|
||||
c.status, c.next_attempt_at, c.updated_at
|
||||
FROM candidates c
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM dispatch_outbox_user_heads existing
|
||||
WHERE existing.target_user_id = c.target_user_id
|
||||
)
|
||||
ON CONFLICT (target_user_id) DO NOTHING;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION dispatch_outbox_maintain_user_head()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
removed_head bigint;
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE' THEN
|
||||
UPDATE dispatch_outbox_user_heads
|
||||
SET status = NEW.status,
|
||||
next_attempt_at = NEW.next_attempt_at,
|
||||
updated_at = NEW.updated_at
|
||||
WHERE target_user_id = NEW.target_user_id
|
||||
AND head_id = NEW.id;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
DELETE FROM dispatch_outbox_user_heads
|
||||
WHERE target_user_id = OLD.target_user_id
|
||||
AND head_id = OLD.id
|
||||
RETURNING head_id INTO removed_head;
|
||||
|
||||
IF removed_head IS NOT NULL THEN
|
||||
INSERT INTO dispatch_outbox_user_heads (
|
||||
target_user_id, head_id, head_pts, status, next_attempt_at, updated_at
|
||||
)
|
||||
SELECT target_user_id, id, pts, status, next_attempt_at, updated_at
|
||||
FROM dispatch_outbox
|
||||
WHERE target_user_id = OLD.target_user_id
|
||||
ORDER BY pts ASC, id ASC
|
||||
LIMIT 1
|
||||
ON CONFLICT (target_user_id) DO UPDATE
|
||||
SET head_id = EXCLUDED.head_id,
|
||||
head_pts = EXCLUDED.head_pts,
|
||||
status = EXCLUDED.status,
|
||||
next_attempt_at = EXCLUDED.next_attempt_at,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
WHERE (EXCLUDED.head_pts, EXCLUDED.head_id) <
|
||||
(dispatch_outbox_user_heads.head_pts, dispatch_outbox_user_heads.head_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER dispatch_outbox_insert_user_head
|
||||
AFTER INSERT ON dispatch_outbox
|
||||
REFERENCING NEW TABLE AS new_rows
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION dispatch_outbox_insert_user_heads();
|
||||
|
||||
-- Repair any marker that may be absent when upgrading from an interrupted or
|
||||
-- older deployment. Startup migration runs before the dispatcher starts.
|
||||
INSERT INTO dispatch_outbox_user_heads (
|
||||
target_user_id, head_id, head_pts, status, next_attempt_at, updated_at
|
||||
)
|
||||
SELECT DISTINCT ON (d.target_user_id)
|
||||
d.target_user_id, d.id, d.pts, d.status, d.next_attempt_at, d.updated_at
|
||||
FROM dispatch_outbox d
|
||||
LEFT JOIN dispatch_outbox_user_heads h USING (target_user_id)
|
||||
WHERE h.target_user_id IS NULL
|
||||
ORDER BY d.target_user_id, d.pts, d.id
|
||||
ON CONFLICT (target_user_id) DO NOTHING;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
DROP TRIGGER IF EXISTS channel_message_reactions_dialog_top_projection_changed ON channel_message_reactions;
|
||||
DROP FUNCTION IF EXISTS telesrv_notify_channel_top_reactions_read_model();
|
||||
|
||||
DROP TRIGGER IF EXISTS channel_messages_dialog_top_projection_changed ON channel_messages;
|
||||
DROP FUNCTION IF EXISTS telesrv_notify_channel_top_message_read_model();
|
||||
|
||||
DROP TRIGGER IF EXISTS private_message_reactions_dialog_top_projection_changed ON private_message_reactions;
|
||||
DROP FUNCTION IF EXISTS telesrv_notify_private_top_reactions_read_model();
|
||||
|
||||
DROP TRIGGER IF EXISTS message_boxes_dialog_top_projection_changed ON message_boxes;
|
||||
DROP FUNCTION IF EXISTS telesrv_notify_private_top_message_read_model();
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
-- Complete the dialog_light/channel_base dependency closure for cached dialog
|
||||
-- top-message projections. These triggers only bump read-model versions; they
|
||||
-- do not allocate PTS or mutate message/update facts.
|
||||
|
||||
CREATE OR REPLACE FUNCTION telesrv_notify_private_top_message_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM dialogs d
|
||||
WHERE d.user_id = NEW.owner_user_id
|
||||
AND d.peer_type = NEW.peer_type
|
||||
AND d.peer_id = NEW.peer_id
|
||||
AND d.top_message_id = NEW.box_id
|
||||
) THEN
|
||||
PERFORM telesrv_bump_dialog_light(NEW.owner_user_id, NEW.peer_type, NEW.peer_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER message_boxes_dialog_top_projection_changed
|
||||
AFTER UPDATE OF
|
||||
message_date, body, entities, deleted, edit_date, silent, noforwards,
|
||||
reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id,
|
||||
reply_to_story_id, quote_text, quote_entities, quote_offset,
|
||||
fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date,
|
||||
fwd_saved_from_peer_type, fwd_saved_from_peer_id, fwd_saved_from_msg_id,
|
||||
media, media_unread, reaction_unread, ttl_period, expires_at, pinned,
|
||||
saved_peer_type, saved_peer_id, reply_markup, via_bot_id, rich_message,
|
||||
grouped_id, effect, hide_edited
|
||||
ON message_boxes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION telesrv_notify_private_top_message_read_model();
|
||||
|
||||
CREATE OR REPLACE FUNCTION telesrv_notify_private_top_reactions_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
changed_sender_id BIGINT;
|
||||
changed_message_id BIGINT;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
changed_sender_id := OLD.message_sender_id;
|
||||
changed_message_id := OLD.private_message_id;
|
||||
ELSE
|
||||
changed_sender_id := NEW.message_sender_id;
|
||||
changed_message_id := NEW.private_message_id;
|
||||
END IF;
|
||||
|
||||
PERFORM telesrv_bump_dialog_light(b.owner_user_id, b.peer_type, b.peer_id)
|
||||
FROM message_boxes b
|
||||
JOIN dialogs d
|
||||
ON d.user_id = b.owner_user_id
|
||||
AND d.peer_type = b.peer_type
|
||||
AND d.peer_id = b.peer_id
|
||||
AND d.top_message_id = b.box_id
|
||||
WHERE b.message_sender_id = changed_sender_id
|
||||
AND b.private_message_id = changed_message_id
|
||||
AND NOT b.deleted;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER private_message_reactions_dialog_top_projection_changed
|
||||
AFTER INSERT OR DELETE OR UPDATE
|
||||
ON private_message_reactions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION telesrv_notify_private_top_reactions_read_model();
|
||||
|
||||
CREATE OR REPLACE FUNCTION telesrv_notify_channel_top_message_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM channels c
|
||||
WHERE c.id = NEW.channel_id
|
||||
AND c.top_message_id = NEW.id
|
||||
) THEN
|
||||
PERFORM telesrv_bump_read_model_version('channel_base', 0, 'channel', NEW.channel_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER channel_messages_dialog_top_projection_changed
|
||||
AFTER UPDATE OF
|
||||
sender_user_id, from_peer_type, from_peer_id, send_as_peer_type, send_as_peer_id,
|
||||
message_date, edit_date, post, silent, noforwards, body, entities, reply_to,
|
||||
reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id, fwd_from,
|
||||
discussion_channel_id, discussion_message_id, action, deleted, views_count, media,
|
||||
ttl_period, expires_at, post_author, pinned, via_bot_id, reply_markup,
|
||||
from_boosts_applied, rich_message
|
||||
ON channel_messages
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION telesrv_notify_channel_top_message_read_model();
|
||||
|
||||
CREATE OR REPLACE FUNCTION telesrv_notify_channel_top_reactions_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
changed_channel_id BIGINT;
|
||||
changed_message_id INTEGER;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
changed_channel_id := OLD.channel_id;
|
||||
changed_message_id := OLD.message_id;
|
||||
ELSE
|
||||
changed_channel_id := NEW.channel_id;
|
||||
changed_message_id := NEW.message_id;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM channels c
|
||||
WHERE c.id = changed_channel_id
|
||||
AND c.top_message_id = changed_message_id
|
||||
) THEN
|
||||
PERFORM telesrv_bump_read_model_version('channel_base', 0, 'channel', changed_channel_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER channel_message_reactions_dialog_top_projection_changed
|
||||
AFTER INSERT OR DELETE OR UPDATE
|
||||
ON channel_message_reactions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION telesrv_notify_channel_top_reactions_read_model();
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
DROP TRIGGER IF EXISTS communities_bump_catalog_read_model ON public.communities;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_bump_community_catalog_read_model();
|
||||
DELETE FROM public.read_model_versions
|
||||
WHERE model = 'community_catalog'
|
||||
AND owner_user_id = 0
|
||||
AND peer_type = 'community'
|
||||
AND peer_id = 0;
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
-- Global Community-catalog presence token. messages.getDialogs needs collapsed
|
||||
-- Communities only when at least one live Community exists; without this
|
||||
-- durable invalidation token an empty deployment would repeat the expensive
|
||||
-- owner-membership query on every dialogs page.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bump_community_catalog_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM public.telesrv_bump_read_model_version(
|
||||
'community_catalog',
|
||||
0,
|
||||
'community',
|
||||
0
|
||||
);
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS communities_bump_catalog_read_model ON public.communities;
|
||||
CREATE TRIGGER communities_bump_catalog_read_model
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.communities
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_bump_community_catalog_read_model();
|
||||
|
||||
SELECT public.telesrv_bump_read_model_version(
|
||||
'community_catalog',
|
||||
0,
|
||||
'community',
|
||||
0
|
||||
);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
DROP FUNCTION IF EXISTS public.telesrv_advance_auth_session_layer(
|
||||
bigint,
|
||||
bigint,
|
||||
integer,
|
||||
bigint,
|
||||
timestamptz
|
||||
);
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
-- Advance one explicit invokeWithLayer observation without paying one client
|
||||
-- round trip for every lock, comparison and projection update. The caller
|
||||
-- still owns the surrounding transaction/savepoint so identity_changed can
|
||||
-- roll the complete attempt back and retry with a fresh READ COMMITTED view.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_advance_auth_session_layer(
|
||||
p_raw_auth_key_id bigint,
|
||||
p_session_id bigint,
|
||||
p_layer integer,
|
||||
p_msg_id bigint,
|
||||
p_expires_at timestamptz
|
||||
)
|
||||
RETURNS TABLE (
|
||||
advance_status text,
|
||||
current_layer integer,
|
||||
current_msg_id bigint,
|
||||
current_observation_id bigint,
|
||||
current_expires_at timestamptz,
|
||||
shared_default boolean,
|
||||
applied boolean
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_now timestamptz := now();
|
||||
v_hint_expiry integer;
|
||||
v_hint_perm_id bigint;
|
||||
v_hint_bound boolean := false;
|
||||
v_hint_has_identity boolean := false;
|
||||
v_hint_identity_id bigint;
|
||||
v_raw_expiry integer;
|
||||
v_actual_perm_id bigint;
|
||||
v_actual_bound boolean := false;
|
||||
v_actual_has_identity boolean := false;
|
||||
v_actual_identity_id bigint;
|
||||
v_perm_expiry integer;
|
||||
v_current_found boolean := false;
|
||||
v_current_layer integer := 0;
|
||||
v_current_msg_id bigint := 0;
|
||||
v_current_observation_id bigint := 0;
|
||||
v_current_expires_at timestamptz := p_expires_at;
|
||||
v_shared_default boolean := false;
|
||||
v_observation_id bigint;
|
||||
v_key_ids bigint[];
|
||||
v_updated integer;
|
||||
BEGIN
|
||||
IF p_layer <= 0
|
||||
OR p_msg_id <= 0
|
||||
OR p_msg_id % 4 <> 0
|
||||
OR (p_msg_id & 4294967295) = 0
|
||||
OR p_expires_at IS NULL
|
||||
OR NOT v_now < p_expires_at
|
||||
OR p_expires_at - interval '301 seconds' > v_now + interval '30 seconds'
|
||||
THEN
|
||||
RETURN QUERY SELECT
|
||||
'evidence_invalid'::text, 0, 0::bigint, 0::bigint,
|
||||
COALESCE(p_expires_at, v_now), false, false;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Lock-free identity hint. It is deliberately revalidated after the raw
|
||||
-- row lock; a newly committed first bind returns identity_changed rather
|
||||
-- than taking a permanent identity lock in raw->identity order.
|
||||
SELECT key.expires_at, binding.perm_auth_key_id
|
||||
INTO v_hint_expiry, v_hint_perm_id
|
||||
FROM public.auth_keys AS key
|
||||
LEFT JOIN public.temp_auth_key_bindings AS binding
|
||||
ON binding.temp_auth_key_id = key.auth_key_id
|
||||
WHERE key.auth_key_id = p_raw_auth_key_id;
|
||||
IF NOT FOUND THEN
|
||||
RETURN QUERY SELECT
|
||||
'auth_key_not_found'::text, 0, 0::bigint, 0::bigint,
|
||||
p_expires_at, false, false;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
v_hint_bound := v_hint_perm_id IS NOT NULL;
|
||||
v_hint_has_identity := v_hint_bound OR v_hint_expiry = 0;
|
||||
IF v_hint_has_identity THEN
|
||||
v_hint_identity_id := CASE
|
||||
WHEN v_hint_bound THEN v_hint_perm_id
|
||||
ELSE p_raw_auth_key_id
|
||||
END;
|
||||
PERFORM pg_advisory_xact_lock(
|
||||
1096111176::integer,
|
||||
hashint8(v_hint_identity_id)::integer
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT key.expires_at
|
||||
INTO v_raw_expiry
|
||||
FROM public.auth_keys AS key
|
||||
WHERE key.auth_key_id = p_raw_auth_key_id
|
||||
FOR UPDATE;
|
||||
IF NOT FOUND THEN
|
||||
RETURN QUERY SELECT
|
||||
'auth_key_not_found'::text, 0, 0::bigint, 0::bigint,
|
||||
p_expires_at, false, false;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
v_actual_perm_id := NULL;
|
||||
SELECT binding.perm_auth_key_id
|
||||
INTO v_actual_perm_id
|
||||
FROM public.temp_auth_key_bindings AS binding
|
||||
WHERE binding.temp_auth_key_id = p_raw_auth_key_id;
|
||||
v_actual_bound := FOUND;
|
||||
IF NOT v_actual_bound THEN
|
||||
v_actual_perm_id := p_raw_auth_key_id;
|
||||
END IF;
|
||||
v_actual_has_identity := v_actual_bound OR v_raw_expiry = 0;
|
||||
v_actual_identity_id := v_actual_perm_id;
|
||||
|
||||
IF v_actual_has_identity <> v_hint_has_identity
|
||||
OR (v_actual_has_identity AND v_actual_identity_id <> v_hint_identity_id)
|
||||
OR v_actual_bound <> v_hint_bound
|
||||
OR v_raw_expiry <> v_hint_expiry
|
||||
THEN
|
||||
RETURN QUERY SELECT
|
||||
'identity_changed'::text, 0, 0::bigint, 0::bigint,
|
||||
p_expires_at, false, false;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF v_actual_bound THEN
|
||||
SELECT key.expires_at
|
||||
INTO v_perm_expiry
|
||||
FROM public.auth_keys AS key
|
||||
WHERE key.auth_key_id = v_actual_perm_id
|
||||
FOR UPDATE;
|
||||
IF NOT FOUND OR v_raw_expiry <= 0 OR v_perm_expiry <> 0 THEN
|
||||
RETURN QUERY SELECT
|
||||
'binding_invalid'::text, 0, 0::bigint, 0::bigint,
|
||||
p_expires_at, false, false;
|
||||
RETURN;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
SELECT evidence.layer,
|
||||
evidence.msg_id,
|
||||
evidence.observation_id,
|
||||
evidence.expires_at
|
||||
INTO v_current_layer,
|
||||
v_current_msg_id,
|
||||
v_current_observation_id,
|
||||
v_current_expires_at
|
||||
FROM public.auth_key_session_layers AS evidence
|
||||
WHERE evidence.raw_auth_key_id = p_raw_auth_key_id
|
||||
AND evidence.session_id = p_session_id
|
||||
FOR UPDATE;
|
||||
v_current_found := FOUND;
|
||||
|
||||
IF v_current_found AND v_now < v_current_expires_at THEN
|
||||
IF p_msg_id < v_current_msg_id THEN
|
||||
SELECT key.layer = v_current_layer
|
||||
AND key.layer_observation_id = v_current_observation_id
|
||||
INTO v_shared_default
|
||||
FROM public.auth_keys AS key
|
||||
WHERE key.auth_key_id = v_actual_perm_id;
|
||||
RETURN QUERY SELECT
|
||||
'ok'::text, v_current_layer, v_current_msg_id,
|
||||
v_current_observation_id, v_current_expires_at,
|
||||
v_shared_default, false;
|
||||
RETURN;
|
||||
END IF;
|
||||
IF p_msg_id = v_current_msg_id THEN
|
||||
IF p_layer <> v_current_layer THEN
|
||||
RETURN QUERY SELECT
|
||||
'conflict'::text, v_current_layer, v_current_msg_id,
|
||||
v_current_observation_id, v_current_expires_at,
|
||||
false, false;
|
||||
RETURN;
|
||||
END IF;
|
||||
SELECT key.layer = v_current_layer
|
||||
AND key.layer_observation_id = v_current_observation_id
|
||||
INTO v_shared_default
|
||||
FROM public.auth_keys AS key
|
||||
WHERE key.auth_key_id = v_actual_perm_id;
|
||||
RETURN QUERY SELECT
|
||||
'ok'::text, v_current_layer, v_current_msg_id,
|
||||
v_current_observation_id, v_current_expires_at,
|
||||
v_shared_default, false;
|
||||
RETURN;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
v_observation_id := nextval('public.auth_key_layer_observation_seq');
|
||||
INSERT INTO public.auth_key_session_layers (
|
||||
raw_auth_key_id,
|
||||
session_id,
|
||||
layer,
|
||||
msg_id,
|
||||
observation_id,
|
||||
expires_at
|
||||
) VALUES (
|
||||
p_raw_auth_key_id,
|
||||
p_session_id,
|
||||
p_layer,
|
||||
p_msg_id,
|
||||
v_observation_id,
|
||||
p_expires_at
|
||||
)
|
||||
ON CONFLICT (raw_auth_key_id, session_id) DO UPDATE SET
|
||||
layer = EXCLUDED.layer,
|
||||
msg_id = EXCLUDED.msg_id,
|
||||
observation_id = EXCLUDED.observation_id,
|
||||
expires_at = EXCLUDED.expires_at
|
||||
RETURNING layer, msg_id, observation_id, expires_at
|
||||
INTO v_current_layer,
|
||||
v_current_msg_id,
|
||||
v_current_observation_id,
|
||||
v_current_expires_at;
|
||||
|
||||
v_key_ids := ARRAY[p_raw_auth_key_id];
|
||||
IF v_actual_perm_id <> p_raw_auth_key_id THEN
|
||||
v_key_ids := array_append(v_key_ids, v_actual_perm_id);
|
||||
END IF;
|
||||
UPDATE public.auth_keys AS key
|
||||
SET layer = p_layer,
|
||||
layer_observation_id = v_observation_id
|
||||
WHERE key.auth_key_id = ANY(v_key_ids)
|
||||
AND key.layer_observation_id < v_observation_id;
|
||||
GET DIAGNOSTICS v_updated = ROW_COUNT;
|
||||
IF v_updated <> cardinality(v_key_ids) THEN
|
||||
RAISE EXCEPTION
|
||||
'publish auth session Layer defaults updated % of % locked keys',
|
||||
v_updated,
|
||||
cardinality(v_key_ids)
|
||||
USING ERRCODE = '23000';
|
||||
END IF;
|
||||
|
||||
UPDATE public.authorizations AS authz
|
||||
SET layer = p_layer
|
||||
WHERE authz.auth_key_id = ANY(v_key_ids);
|
||||
|
||||
RETURN QUERY SELECT
|
||||
'ok'::text, v_current_layer, v_current_msg_id,
|
||||
v_current_observation_id, v_current_expires_at,
|
||||
true, true;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.telesrv_advance_auth_session_layer(
|
||||
bigint, bigint, integer, bigint, timestamptz
|
||||
) IS 'Atomically advances durable invokeWithLayer evidence while preserving auth identity lock order';
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
DROP TRIGGER IF EXISTS custom_verifications_peer_identity_changed ON public.custom_verifications;
|
||||
DROP TRIGGER IF EXISTS peer_usernames_peer_identity_changed ON public.peer_usernames;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_notify_peer_identity_row();
|
||||
DROP FUNCTION IF EXISTS public.telesrv_bump_peer_identity(text, bigint);
|
||||
|
||||
DELETE FROM public.read_model_versions
|
||||
WHERE model = 'peer_identity';
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
-- Viewer-independent peer decorations are projected on every peer-bearing RPC
|
||||
-- response. Keep one durable version token for username registry vectors and
|
||||
-- third-party bot-verification marks so positive and negative L1 entries can be
|
||||
-- reused without one PostgreSQL query per response page.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bump_peer_identity(
|
||||
p_peer_type text,
|
||||
p_peer_id bigint
|
||||
) RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF COALESCE(p_peer_id, 0) = 0 OR p_peer_type NOT IN ('user', 'channel') THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
PERFORM public.telesrv_bump_read_model_version(
|
||||
'peer_identity', 0, p_peer_type, p_peer_id
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_peer_identity_row()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
PERFORM public.telesrv_bump_peer_identity(OLD.peer_type, OLD.peer_id);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'UPDATE'
|
||||
AND (OLD.peer_type, OLD.peer_id) IS DISTINCT FROM (NEW.peer_type, NEW.peer_id) THEN
|
||||
PERFORM public.telesrv_bump_peer_identity(OLD.peer_type, OLD.peer_id);
|
||||
END IF;
|
||||
PERFORM public.telesrv_bump_peer_identity(NEW.peer_type, NEW.peer_id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS peer_usernames_peer_identity_changed ON public.peer_usernames;
|
||||
CREATE TRIGGER peer_usernames_peer_identity_changed
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.peer_usernames
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_peer_identity_row();
|
||||
|
||||
DROP TRIGGER IF EXISTS custom_verifications_peer_identity_changed ON public.custom_verifications;
|
||||
CREATE TRIGGER custom_verifications_peer_identity_changed
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.custom_verifications
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_peer_identity_row();
|
||||
|
||||
-- Seed every durable peer, including peers with no registry row/mark. This is
|
||||
-- what makes an empty result a versioned negative fact rather than a TTL guess.
|
||||
INSERT INTO public.read_model_versions (
|
||||
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
|
||||
)
|
||||
SELECT 'peer_identity', 0, 'user', u.id, 1,
|
||||
public.telesrv_random_read_model_hash(), now()
|
||||
FROM public.users u
|
||||
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.read_model_versions (
|
||||
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
|
||||
)
|
||||
SELECT 'peer_identity', 0, 'channel', c.id, 1,
|
||||
public.telesrv_random_read_model_hash(), now()
|
||||
FROM public.channels c
|
||||
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
DROP TRIGGER IF EXISTS users_story_projection_versions_changed ON public.users;
|
||||
DROP TRIGGER IF EXISTS channels_story_projection_version_changed ON public.channels;
|
||||
DROP TRIGGER IF EXISTS telesrv_stories_story_peer_read_model ON public.stories;
|
||||
DROP TRIGGER IF EXISTS telesrv_story_hidden_peers_story_peer_read_model ON public.story_hidden_peers;
|
||||
|
||||
DROP FUNCTION IF EXISTS public.telesrv_maintain_user_story_projection_versions();
|
||||
DROP FUNCTION IF EXISTS public.telesrv_maintain_channel_story_projection_version();
|
||||
DROP FUNCTION IF EXISTS public.telesrv_notify_story_row_read_models();
|
||||
DROP FUNCTION IF EXISTS public.telesrv_notify_story_hidden_row_read_models();
|
||||
DROP FUNCTION IF EXISTS public.telesrv_bump_story_hidden_list(bigint);
|
||||
DROP FUNCTION IF EXISTS public.telesrv_bump_story_peer(text, bigint);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_story_peer_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
o_type text;
|
||||
o_id bigint;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
o_type := OLD.owner_peer_type;
|
||||
o_id := OLD.owner_peer_id;
|
||||
ELSE
|
||||
o_type := NEW.owner_peer_type;
|
||||
o_id := NEW.owner_peer_id;
|
||||
END IF;
|
||||
PERFORM public.telesrv_bump_read_model_version('story_peer', 0, o_type, o_id);
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER telesrv_stories_story_peer_read_model
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.stories
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_story_peer_read_model();
|
||||
|
||||
CREATE TRIGGER telesrv_story_hidden_peers_story_peer_read_model
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.story_hidden_peers
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_story_peer_read_model();
|
||||
|
||||
DELETE FROM public.read_model_versions WHERE model = 'story_hidden_list';
|
||||
-- story_peer rows seeded for peers without stories are harmless under the
|
||||
-- previous contract and are retained so rollback cannot erase real versions.
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
-- Split story peer projection into a shared active-story gate and one sparse
|
||||
-- hidden-peer snapshot per viewer. Both positive and negative facts receive a
|
||||
-- durable token; active expiry is still enforced by expire_date at read time.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bump_story_peer(
|
||||
p_peer_type text,
|
||||
p_peer_id bigint
|
||||
) RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF COALESCE(p_peer_type, '') NOT IN ('user', 'channel')
|
||||
OR COALESCE(p_peer_id, 0) = 0 THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
PERFORM public.telesrv_bump_read_model_version(
|
||||
'story_peer', 0, p_peer_type, p_peer_id
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bump_story_hidden_list(
|
||||
p_viewer_user_id bigint
|
||||
) RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF COALESCE(p_viewer_user_id, 0) = 0 THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
PERFORM public.telesrv_bump_read_model_version(
|
||||
'story_hidden_list', p_viewer_user_id, 'user', p_viewer_user_id
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_story_row_read_models()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
PERFORM public.telesrv_bump_story_peer(OLD.owner_peer_type, OLD.owner_peer_id);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
IF TG_OP = 'UPDATE'
|
||||
AND (OLD.owner_peer_type, OLD.owner_peer_id)
|
||||
IS DISTINCT FROM (NEW.owner_peer_type, NEW.owner_peer_id) THEN
|
||||
PERFORM public.telesrv_bump_story_peer(OLD.owner_peer_type, OLD.owner_peer_id);
|
||||
END IF;
|
||||
PERFORM public.telesrv_bump_story_peer(NEW.owner_peer_type, NEW.owner_peer_id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_story_hidden_row_read_models()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
PERFORM public.telesrv_bump_story_peer(OLD.owner_peer_type, OLD.owner_peer_id);
|
||||
PERFORM public.telesrv_bump_story_hidden_list(OLD.viewer_user_id);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
IF TG_OP = 'UPDATE' THEN
|
||||
IF (OLD.owner_peer_type, OLD.owner_peer_id)
|
||||
IS DISTINCT FROM (NEW.owner_peer_type, NEW.owner_peer_id) THEN
|
||||
PERFORM public.telesrv_bump_story_peer(OLD.owner_peer_type, OLD.owner_peer_id);
|
||||
END IF;
|
||||
IF OLD.viewer_user_id IS DISTINCT FROM NEW.viewer_user_id THEN
|
||||
PERFORM public.telesrv_bump_story_hidden_list(OLD.viewer_user_id);
|
||||
END IF;
|
||||
END IF;
|
||||
PERFORM public.telesrv_bump_story_peer(NEW.owner_peer_type, NEW.owner_peer_id);
|
||||
PERFORM public.telesrv_bump_story_hidden_list(NEW.viewer_user_id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS telesrv_stories_story_peer_read_model ON public.stories;
|
||||
CREATE TRIGGER telesrv_stories_story_peer_read_model
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.stories
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_story_row_read_models();
|
||||
|
||||
DROP TRIGGER IF EXISTS telesrv_story_hidden_peers_story_peer_read_model ON public.story_hidden_peers;
|
||||
CREATE TRIGGER telesrv_story_hidden_peers_story_peer_read_model
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.story_hidden_peers
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_story_hidden_row_read_models();
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_maintain_user_story_projection_versions()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
PERFORM public.telesrv_bump_story_peer('user', OLD.id);
|
||||
PERFORM public.telesrv_bump_story_hidden_list(OLD.id);
|
||||
DELETE FROM public.read_model_versions
|
||||
WHERE (model = 'story_peer' AND owner_user_id = 0
|
||||
AND peer_type = 'user' AND peer_id = OLD.id)
|
||||
OR (model = 'story_hidden_list' AND owner_user_id = OLD.id
|
||||
AND peer_type = 'user' AND peer_id = OLD.id);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
PERFORM public.telesrv_bump_story_peer('user', NEW.id);
|
||||
PERFORM public.telesrv_bump_story_hidden_list(NEW.id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS users_story_projection_versions_changed ON public.users;
|
||||
CREATE TRIGGER users_story_projection_versions_changed
|
||||
AFTER INSERT OR DELETE ON public.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_maintain_user_story_projection_versions();
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_maintain_channel_story_projection_version()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
PERFORM public.telesrv_bump_story_peer('channel', OLD.id);
|
||||
DELETE FROM public.read_model_versions
|
||||
WHERE model = 'story_peer' AND owner_user_id = 0
|
||||
AND peer_type = 'channel' AND peer_id = OLD.id;
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
PERFORM public.telesrv_bump_story_peer('channel', NEW.id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS channels_story_projection_version_changed ON public.channels;
|
||||
CREATE TRIGGER channels_story_projection_version_changed
|
||||
AFTER INSERT OR DELETE ON public.channels
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_maintain_channel_story_projection_version();
|
||||
|
||||
INSERT INTO public.read_model_versions (
|
||||
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
|
||||
)
|
||||
SELECT 'story_peer', 0, 'user', u.id, 1,
|
||||
public.telesrv_random_read_model_hash(), now()
|
||||
FROM public.users u
|
||||
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.read_model_versions (
|
||||
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
|
||||
)
|
||||
SELECT 'story_peer', 0, 'channel', c.id, 1,
|
||||
public.telesrv_random_read_model_hash(), now()
|
||||
FROM public.channels c
|
||||
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.read_model_versions (
|
||||
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
|
||||
)
|
||||
SELECT 'story_hidden_list', u.id, 'user', u.id, 1,
|
||||
public.telesrv_random_read_model_hash(), now()
|
||||
FROM public.users u
|
||||
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
DROP TRIGGER IF EXISTS users_dialog_owner_read_model ON public.users;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_seed_dialog_owner_read_model();
|
||||
|
||||
DELETE FROM public.read_model_versions WHERE model = 'dialog_owner';
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_dialog_light_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
owner_id bigint;
|
||||
peer_type text;
|
||||
peer_id bigint;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
owner_id := OLD.user_id;
|
||||
peer_type := OLD.peer_type;
|
||||
peer_id := OLD.peer_id;
|
||||
ELSE
|
||||
owner_id := NEW.user_id;
|
||||
peer_type := NEW.peer_type;
|
||||
peer_id := NEW.peer_id;
|
||||
END IF;
|
||||
|
||||
PERFORM public.telesrv_bump_dialog_light(owner_id, peer_type, peer_id);
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_dialog_light_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
owner_id bigint;
|
||||
channel_id bigint;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
owner_id := OLD.user_id;
|
||||
channel_id := OLD.channel_id;
|
||||
ELSE
|
||||
owner_id := NEW.user_id;
|
||||
channel_id := NEW.channel_id;
|
||||
END IF;
|
||||
|
||||
PERFORM public.telesrv_bump_dialog_light(owner_id, 'channel', channel_id);
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_member_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
channel_id bigint;
|
||||
user_id bigint;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
channel_id := OLD.channel_id;
|
||||
user_id := OLD.user_id;
|
||||
ELSE
|
||||
channel_id := NEW.channel_id;
|
||||
user_id := NEW.user_id;
|
||||
END IF;
|
||||
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_member', user_id, 'channel', channel_id);
|
||||
PERFORM public.telesrv_bump_dialog_light(user_id, 'channel', channel_id);
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bump_dialog_light(
|
||||
p_owner_user_id bigint,
|
||||
p_peer_type text,
|
||||
p_peer_id bigint
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF COALESCE(p_owner_user_id, 0) = 0
|
||||
OR COALESCE(p_peer_id, 0) = 0
|
||||
OR COALESCE(p_peer_type, '') = ''
|
||||
THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
PERFORM public.telesrv_bump_read_model_version(
|
||||
'dialog_light', p_owner_user_id, p_peer_type, p_peer_id
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP FUNCTION IF EXISTS public.telesrv_bump_dialog_owner(bigint);
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
-- Owner collection generation for the Redis-backed messages.getDialogs header
|
||||
-- snapshot. Shared channel mutations remain channel_base-only; membership and
|
||||
-- owner-local dialog mutations advance this O(1) owner token.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bump_dialog_owner(p_owner_user_id bigint)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF COALESCE(p_owner_user_id, 0) = 0 THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
PERFORM public.telesrv_bump_read_model_version(
|
||||
'dialog_owner', p_owner_user_id, 'user', p_owner_user_id
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- dialog_owner is the owner-wide aggregate of every exact dialog_light
|
||||
-- dependency. Extending the common helper closes private top-message,
|
||||
-- reaction, contact/profile and future exact-dialog invalidation paths without
|
||||
-- copying owner bumps into every trigger.
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bump_dialog_light(
|
||||
p_owner_user_id bigint,
|
||||
p_peer_type text,
|
||||
p_peer_id bigint
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF COALESCE(p_owner_user_id, 0) = 0
|
||||
OR COALESCE(p_peer_id, 0) = 0
|
||||
OR COALESCE(p_peer_type, '') = ''
|
||||
THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
PERFORM public.telesrv_bump_read_model_version(
|
||||
'dialog_light', p_owner_user_id, p_peer_type, p_peer_id
|
||||
);
|
||||
PERFORM public.telesrv_bump_dialog_owner(p_owner_user_id);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_seed_dialog_owner_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM public.telesrv_bump_dialog_owner(NEW.id);
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER users_dialog_owner_read_model
|
||||
AFTER INSERT ON public.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_seed_dialog_owner_read_model();
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_dialog_light_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_owner_id bigint;
|
||||
old_peer_type text;
|
||||
old_peer_id bigint;
|
||||
new_owner_id bigint;
|
||||
new_peer_type text;
|
||||
new_peer_id bigint;
|
||||
BEGIN
|
||||
IF TG_OP <> 'INSERT' THEN
|
||||
old_owner_id := OLD.user_id;
|
||||
old_peer_type := OLD.peer_type;
|
||||
old_peer_id := OLD.peer_id;
|
||||
PERFORM public.telesrv_bump_dialog_light(old_owner_id, old_peer_type, old_peer_id);
|
||||
END IF;
|
||||
|
||||
IF TG_OP <> 'DELETE' THEN
|
||||
new_owner_id := NEW.user_id;
|
||||
new_peer_type := NEW.peer_type;
|
||||
new_peer_id := NEW.peer_id;
|
||||
IF TG_OP = 'INSERT'
|
||||
OR (new_owner_id, new_peer_type, new_peer_id)
|
||||
IS DISTINCT FROM (old_owner_id, old_peer_type, old_peer_id)
|
||||
THEN
|
||||
PERFORM public.telesrv_bump_dialog_light(new_owner_id, new_peer_type, new_peer_id);
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_dialog_light_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_owner_id bigint;
|
||||
old_channel_id bigint;
|
||||
new_owner_id bigint;
|
||||
new_channel_id bigint;
|
||||
BEGIN
|
||||
IF TG_OP <> 'INSERT' THEN
|
||||
old_owner_id := OLD.user_id;
|
||||
old_channel_id := OLD.channel_id;
|
||||
PERFORM public.telesrv_bump_dialog_light(old_owner_id, 'channel', old_channel_id);
|
||||
END IF;
|
||||
|
||||
IF TG_OP <> 'DELETE' THEN
|
||||
new_owner_id := NEW.user_id;
|
||||
new_channel_id := NEW.channel_id;
|
||||
IF TG_OP = 'INSERT'
|
||||
OR (new_owner_id, new_channel_id) IS DISTINCT FROM (old_owner_id, old_channel_id)
|
||||
THEN
|
||||
PERFORM public.telesrv_bump_dialog_light(new_owner_id, 'channel', new_channel_id);
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_member_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_channel_id bigint;
|
||||
old_user_id bigint;
|
||||
new_channel_id bigint;
|
||||
new_user_id bigint;
|
||||
BEGIN
|
||||
IF TG_OP <> 'INSERT' THEN
|
||||
old_channel_id := OLD.channel_id;
|
||||
old_user_id := OLD.user_id;
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_member', old_user_id, 'channel', old_channel_id);
|
||||
PERFORM public.telesrv_bump_dialog_light(old_user_id, 'channel', old_channel_id);
|
||||
END IF;
|
||||
|
||||
IF TG_OP <> 'DELETE' THEN
|
||||
new_channel_id := NEW.channel_id;
|
||||
new_user_id := NEW.user_id;
|
||||
IF TG_OP = 'INSERT'
|
||||
OR (new_user_id, new_channel_id) IS DISTINCT FROM (old_user_id, old_channel_id)
|
||||
THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_member', new_user_id, 'channel', new_channel_id);
|
||||
PERFORM public.telesrv_bump_dialog_light(new_user_id, 'channel', new_channel_id);
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
INSERT INTO public.read_model_versions (
|
||||
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
|
||||
)
|
||||
SELECT 'dialog_owner', u.id, 'user', u.id, 1,
|
||||
public.telesrv_random_read_model_hash(), now()
|
||||
FROM public.users AS u
|
||||
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO NOTHING;
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
DROP FUNCTION IF EXISTS public.telesrv_bump_channel_membership_read_models(bigint, bigint[]);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_member_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_channel_id bigint;
|
||||
old_user_id bigint;
|
||||
new_channel_id bigint;
|
||||
new_user_id bigint;
|
||||
BEGIN
|
||||
IF TG_OP <> 'INSERT' THEN
|
||||
old_channel_id := OLD.channel_id;
|
||||
old_user_id := OLD.user_id;
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_member', old_user_id, 'channel', old_channel_id);
|
||||
PERFORM public.telesrv_bump_dialog_light(old_user_id, 'channel', old_channel_id);
|
||||
END IF;
|
||||
IF TG_OP <> 'DELETE' THEN
|
||||
new_channel_id := NEW.channel_id;
|
||||
new_user_id := NEW.user_id;
|
||||
IF TG_OP = 'INSERT' OR (new_user_id, new_channel_id) IS DISTINCT FROM (old_user_id, old_channel_id) THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_member', new_user_id, 'channel', new_channel_id);
|
||||
PERFORM public.telesrv_bump_dialog_light(new_user_id, 'channel', new_channel_id);
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_participants_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_channel_id bigint;
|
||||
new_channel_id bigint;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN old_channel_id := OLD.channel_id;
|
||||
ELSIF TG_OP = 'INSERT' THEN new_channel_id := NEW.channel_id;
|
||||
ELSE old_channel_id := OLD.channel_id; new_channel_id := NEW.channel_id;
|
||||
END IF;
|
||||
IF old_channel_id IS NOT NULL THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_participants', 0, 'channel', old_channel_id);
|
||||
END IF;
|
||||
IF new_channel_id IS NOT NULL AND new_channel_id IS DISTINCT FROM old_channel_id THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_participants', 0, 'channel', new_channel_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_dialog_light_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_owner_id bigint;
|
||||
old_channel_id bigint;
|
||||
new_owner_id bigint;
|
||||
new_channel_id bigint;
|
||||
BEGIN
|
||||
IF TG_OP <> 'INSERT' THEN
|
||||
old_owner_id := OLD.user_id; old_channel_id := OLD.channel_id;
|
||||
PERFORM public.telesrv_bump_dialog_light(old_owner_id, 'channel', old_channel_id);
|
||||
END IF;
|
||||
IF TG_OP <> 'DELETE' THEN
|
||||
new_owner_id := NEW.user_id; new_channel_id := NEW.channel_id;
|
||||
IF TG_OP = 'INSERT' OR (new_owner_id, new_channel_id) IS DISTINCT FROM (old_owner_id, old_channel_id) THEN
|
||||
PERFORM public.telesrv_bump_dialog_light(new_owner_id, 'channel', new_channel_id);
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_active_memberships_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_owner bigint;
|
||||
new_owner bigint;
|
||||
changed boolean;
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
old_owner := OLD.user_id;
|
||||
IF old_owner <> 0 THEN PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', old_owner, 'user', old_owner); END IF;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
new_owner := NEW.user_id;
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
IF new_owner <> 0 THEN PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', new_owner, 'user', new_owner); END IF;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
old_owner := OLD.user_id;
|
||||
changed := OLD.user_id IS DISTINCT FROM NEW.user_id OR OLD.channel_id IS DISTINCT FROM NEW.channel_id OR
|
||||
OLD.status IS DISTINCT FROM NEW.status OR OLD.deleted IS DISTINCT FROM NEW.deleted;
|
||||
IF changed THEN
|
||||
IF old_owner <> 0 THEN PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', old_owner, 'user', old_owner); END IF;
|
||||
IF new_owner <> 0 AND new_owner IS DISTINCT FROM old_owner THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', new_owner, 'user', new_owner);
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP FUNCTION IF EXISTS public.telesrv_membership_batch_active();
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
-- Batch membership mutations are one durable state transition. Row triggers
|
||||
-- remain authoritative for ordinary statements, while the explicitly scoped
|
||||
-- batch path suppresses their per-row write amplification and advances every
|
||||
-- distinct dependency once before the transaction may commit.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_membership_batch_active()
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $$
|
||||
SELECT COALESCE(current_setting('telesrv.membership_batch_mode', true), '') = 'on'
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_member_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_channel_id bigint;
|
||||
old_user_id bigint;
|
||||
new_channel_id bigint;
|
||||
new_user_id bigint;
|
||||
BEGIN
|
||||
IF public.telesrv_membership_batch_active() THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
IF TG_OP <> 'INSERT' THEN
|
||||
old_channel_id := OLD.channel_id;
|
||||
old_user_id := OLD.user_id;
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_member', old_user_id, 'channel', old_channel_id);
|
||||
PERFORM public.telesrv_bump_dialog_light(old_user_id, 'channel', old_channel_id);
|
||||
END IF;
|
||||
|
||||
IF TG_OP <> 'DELETE' THEN
|
||||
new_channel_id := NEW.channel_id;
|
||||
new_user_id := NEW.user_id;
|
||||
IF TG_OP = 'INSERT'
|
||||
OR (new_user_id, new_channel_id) IS DISTINCT FROM (old_user_id, old_channel_id)
|
||||
THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_member', new_user_id, 'channel', new_channel_id);
|
||||
PERFORM public.telesrv_bump_dialog_light(new_user_id, 'channel', new_channel_id);
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_participants_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_channel_id bigint;
|
||||
new_channel_id bigint;
|
||||
BEGIN
|
||||
IF public.telesrv_membership_batch_active() THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
old_channel_id := OLD.channel_id;
|
||||
ELSIF TG_OP = 'INSERT' THEN
|
||||
new_channel_id := NEW.channel_id;
|
||||
ELSE
|
||||
old_channel_id := OLD.channel_id;
|
||||
new_channel_id := NEW.channel_id;
|
||||
END IF;
|
||||
|
||||
IF old_channel_id IS NOT NULL THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_participants', 0, 'channel', old_channel_id);
|
||||
END IF;
|
||||
IF new_channel_id IS NOT NULL AND new_channel_id IS DISTINCT FROM old_channel_id THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_participants', 0, 'channel', new_channel_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_dialog_light_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_owner_id bigint;
|
||||
old_channel_id bigint;
|
||||
new_owner_id bigint;
|
||||
new_channel_id bigint;
|
||||
BEGIN
|
||||
IF public.telesrv_membership_batch_active() THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
IF TG_OP <> 'INSERT' THEN
|
||||
old_owner_id := OLD.user_id;
|
||||
old_channel_id := OLD.channel_id;
|
||||
PERFORM public.telesrv_bump_dialog_light(old_owner_id, 'channel', old_channel_id);
|
||||
END IF;
|
||||
|
||||
IF TG_OP <> 'DELETE' THEN
|
||||
new_owner_id := NEW.user_id;
|
||||
new_channel_id := NEW.channel_id;
|
||||
IF TG_OP = 'INSERT'
|
||||
OR (new_owner_id, new_channel_id) IS DISTINCT FROM (old_owner_id, old_channel_id)
|
||||
THEN
|
||||
PERFORM public.telesrv_bump_dialog_light(new_owner_id, 'channel', new_channel_id);
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_channel_active_memberships_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_owner bigint;
|
||||
new_owner bigint;
|
||||
changed boolean;
|
||||
BEGIN
|
||||
IF public.telesrv_membership_batch_active() THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
old_owner := OLD.user_id;
|
||||
IF old_owner <> 0 THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', old_owner, 'user', old_owner);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
new_owner := NEW.user_id;
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
IF new_owner <> 0 THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', new_owner, 'user', new_owner);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
old_owner := OLD.user_id;
|
||||
changed :=
|
||||
OLD.user_id IS DISTINCT FROM NEW.user_id OR
|
||||
OLD.channel_id IS DISTINCT FROM NEW.channel_id OR
|
||||
OLD.status IS DISTINCT FROM NEW.status OR
|
||||
OLD.deleted IS DISTINCT FROM NEW.deleted;
|
||||
IF changed THEN
|
||||
IF old_owner <> 0 THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', old_owner, 'user', old_owner);
|
||||
END IF;
|
||||
IF new_owner <> 0 AND new_owner IS DISTINCT FROM old_owner THEN
|
||||
PERFORM public.telesrv_bump_read_model_version('channel_active_memberships', new_owner, 'user', new_owner);
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bump_channel_membership_read_models(
|
||||
p_channel_id bigint,
|
||||
p_user_ids bigint[]
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
changed record;
|
||||
BEGIN
|
||||
IF COALESCE(p_channel_id, 0) <= 0 OR COALESCE(cardinality(p_user_ids), 0) = 0 THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
FOR changed IN
|
||||
WITH requested AS MATERIALIZED (
|
||||
SELECT DISTINCT user_id
|
||||
FROM unnest(p_user_ids) AS input(user_id)
|
||||
WHERE user_id > 0
|
||||
), keys AS MATERIALIZED (
|
||||
SELECT 10 AS priority, 'channel_participants'::text AS model, 0::bigint AS owner_user_id,
|
||||
'channel'::text AS peer_type, p_channel_id AS peer_id
|
||||
UNION
|
||||
SELECT 20, 'channel_member', user_id, 'channel', p_channel_id FROM requested
|
||||
UNION
|
||||
SELECT 30, 'dialog_light', user_id, 'channel', p_channel_id FROM requested
|
||||
UNION
|
||||
SELECT 40, 'dialog_owner', user_id, 'user', user_id FROM requested
|
||||
UNION
|
||||
SELECT 50, 'channel_active_memberships', user_id, 'user', user_id FROM requested
|
||||
), bumped AS (
|
||||
INSERT INTO public.read_model_versions (
|
||||
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
|
||||
)
|
||||
SELECT model, owner_user_id, peer_type, peer_id, 1,
|
||||
public.telesrv_random_read_model_hash(), now()
|
||||
FROM keys
|
||||
ORDER BY priority, owner_user_id, peer_type, peer_id
|
||||
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO UPDATE SET
|
||||
version = public.read_model_versions.version + 1,
|
||||
hash = public.telesrv_random_read_model_hash(),
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING model, owner_user_id, peer_type, peer_id, version, hash
|
||||
)
|
||||
SELECT model, owner_user_id, peer_type, peer_id, version, hash
|
||||
FROM bumped
|
||||
ORDER BY model, owner_user_id, peer_type, peer_id
|
||||
LOOP
|
||||
PERFORM pg_notify(
|
||||
'telesrv_read_model_changed',
|
||||
json_build_object(
|
||||
'model', changed.model,
|
||||
'owner_user_id', changed.owner_user_id,
|
||||
'peer_type', changed.peer_type,
|
||||
'peer_id', changed.peer_id,
|
||||
'version', changed.version,
|
||||
'hash', changed.hash
|
||||
)::text
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
CREATE OR REPLACE FUNCTION public.telesrv_bump_channel_membership_read_models(
|
||||
p_channel_id bigint,
|
||||
p_user_ids bigint[]
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
changed record;
|
||||
BEGIN
|
||||
IF COALESCE(p_channel_id, 0) <= 0 OR COALESCE(cardinality(p_user_ids), 0) = 0 THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
FOR changed IN
|
||||
WITH requested AS MATERIALIZED (
|
||||
SELECT DISTINCT user_id
|
||||
FROM unnest(p_user_ids) AS input(user_id)
|
||||
WHERE user_id > 0
|
||||
), keys AS MATERIALIZED (
|
||||
SELECT 'channel_participants'::text AS model, 0::bigint AS owner_user_id,
|
||||
'channel'::text AS peer_type, p_channel_id AS peer_id
|
||||
UNION
|
||||
SELECT 'channel_member', user_id, 'channel', p_channel_id FROM requested
|
||||
UNION
|
||||
SELECT 'dialog_light', user_id, 'channel', p_channel_id FROM requested
|
||||
UNION
|
||||
SELECT 'dialog_owner', user_id, 'user', user_id FROM requested
|
||||
UNION
|
||||
SELECT 'channel_active_memberships', user_id, 'user', user_id FROM requested
|
||||
), bumped AS (
|
||||
INSERT INTO public.read_model_versions (
|
||||
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
|
||||
)
|
||||
SELECT model, owner_user_id, peer_type, peer_id, 1,
|
||||
public.telesrv_random_read_model_hash(), now()
|
||||
FROM keys
|
||||
ORDER BY model, owner_user_id, peer_type, peer_id
|
||||
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO UPDATE SET
|
||||
version = public.read_model_versions.version + 1,
|
||||
hash = public.telesrv_random_read_model_hash(),
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING model, owner_user_id, peer_type, peer_id, version, hash
|
||||
)
|
||||
SELECT model, owner_user_id, peer_type, peer_id, version, hash
|
||||
FROM bumped
|
||||
ORDER BY model, owner_user_id, peer_type, peer_id
|
||||
LOOP
|
||||
PERFORM pg_notify(
|
||||
'telesrv_read_model_changed',
|
||||
json_build_object(
|
||||
'model', changed.model,
|
||||
'owner_user_id', changed.owner_user_id,
|
||||
'peer_type', changed.peer_type,
|
||||
'peer_id', changed.peer_id,
|
||||
'version', changed.version,
|
||||
'hash', changed.hash
|
||||
)::text
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
-- 20260901000017 initially ordered version keys lexically, placing
|
||||
-- channel_active_memberships before the member/dialog keys. Ordinary single-row
|
||||
-- membership writes acquire those keys in the opposite order, so a concurrent
|
||||
-- createChannel and batch invite could deadlock. Use the canonical physical
|
||||
-- mutation order: participants -> member -> dialog -> owner -> active index.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bump_channel_membership_read_models(
|
||||
p_channel_id bigint,
|
||||
p_user_ids bigint[]
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
changed record;
|
||||
BEGIN
|
||||
IF COALESCE(p_channel_id, 0) <= 0 OR COALESCE(cardinality(p_user_ids), 0) = 0 THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
FOR changed IN
|
||||
WITH requested AS MATERIALIZED (
|
||||
SELECT DISTINCT user_id
|
||||
FROM unnest(p_user_ids) AS input(user_id)
|
||||
WHERE user_id > 0
|
||||
), keys AS MATERIALIZED (
|
||||
SELECT 10 AS priority, 'channel_participants'::text AS model, 0::bigint AS owner_user_id,
|
||||
'channel'::text AS peer_type, p_channel_id AS peer_id
|
||||
UNION
|
||||
SELECT 20, 'channel_member', user_id, 'channel', p_channel_id FROM requested
|
||||
UNION
|
||||
SELECT 30, 'dialog_light', user_id, 'channel', p_channel_id FROM requested
|
||||
UNION
|
||||
SELECT 40, 'dialog_owner', user_id, 'user', user_id FROM requested
|
||||
UNION
|
||||
SELECT 50, 'channel_active_memberships', user_id, 'user', user_id FROM requested
|
||||
), bumped AS (
|
||||
INSERT INTO public.read_model_versions (
|
||||
model, owner_user_id, peer_type, peer_id, version, hash, updated_at
|
||||
)
|
||||
SELECT model, owner_user_id, peer_type, peer_id, 1,
|
||||
public.telesrv_random_read_model_hash(), now()
|
||||
FROM keys
|
||||
ORDER BY priority, owner_user_id, peer_type, peer_id
|
||||
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO UPDATE SET
|
||||
version = public.read_model_versions.version + 1,
|
||||
hash = public.telesrv_random_read_model_hash(),
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING model, owner_user_id, peer_type, peer_id, version, hash
|
||||
)
|
||||
SELECT model, owner_user_id, peer_type, peer_id, version, hash
|
||||
FROM bumped
|
||||
ORDER BY model, owner_user_id, peer_type, peer_id
|
||||
LOOP
|
||||
PERFORM pg_notify(
|
||||
'telesrv_read_model_changed',
|
||||
json_build_object(
|
||||
'model', changed.model,
|
||||
'owner_user_id', changed.owner_user_id,
|
||||
'peer_type', changed.peer_type,
|
||||
'peer_id', changed.peer_id,
|
||||
'version', changed.version,
|
||||
'hash', changed.hash
|
||||
)::text
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
DROP TRIGGER IF EXISTS channels_peer_identity_created ON public.channels;
|
||||
DROP TRIGGER IF EXISTS users_peer_identity_created ON public.users;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_seed_peer_identity_on_insert();
|
||||
|
||||
-- Backfilled read_model_versions rows are intentionally retained. 20260901000014 defines
|
||||
-- one token for every durable peer; deleting them would violate the older
|
||||
-- migration's contract after rollback and make negative facts uncacheable.
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
-- 20260901000014 seeded only the peers that existed when that migration ran. Real account
|
||||
-- provisioning and channel creation after deployment therefore had no durable
|
||||
-- token, so their empty username/verification facts were deliberately
|
||||
-- uncacheable. Maintain the token at the peer lifecycle boundary instead.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_seed_peer_identity_on_insert()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF TG_TABLE_NAME = 'users' THEN
|
||||
PERFORM public.telesrv_bump_peer_identity('user', NEW.id);
|
||||
ELSIF TG_TABLE_NAME = 'channels' THEN
|
||||
PERFORM public.telesrv_bump_peer_identity('channel', NEW.id);
|
||||
ELSE
|
||||
RAISE EXCEPTION 'unsupported peer identity seed table: %', TG_TABLE_NAME;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS users_peer_identity_created ON public.users;
|
||||
CREATE TRIGGER users_peer_identity_created
|
||||
AFTER INSERT ON public.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_seed_peer_identity_on_insert();
|
||||
|
||||
DROP TRIGGER IF EXISTS channels_peer_identity_created ON public.channels;
|
||||
CREATE TRIGGER channels_peer_identity_created
|
||||
AFTER INSERT ON public.channels
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_seed_peer_identity_on_insert();
|
||||
|
||||
-- Use the same bump helper rather than a silent INSERT so already-running
|
||||
-- instances that cached a missing/zero token receive exact-key invalidation.
|
||||
DO $$
|
||||
DECLARE
|
||||
peer_row record;
|
||||
BEGIN
|
||||
FOR peer_row IN
|
||||
SELECT u.id
|
||||
FROM public.users u
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM public.read_model_versions v
|
||||
WHERE v.model = 'peer_identity'
|
||||
AND v.owner_user_id = 0
|
||||
AND v.peer_type = 'user'
|
||||
AND v.peer_id = u.id
|
||||
)
|
||||
ORDER BY u.id
|
||||
LOOP
|
||||
PERFORM public.telesrv_bump_peer_identity('user', peer_row.id);
|
||||
END LOOP;
|
||||
|
||||
FOR peer_row IN
|
||||
SELECT c.id
|
||||
FROM public.channels c
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM public.read_model_versions v
|
||||
WHERE v.model = 'peer_identity'
|
||||
AND v.owner_user_id = 0
|
||||
AND v.peer_type = 'channel'
|
||||
AND v.peer_id = c.id
|
||||
)
|
||||
ORDER BY c.id
|
||||
LOOP
|
||||
PERFORM public.telesrv_bump_peer_identity('channel', peer_row.id);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
DROP FUNCTION IF EXISTS public.telesrv_bind_temp_auth_key(
|
||||
bigint,
|
||||
bigint,
|
||||
bigint,
|
||||
bigint,
|
||||
integer,
|
||||
bytea
|
||||
);
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
-- Collapse the identity-sensitive auth.bindTempAuthKey write boundary into one
|
||||
-- database call. The caller still owns the surrounding transaction/savepoint;
|
||||
-- this function preserves the global identity -> raw temp -> permanent row
|
||||
-- lock order shared with selector advance, revocation and direct deletion.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bind_temp_auth_key(
|
||||
p_temp_auth_key_id bigint,
|
||||
p_perm_auth_key_id bigint,
|
||||
p_nonce bigint,
|
||||
p_temp_session_id bigint,
|
||||
p_expires_at integer,
|
||||
p_encrypted_message bytea
|
||||
)
|
||||
RETURNS TABLE (
|
||||
bind_status text,
|
||||
merged_layer integer,
|
||||
merged_observation_id bigint
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_temp_expiry integer;
|
||||
v_temp_layer integer;
|
||||
v_temp_observation_id bigint;
|
||||
v_current_perm_id bigint;
|
||||
v_perm_expiry integer;
|
||||
v_perm_layer integer;
|
||||
v_perm_observation_id bigint;
|
||||
v_merged_layer integer;
|
||||
v_merged_observation_id bigint;
|
||||
v_updated integer;
|
||||
BEGIN
|
||||
IF p_expires_at <= 0 OR p_temp_auth_key_id = p_perm_auth_key_id THEN
|
||||
RETURN QUERY SELECT 'binding_invalid'::text, 0, 0::bigint;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
PERFORM pg_advisory_xact_lock(
|
||||
1096111176::integer,
|
||||
hashint8(p_perm_auth_key_id)::integer
|
||||
);
|
||||
|
||||
SELECT key.expires_at, key.layer, key.layer_observation_id
|
||||
INTO v_temp_expiry, v_temp_layer, v_temp_observation_id
|
||||
FROM public.auth_keys AS key
|
||||
WHERE key.auth_key_id = p_temp_auth_key_id
|
||||
FOR UPDATE;
|
||||
IF NOT FOUND OR v_temp_expiry <= 0 OR v_temp_expiry <> p_expires_at THEN
|
||||
RETURN QUERY SELECT 'binding_invalid'::text, 0, 0::bigint;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT binding.perm_auth_key_id
|
||||
INTO v_current_perm_id
|
||||
FROM public.temp_auth_key_bindings AS binding
|
||||
WHERE binding.temp_auth_key_id = p_temp_auth_key_id;
|
||||
IF FOUND AND v_current_perm_id <> p_perm_auth_key_id THEN
|
||||
RETURN QUERY SELECT 'already_bound'::text, 0, 0::bigint;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT key.expires_at, key.layer, key.layer_observation_id
|
||||
INTO v_perm_expiry, v_perm_layer, v_perm_observation_id
|
||||
FROM public.auth_keys AS key
|
||||
WHERE key.auth_key_id = p_perm_auth_key_id
|
||||
FOR UPDATE;
|
||||
IF NOT FOUND OR v_perm_expiry <> 0 THEN
|
||||
RETURN QUERY SELECT 'binding_invalid'::text, 0, 0::bigint;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF v_temp_layer < 0
|
||||
OR v_perm_layer < 0
|
||||
OR v_temp_observation_id < 0
|
||||
OR v_perm_observation_id < 0
|
||||
OR (v_temp_observation_id > 0 AND v_temp_layer = 0)
|
||||
OR (v_perm_observation_id > 0 AND v_perm_layer = 0)
|
||||
THEN
|
||||
RETURN QUERY SELECT 'layer_invalid'::text, 0, 0::bigint;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF v_temp_observation_id > v_perm_observation_id THEN
|
||||
v_merged_layer := v_temp_layer;
|
||||
v_merged_observation_id := v_temp_observation_id;
|
||||
ELSIF v_perm_observation_id > v_temp_observation_id THEN
|
||||
v_merged_layer := v_perm_layer;
|
||||
v_merged_observation_id := v_perm_observation_id;
|
||||
ELSIF v_temp_observation_id > 0 AND v_temp_layer <> v_perm_layer THEN
|
||||
RETURN QUERY SELECT 'layer_conflict'::text, 0, 0::bigint;
|
||||
RETURN;
|
||||
ELSIF v_temp_observation_id > 0 THEN
|
||||
v_merged_layer := v_temp_layer;
|
||||
v_merged_observation_id := v_temp_observation_id;
|
||||
ELSE
|
||||
v_merged_layer := v_perm_layer;
|
||||
v_merged_observation_id := 0;
|
||||
END IF;
|
||||
|
||||
INSERT INTO public.temp_auth_key_bindings (
|
||||
temp_auth_key_id,
|
||||
perm_auth_key_id,
|
||||
nonce,
|
||||
temp_session_id,
|
||||
expires_at,
|
||||
encrypted_message
|
||||
) VALUES (
|
||||
p_temp_auth_key_id,
|
||||
p_perm_auth_key_id,
|
||||
p_nonce,
|
||||
p_temp_session_id,
|
||||
p_expires_at,
|
||||
p_encrypted_message
|
||||
)
|
||||
ON CONFLICT (temp_auth_key_id) DO UPDATE SET
|
||||
nonce = EXCLUDED.nonce,
|
||||
temp_session_id = EXCLUDED.temp_session_id,
|
||||
expires_at = EXCLUDED.expires_at,
|
||||
encrypted_message = EXCLUDED.encrypted_message,
|
||||
created_at = now()
|
||||
WHERE public.temp_auth_key_bindings.perm_auth_key_id = EXCLUDED.perm_auth_key_id;
|
||||
GET DIAGNOSTICS v_updated = ROW_COUNT;
|
||||
IF v_updated <> 1 THEN
|
||||
RETURN QUERY SELECT 'binding_invalid'::text, 0, 0::bigint;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
UPDATE public.auth_keys AS key
|
||||
SET layer = v_merged_layer,
|
||||
layer_observation_id = v_merged_observation_id
|
||||
WHERE key.auth_key_id = ANY(
|
||||
ARRAY[p_temp_auth_key_id, p_perm_auth_key_id]::bigint[]
|
||||
);
|
||||
GET DIAGNOSTICS v_updated = ROW_COUNT;
|
||||
IF v_updated <> 2 THEN
|
||||
RAISE EXCEPTION
|
||||
'merge bound auth key Layer defaults updated % of 2 locked keys',
|
||||
v_updated
|
||||
USING ERRCODE = '23000';
|
||||
END IF;
|
||||
|
||||
UPDATE public.authorizations AS authz
|
||||
SET layer = v_merged_layer
|
||||
WHERE authz.auth_key_id = ANY(
|
||||
ARRAY[p_temp_auth_key_id, p_perm_auth_key_id]::bigint[]
|
||||
);
|
||||
|
||||
RETURN QUERY SELECT 'ok'::text, v_merged_layer, v_merged_observation_id;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.telesrv_bind_temp_auth_key(
|
||||
bigint, bigint, bigint, bigint, integer, bytea
|
||||
) IS 'Atomically binds a temporary auth key and returns the committed Layer observation while preserving identity lock order';
|
||||
|
|
@ -0,0 +1 @@
|
|||
DROP INDEX IF EXISTS public.bootstrap_update_jobs_pending_auth_idx;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
CREATE INDEX IF NOT EXISTS bootstrap_update_jobs_pending_auth_idx
|
||||
ON public.bootstrap_update_jobs (user_id, auth_key_id, id)
|
||||
WHERE (status)::text = 'pending'::text;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
DROP TRIGGER IF EXISTS channels_monoforum_active_membership_changed ON public.channels;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_notify_monoforum_channel_visibility_read_model();
|
||||
DROP FUNCTION IF EXISTS public.telesrv_bump_monoforum_visibility_owners(bigint, bigint);
|
||||
|
||||
DROP TRIGGER IF EXISTS channel_members_monoforum_manager_visibility_changed ON public.channel_members;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_notify_monoforum_manager_visibility_read_model();
|
||||
|
||||
DROP TRIGGER IF EXISTS channel_messages_monoforum_active_membership_changed ON public.channel_messages;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_notify_monoforum_message_visibility_read_model();
|
||||
DROP FUNCTION IF EXISTS public.telesrv_bump_channel_active_membership_owner(bigint);
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
-- Extend the owner-scoped channel_active_memberships generation to every
|
||||
-- durable predicate used by ListActiveChannelIDsForUser. Ordinary memberships
|
||||
-- are covered by user_channel_member_index; these triggers cover monoforum
|
||||
-- subscriber message visibility, manager rights, and parent/mono enablement.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bump_channel_active_membership_owner(p_user_id bigint)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF COALESCE(p_user_id, 0) <= 0 THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
PERFORM public.telesrv_bump_read_model_version(
|
||||
'channel_active_memberships', p_user_id, 'user', p_user_id
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_monoforum_message_visibility_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_owner bigint := 0;
|
||||
new_owner bigint := 0;
|
||||
BEGIN
|
||||
IF TG_OP <> 'INSERT'
|
||||
AND OLD.saved_peer_type = 'user'
|
||||
AND OLD.saved_peer_id > 0
|
||||
AND NOT OLD.deleted
|
||||
THEN
|
||||
old_owner := OLD.saved_peer_id;
|
||||
END IF;
|
||||
IF TG_OP <> 'DELETE'
|
||||
AND NEW.saved_peer_type = 'user'
|
||||
AND NEW.saved_peer_id > 0
|
||||
AND NOT NEW.deleted
|
||||
THEN
|
||||
new_owner := NEW.saved_peer_id;
|
||||
END IF;
|
||||
|
||||
IF old_owner > 0 THEN
|
||||
PERFORM public.telesrv_bump_channel_active_membership_owner(old_owner);
|
||||
END IF;
|
||||
IF new_owner > 0 AND new_owner IS DISTINCT FROM old_owner THEN
|
||||
PERFORM public.telesrv_bump_channel_active_membership_owner(new_owner);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER channel_messages_monoforum_active_membership_changed
|
||||
AFTER INSERT OR DELETE OR UPDATE OF channel_id, saved_peer_type, saved_peer_id, deleted
|
||||
ON public.channel_messages
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_monoforum_message_visibility_read_model();
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_monoforum_manager_visibility_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF OLD.role IS NOT DISTINCT FROM NEW.role
|
||||
AND OLD.admin_rights IS NOT DISTINCT FROM NEW.admin_rights
|
||||
THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM public.channels AS parent
|
||||
WHERE parent.id = NEW.channel_id
|
||||
AND parent.linked_monoforum_id <> 0
|
||||
AND parent.broadcast_messages_allowed
|
||||
AND NOT parent.deleted
|
||||
) THEN
|
||||
PERFORM public.telesrv_bump_channel_active_membership_owner(NEW.user_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER channel_members_monoforum_manager_visibility_changed
|
||||
AFTER UPDATE OF role, admin_rights ON public.channel_members
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_monoforum_manager_visibility_read_model();
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_bump_monoforum_visibility_owners(
|
||||
p_parent_id bigint,
|
||||
p_monoforum_id bigint
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
affected record;
|
||||
BEGIN
|
||||
IF COALESCE(p_parent_id, 0) <= 0 OR COALESCE(p_monoforum_id, 0) <= 0 THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
FOR affected IN
|
||||
SELECT DISTINCT owner_id
|
||||
FROM (
|
||||
SELECT member.user_id AS owner_id
|
||||
FROM public.user_channel_member_index AS member
|
||||
WHERE member.channel_id = p_parent_id
|
||||
AND member.status = 'active'
|
||||
AND NOT member.deleted
|
||||
AND member.role IN ('creator', 'admin')
|
||||
UNION
|
||||
SELECT message.saved_peer_id AS owner_id
|
||||
FROM public.channel_messages AS message
|
||||
WHERE message.channel_id = p_monoforum_id
|
||||
AND message.saved_peer_type = 'user'
|
||||
AND message.saved_peer_id > 0
|
||||
AND NOT message.deleted
|
||||
) AS owners
|
||||
WHERE owner_id > 0
|
||||
ORDER BY owner_id
|
||||
LOOP
|
||||
PERFORM public.telesrv_bump_channel_active_membership_owner(affected.owner_id);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_notify_monoforum_channel_visibility_read_model()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
old_parent_id bigint := 0;
|
||||
old_monoforum_id bigint := 0;
|
||||
new_parent_id bigint := 0;
|
||||
new_monoforum_id bigint := 0;
|
||||
BEGIN
|
||||
IF (OLD.broadcast_messages_allowed, OLD.linked_monoforum_id, OLD.deleted, OLD.monoforum)
|
||||
IS NOT DISTINCT FROM
|
||||
(NEW.broadcast_messages_allowed, NEW.linked_monoforum_id, NEW.deleted, NEW.monoforum)
|
||||
THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
IF OLD.linked_monoforum_id > 0 THEN
|
||||
IF OLD.monoforum THEN
|
||||
old_parent_id := OLD.linked_monoforum_id;
|
||||
old_monoforum_id := OLD.id;
|
||||
ELSE
|
||||
old_parent_id := OLD.id;
|
||||
old_monoforum_id := OLD.linked_monoforum_id;
|
||||
END IF;
|
||||
END IF;
|
||||
IF NEW.linked_monoforum_id > 0 THEN
|
||||
IF NEW.monoforum THEN
|
||||
new_parent_id := NEW.linked_monoforum_id;
|
||||
new_monoforum_id := NEW.id;
|
||||
ELSE
|
||||
new_parent_id := NEW.id;
|
||||
new_monoforum_id := NEW.linked_monoforum_id;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF old_parent_id > 0 THEN
|
||||
PERFORM public.telesrv_bump_monoforum_visibility_owners(old_parent_id, old_monoforum_id);
|
||||
END IF;
|
||||
IF new_parent_id > 0
|
||||
AND (new_parent_id, new_monoforum_id)
|
||||
IS DISTINCT FROM (old_parent_id, old_monoforum_id)
|
||||
THEN
|
||||
PERFORM public.telesrv_bump_monoforum_visibility_owners(new_parent_id, new_monoforum_id);
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER channels_monoforum_active_membership_changed
|
||||
AFTER UPDATE OF broadcast_messages_allowed, linked_monoforum_id, deleted, monoforum
|
||||
ON public.channels
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_notify_monoforum_channel_visibility_read_model();
|
||||
|
||||
-- Existing monoforum owners need a fresh generation before any shared page can
|
||||
-- be populated under the expanded dependency contract.
|
||||
DO $$
|
||||
DECLARE
|
||||
linked record;
|
||||
BEGIN
|
||||
FOR linked IN
|
||||
SELECT parent.id AS parent_id, parent.linked_monoforum_id AS monoforum_id
|
||||
FROM public.channels AS parent
|
||||
WHERE parent.linked_monoforum_id > 0
|
||||
LOOP
|
||||
PERFORM public.telesrv_bump_monoforum_visibility_owners(
|
||||
linked.parent_id, linked.monoforum_id
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
ALTER TABLE public.bot_verifier_settings
|
||||
DROP CONSTRAINT bot_verifier_settings_default_description_check,
|
||||
ADD CONSTRAINT bot_verifier_settings_default_description_check
|
||||
CHECK (octet_length(default_description) <= 280);
|
||||
|
||||
ALTER TABLE public.custom_verification_requests
|
||||
DROP CONSTRAINT custom_verification_requests_requested_description_check,
|
||||
ADD CONSTRAINT custom_verification_requests_requested_description_check
|
||||
CHECK (octet_length(requested_description) <= 280);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
ALTER TABLE public.bot_verifier_settings
|
||||
DROP CONSTRAINT bot_verifier_settings_default_description_check,
|
||||
ADD CONSTRAINT bot_verifier_settings_default_description_check
|
||||
CHECK (octet_length(default_description) <= 512);
|
||||
|
||||
ALTER TABLE public.custom_verification_requests
|
||||
DROP CONSTRAINT custom_verification_requests_requested_description_check,
|
||||
ADD CONSTRAINT custom_verification_requests_requested_description_check
|
||||
CHECK (octet_length(requested_description) <= 512);
|
||||
Loading…
Add table
Add a link
Reference in a new issue