-- 0022_channels: supergroup/channel storage. -- -- Channel messages are single-copy. Per-user dialog/read state is stored separately. -- Channel pts is scoped by channel_id and persisted in channel_update_events. CREATE TABLE IF NOT EXISTS channels ( id BIGINT NOT NULL, access_hash BIGINT NOT NULL, creator_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT, title TEXT NOT NULL, about TEXT NOT NULL DEFAULT '', username TEXT, broadcast BOOLEAN NOT NULL DEFAULT false, megagroup BOOLEAN NOT NULL DEFAULT false, forum BOOLEAN NOT NULL DEFAULT false, forum_tabs BOOLEAN NOT NULL DEFAULT false, noforwards BOOLEAN NOT NULL DEFAULT false, join_to_send BOOLEAN NOT NULL DEFAULT false, join_request BOOLEAN NOT NULL DEFAULT false, signatures BOOLEAN NOT NULL DEFAULT false, pre_history_hidden BOOLEAN NOT NULL DEFAULT false, participants_hidden BOOLEAN NOT NULL DEFAULT false, antispam BOOLEAN NOT NULL DEFAULT false, linked_chat_id BIGINT NOT NULL DEFAULT 0, slowmode_seconds INT NOT NULL DEFAULT 0, default_banned_rights JSONB NOT NULL DEFAULT '{}'::jsonb, available_reactions JSONB NOT NULL DEFAULT '{}'::jsonb, color_set BOOLEAN NOT NULL DEFAULT false, color INT NOT NULL DEFAULT 0, color_background_emoji_id BIGINT NOT NULL DEFAULT 0, profile_color_set BOOLEAN NOT NULL DEFAULT false, profile_color INT NOT NULL DEFAULT 0, profile_color_background_emoji_id BIGINT NOT NULL DEFAULT 0, emoji_status_document_id BIGINT NOT NULL DEFAULT 0, emoji_status_until INT NOT NULL DEFAULT 0, participants_count INT NOT NULL DEFAULT 0, admins_count INT NOT NULL DEFAULT 0, kicked_count INT NOT NULL DEFAULT 0, banned_count INT NOT NULL DEFAULT 0, top_message_id INT NOT NULL DEFAULT 0, pts INT NOT NULL DEFAULT 0, admin_log_seq BIGINT NOT NULL DEFAULT 0, ttl_period INT NOT NULL DEFAULT 0, date INT NOT NULL, deleted BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (id), CONSTRAINT channels_kind_check CHECK ( ((broadcast AND NOT megagroup AND NOT forum) OR (megagroup AND NOT broadcast)) AND (NOT forum_tabs OR forum) ), CONSTRAINT channels_title_nonempty_check CHECK (title <> '') ) PARTITION BY HASH (id); CREATE UNIQUE INDEX IF NOT EXISTS channels_access_hash_idx ON channels (id, access_hash); CREATE INDEX IF NOT EXISTS channels_creator_idx ON channels (creator_user_id, id DESC) WHERE NOT deleted; CREATE INDEX IF NOT EXISTS channels_linked_chat_idx ON channels (linked_chat_id) WHERE linked_chat_id <> 0 AND NOT deleted; DO $$ DECLARE i int; BEGIN FOR i IN 0..63 LOOP EXECUTE format( 'CREATE TABLE IF NOT EXISTS channels_p%s PARTITION OF channels FOR VALUES WITH (MODULUS 64, REMAINDER %s)', lpad(i::text, 2, '0'), i ); END LOOP; END $$; -- PostgreSQL global unique indexes on partitioned tables must include the partition key. -- Keep username uniqueness in a compact lookup table instead of relying on channels(username). CREATE TABLE IF NOT EXISTS channel_usernames ( username_lower TEXT PRIMARY KEY, channel_id BIGINT NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT channel_usernames_nonempty_check CHECK (username_lower <> '') ); CREATE TABLE IF NOT EXISTS channel_members ( channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, inviter_user_id BIGINT NOT NULL DEFAULT 0, role VARCHAR(16) NOT NULL DEFAULT 'member', status VARCHAR(16) NOT NULL DEFAULT 'active', joined_at INT NOT NULL DEFAULT 0, left_at INT NOT NULL DEFAULT 0, admin_rights JSONB NOT NULL DEFAULT '{}'::jsonb, banned_rights JSONB NOT NULL DEFAULT '{}'::jsonb, rank TEXT NOT NULL DEFAULT '', available_min_id INT NOT NULL DEFAULT 0, available_min_pts INT NOT NULL DEFAULT 0, read_inbox_max_id INT NOT NULL DEFAULT 0, read_inbox_date INT NOT NULL DEFAULT 0, read_outbox_max_id INT NOT NULL DEFAULT 0, unread_mark BOOLEAN NOT NULL DEFAULT false, slowmode_last_send_date INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (channel_id, user_id), CONSTRAINT channel_members_role_check CHECK (role IN ('creator', 'admin', 'member')), CONSTRAINT channel_members_status_check CHECK (status IN ('active', 'left', 'kicked', 'banned')) ) PARTITION BY HASH (channel_id); CREATE INDEX IF NOT EXISTS channel_members_user_active_idx ON channel_members (user_id, channel_id) WHERE status = 'active'; CREATE INDEX IF NOT EXISTS channel_members_user_left_idx ON channel_members (user_id, left_at DESC, channel_id DESC) WHERE status = 'left'; CREATE INDEX IF NOT EXISTS channel_members_channel_role_idx ON channel_members (channel_id, role, user_id) WHERE status = 'active'; CREATE INDEX IF NOT EXISTS channel_members_read_participants_idx ON channel_members (channel_id, read_inbox_max_id, user_id) WHERE status = 'active'; DO $$ DECLARE i int; BEGIN FOR i IN 0..63 LOOP EXECUTE format( 'CREATE TABLE IF NOT EXISTS channel_members_p%s PARTITION OF channel_members FOR VALUES WITH (MODULUS 64, REMAINDER %s)', lpad(i::text, 2, '0'), i ); END LOOP; END $$; CREATE TABLE IF NOT EXISTS channel_messages ( channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, id INT NOT NULL, random_id BIGINT NOT NULL DEFAULT 0, sender_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT, from_peer_type VARCHAR(16) NOT NULL DEFAULT 'user', from_peer_id BIGINT NOT NULL, send_as_peer_type VARCHAR(16), send_as_peer_id BIGINT, message_date INT NOT NULL, edit_date INT NOT NULL DEFAULT 0, post BOOLEAN NOT NULL DEFAULT false, silent BOOLEAN NOT NULL DEFAULT false, noforwards BOOLEAN NOT NULL DEFAULT false, body TEXT NOT NULL DEFAULT '', entities JSONB NOT NULL DEFAULT '[]'::jsonb, reply_to JSONB NOT NULL DEFAULT '{}'::jsonb, reply_to_msg_id INT NOT NULL DEFAULT 0, reply_to_peer_type VARCHAR(16) NOT NULL DEFAULT '', reply_to_peer_id BIGINT NOT NULL DEFAULT 0, reply_to_top_id INT NOT NULL DEFAULT 0, fwd_from JSONB NOT NULL DEFAULT '{}'::jsonb, discussion_channel_id BIGINT NOT NULL DEFAULT 0, discussion_message_id INT NOT NULL DEFAULT 0, action JSONB NOT NULL DEFAULT '{}'::jsonb, pts INT NOT NULL, deleted BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (channel_id, id), CONSTRAINT channel_messages_peer_type_check CHECK ( from_peer_type IN ('user', 'channel') AND (send_as_peer_type IS NULL OR send_as_peer_type IN ('user', 'channel')) AND (reply_to_peer_type = '' OR reply_to_peer_type IN ('user', 'channel')) ), CONSTRAINT channel_messages_content_check CHECK (body <> '' OR action <> '{}'::jsonb) ) PARTITION BY HASH (channel_id); CREATE UNIQUE INDEX IF NOT EXISTS channel_messages_random_idx ON channel_messages (channel_id, sender_user_id, random_id) WHERE random_id <> 0; CREATE INDEX IF NOT EXISTS channel_messages_history_idx ON channel_messages (channel_id, id DESC) WHERE NOT deleted; CREATE INDEX IF NOT EXISTS channel_messages_sender_history_idx ON channel_messages (channel_id, sender_user_id, id DESC) WHERE NOT deleted; CREATE INDEX IF NOT EXISTS channel_messages_date_idx ON channel_messages (channel_id, message_date DESC, id DESC) WHERE NOT deleted; CREATE INDEX IF NOT EXISTS channel_messages_reply_thread_idx ON channel_messages (channel_id, reply_to_top_id, id DESC) WHERE reply_to_top_id > 0 AND NOT deleted; CREATE INDEX IF NOT EXISTS channel_messages_discussion_ref_idx ON channel_messages (discussion_channel_id, discussion_message_id) WHERE discussion_channel_id <> 0 AND discussion_message_id <> 0 AND NOT deleted; DO $$ DECLARE i int; BEGIN FOR i IN 0..63 LOOP EXECUTE format( 'CREATE TABLE IF NOT EXISTS channel_messages_p%s PARTITION OF channel_messages FOR VALUES WITH (MODULUS 64, REMAINDER %s)', lpad(i::text, 2, '0'), i ); END LOOP; END $$; CREATE TABLE IF NOT EXISTS channel_update_events ( channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, pts INT NOT NULL, pts_count INT NOT NULL DEFAULT 1, date INT NOT NULL, event_type VARCHAR(32) NOT NULL, message_id INT NOT NULL DEFAULT 0, message_ids JSONB NOT NULL DEFAULT '[]'::jsonb, sender_user_id BIGINT NOT NULL DEFAULT 0, user_ids JSONB NOT NULL DEFAULT '[]'::jsonb, payload JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (channel_id, pts), CONSTRAINT channel_update_events_pts_count_check CHECK (pts_count > 0), CONSTRAINT channel_update_events_type_check CHECK ( event_type IN ( 'new_channel_message', 'edit_channel_message', 'delete_channel_messages', 'channel_participant', 'pinned_channel_messages', 'noop' ) ) ) PARTITION BY HASH (channel_id); CREATE INDEX IF NOT EXISTS channel_update_events_scan_idx ON channel_update_events (channel_id, pts ASC); DO $$ DECLARE i int; BEGIN FOR i IN 0..63 LOOP EXECUTE format( 'CREATE TABLE IF NOT EXISTS channel_update_events_p%s PARTITION OF channel_update_events FOR VALUES WITH (MODULUS 64, REMAINDER %s)', lpad(i::text, 2, '0'), i ); END LOOP; END $$; CREATE TABLE IF NOT EXISTS channel_admin_log_events ( channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, id BIGINT NOT NULL, actor_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT, event_date INT NOT NULL, event_type VARCHAR(48) NOT NULL, prev_string TEXT NOT NULL DEFAULT '', new_string TEXT NOT NULL DEFAULT '', prev_bool BOOLEAN NOT NULL DEFAULT false, new_bool BOOLEAN NOT NULL DEFAULT false, prev_int INT NOT NULL DEFAULT 0, new_int INT NOT NULL DEFAULT 0, prev_participant JSONB NOT NULL DEFAULT '{}'::jsonb, new_participant JSONB NOT NULL DEFAULT '{}'::jsonb, participant JSONB NOT NULL DEFAULT '{}'::jsonb, message JSONB NOT NULL DEFAULT '{}'::jsonb, prev_message JSONB NOT NULL DEFAULT '{}'::jsonb, new_message JSONB NOT NULL DEFAULT '{}'::jsonb, query TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (channel_id, id), CONSTRAINT channel_admin_log_events_type_check CHECK ( event_type IN ( 'change_title', 'change_username', 'change_linked_chat', 'toggle_signatures', 'toggle_pre_history_hidden', 'toggle_forum', 'toggle_anti_spam', 'toggle_slow_mode', 'participant_invite', 'participant_join', 'participant_leave', 'participant_promote', 'participant_demote', 'participant_ban', 'participant_unban', 'participant_kick', 'participant_unkick', 'update_pinned', 'send_message', 'edit_message', 'delete_message' ) ) ) PARTITION BY HASH (channel_id); CREATE INDEX IF NOT EXISTS channel_admin_log_events_scan_idx ON channel_admin_log_events (channel_id, id DESC); CREATE INDEX IF NOT EXISTS channel_admin_log_events_actor_idx ON channel_admin_log_events (channel_id, actor_user_id, id DESC); CREATE INDEX IF NOT EXISTS channel_admin_log_events_type_idx ON channel_admin_log_events (channel_id, event_type, id DESC); DO $$ DECLARE i int; BEGIN FOR i IN 0..63 LOOP EXECUTE format( 'CREATE TABLE IF NOT EXISTS channel_admin_log_events_p%s PARTITION OF channel_admin_log_events FOR VALUES WITH (MODULUS 64, REMAINDER %s)', lpad(i::text, 2, '0'), i ); END LOOP; END $$; CREATE TABLE IF NOT EXISTS channel_dialogs ( user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, folder_id INT NOT NULL DEFAULT 0, top_message_id INT NOT NULL DEFAULT 0, top_message_date INT NOT NULL DEFAULT 0, read_inbox_max_id INT NOT NULL DEFAULT 0, read_outbox_max_id INT NOT NULL DEFAULT 0, unread_count INT NOT NULL DEFAULT 0, unread_mentions_count INT NOT NULL DEFAULT 0, unread_reactions_count INT NOT NULL DEFAULT 0, pinned BOOLEAN NOT NULL DEFAULT false, pinned_order INT NOT NULL DEFAULT 0, unread_mark BOOLEAN NOT NULL DEFAULT false, view_forum_as_messages BOOLEAN NOT NULL DEFAULT false, notify_settings JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (user_id, channel_id), CONSTRAINT channel_dialogs_folder_id_check CHECK (folder_id >= 0) ) PARTITION BY HASH (user_id); CREATE INDEX IF NOT EXISTS channel_dialogs_user_top_idx ON channel_dialogs (user_id, folder_id, pinned DESC, pinned_order DESC, top_message_date DESC, top_message_id DESC, channel_id DESC); CREATE INDEX IF NOT EXISTS channel_dialogs_pinned_idx ON channel_dialogs (user_id, pinned) WHERE pinned; DO $$ DECLARE i int; BEGIN FOR i IN 0..63 LOOP EXECUTE format( 'CREATE TABLE IF NOT EXISTS channel_dialogs_p%s PARTITION OF channel_dialogs FOR VALUES WITH (MODULUS 64, REMAINDER %s)', lpad(i::text, 2, '0'), i ); END LOOP; END $$; CREATE TABLE IF NOT EXISTS channel_invites ( channel_id BIGINT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, invite_id BIGINT NOT NULL, hash TEXT NOT NULL, admin_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT, title TEXT NOT NULL DEFAULT '', permanent BOOLEAN NOT NULL DEFAULT false, revoked BOOLEAN NOT NULL DEFAULT false, request_needed BOOLEAN NOT NULL DEFAULT false, expire_date INT, usage_limit INT, usage_count INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (channel_id, invite_id) ) PARTITION BY HASH (channel_id); -- Keep invite hash uniqueness outside the partitioned table because PostgreSQL -- requires global unique indexes on partitions to include the partition key. CREATE TABLE IF NOT EXISTS channel_invite_hashes ( hash TEXT PRIMARY KEY, channel_id BIGINT NOT NULL, invite_id BIGINT NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT channel_invite_hashes_nonempty_check CHECK (hash <> '') ); DO $$ DECLARE i int; BEGIN FOR i IN 0..63 LOOP EXECUTE format( 'CREATE TABLE IF NOT EXISTS channel_invites_p%s PARTITION OF channel_invites FOR VALUES WITH (MODULUS 64, REMAINDER %s)', lpad(i::text, 2, '0'), i ); END LOOP; END $$;