chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -12,6 +12,20 @@ services:
postgres:
image: postgres:17-alpine
container_name: telesrv-postgres
# 全库已去分区普通表max_locks_per_transaction 默认 64 已够512 仅留余量,对内存近乎零开销。
# pg_stat_statements聚合各 SQL 的累计/平均耗时与调用次数,用于定位 postgres 容器 CPU 热点
# telesrv 是宿主进程docker stats 里的 CPU 尖峰是本容器)。需 CREATE EXTENSION 后才有视图:
# docker exec -i telesrv-postgres psql -U telesrv -d telesrv -c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"
command:
- "postgres"
- "-c"
- "max_locks_per_transaction=512"
- "-c"
- "shared_preload_libraries=pg_stat_statements"
- "-c"
- "pg_stat_statements.track=all"
- "-c"
- "track_io_timing=on"
environment:
POSTGRES_DB: telesrv
POSTGRES_USER: telesrv

View file

@ -1,4 +1,6 @@
-- 0001_init 回滚:按外键依赖逆序删表。
DROP TABLE IF EXISTS authorizations;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS auth_keys;
-- 0001_init 回滚no-op本文件是全新项目由 0001-0140 迁移链压缩而来的初始 schema。
--
-- 全新项目明确不保留回退路径(沿用 0011 旧迁移的同一约定)。重置数据库请重建:
-- `docker compose -f deploy/docker-compose.yml down -v` 销毁卷后重新迁移,而非 golang-migrate down。
-- golang-migrate 执行本文件即把版本回退到 0不删除任何对象或数据。
SELECT 1;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS public.channel_topic_read;

View file

@ -0,0 +1,23 @@
-- per-(user, channel, topic) 独立已读水位。
-- 背景forum 话题已读此前被频道级单一水位 channel_members.read_inbox_max_id 承载,
-- 读一个 topic 会污染其它 topic 的未读现算reply_to_top_id 过滤 + id > 频道级水位)。
-- channel_forum_topics 那几个 read_*_max_id 列无 user 维度PK=channel_id,topic_id
-- 无法承载 per-viewer 已读故新建本表topic_id=1 表示 General。
CREATE TABLE public.channel_topic_read (
channel_id bigint NOT NULL,
user_id bigint NOT NULL,
topic_id integer NOT NULL,
read_inbox_max_id integer DEFAULT 0 NOT NULL,
read_outbox_max_id integer DEFAULT 0 NOT NULL,
read_inbox_date integer DEFAULT 0 NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT channel_topic_read_pkey PRIMARY KEY (channel_id, user_id, topic_id),
CONSTRAINT channel_topic_read_topic_check CHECK (topic_id > 0),
CONSTRAINT channel_topic_read_channel_fkey FOREIGN KEY (channel_id)
REFERENCES public.channels (id) ON DELETE CASCADE
);
-- outbox 回执反查:某 topic 内某 viewer 自己消息被读到哪 = 该 topic 其他成员 read_inbox_max_id 的最大值。
-- (channel_id, topic_id, read_inbox_max_id DESC) 让 MAX 走 index-only避免每帧全表聚合。
CREATE INDEX channel_topic_read_outbox_idx
ON public.channel_topic_read (channel_id, topic_id, read_inbox_max_id DESC);

View file

@ -1,5 +0,0 @@
DROP TABLE IF EXISTS lang_pack_strings;
DROP TABLE IF EXISTS lang_packs;
DROP TABLE IF EXISTS dialogs;
DROP TABLE IF EXISTS contacts;
DROP TABLE IF EXISTS update_states;

View file

@ -1,73 +0,0 @@
-- 0002_phase1_business: first-stage business persistence for startup RPCs.
--
-- 表结构按 telesrv domain/store 边界建模,不引入旧工程依赖。
CREATE TABLE IF NOT EXISTS update_states (
auth_key_id BIGINT PRIMARY KEY REFERENCES auth_keys(auth_key_id) ON DELETE CASCADE,
pts INT NOT NULL DEFAULT 0,
qts INT NOT NULL DEFAULT 0,
date INT NOT NULL DEFAULT 0,
seq INT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS contacts (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
contact_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
mutual BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, contact_user_id)
);
CREATE INDEX IF NOT EXISTS contacts_contact_user_id_idx ON contacts (contact_user_id);
CREATE TABLE IF NOT EXISTS dialogs (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
peer_type VARCHAR(16) NOT NULL,
peer_id BIGINT NOT NULL,
top_message_id 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,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, peer_type, peer_id),
CONSTRAINT dialogs_peer_type_check CHECK (peer_type IN ('user'))
);
CREATE INDEX IF NOT EXISTS dialogs_user_updated_idx ON dialogs (user_id, updated_at DESC);
CREATE INDEX IF NOT EXISTS dialogs_user_pinned_idx ON dialogs (user_id, pinned) WHERE pinned;
CREATE TABLE IF NOT EXISTS lang_packs (
lang_pack VARCHAR(32) NOT NULL,
lang_code VARCHAR(64) NOT NULL,
version INT NOT NULL,
strings_count INT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (lang_pack, lang_code)
);
CREATE TABLE IF NOT EXISTS lang_pack_strings (
lang_pack VARCHAR(32) NOT NULL,
lang_code VARCHAR(64) NOT NULL,
key VARCHAR(128) NOT NULL,
version INT NOT NULL,
pluralized BOOLEAN NOT NULL DEFAULT false,
value TEXT NOT NULL DEFAULT '',
zero_value TEXT NOT NULL DEFAULT '',
one_value TEXT NOT NULL DEFAULT '',
two_value TEXT NOT NULL DEFAULT '',
few_value TEXT NOT NULL DEFAULT '',
many_value TEXT NOT NULL DEFAULT '',
other_value TEXT NOT NULL DEFAULT '',
deleted BOOLEAN NOT NULL DEFAULT false,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (lang_pack, lang_code, key),
FOREIGN KEY (lang_pack, lang_code) REFERENCES lang_packs(lang_pack, lang_code) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS lang_pack_strings_pack_version_idx
ON lang_pack_strings (lang_pack, lang_code, version);

View file

@ -1,5 +0,0 @@
DROP TABLE IF EXISTS temp_auth_key_bindings;
DROP TABLE IF EXISTS country_codes;
DROP TABLE IF EXISTS countries;
DROP TABLE IF EXISTS app_configs;
DROP TABLE IF EXISTS account_passwords;

View file

@ -1,64 +0,0 @@
-- 0003_startup_config_security: data-backed startup config, countries and account security.
CREATE TABLE IF NOT EXISTS account_passwords (
user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
has_recovery BOOLEAN NOT NULL DEFAULT false,
has_secure_values BOOLEAN NOT NULL DEFAULT false,
has_password BOOLEAN NOT NULL DEFAULT false,
hint VARCHAR(256) NOT NULL DEFAULT '',
email_unconfirmed_pattern VARCHAR(256) NOT NULL DEFAULT '',
login_email_pattern VARCHAR(256) NOT NULL DEFAULT '',
secure_random BYTEA NOT NULL DEFAULT decode('74656c657372762d746465736b746f702d6465762d7365637572652d72616e64', 'hex'),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS app_configs (
client VARCHAR(64) PRIMARY KEY,
hash INT NOT NULL,
config_json JSONB NOT NULL DEFAULT '{}'::jsonb,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS countries (
iso2 VARCHAR(2) PRIMARY KEY,
default_name VARCHAR(128) NOT NULL,
name VARCHAR(128) NOT NULL DEFAULT '',
hidden BOOLEAN NOT NULL DEFAULT false,
order_index INT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS country_codes (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
iso2 VARCHAR(2) NOT NULL REFERENCES countries(iso2) ON DELETE CASCADE,
country_code VARCHAR(16) NOT NULL,
prefixes TEXT[] NOT NULL DEFAULT '{}',
patterns TEXT[] NOT NULL DEFAULT '{}',
order_index INT NOT NULL DEFAULT 0,
UNIQUE (iso2, country_code)
);
CREATE TABLE IF NOT EXISTS temp_auth_key_bindings (
temp_auth_key_id BIGINT PRIMARY KEY REFERENCES auth_keys(auth_key_id) ON DELETE CASCADE,
perm_auth_key_id BIGINT NOT NULL,
nonce BIGINT NOT NULL,
expires_at INT NOT NULL,
encrypted_message BYTEA NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO app_configs (client, hash, config_json)
VALUES ('tdesktop', 1, '{}'::jsonb)
ON CONFLICT (client) DO NOTHING;
INSERT INTO countries (iso2, default_name, name, hidden, order_index)
VALUES
('US', 'United States', '', false, 10),
('CN', 'China', '', false, 20)
ON CONFLICT (iso2) DO NOTHING;
INSERT INTO country_codes (iso2, country_code, prefixes, patterns, order_index)
VALUES
('US', '1', ARRAY['1'], '{}'::text[], 10),
('CN', '86', ARRAY['86'], '{}'::text[], 20)
ON CONFLICT (iso2, country_code) DO NOTHING;

View file

@ -0,0 +1,14 @@
-- 回滚:恢复不含 forum 话题已读两种事件类型的原始白名单(与 0001 一致)。
ALTER TABLE public.user_update_events DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE public.user_update_events ADD CONSTRAINT user_update_events_type_check CHECK (
(event_type)::text = ANY (ARRAY[
'new_message', 'read_history_inbox', 'read_history_outbox', 'read_message_contents',
'edit_message', 'message_reactions', 'message_poll', 'draft_message', 'quick_replies',
'new_quick_reply', 'delete_quick_reply', 'quick_reply_message', 'delete_quick_reply_messages',
'contacts_reset', 'dialog_pinned', 'pinned_dialogs', 'pinned_messages', 'dialog_unread_mark',
'peer_settings', 'peer_story_blocked', 'delete_messages', 'dialog_filter', 'dialog_filter_order',
'dialog_filters', 'folder_peers', 'channel_available_messages', 'channel_view_forum_as_messages',
'channel_state', 'saved_dialog_pinned', 'pinned_saved_dialogs', 'story', 'read_stories',
'sent_story_reaction', 'new_story_reaction', 'noop'
]::text[])
);

View file

@ -0,0 +1,20 @@
-- 修复forum 话题已读0002/a180fbc新增了 user_update_events 事件类型
-- read_channel_discussion_inbox / read_channel_discussion_outboxupdateReadChannelDiscussionInbox/
-- Outbox 的 durable 多设备同步),但 0001 的 user_update_events_type_check 白名单未包含它们。
-- 后果:真 PostgreSQL 上 messages.readDiscussion 首次标已读时 append durable 事件被 CHECK
-- 约束拒绝SQLSTATE 23514→ RPC 返回 500且话题已读无法进入 durable / getDifference
-- 用户其它设备收不到 per-topic 已读。memory store 无 CHECK 约束故单测未暴露,真双机才发现。
ALTER TABLE public.user_update_events DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE public.user_update_events ADD CONSTRAINT user_update_events_type_check CHECK (
(event_type)::text = ANY (ARRAY[
'new_message', 'read_history_inbox', 'read_history_outbox', 'read_message_contents',
'edit_message', 'message_reactions', 'message_poll', 'draft_message', 'quick_replies',
'new_quick_reply', 'delete_quick_reply', 'quick_reply_message', 'delete_quick_reply_messages',
'contacts_reset', 'dialog_pinned', 'pinned_dialogs', 'pinned_messages', 'dialog_unread_mark',
'peer_settings', 'peer_story_blocked', 'delete_messages', 'dialog_filter', 'dialog_filter_order',
'dialog_filters', 'folder_peers', 'channel_available_messages', 'channel_view_forum_as_messages',
'channel_state', 'saved_dialog_pinned', 'pinned_saved_dialogs', 'story', 'read_stories',
'sent_story_reaction', 'new_story_reaction', 'noop',
'read_channel_discussion_inbox', 'read_channel_discussion_outbox'
]::text[])
);

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS public.account_settings;

View file

@ -0,0 +1,23 @@
-- 账号级单例设置(每用户一行):全局隐私开关、账号自毁期限、敏感内容开关、
-- 联系人注册通知静音。此前这些 account.* RPC 为硬编码回显 stub不持久化
-- account.get/setGlobalPrivacySettings、get/setAccountTTL、get/setContentSettings、
-- get/setContactSignUpNotification。本表落地真实持久化。
CREATE TABLE public.account_settings (
user_id bigint NOT NULL,
archive_and_mute_new_noncontact_peers boolean DEFAULT false NOT NULL,
keep_archived_unmuted boolean DEFAULT false NOT NULL,
keep_archived_folders boolean DEFAULT false NOT NULL,
hide_read_marks boolean DEFAULT false NOT NULL,
new_noncontact_peers_require_premium boolean DEFAULT false NOT NULL,
display_gifts_button boolean DEFAULT false NOT NULL,
noncontact_peers_paid_stars bigint DEFAULT 0 NOT NULL,
account_ttl_days integer DEFAULT 365 NOT NULL,
sensitive_content_enabled boolean DEFAULT false NOT NULL,
contact_signup_silent boolean DEFAULT false NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT account_settings_account_ttl_days_check CHECK ((account_ttl_days > 0)),
CONSTRAINT account_settings_noncontact_peers_paid_stars_check CHECK ((noncontact_peers_paid_stars >= 0))
);
ALTER TABLE ONLY public.account_settings
ADD CONSTRAINT account_settings_pkey PRIMARY KEY (user_id);

View file

@ -1,2 +0,0 @@
ALTER TABLE temp_auth_key_bindings
DROP COLUMN IF EXISTS temp_session_id;

View file

@ -1,4 +0,0 @@
-- 0004_temp_auth_key_binding_session: persist validated bind_auth_key_inner session id.
ALTER TABLE temp_auth_key_bindings
ADD COLUMN IF NOT EXISTS temp_session_id BIGINT NOT NULL DEFAULT 0;

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS public.notify_settings;

View file

@ -0,0 +1,24 @@
-- per-scope 通知设置(每用户多行):具体 peer / forum 话题 / 三类全局默认
-- users/chats/broadcasts。此前 account.get/update/resetNotifySettings 为回显
-- stub不持久化mute 重启即丢、dialog 列表静音状态不正确。本表落地真实持久化。
-- 可空 bool/int 列表示“该项未设置=按所属类别继承默认”(与 TL flag-optional 一致)。
CREATE TABLE public.notify_settings (
owner_user_id bigint NOT NULL,
scope_kind text NOT NULL,
peer_type text DEFAULT ''::text NOT NULL,
peer_id bigint DEFAULT 0 NOT NULL,
topic_id integer DEFAULT 0 NOT NULL,
show_previews boolean,
silent boolean,
mute_until integer,
stories_muted boolean,
stories_hide_sender boolean,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT notify_settings_scope_kind_check CHECK ((scope_kind = ANY (ARRAY['peer'::text, 'users'::text, 'chats'::text, 'broadcasts'::text])))
);
ALTER TABLE ONLY public.notify_settings
ADD CONSTRAINT notify_settings_pkey PRIMARY KEY (owner_user_id, scope_kind, peer_type, peer_id, topic_id);
-- dialog 列表批量读:按 owner + 一组 peer 取整-peertopic 0设置。
CREATE INDEX notify_settings_owner_peer_idx ON public.notify_settings USING btree (owner_user_id, peer_type, peer_id) WHERE ((scope_kind = 'peer'::text) AND (topic_id = 0));

View file

@ -1,11 +0,0 @@
DROP INDEX IF EXISTS messages_owner_date_idx;
DROP INDEX IF EXISTS messages_owner_dialog_idx;
DROP TABLE IF EXISTS messages;
DROP INDEX IF EXISTS dialogs_user_top_message_idx;
ALTER TABLE dialogs DROP COLUMN IF EXISTS top_message_date;
DELETE FROM users WHERE id = 777000;
ALTER TABLE users
DROP COLUMN IF EXISTS support,
DROP COLUMN IF EXISTS verified;

View file

@ -1,44 +0,0 @@
-- 0005_system_login_messages: official system account and first message persistence.
--
-- 777000 官方账号与登录消息推送;表结构按 telesrv domain/store 边界建模。
ALTER TABLE users
ADD COLUMN IF NOT EXISTS verified BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS support BOOLEAN NOT NULL DEFAULT false;
INSERT INTO users (id, access_hash, phone, first_name, last_name, username, country_code, verified, support)
VALUES (777000, 6599886787491911851, '42777', 'Telegram', '', 'telegram', '', true, true)
ON CONFLICT (id) DO UPDATE SET
access_hash = EXCLUDED.access_hash,
phone = EXCLUDED.phone,
first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name,
username = EXCLUDED.username,
verified = EXCLUDED.verified,
support = EXCLUDED.support,
updated_at = now();
ALTER TABLE dialogs
ADD COLUMN IF NOT EXISTS top_message_date INT NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS dialogs_user_top_message_idx
ON dialogs (user_id, pinned DESC, top_message_date DESC, top_message_id DESC, peer_id DESC);
CREATE TABLE IF NOT EXISTS messages (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
owner_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
peer_type VARCHAR(16) NOT NULL,
peer_id BIGINT NOT NULL,
from_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
message_date INT NOT NULL,
outgoing BOOLEAN NOT NULL DEFAULT false,
body TEXT NOT NULL DEFAULT '',
entities JSONB NOT NULL DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT messages_peer_type_check CHECK (peer_type IN ('user'))
);
CREATE INDEX IF NOT EXISTS messages_owner_dialog_idx
ON messages (owner_user_id, peer_type, peer_id, id DESC);
CREATE INDEX IF NOT EXISTS messages_owner_date_idx
ON messages (owner_user_id, message_date DESC, id DESC);

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS public.user_sticker_collections;

View file

@ -0,0 +1,18 @@
-- per-user 个人贴纸/GIF 集合:收藏贴纸 / 最近贴纸 / attach 最近贴纸 / 保存的 GIF。
-- 此前 messages.faveSticker/saveRecentSticker/saveGif 未注册(NOT_IMPLEMENTED)、
-- getFaved/getRecent/getSavedGifs 返回空 stub。本表落地真实持久化。
-- used_at 为入列/最近使用时刻,读取按 used_at DESC最新在前document_id 引用
-- documents 表,读路径经 Files.GetDocuments 解析为完整文档。
CREATE TABLE public.user_sticker_collections (
owner_user_id bigint NOT NULL,
kind text NOT NULL,
document_id bigint NOT NULL,
used_at integer DEFAULT 0 NOT NULL,
CONSTRAINT user_sticker_collections_kind_check CHECK ((kind = ANY (ARRAY['faved'::text, 'recent'::text, 'recent_attached'::text, 'gif'::text])))
);
ALTER TABLE ONLY public.user_sticker_collections
ADD CONSTRAINT user_sticker_collections_pkey PRIMARY KEY (owner_user_id, kind, document_id);
-- 读取/截断按 (owner, kind, used_at DESC)。
CREATE INDEX user_sticker_collections_order_idx ON public.user_sticker_collections USING btree (owner_user_id, kind, used_at DESC);

View file

@ -1,2 +0,0 @@
DROP INDEX IF EXISTS update_events_auth_pts_idx;
DROP TABLE IF EXISTS update_events;

View file

@ -1,16 +0,0 @@
-- 0006_update_events: minimal auth-key update queue for getDifference补偿.
CREATE TABLE IF NOT EXISTS update_events (
auth_key_id BIGINT NOT NULL REFERENCES auth_keys(auth_key_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 REFERENCES messages(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (auth_key_id, pts),
CONSTRAINT update_events_type_check CHECK (event_type IN ('new_message'))
);
CREATE INDEX IF NOT EXISTS update_events_auth_pts_idx
ON update_events (auth_key_id, pts);

View file

@ -0,0 +1 @@
DROP INDEX IF EXISTS public.dialogs_peer_id_idx;

View file

@ -0,0 +1,9 @@
-- dialogs 按 peer_id 的反向索引。
-- dialog_light 失效扇出函数 telesrv_bump_private_dialog_light_for_user 按
-- peer_id 过滤 dialogs"哪些用户与该 user 有私聊会话"),但 dialogs 上所有
-- 既有索引都以 user_id 前导(主键 + dialogs_user_folder_top_message_idx +
-- dialogs_user_pinned_order_idx该按 peer_id 的查询此前只能 Seq Scan。
-- dialogs 有 CHECK (peer_type = 'user'),故单列 (peer_id) 即精确命中、无需
-- peer_type 前缀,与 contacts.contacts_contact_user_id_idx 对称。资料变更 /
-- premium 到期触发扇出时,从全表扫降为索引扫(随表规模增长收益放大)。
CREATE INDEX dialogs_peer_id_idx ON public.dialogs USING btree (peer_id);

View file

@ -1,15 +0,0 @@
ALTER TABLE update_events
DROP CONSTRAINT IF EXISTS update_events_peer_type_check;
ALTER TABLE update_events
DROP CONSTRAINT IF EXISTS update_events_type_check;
ALTER TABLE update_events
ADD CONSTRAINT update_events_type_check
CHECK (event_type IN ('new_message'));
ALTER TABLE update_events
DROP COLUMN IF EXISTS still_unread_count,
DROP COLUMN IF EXISTS max_id,
DROP COLUMN IF EXISTS peer_id,
DROP COLUMN IF EXISTS peer_type;

View file

@ -1,23 +0,0 @@
-- 0007_read_history_events: persist readHistory update events for getDifference.
--
-- updateReadHistoryInbox真正发生已读推进时递增 pts并给其它 session / getDifference 留可补偿事件。
ALTER TABLE update_events
ADD COLUMN IF NOT EXISTS peer_type VARCHAR(16),
ADD COLUMN IF NOT EXISTS peer_id BIGINT,
ADD COLUMN IF NOT EXISTS max_id INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS still_unread_count INT NOT NULL DEFAULT 0;
ALTER TABLE update_events
DROP CONSTRAINT IF EXISTS update_events_type_check;
ALTER TABLE update_events
ADD CONSTRAINT update_events_type_check
CHECK (event_type IN ('new_message', 'read_history_inbox'));
ALTER TABLE update_events
DROP CONSTRAINT IF EXISTS update_events_peer_type_check;
ALTER TABLE update_events
ADD CONSTRAINT update_events_peer_type_check
CHECK (peer_type IS NULL OR peer_type IN ('user'));

View file

@ -0,0 +1,3 @@
ALTER TABLE public.channel_messages DROP COLUMN IF EXISTS grouped_id;
ALTER TABLE public.message_boxes DROP COLUMN IF EXISTS grouped_id;
ALTER TABLE public.private_messages DROP COLUMN IF EXISTS grouped_id;

View file

@ -0,0 +1,6 @@
-- 相册分组 id同一次 messages.sendMultiMedia 的各条消息共享一个非零 grouped_id
-- 客户端据此把它们渲染成一个相册组。此前 sendMultiMedia 不绑定 grouped_id各条独立气泡
-- 镜像 via_bot_id 的三张表:共享私聊主体 + 每 owner 收件箱 + 频道消息。
ALTER TABLE public.private_messages ADD COLUMN grouped_id bigint DEFAULT 0 NOT NULL;
ALTER TABLE public.message_boxes ADD COLUMN grouped_id bigint DEFAULT 0 NOT NULL;
ALTER TABLE public.channel_messages ADD COLUMN grouped_id bigint DEFAULT 0 NOT NULL;

View file

@ -1,8 +0,0 @@
-- Revert generated ordinary user ids to the original first-phase base when possible.
-- Existing users keep their ids; the next generated id is never moved below MAX(id)+1.
SELECT setval(
pg_get_serial_sequence('users', 'id'),
GREATEST((SELECT COALESCE(MAX(id), 0) FROM users), 999999999),
true
);

View file

@ -1,10 +0,0 @@
-- 0008_user_id_sequence_base: move ordinary user ids to the agreed timestamp range.
--
-- Base: 2026-06-01 00:00:00 Asia/Shanghai => Unix seconds 1780243200.
-- Existing users keep their ids; the next generated id is at least this base.
SELECT setval(
pg_get_serial_sequence('users', 'id'),
GREATEST((SELECT COALESCE(MAX(id), 0) FROM users), 1780243199),
true
);

View file

@ -1,13 +0,0 @@
-- 0009 rollback: remove second-stage private message pipeline tables.
DROP TABLE IF EXISTS dispatch_outbox;
DROP TABLE IF EXISTS user_update_events;
DROP TABLE IF EXISTS dialogs;
DROP TABLE IF EXISTS message_boxes;
DROP TABLE IF EXISTS private_messages;
ALTER TABLE IF EXISTS dialogs_legacy RENAME TO dialogs;
ALTER TABLE IF EXISTS messages_legacy RENAME TO messages;
ALTER TABLE update_states DROP CONSTRAINT IF EXISTS update_states_pkey;
ALTER TABLE update_states DROP COLUMN IF EXISTS user_id;
ALTER TABLE update_states ADD PRIMARY KEY (auth_key_id);

View file

@ -1,239 +0,0 @@
-- 0009_private_message_pipeline: second-stage private text message storage.
--
-- Large tables are partitioned from the first version of the message module:
-- message_boxes/dialogs/user_update_events/dispatch_outbox by owner/target user,
-- private_messages by sender user for random_id idempotency locality.
ALTER TABLE IF EXISTS messages RENAME TO messages_legacy;
ALTER TABLE IF EXISTS dialogs RENAME TO dialogs_legacy;
CREATE TABLE IF NOT EXISTS private_messages (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
sender_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
recipient_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
random_id BIGINT NOT NULL DEFAULT 0,
message_date INT NOT NULL,
body TEXT NOT NULL DEFAULT '',
entities JSONB NOT NULL DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (sender_user_id, id),
CONSTRAINT private_messages_nonempty_body CHECK (body <> '')
) PARTITION BY HASH (sender_user_id);
CREATE UNIQUE INDEX IF NOT EXISTS private_messages_sender_random_idx
ON private_messages (sender_user_id, random_id)
WHERE random_id <> 0;
CREATE INDEX IF NOT EXISTS private_messages_recipient_date_idx
ON private_messages (recipient_user_id, message_date DESC, id DESC);
DO $$
DECLARE
i int;
BEGIN
FOR i IN 0..63 LOOP
EXECUTE format(
'CREATE TABLE IF NOT EXISTS private_messages_p%s PARTITION OF private_messages FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
lpad(i::text, 2, '0'),
i
);
END LOOP;
END $$;
CREATE TABLE IF NOT EXISTS message_boxes (
owner_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
box_id INT NOT NULL,
private_message_id BIGINT NOT NULL,
message_sender_id BIGINT NOT NULL,
peer_type VARCHAR(16) NOT NULL,
peer_id BIGINT NOT NULL,
from_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
message_date INT NOT NULL,
outgoing BOOLEAN NOT NULL DEFAULT false,
body TEXT NOT NULL DEFAULT '',
entities JSONB NOT NULL DEFAULT '[]'::jsonb,
pts INT NOT NULL DEFAULT 0,
deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (owner_user_id, box_id),
UNIQUE (owner_user_id, private_message_id),
CONSTRAINT message_boxes_peer_type_check CHECK (peer_type IN ('user')),
FOREIGN KEY (message_sender_id, private_message_id)
REFERENCES private_messages(sender_user_id, id) ON DELETE CASCADE
) PARTITION BY HASH (owner_user_id);
CREATE INDEX IF NOT EXISTS message_boxes_dialog_seek_idx
ON message_boxes (owner_user_id, peer_type, peer_id, box_id DESC)
WHERE NOT deleted;
CREATE INDEX IF NOT EXISTS message_boxes_owner_date_idx
ON message_boxes (owner_user_id, message_date DESC, box_id DESC)
WHERE NOT deleted;
CREATE INDEX IF NOT EXISTS message_boxes_private_lookup_idx
ON message_boxes (private_message_id, owner_user_id);
DO $$
DECLARE
i int;
BEGIN
FOR i IN 0..63 LOOP
EXECUTE format(
'CREATE TABLE IF NOT EXISTS message_boxes_p%s PARTITION OF message_boxes FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
lpad(i::text, 2, '0'),
i
);
END LOOP;
END $$;
CREATE TABLE IF NOT EXISTS dialogs (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
peer_type VARCHAR(16) NOT NULL,
peer_id BIGINT NOT NULL,
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,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, peer_type, peer_id),
CONSTRAINT dialogs_peer_type_check CHECK (peer_type IN ('user'))
) PARTITION BY HASH (user_id);
CREATE INDEX IF NOT EXISTS dialogs_user_top_message_idx
ON dialogs (user_id, pinned DESC, top_message_date DESC, top_message_id DESC, peer_id DESC);
CREATE INDEX IF NOT EXISTS dialogs_user_pinned_idx
ON dialogs (user_id, pinned) WHERE pinned;
DO $$
DECLARE
i int;
BEGIN
FOR i IN 0..63 LOOP
EXECUTE format(
'CREATE TABLE IF NOT EXISTS dialogs_p%s PARTITION OF dialogs FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
lpad(i::text, 2, '0'),
i
);
END LOOP;
END $$;
CREATE TABLE IF NOT EXISTS user_update_events (
user_id BIGINT NOT NULL REFERENCES users(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_box_id INT,
peer_type VARCHAR(16),
peer_id BIGINT,
max_id INT NOT NULL DEFAULT 0,
still_unread_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, pts),
CONSTRAINT user_update_events_type_check CHECK (event_type IN ('new_message', 'read_history_inbox', 'noop')),
CONSTRAINT user_update_events_peer_type_check CHECK (peer_type IS NULL OR peer_type IN ('user')),
FOREIGN KEY (user_id, message_box_id) REFERENCES message_boxes(owner_user_id, box_id) ON DELETE CASCADE
) PARTITION BY HASH (user_id);
DO $$
DECLARE
i int;
BEGIN
FOR i IN 0..63 LOOP
EXECUTE format(
'CREATE TABLE IF NOT EXISTS user_update_events_p%s PARTITION OF user_update_events FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
lpad(i::text, 2, '0'),
i
);
END LOOP;
END $$;
CREATE TABLE IF NOT EXISTS dispatch_outbox (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
target_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
pts INT NOT NULL,
event_type VARCHAR(32) NOT NULL,
exclude_session_id BIGINT NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
attempts INT NOT NULL DEFAULT 0,
next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_error TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (target_user_id, id),
CONSTRAINT dispatch_outbox_status_check CHECK (status IN ('pending', 'dispatching', 'delivered', 'failed')),
FOREIGN KEY (target_user_id, pts) REFERENCES user_update_events(user_id, pts) ON DELETE CASCADE
) PARTITION BY HASH (target_user_id);
CREATE INDEX IF NOT EXISTS dispatch_outbox_pending_idx
ON dispatch_outbox (status, next_attempt_at, target_user_id, id)
WHERE status = 'pending';
DO $$
DECLARE
i int;
BEGIN
FOR i IN 0..63 LOOP
EXECUTE format(
'CREATE TABLE IF NOT EXISTS dispatch_outbox_p%s PARTITION OF dispatch_outbox FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
lpad(i::text, 2, '0'),
i
);
END LOOP;
END $$;
ALTER TABLE update_states
ADD COLUMN IF NOT EXISTS user_id BIGINT NOT NULL DEFAULT 0;
ALTER TABLE update_states DROP CONSTRAINT IF EXISTS update_states_pkey;
ALTER TABLE update_states ADD PRIMARY KEY (auth_key_id, user_id);
CREATE INDEX IF NOT EXISTS update_states_user_id_idx ON update_states (user_id);
DO $$
DECLARE
r record;
private_id bigint;
BEGIN
IF to_regclass('messages_legacy') IS NULL THEN
RETURN;
END IF;
FOR r IN
SELECT id, owner_user_id, peer_type, peer_id, from_user_id, message_date, outgoing, body, entities
FROM messages_legacy
ORDER BY owner_user_id, id
LOOP
INSERT INTO private_messages (
sender_user_id, recipient_user_id, random_id, message_date, body, entities
) VALUES (
r.from_user_id,
r.owner_user_id,
0,
r.message_date,
r.body,
r.entities
)
RETURNING id INTO private_id;
INSERT INTO message_boxes (
owner_user_id, box_id, private_message_id, message_sender_id, peer_type, peer_id,
from_user_id, message_date, outgoing, body, entities
) VALUES (
r.owner_user_id, r.id, private_id, r.from_user_id, r.peer_type, r.peer_id,
r.from_user_id, r.message_date, r.outgoing, r.body, r.entities
)
ON CONFLICT (owner_user_id, box_id) DO NOTHING;
END LOOP;
END $$;
INSERT INTO dialogs (
user_id, peer_type, peer_id, top_message_id, top_message_date,
read_inbox_max_id, read_outbox_max_id, unread_count,
unread_mentions_count, unread_reactions_count, pinned, updated_at
)
SELECT
user_id, peer_type, peer_id, top_message_id, top_message_date,
read_inbox_max_id, read_outbox_max_id, unread_count,
unread_mentions_count, unread_reactions_count, pinned, updated_at
FROM dialogs_legacy
ON CONFLICT (user_id, peer_type, peer_id) DO NOTHING;

View file

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS public.stars_transactions;
DROP TABLE IF EXISTS public.stars_balances;

View file

@ -0,0 +1,29 @@
-- Stars 本地账本per-user 余额 + 交易流水。此前 payments.getStarsStatus 余额恒 0、
-- getStarsTransactions 未注册、sendPaidReaction 恒返 BALANCE_TOO_LOW——无任何余额持久化。
-- 本地账本非真实支付起始余额走惰性首读授予granted 布尔幂等,新老账号都覆盖、免回填)。
-- 借记原子性由 store 层 withTx 保证SELECT ... FOR UPDATE + CHECK(balance>=0) + UPDATE + INSERT
CREATE TABLE public.stars_balances (
user_id bigint NOT NULL,
balance bigint DEFAULT 0 NOT NULL,
granted boolean DEFAULT false NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT stars_balances_pkey PRIMARY KEY (user_id),
CONSTRAINT stars_balances_balance_nonneg CHECK ((balance >= 0))
);
-- 流水amount 带符号(贷记 > 0 / 借记 < 0peer_* 为对手方grant/topup 等无对手时为空)。
CREATE TABLE public.stars_transactions (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
user_id bigint NOT NULL,
peer_type text DEFAULT '' NOT NULL,
peer_id bigint DEFAULT 0 NOT NULL,
amount bigint NOT NULL,
reason text DEFAULT 'adjust' NOT NULL,
title text DEFAULT '' NOT NULL,
description text DEFAULT '' NOT NULL,
date integer DEFAULT 0 NOT NULL,
CONSTRAINT stars_transactions_pkey PRIMARY KEY (id)
);
-- keyset 分页WHERE user_id=$1 [AND id < cursor] ORDER BY id DESC LIMIT n。
CREATE INDEX stars_transactions_user_id_idx ON public.stars_transactions USING btree (user_id, id DESC);

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS public.channel_message_paid_reactions;

View file

@ -0,0 +1,17 @@
-- 频道帖子付费 reactionmessages.sendPaidReaction每个 reactor 对一条频道消息累计
-- 投入的 Stars 星数 + 是否匿名。此前 sendPaidReaction 恒返 BALANCE_TOO_LOW无账本无累计
-- 配合 Stars 账本(迁移 0009rpc 先从账本 Debit再在此累计消息上展示 ReactionPaid 总星数
-- 与 top reactors 排行。星数为正、按 (channel,message,user) 累加。
CREATE TABLE public.channel_message_paid_reactions (
channel_id bigint NOT NULL,
message_id integer NOT NULL,
reactor_user_id bigint NOT NULL,
stars bigint DEFAULT 0 NOT NULL,
anonymous boolean DEFAULT false NOT NULL,
reaction_date integer DEFAULT 0 NOT NULL,
CONSTRAINT channel_message_paid_reactions_pkey PRIMARY KEY (channel_id, message_id, reactor_user_id),
CONSTRAINT channel_message_paid_reactions_stars_pos CHECK ((stars > 0))
);
-- top reactors 排行按 (channel, message, stars DESC)。
CREATE INDEX channel_message_paid_reactions_top_idx ON public.channel_message_paid_reactions USING btree (channel_id, message_id, stars DESC);

View file

@ -1,2 +0,0 @@
DROP INDEX IF EXISTS dispatch_outbox_dispatching_stale_idx;
DROP INDEX IF EXISTS message_boxes_dialog_date_seek_idx;

View file

@ -1,9 +0,0 @@
-- 0010_message_performance_indexes: indexes for second-stage message seek paths.
CREATE INDEX IF NOT EXISTS message_boxes_dialog_date_seek_idx
ON message_boxes (owner_user_id, peer_type, peer_id, message_date DESC, box_id DESC)
WHERE NOT deleted;
CREATE INDEX IF NOT EXISTS dispatch_outbox_dispatching_stale_idx
ON dispatch_outbox (status, updated_at, target_user_id, id)
WHERE status = 'dispatching';

View file

@ -1,6 +0,0 @@
-- 0011 down (no-op): 本迁移清除的是已被取代、运行时零引用的死表
-- update_events / messages_legacy / dialogs_legacy
--
-- 全新项目明确不保留回退路径,故不在此重建这些表;历史结构可查 0002 / 0005 / 0006 / 0007 迁移脚本(保留未删)。
-- golang-migrate 执行本文件即把版本回退到 0010不恢复任何死表或其数据。
SELECT 1;

View file

@ -1,13 +0,0 @@
-- 0011_drop_dead_tables: 清除已被取代、运行时零引用的遗留表(全新项目,不保留回退)。
--
-- - update_events : 一阶段 auth_key 维度 update 队列0006 建 / 0007 扩展),二阶段被
-- user_update_events 取代;无任何 query / Go 引用,仅在 sqlc 留下孤儿 model。
-- - messages_legacy: 0009 由旧 messages0005重命名保留的迁移残骸数据已迁入 private_messages + message_boxes。
-- - dialogs_legacy : 0009 由旧 dialogs0002重命名保留的迁移残骸数据已迁入新 dialogs。
--
-- 顺序要求update_events.message_id 外键指向 messages_legacy原 messages故先删 update_events。
-- 删除后需重跑 `sqlc generate`models.go 中 UpdateEvent / MessagesLegacy / DialogsLegacy 孤儿 model 会自动消失。
DROP TABLE IF EXISTS update_events;
DROP TABLE IF EXISTS messages_legacy;
DROP TABLE IF EXISTS dialogs_legacy;

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS public.user_star_gifts;

View file

@ -0,0 +1,24 @@
-- Star giftpayments.sendStarsForm + inputInvoiceStarGift用户花 Stars 给另一用户送礼物
-- (贴纸式收藏品)。此前 getStarGifts/getSavedStarGifts 返回空桩、getPaymentForm/sendStarsForm
-- 未注册。目录是从已 seed 的 animated_emoji 合成的静态内存表(不入库);本表存「已收到的礼物
-- 实例」。配合 Stars 账本(迁移 0009发礼 Debit、转换回 Stars 时 Credit。
-- msg_id 是礼物在 owner 私聊里的服务消息 idmessageActionStarGift是 save/convert 的身份键
-- (对应 inputSavedStarGiftUser.msg_id
CREATE TABLE public.user_star_gifts (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
owner_user_id bigint NOT NULL,
from_user_id bigint DEFAULT 0 NOT NULL,
gift_id bigint NOT NULL,
msg_id integer NOT NULL,
gift_date integer DEFAULT 0 NOT NULL,
name_hidden boolean DEFAULT false NOT NULL,
unsaved boolean DEFAULT false NOT NULL,
converted boolean DEFAULT false NOT NULL,
convert_stars bigint DEFAULT 0 NOT NULL,
message text DEFAULT '' NOT NULL,
CONSTRAINT user_star_gifts_pkey PRIMARY KEY (id),
CONSTRAINT user_star_gifts_owner_msg_uniq UNIQUE (owner_user_id, msg_id)
);
-- getSavedStarGifts keyset 分页按 (owner, gift_date DESC, id DESC)。
CREATE INDEX user_star_gifts_owner_idx ON public.user_star_gifts USING btree (owner_user_id, gift_date DESC, id DESC);

View file

@ -0,0 +1,2 @@
ALTER TABLE public.message_boxes DROP COLUMN reply_to_story_id;
ALTER TABLE public.private_messages DROP COLUMN reply_to_story_id;

View file

@ -0,0 +1,7 @@
-- Story 回复(评论):用户对一条 story 回复时,客户端发 messages.sendMessage(reply_to=
-- inputReplyToStory{peer, story_id}),消息落入与 story 作者的私聊,并投影为
-- messageReplyStoryHeader。此前 telesrv 直接拒STORY_ID_INVALID导致评论发送失败。
-- 复用已有的 reply_to_* 列族,新增 reply_to_story_id 持久化被回复的 story id普通消息回复恒 0
-- 双盒(私聊主体 + per-owner 投影)都需要该列以便重读历史时还原 story 回复头。
ALTER TABLE public.private_messages ADD COLUMN reply_to_story_id integer DEFAULT 0 NOT NULL;
ALTER TABLE public.message_boxes ADD COLUMN reply_to_story_id integer DEFAULT 0 NOT NULL;

View file

@ -1,5 +0,0 @@
-- 0012 down: 恢复宽 status CHECK含 'delivered')以保持迁移链完整。
-- 注意:方案 A 已删除的 delivered 行不可恢复query 层DELETE回退需手动改回 UPDATE本 down 不涉及代码。
ALTER TABLE dispatch_outbox DROP CONSTRAINT IF EXISTS dispatch_outbox_status_check;
ALTER TABLE dispatch_outbox ADD CONSTRAINT dispatch_outbox_status_check
CHECK (status IN ('pending', 'dispatching', 'delivered', 'failed'));

View file

@ -1,12 +0,0 @@
-- 0012_outbox_delete_on_deliver: outbox 投递成功改为直接 DELETE方案 A杜绝 delivered 行无限堆积。
--
-- 配合 query 改动MarkDispatchDelivered / MarkDispatchDeliveredBatch 由 UPDATE status='delivered' 改为 DELETE
-- 1) 清理改造前堆积的存量 delivered 行;
-- 2) 收紧 status CHECK移除不再使用的 'delivered'(状态机只剩 pending / dispatching / failed
-- dispatch_outbox 为 HASH 分区表,父表 DELETE / ALTER CONSTRAINT 自动作用于全部分区。
DELETE FROM dispatch_outbox WHERE status = 'delivered';
ALTER TABLE dispatch_outbox DROP CONSTRAINT IF EXISTS dispatch_outbox_status_check;
ALTER TABLE dispatch_outbox ADD CONSTRAINT dispatch_outbox_status_check
CHECK (status IN ('pending', 'dispatching', 'failed'));

View file

@ -0,0 +1,2 @@
DROP TRIGGER IF EXISTS users_bot_full_read_model_changed ON public.users;
DROP FUNCTION IF EXISTS public.telesrv_notify_bot_full_read_model();

View file

@ -0,0 +1,25 @@
-- bot 资料(name/about/description/commands/menu_button)变更都会 bump users.bot_info_version
-- (BumpBotInfoVersion,见 bot.sql)。群信息页的 ChannelFull.bot_info 由 RPC 层
-- channelFullBotInfoCache 缓存(键=viewer+channel,无法按 bot 定位),其跨实例失效此前只挂在
-- channel_base/channel_member 事件上——bot 自身改资料(经 BotFather 本地路径,或其它实例的
-- bots.* RPC)不会失效该缓存,导致群信息页里该 bot 的简介/命令陈旧最长 30 分钟(TTL)。
--
-- 这里给 bot 资料变更单独发一个 'bot_full' read-model 事件;ReadModelChangeListener 收到即
-- flush channelFullBotInfoCache(本地 BotFather 路径与跨实例 bots.* RPC 两条更新路径都覆盖)。
-- 与既有 user_base 事件(覆盖 RPC 投影/Redis user:base/bot 资料缓存)互补,不改动 user_base 热路径。
CREATE FUNCTION public.telesrv_notify_bot_full_read_model() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.is_bot AND (OLD.bot_info_version IS DISTINCT FROM NEW.bot_info_version) THEN
PERFORM telesrv_bump_read_model_version('bot_full', NEW.id, 'user', NEW.id);
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER users_bot_full_read_model_changed
AFTER UPDATE ON public.users
FOR EACH ROW
EXECUTE FUNCTION public.telesrv_notify_bot_full_read_model();

View file

@ -1,17 +0,0 @@
DROP INDEX IF EXISTS dialogs_user_pinned_order_idx;
ALTER TABLE dialogs
DROP COLUMN IF EXISTS hidden_peer_settings_bar,
DROP COLUMN IF EXISTS unread_mark,
DROP COLUMN IF EXISTS pinned_order;
DROP INDEX IF EXISTS contacts_user_name_idx;
ALTER TABLE contacts
DROP COLUMN IF EXISTS stories_hidden,
DROP COLUMN IF EXISTS close_friend,
DROP COLUMN IF EXISTS note_entities,
DROP COLUMN IF EXISTS note,
DROP COLUMN IF EXISTS contact_last_name,
DROP COLUMN IF EXISTS contact_first_name,
DROP COLUMN IF EXISTS contact_phone;

View file

@ -1,25 +0,0 @@
-- 0013_contact_profiles_and_dialog_pins: owner-scoped contact profile fields and pin order.
--
-- contact_* columns are deliberately scoped to (user_id, contact_user_id): they model the
-- current account's saved name/phone/note for a peer and must not mutate users global data.
ALTER TABLE contacts
ADD COLUMN IF NOT EXISTS contact_phone VARCHAR(32) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS contact_first_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS contact_last_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS note TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS note_entities JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS close_friend BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS stories_hidden BOOLEAN NOT NULL DEFAULT false;
CREATE INDEX IF NOT EXISTS contacts_user_name_idx
ON contacts (user_id, contact_first_name, contact_last_name, contact_user_id);
ALTER TABLE dialogs
ADD COLUMN IF NOT EXISTS pinned_order INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS unread_mark BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS hidden_peer_settings_bar BOOLEAN NOT NULL DEFAULT false;
CREATE INDEX IF NOT EXISTS dialogs_user_pinned_order_idx
ON dialogs (user_id, pinned, pinned_order, top_message_date DESC, top_message_id DESC, peer_id DESC)
WHERE pinned;

View file

@ -1,10 +0,0 @@
ALTER TABLE user_update_events
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE user_update_events
ADD CONSTRAINT user_update_events_type_check CHECK (
event_type IN ('new_message', 'read_history_inbox', 'noop')
);
ALTER TABLE user_update_events
DROP COLUMN IF EXISTS event_bool;

View file

@ -1,25 +0,0 @@
-- 0014_settings_update_events: durable updates for contacts/dialog settings.
--
-- Online push is not enough: offline sessions must recover contact resets,
-- dialog pin order changes, manual unread marks, and peer settings changes
-- through updates.getDifference.
ALTER TABLE user_update_events
ADD COLUMN IF NOT EXISTS event_bool BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE user_update_events
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE user_update_events
ADD CONSTRAINT user_update_events_type_check CHECK (
event_type IN (
'new_message',
'read_history_inbox',
'contacts_reset',
'dialog_pinned',
'pinned_dialogs',
'dialog_unread_mark',
'peer_settings',
'noop'
)
);

View file

@ -0,0 +1 @@
DROP TABLE IF EXISTS public.web_pages;

View file

@ -0,0 +1,18 @@
-- 链接预览webpage preview解析缓存。键为规范化 URL 的 63-bit 哈希;同一 URL
-- 跨发送/跨实例去重到一行避免对同一链接重复抓取外网。snapshot 是完整的
-- domain.MessageWebPage含已解析卡片字段与内嵌预览图引用与消息 media 列同构,
-- 读时无需重新抓取或 rehydrate 即可投影为 messageMediaWebPage。
--
-- web_page_id 由 url_hash 派生(二者同值),使 pending 占位与 done 解析结果携带
-- 同一 webPage id客户端按 id 关联占位与解析(见异步回填阶段)。
-- state 冗余出来便于后续按 pending/empty 做清扫与刷新。snapshot 不可变(按内容寻址
-- 的预览图 blob 已独立持久化于 photos/blob 存储),跨实例靠 url_hash 唯一约束去重 +
-- 每实例 TTL故本表无需 read-model NOTIFY 触发器。
CREATE TABLE public.web_pages (
url_hash bigint PRIMARY KEY,
web_page_id bigint NOT NULL,
state text NOT NULL,
snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at bigint NOT NULL DEFAULT 0,
refreshed_at bigint NOT NULL DEFAULT 0
);

View file

@ -1,8 +0,0 @@
ALTER TABLE dispatch_outbox
DROP COLUMN IF EXISTS exclude_auth_key_id;
ALTER TABLE user_update_events
DROP COLUMN IF EXISTS peer_settings;
ALTER TABLE user_update_events
DROP COLUMN IF EXISTS event_peers;

View file

@ -1,14 +0,0 @@
-- 0015_update_event_payloads_and_outbox_auth: keep setting-update payloads durable.
--
-- event_peers carries ordered dialog peers for updatePinnedDialogs.order.
-- peer_settings carries updatePeerSettings flags.
-- exclude_auth_key_id makes outbox exclusion precise for same session_id across auth keys.
ALTER TABLE user_update_events
ADD COLUMN IF NOT EXISTS event_peers JSONB NOT NULL DEFAULT '[]'::jsonb;
ALTER TABLE user_update_events
ADD COLUMN IF NOT EXISTS peer_settings JSONB NOT NULL DEFAULT '{}'::jsonb;
ALTER TABLE dispatch_outbox
ADD COLUMN IF NOT EXISTS exclude_auth_key_id BIGINT NOT NULL DEFAULT 0;

View file

@ -0,0 +1,15 @@
-- 回退:从白名单移除 'web_page'(与 0003 形态一致)。
ALTER TABLE public.user_update_events DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE public.user_update_events ADD CONSTRAINT user_update_events_type_check CHECK (
(event_type)::text = ANY (ARRAY[
'new_message', 'read_history_inbox', 'read_history_outbox', 'read_message_contents',
'edit_message', 'message_reactions', 'message_poll', 'draft_message', 'quick_replies',
'new_quick_reply', 'delete_quick_reply', 'quick_reply_message', 'delete_quick_reply_messages',
'contacts_reset', 'dialog_pinned', 'pinned_dialogs', 'pinned_messages', 'dialog_unread_mark',
'peer_settings', 'peer_story_blocked', 'delete_messages', 'dialog_filter', 'dialog_filter_order',
'dialog_filters', 'folder_peers', 'channel_available_messages', 'channel_view_forum_as_messages',
'channel_state', 'saved_dialog_pinned', 'pinned_saved_dialogs', 'story', 'read_stories',
'sent_story_reaction', 'new_story_reaction', 'noop',
'read_channel_discussion_inbox', 'read_channel_discussion_outbox'
]::text[])
);

View file

@ -0,0 +1,18 @@
-- 链接预览异步回填updateWebPage新增 user_update_events 事件类型 'web_page':发送时挂的
-- pending 链接预览占位被带外解析后,经 EditMessage 的 WebPageResolve 模式就地替换并 append
-- 一条 web_page durable 事件(按 webPage id 关联不标记「已编辑」。0001/0003 的白名单未含它,
-- 真 Postgres 上 append 会被 CHECK 约束拒绝SQLSTATE 23514memory store 无约束故单测未暴露。
ALTER TABLE public.user_update_events DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE public.user_update_events ADD CONSTRAINT user_update_events_type_check CHECK (
(event_type)::text = ANY (ARRAY[
'new_message', 'read_history_inbox', 'read_history_outbox', 'read_message_contents',
'edit_message', 'web_page', 'message_reactions', 'message_poll', 'draft_message', 'quick_replies',
'new_quick_reply', 'delete_quick_reply', 'quick_reply_message', 'delete_quick_reply_messages',
'contacts_reset', 'dialog_pinned', 'pinned_dialogs', 'pinned_messages', 'dialog_unread_mark',
'peer_settings', 'peer_story_blocked', 'delete_messages', 'dialog_filter', 'dialog_filter_order',
'dialog_filters', 'folder_peers', 'channel_available_messages', 'channel_view_forum_as_messages',
'channel_state', 'saved_dialog_pinned', 'pinned_saved_dialogs', 'story', 'read_stories',
'sent_story_reaction', 'new_story_reaction', 'noop',
'read_channel_discussion_inbox', 'read_channel_discussion_outbox'
]::text[])
);

View file

@ -0,0 +1,8 @@
-- 回退:从白名单移除 'channel_web_page'。
ALTER TABLE public.channel_update_events DROP CONSTRAINT IF EXISTS channel_update_events_type_check;
ALTER TABLE public.channel_update_events ADD CONSTRAINT channel_update_events_type_check CHECK (
(event_type)::text = ANY (ARRAY[
'new_channel_message', 'edit_channel_message', 'delete_channel_messages',
'channel_participant', 'pinned_channel_messages', 'noop'
]::text[])
);

View file

@ -0,0 +1,12 @@
-- 频道链接预览异步回填updateChannelWebPage新增 channel_update_events 事件类型
-- 'channel_web_page':频道消息的 pending 链接预览被带外解析后,经 EditChannelMessage 的
-- WebPageResolve 模式就地替换并 append 一条 channel_web_page durable 事件。0001 的白名单未含它,
-- 真 Postgres 上 append 会被 CHECK 约束拒绝SQLSTATE 23514memory store 无约束故单测未暴露
-- (同 0015/0003 教训)。
ALTER TABLE public.channel_update_events DROP CONSTRAINT IF EXISTS channel_update_events_type_check;
ALTER TABLE public.channel_update_events ADD CONSTRAINT channel_update_events_type_check CHECK (
(event_type)::text = ANY (ARRAY[
'new_channel_message', 'edit_channel_message', 'channel_web_page', 'delete_channel_messages',
'channel_participant', 'pinned_channel_messages', 'noop'
]::text[])
);

View file

@ -1,21 +0,0 @@
DROP INDEX IF EXISTS message_boxes_private_sender_live_idx;
ALTER TABLE user_update_events
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE user_update_events
ADD CONSTRAINT user_update_events_type_check CHECK (
event_type IN (
'new_message',
'read_history_inbox',
'contacts_reset',
'dialog_pinned',
'pinned_dialogs',
'dialog_unread_mark',
'peer_settings',
'noop'
)
);
ALTER TABLE user_update_events
DROP COLUMN IF EXISTS message_ids;

View file

@ -1,29 +0,0 @@
-- 0016_delete_message_updates: durable owner-view delete message updates.
--
-- message_ids carries updateDeleteMessages.messages for offline getDifference
-- and reliable online outbox delivery.
ALTER TABLE user_update_events
ADD COLUMN IF NOT EXISTS message_ids JSONB NOT NULL DEFAULT '[]'::jsonb;
ALTER TABLE user_update_events
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE user_update_events
ADD CONSTRAINT user_update_events_type_check CHECK (
event_type IN (
'new_message',
'read_history_inbox',
'contacts_reset',
'dialog_pinned',
'pinned_dialogs',
'dialog_unread_mark',
'peer_settings',
'delete_messages',
'noop'
)
);
CREATE INDEX IF NOT EXISTS message_boxes_private_sender_live_idx
ON message_boxes (message_sender_id, private_message_id)
WHERE NOT deleted;

View file

@ -1,46 +0,0 @@
-- 0017_dialog_folders rollback.
ALTER TABLE user_update_events
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE user_update_events
ADD CONSTRAINT user_update_events_type_check CHECK (
event_type IN (
'new_message',
'read_history_inbox',
'contacts_reset',
'dialog_pinned',
'pinned_dialogs',
'dialog_unread_mark',
'peer_settings',
'delete_messages',
'noop'
)
);
ALTER TABLE user_update_events
DROP COLUMN IF EXISTS tags_enabled;
ALTER TABLE user_update_events
DROP COLUMN IF EXISTS filter_id;
ALTER TABLE user_update_events
DROP COLUMN IF EXISTS folder_peers;
ALTER TABLE user_update_events
DROP COLUMN IF EXISTS filter_order;
ALTER TABLE user_update_events
DROP COLUMN IF EXISTS dialog_filter;
DROP TABLE IF EXISTS dialog_filter_settings CASCADE;
DROP TABLE IF EXISTS dialog_filters CASCADE;
DROP INDEX IF EXISTS dialogs_user_folder_top_message_idx;
ALTER TABLE dialogs
DROP CONSTRAINT IF EXISTS dialogs_folder_id_check;
ALTER TABLE dialogs
DROP COLUMN IF EXISTS folder_id;

View file

@ -1,91 +0,0 @@
-- 0017_dialog_folders: archive folder, custom dialog filters, and durable folder updates.
ALTER TABLE dialogs
ADD COLUMN IF NOT EXISTS folder_id INT NOT NULL DEFAULT 0;
ALTER TABLE dialogs
DROP CONSTRAINT IF EXISTS dialogs_folder_id_check;
ALTER TABLE dialogs
ADD CONSTRAINT dialogs_folder_id_check CHECK (folder_id >= 0);
CREATE INDEX IF NOT EXISTS dialogs_user_folder_top_message_idx
ON dialogs (user_id, folder_id, pinned DESC, top_message_date DESC, top_message_id DESC, peer_id DESC);
CREATE TABLE IF NOT EXISTS dialog_filters (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
filter_id INT NOT NULL,
is_chatlist BOOLEAN NOT NULL DEFAULT false,
filter JSONB NOT NULL,
order_value INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, filter_id),
CONSTRAINT dialog_filters_id_check CHECK (filter_id >= 2),
CONSTRAINT dialog_filters_filter_object_check CHECK (jsonb_typeof(filter) = 'object')
) PARTITION BY HASH (user_id);
CREATE INDEX IF NOT EXISTS dialog_filters_user_order_idx
ON dialog_filters (user_id, order_value, filter_id);
CREATE TABLE IF NOT EXISTS dialog_filter_settings (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tags_enabled BOOLEAN NOT NULL DEFAULT false,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id)
) PARTITION BY HASH (user_id);
DO $$
DECLARE
i int;
BEGIN
FOR i IN 0..63 LOOP
EXECUTE format(
'CREATE TABLE IF NOT EXISTS dialog_filters_p%s PARTITION OF dialog_filters FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
lpad(i::text, 2, '0'),
i
);
EXECUTE format(
'CREATE TABLE IF NOT EXISTS dialog_filter_settings_p%s PARTITION OF dialog_filter_settings FOR VALUES WITH (MODULUS 64, REMAINDER %s)',
lpad(i::text, 2, '0'),
i
);
END LOOP;
END $$;
ALTER TABLE user_update_events
ADD COLUMN IF NOT EXISTS dialog_filter JSONB NOT NULL DEFAULT '{}'::jsonb;
ALTER TABLE user_update_events
ADD COLUMN IF NOT EXISTS filter_order JSONB NOT NULL DEFAULT '[]'::jsonb;
ALTER TABLE user_update_events
ADD COLUMN IF NOT EXISTS folder_peers JSONB NOT NULL DEFAULT '[]'::jsonb;
ALTER TABLE user_update_events
ADD COLUMN IF NOT EXISTS filter_id INT NOT NULL DEFAULT 0;
ALTER TABLE user_update_events
ADD COLUMN IF NOT EXISTS tags_enabled BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE user_update_events
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE user_update_events
ADD CONSTRAINT user_update_events_type_check CHECK (
event_type IN (
'new_message',
'read_history_inbox',
'contacts_reset',
'dialog_pinned',
'pinned_dialogs',
'dialog_unread_mark',
'peer_settings',
'delete_messages',
'dialog_filter',
'dialog_filter_order',
'dialog_filters',
'folder_peers',
'noop'
)
);

View file

@ -0,0 +1 @@
ALTER TABLE public.account_passwords DROP COLUMN login_email;

View file

@ -0,0 +1,4 @@
-- 登录邮箱login email独立于 2FA 恢复邮箱的备用登录方式。新设备登录时验证码
-- 改投递到该邮箱auth.sentCodeTypeEmailCode。仅存已确认的真实地址下发时掩码。
ALTER TABLE public.account_passwords
ADD COLUMN login_email character varying(256) DEFAULT ''::character varying NOT NULL;

View file

@ -0,0 +1 @@
DROP TABLE public.passkey_credentials;

View file

@ -0,0 +1,18 @@
-- Passkey(WebAuthn/FIDO2)凭据。每条 = 一个已注册的公钥。sign_count 用 bigint
-- 容纳 uint32;credential_id 为主键(原始字节,对外 base64url)。
CREATE TABLE public.passkey_credentials (
credential_id bytea NOT NULL,
user_id bigint NOT NULL,
public_key bytea NOT NULL,
sign_count bigint DEFAULT 0 NOT NULL,
aaguid bytea DEFAULT '\x'::bytea NOT NULL,
name character varying(128) DEFAULT ''::character varying NOT NULL,
transports text[] DEFAULT '{}'::text[] NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
last_used_at timestamp with time zone,
CONSTRAINT passkey_credentials_pkey PRIMARY KEY (credential_id),
CONSTRAINT passkey_credentials_user_id_fkey FOREIGN KEY (user_id)
REFERENCES public.users(id) ON DELETE CASCADE
);
CREATE INDEX passkey_credentials_user_id_idx ON public.passkey_credentials (user_id);

View file

@ -1,5 +0,0 @@
DROP INDEX IF EXISTS message_boxes_body_trgm_idx;
DROP INDEX IF EXISTS contacts_user_saved_name_trgm_idx;
DROP INDEX IF EXISTS users_name_lower_trgm_idx;
DROP INDEX IF EXISTS users_username_lower_trgm_idx;
DROP INDEX IF EXISTS users_phone_prefix_idx;

View file

@ -1,19 +0,0 @@
-- 0018_user_search_indexes: keep TDesktop global user search bounded as users grow.
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX IF NOT EXISTS users_phone_prefix_idx
ON users (phone text_pattern_ops);
CREATE INDEX IF NOT EXISTS users_username_lower_trgm_idx
ON users USING gin (lower(username) gin_trgm_ops);
CREATE INDEX IF NOT EXISTS users_name_lower_trgm_idx
ON users USING gin (lower(trim(first_name || ' ' || last_name)) gin_trgm_ops);
CREATE INDEX IF NOT EXISTS contacts_user_saved_name_trgm_idx
ON contacts USING gin (lower(trim(contact_first_name || ' ' || contact_last_name)) gin_trgm_ops);
CREATE INDEX IF NOT EXISTS message_boxes_body_trgm_idx
ON message_boxes USING gin (body gin_trgm_ops)
WHERE NOT deleted AND body <> '';

View file

@ -0,0 +1,2 @@
DROP TABLE public.theme_user_installs;
DROP TABLE public.themes;

View file

@ -0,0 +1,37 @@
-- 自定义云主题(account.createTheme/uploadTheme 等)。themes = 用户创建的主题目录,
-- document_id 软引用 documents(无硬外键,避免与媒体表耦合;主题可能比 blob 长存,
-- getTheme 时若 blob 缺失则退化为无 document)。settings = []domain.ThemeSettingsSpec 的
-- JSONB(仅 accent 主题非空)。theme_user_installs = 每用户已安装/已存主题列表。
CREATE TABLE public.themes (
id bigint NOT NULL,
access_hash bigint DEFAULT 0 NOT NULL,
creator_user_id bigint NOT NULL,
slug text NOT NULL,
title text DEFAULT ''::text NOT NULL,
emoticon text DEFAULT ''::text NOT NULL,
for_chat boolean DEFAULT false NOT NULL,
document_id bigint DEFAULT 0 NOT NULL,
settings jsonb DEFAULT '[]'::jsonb NOT NULL,
installs_count integer DEFAULT 0 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT themes_pkey PRIMARY KEY (id),
CONSTRAINT themes_slug_key UNIQUE (slug),
CONSTRAINT themes_creator_user_id_fkey FOREIGN KEY (creator_user_id)
REFERENCES public.users(id) ON DELETE CASCADE
);
CREATE INDEX themes_creator_user_id_idx ON public.themes (creator_user_id);
CREATE TABLE public.theme_user_installs (
user_id bigint NOT NULL,
theme_id bigint NOT NULL,
dark boolean DEFAULT false NOT NULL,
installed_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT theme_user_installs_pkey PRIMARY KEY (user_id, theme_id),
CONSTRAINT theme_user_installs_user_id_fkey FOREIGN KEY (user_id)
REFERENCES public.users(id) ON DELETE CASCADE,
CONSTRAINT theme_user_installs_theme_id_fkey FOREIGN KEY (theme_id)
REFERENCES public.themes(id) ON DELETE CASCADE
);
CREATE INDEX theme_user_installs_user_id_idx ON public.theme_user_installs (user_id);

View file

@ -1 +0,0 @@
DROP INDEX IF EXISTS users_username_lower_unique_idx;

View file

@ -1,5 +0,0 @@
-- 0019_usernames: primary username lifecycle.
CREATE UNIQUE INDEX IF NOT EXISTS users_username_lower_unique_idx
ON users (lower(username))
WHERE username <> '';

View file

@ -0,0 +1,2 @@
ALTER TABLE public.channels DROP COLUMN IF EXISTS linked_monoforum_id;
ALTER TABLE public.channels DROP COLUMN IF EXISTS monoforum;

View file

@ -0,0 +1,6 @@
-- 频道私信(Direct Messages):母广播频道关联一个 monoforum 虚拟频道承载订阅者私信。
-- monoforum=true 标记该频道是 monoforum;linked_monoforum_id 双向关联母频道 ↔ monoforum。
ALTER TABLE public.channels
ADD COLUMN monoforum boolean DEFAULT false NOT NULL;
ALTER TABLE public.channels
ADD COLUMN linked_monoforum_id bigint DEFAULT 0 NOT NULL;

View file

@ -1,34 +0,0 @@
ALTER TABLE user_update_events
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE user_update_events
ADD CONSTRAINT user_update_events_type_check CHECK (
event_type IN (
'new_message',
'read_history_inbox',
'contacts_reset',
'dialog_pinned',
'pinned_dialogs',
'dialog_unread_mark',
'peer_settings',
'delete_messages',
'dialog_filter',
'dialog_filter_order',
'dialog_filters',
'folder_peers',
'noop'
)
);
DROP INDEX IF EXISTS message_boxes_private_sender_owner_idx;
DROP INDEX IF EXISTS user_update_events_read_outbox_idx;
DROP INDEX IF EXISTS message_boxes_read_receipt_idx;
ALTER TABLE message_boxes
DROP COLUMN IF EXISTS edit_date;
ALTER TABLE private_messages
DROP COLUMN IF EXISTS edit_date;
ALTER TABLE users
DROP COLUMN IF EXISTS about;

View file

@ -1,46 +0,0 @@
-- 0020_profile_message_state: profile about, message edits, and read outbox updates.
ALTER TABLE users
ADD COLUMN IF NOT EXISTS about VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE private_messages
ADD COLUMN IF NOT EXISTS edit_date INT NOT NULL DEFAULT 0;
ALTER TABLE message_boxes
ADD COLUMN IF NOT EXISTS edit_date INT NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS message_boxes_read_receipt_idx
ON message_boxes (owner_user_id, peer_type, peer_id, box_id DESC)
WHERE NOT deleted AND NOT outgoing;
CREATE INDEX IF NOT EXISTS message_boxes_private_sender_owner_idx
ON message_boxes (message_sender_id, private_message_id, owner_user_id)
WHERE NOT deleted;
CREATE INDEX IF NOT EXISTS user_update_events_read_outbox_idx
ON user_update_events (user_id, peer_type, peer_id, max_id, date)
WHERE event_type = 'read_history_outbox';
ALTER TABLE user_update_events
DROP CONSTRAINT IF EXISTS user_update_events_type_check;
ALTER TABLE user_update_events
ADD CONSTRAINT user_update_events_type_check CHECK (
event_type IN (
'new_message',
'read_history_inbox',
'read_history_outbox',
'edit_message',
'contacts_reset',
'dialog_pinned',
'pinned_dialogs',
'dialog_unread_mark',
'peer_settings',
'delete_messages',
'dialog_filter',
'dialog_filter_order',
'dialog_filters',
'folder_peers',
'noop'
)
);

View file

@ -0,0 +1,3 @@
DROP INDEX IF EXISTS channel_messages_monoforum_sublist_idx;
ALTER TABLE public.channel_messages DROP COLUMN IF EXISTS saved_peer_id;
ALTER TABLE public.channel_messages DROP COLUMN IF EXISTS saved_peer_type;

View file

@ -0,0 +1,9 @@
-- 频道私信(monoforum):私信消息存进 channel_messages(复用 channel pts/事件/difference),
-- 用 saved_peer 维度按订阅者分子会话。普通频道消息 saved_peer 为空,部分索引仅覆盖 monoforum 行。
ALTER TABLE public.channel_messages
ADD COLUMN saved_peer_type character varying(16) DEFAULT ''::character varying NOT NULL;
ALTER TABLE public.channel_messages
ADD COLUMN saved_peer_id bigint DEFAULT 0 NOT NULL;
CREATE INDEX channel_messages_monoforum_sublist_idx ON public.channel_messages
USING btree (channel_id, saved_peer_id, id DESC)
WHERE ((saved_peer_id <> 0) AND (NOT deleted));

View file

@ -1,31 +0,0 @@
DROP INDEX IF EXISTS message_boxes_reply_lookup_idx;
ALTER TABLE message_boxes
DROP COLUMN IF EXISTS fwd_date,
DROP COLUMN IF EXISTS fwd_from_name,
DROP COLUMN IF EXISTS fwd_from_peer_id,
DROP COLUMN IF EXISTS fwd_from_peer_type,
DROP COLUMN IF EXISTS quote_offset,
DROP COLUMN IF EXISTS quote_entities,
DROP COLUMN IF EXISTS quote_text,
DROP COLUMN IF EXISTS reply_to_top_id,
DROP COLUMN IF EXISTS reply_to_peer_id,
DROP COLUMN IF EXISTS reply_to_peer_type,
DROP COLUMN IF EXISTS reply_to_msg_id,
DROP COLUMN IF EXISTS noforwards,
DROP COLUMN IF EXISTS silent;
ALTER TABLE private_messages
DROP COLUMN IF EXISTS fwd_date,
DROP COLUMN IF EXISTS fwd_from_name,
DROP COLUMN IF EXISTS fwd_from_peer_id,
DROP COLUMN IF EXISTS fwd_from_peer_type,
DROP COLUMN IF EXISTS quote_offset,
DROP COLUMN IF EXISTS quote_entities,
DROP COLUMN IF EXISTS quote_text,
DROP COLUMN IF EXISTS reply_to_top_id,
DROP COLUMN IF EXISTS reply_to_peer_id,
DROP COLUMN IF EXISTS reply_to_peer_type,
DROP COLUMN IF EXISTS reply_to_msg_id,
DROP COLUMN IF EXISTS noforwards,
DROP COLUMN IF EXISTS silent;

View file

@ -1,35 +0,0 @@
-- 0021_message_reply_forward: private-message silent/reply/forward metadata.
ALTER TABLE private_messages
ADD COLUMN IF NOT EXISTS silent BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS noforwards BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS reply_to_msg_id INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS reply_to_peer_type VARCHAR(16) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS reply_to_peer_id BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS reply_to_top_id INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS quote_text TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS quote_entities JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS quote_offset INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS fwd_from_peer_type VARCHAR(16) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS fwd_from_peer_id BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS fwd_from_name TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS fwd_date INT NOT NULL DEFAULT 0;
ALTER TABLE message_boxes
ADD COLUMN IF NOT EXISTS silent BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS noforwards BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS reply_to_msg_id INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS reply_to_peer_type VARCHAR(16) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS reply_to_peer_id BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS reply_to_top_id INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS quote_text TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS quote_entities JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS quote_offset INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS fwd_from_peer_type VARCHAR(16) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS fwd_from_peer_id BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS fwd_from_name TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS fwd_date INT NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS message_boxes_reply_lookup_idx
ON message_boxes (owner_user_id, peer_type, peer_id, box_id)
WHERE NOT deleted;

View file

@ -0,0 +1,3 @@
DROP INDEX IF EXISTS channel_messages_random_idx;
CREATE UNIQUE INDEX channel_messages_random_idx ON public.channel_messages
USING btree (channel_id, sender_user_id, random_id) WHERE (random_id <> 0);

View file

@ -0,0 +1,6 @@
-- 频道私信(monoforum):同一发件人(尤其频道管理员)可向不同订阅者子会话用相同 random_id 发消息,
-- 幂等唯一性必须含 saved_peer,否则跨子会话复用 random_id 会被旧三元组唯一索引误拒/误判为重复。
-- 普通频道消息 saved_peer_id=0 恒定,新四元组等价于原 (channel_id,sender_user_id,random_id),无行为变化。
DROP INDEX IF EXISTS channel_messages_random_idx;
CREATE UNIQUE INDEX channel_messages_random_idx ON public.channel_messages
USING btree (channel_id, sender_user_id, saved_peer_id, random_id) WHERE (random_id <> 0);

View file

@ -1,9 +0,0 @@
DROP TABLE IF EXISTS channel_invites CASCADE;
DROP TABLE IF EXISTS channel_dialogs CASCADE;
DROP TABLE IF EXISTS channel_admin_log_events CASCADE;
DROP TABLE IF EXISTS channel_update_events CASCADE;
DROP TABLE IF EXISTS channel_messages CASCADE;
DROP TABLE IF EXISTS channel_members CASCADE;
DROP TABLE IF EXISTS channel_usernames CASCADE;
DROP TABLE IF EXISTS channel_invite_hashes CASCADE;
DROP TABLE IF EXISTS channels CASCADE;

View file

@ -1,398 +0,0 @@
-- 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 $$;

View file

@ -1,18 +0,0 @@
DROP INDEX IF EXISTS channel_invites_hash_lookup_idx;
ALTER TABLE channel_update_events
DROP CONSTRAINT IF EXISTS channel_update_events_type_check;
ALTER TABLE channel_update_events
ADD CONSTRAINT channel_update_events_type_check CHECK (
event_type IN (
'new_channel_message',
'edit_channel_message',
'delete_channel_messages',
'channel_participant',
'noop'
)
);
ALTER TABLE channels
DROP COLUMN IF EXISTS pinned_message_id;

View file

@ -1,22 +0,0 @@
-- 0023_channel_admin_invites: metadata needed by channel admin/pin/invite RPCs.
ALTER TABLE channels
ADD COLUMN IF NOT EXISTS pinned_message_id INT NOT NULL DEFAULT 0;
ALTER TABLE channel_update_events
DROP CONSTRAINT IF EXISTS channel_update_events_type_check;
ALTER TABLE channel_update_events
ADD 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'
)
);
CREATE INDEX IF NOT EXISTS channel_invites_hash_lookup_idx
ON channel_invite_hashes (hash, channel_id, invite_id);

View file

@ -0,0 +1,2 @@
ALTER TABLE public.message_boxes DROP COLUMN effect;
ALTER TABLE public.private_messages DROP COLUMN effect;

View file

@ -0,0 +1,6 @@
-- 消息特效message effectmessages.sendMessage/sendMedia 的 effect:flags2.2?long
-- 私聊 1-1 专属动画特效(🎉/👍 等,发送方与接收方双向播放)。官方所有客户端仅在私聊
-- 显示特效选择器,群/频道从不渲染,故只镜像私聊两张表(共享主体 + 每 owner 收件箱),
-- channel_messages 不加列。镜像 via_bot_id / grouped_id 的标量列写法bigint0 表无特效)。
ALTER TABLE public.private_messages ADD COLUMN effect bigint DEFAULT 0 NOT NULL;
ALTER TABLE public.message_boxes ADD COLUMN effect bigint DEFAULT 0 NOT NULL;

View file

@ -1,6 +0,0 @@
-- No-op by design.
--
-- These columns/table are part of the fresh 0022 channel schema. This migration
-- only backfills older developer databases that had already applied an earlier
-- 0022 draft, so dropping the objects on a one-step rollback would corrupt fresh
-- schemas where 0022 legitimately owns them.

View file

@ -1,78 +0,0 @@
-- 0024_channel_admin_log_backfill: bring existing developer DBs forward after
-- channel settings/admin-log fields were added to the initial 0022 draft.
--
-- Fresh databases already get these objects from 0022; every statement here is
-- idempotent so old local databases can migrate without reset.
ALTER TABLE channels
ADD COLUMN IF NOT EXISTS pre_history_hidden BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS slowmode_seconds INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS admin_log_seq BIGINT NOT NULL DEFAULT 0;
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 $$;

View file

@ -0,0 +1,5 @@
ALTER TABLE public.users
DROP COLUMN personal_channel_id,
DROP COLUMN birthday_year,
DROP COLUMN birthday_month,
DROP COLUMN birthday_day;

View file

@ -0,0 +1,8 @@
-- account.updateBirthday / account.updatePersonalChannel个人资料里的「生日」与「个人频道」。
-- 生日存月/日/年(全 0 表示未设置year=0 表示只填月日不含年份);个人频道存目标频道 id
-- 0 表示未设置/已清除),资料投影时按 id 取频道对象与其最新一帖 id。
ALTER TABLE public.users
ADD COLUMN birthday_day integer DEFAULT 0 NOT NULL,
ADD COLUMN birthday_month integer DEFAULT 0 NOT NULL,
ADD COLUMN birthday_year integer DEFAULT 0 NOT NULL,
ADD COLUMN personal_channel_id bigint DEFAULT 0 NOT NULL;

View file

@ -1,4 +0,0 @@
-- No-op by design.
--
-- Fresh databases own this column from 0022; this migration only repairs older
-- developer databases that had already applied an earlier 0022 draft.

View file

@ -1,5 +0,0 @@
-- 0025_channel_member_slowmode_backfill: idempotent compatibility migration for
-- developer databases that applied an earlier 0022 channel schema draft.
ALTER TABLE channel_members
ADD COLUMN IF NOT EXISTS slowmode_last_send_date INT NOT NULL DEFAULT 0;

View file

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS public.channel_usernames (
username_lower text NOT NULL,
channel_id bigint NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT channel_usernames_nonempty_check CHECK (username_lower <> '')
);
ALTER TABLE ONLY public.channel_usernames
ADD CONSTRAINT channel_usernames_pkey PRIMARY KEY (username_lower);
INSERT INTO public.channel_usernames (username_lower, channel_id)
SELECT username_lower, peer_id
FROM public.peer_usernames
WHERE peer_type = 'channel';
DROP TRIGGER IF EXISTS users_delete_peer_username ON public.users;
DROP TRIGGER IF EXISTS channels_delete_peer_username ON public.channels;
DROP FUNCTION IF EXISTS public.delete_user_peer_username();
DROP FUNCTION IF EXISTS public.delete_channel_peer_username();
DROP TABLE IF EXISTS public.peer_usernames;

View file

@ -0,0 +1,51 @@
CREATE TABLE IF NOT EXISTS public.peer_usernames (
username_lower text NOT NULL,
peer_type text NOT NULL,
peer_id bigint NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT peer_usernames_pkey PRIMARY KEY (username_lower),
CONSTRAINT peer_usernames_nonempty_check CHECK (username_lower <> ''),
CONSTRAINT peer_usernames_peer_id_check CHECK (peer_id <> 0),
CONSTRAINT peer_usernames_peer_type_check CHECK (peer_type IN ('user', 'channel'))
);
CREATE UNIQUE INDEX IF NOT EXISTS peer_usernames_peer_unique_idx
ON public.peer_usernames (peer_type, peer_id);
INSERT INTO public.peer_usernames (username_lower, peer_type, peer_id)
SELECT lower(username), 'user', id
FROM public.users
WHERE username <> '';
INSERT INTO public.peer_usernames (username_lower, peer_type, peer_id)
SELECT lower(username), 'channel', id
FROM public.channels
WHERE NOT deleted AND COALESCE(username, '') <> '';
CREATE OR REPLACE FUNCTION public.delete_user_peer_username() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
DELETE FROM public.peer_usernames WHERE peer_type = 'user' AND peer_id = OLD.id;
RETURN OLD;
END;
$$;
CREATE OR REPLACE FUNCTION public.delete_channel_peer_username() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
DELETE FROM public.peer_usernames WHERE peer_type = 'channel' AND peer_id = OLD.id;
RETURN OLD;
END;
$$;
CREATE TRIGGER users_delete_peer_username
AFTER DELETE ON public.users
FOR EACH ROW EXECUTE FUNCTION public.delete_user_peer_username();
CREATE TRIGGER channels_delete_peer_username
AFTER DELETE ON public.channels
FOR EACH ROW EXECUTE FUNCTION public.delete_channel_peer_username();
DROP TABLE IF EXISTS public.channel_usernames;

View file

@ -1 +0,0 @@
-- No-op: read receipt dates and TDesktop app config keys are forward-compatible.

Some files were not shown because too many files have changed in this diff Show more