perf(api,frontend): index hot queries, paginate chats, fix realtime

This commit is contained in:
hh
2026-08-06 01:52:41 +02:00
parent 9d767d2531
commit 525ce024bc
27 changed files with 831 additions and 268 deletions
+121 -70
View File
@@ -1,5 +1,6 @@
import asyncpg
from utils.policy.models import FolderSpec
from utils.read.accounts import self_user_id
from utils.read.message_view import build_message_view, load_raw, media_ref_from
from utils.read.models import (
@@ -43,81 +44,131 @@ def _single_media(
return [ref] if ref else []
def _peer_title(
first: str | None, last: str | None, username: str | None
) -> str | None:
name = " ".join(part for part in (first, last) if part)
return name or username
_ALL_IDS = """
SELECT chat_id FROM chat_stats WHERE account_id = $1
UNION
SELECT chat_id FROM dialogs WHERE account_id = $1
UNION
SELECT scope_id AS chat_id FROM capture_policy
WHERE account_id = $1 AND scope_type = 'chat' AND scope_id IS NOT NULL
"""
_ONE_ID = """
SELECT chat_id FROM chat_stats WHERE account_id = $1 AND chat_id = $2
UNION
SELECT chat_id FROM dialogs WHERE account_id = $1 AND chat_id = $2
UNION
SELECT scope_id AS chat_id FROM capture_policy
WHERE account_id = $1 AND scope_type = 'chat' AND scope_id = $2
"""
_CHAT_ROWS = """
WITH ids AS ({ids})
SELECT ids.chat_id,
COALESCE(cs.message_count, 0) AS message_count,
cs.last_date, cs.last_text, cs.last_sender_id,
COALESCE(named.title,
NULLIF(trim(concat_ws(' ', p.first_name, p.last_name)), ''),
p.username) AS title,
COALESCE(typed.is_broadcast, false) AS is_broadcast,
COALESCE((p.raw->>'is_bot')::bool, (p.raw->>'bot')::bool,
p.raw->>'type' = 'ChatType.BOT', false) AS is_bot,
COALESCE((p.raw->>'is_contact')::bool, (p.raw->>'contact')::bool,
false) AS is_contact,
EXISTS (SELECT 1 FROM avatars a
WHERE a.account_id = $1 AND a.owner_id = ids.chat_id) AS has_avatar
FROM ids
LEFT JOIN chat_stats cs ON cs.account_id = $1 AND cs.chat_id = ids.chat_id
LEFT JOIN peers p ON p.account_id = $1 AND p.peer_id = ids.chat_id
LEFT JOIN LATERAL (
SELECT ch.title FROM chat_history ch
WHERE ch.account_id = $1 AND ch.chat_id = ids.chat_id AND ch.title IS NOT NULL
ORDER BY ch.ts DESC LIMIT 1
) named ON true
LEFT JOIN LATERAL (
SELECT COALESCE(ch.raw->'chat'->>'type', ch.raw->>'type')
= 'ChatType.CHANNEL' AS is_broadcast
FROM chat_history ch
WHERE ch.account_id = $1 AND ch.chat_id = ids.chat_id
AND COALESCE(ch.raw->'chat'->>'type', ch.raw->>'type') IS NOT NULL
ORDER BY ch.ts DESC LIMIT 1
) typed ON true
"""
def _folder_filter(base: int) -> str:
return (
f"NOT (chat.chat_id = ANY(${base + 1}::bigint[])) "
f"AND (chat.chat_id = ANY(${base + 2}::bigint[]) "
f"OR (NOT ${base + 3}::bool AND CASE "
f"WHEN chat.is_broadcast THEN ${base + 4}::bool "
f"WHEN chat.chat_id < 0 THEN ${base + 5}::bool "
f"WHEN chat.is_bot THEN ${base + 6}::bool "
f"WHEN chat.is_contact THEN ${base + 7}::bool "
f"ELSE ${base + 8}::bool END))"
)
def _folder_params(folder: FolderSpec) -> list[object]:
return [
sorted(folder.exclude_ids),
sorted(folder.include_ids | folder.pinned_ids),
folder.is_chatlist,
folder.broadcasts,
folder.groups,
folder.bots,
folder.contacts,
folder.non_contacts,
]
def _chat_item(row: asyncpg.Record) -> ChatListItem:
return ChatListItem(
chat_id=row["chat_id"],
title=row["title"],
kind="private" if row["chat_id"] > 0 else "group",
has_avatar=row["has_avatar"],
is_bot=bool(row["is_bot"]),
is_contact=bool(row["is_contact"]),
is_broadcast=bool(row["is_broadcast"]),
message_count=row["message_count"],
last_date=row["last_date"],
last_text=row["last_text"],
last_sender_id=row["last_sender_id"],
)
async def list_chats(
pool: asyncpg.Pool, account_id: int, page: Page
pool: asyncpg.Pool,
account_id: int,
page: Page,
*,
folder: FolderSpec | None = None,
search: str | None = None,
) -> list[ChatListItem]:
rows = await pool.fetch(
"WITH ids AS ("
"SELECT DISTINCT chat_id FROM messages WHERE account_id = $1 "
"UNION SELECT chat_id FROM dialogs WHERE account_id = $1 "
"UNION SELECT scope_id FROM capture_policy WHERE account_id = $1 "
"AND scope_type = 'chat' AND scope_id IS NOT NULL), "
"agg AS (SELECT chat_id, count(*) AS message_count, max(date) AS last_date "
"FROM messages WHERE account_id = $1 GROUP BY chat_id) "
"SELECT ids.chat_id, COALESCE(agg.message_count, 0) AS message_count, "
"agg.last_date AS last_date, "
"(SELECT p.first_name FROM peers p "
"WHERE p.account_id = $1 AND p.peer_id = ids.chat_id) AS first_name, "
"(SELECT p.last_name FROM peers p "
"WHERE p.account_id = $1 AND p.peer_id = ids.chat_id) AS last_name, "
"(SELECT p.username FROM peers p "
"WHERE p.account_id = $1 AND p.peer_id = ids.chat_id) AS username, "
"(SELECT ch.title FROM chat_history ch "
"WHERE ch.account_id = $1 AND ch.chat_id = ids.chat_id "
"AND ch.title IS NOT NULL ORDER BY ch.ts DESC LIMIT 1) AS group_title, "
"EXISTS (SELECT 1 FROM avatars a "
"WHERE a.account_id = $1 AND a.owner_id = ids.chat_id) AS has_avatar, "
"(SELECT COALESCE((p.raw->>'is_bot')::bool, (p.raw->>'bot')::bool, "
"p.raw->>'type' = 'ChatType.BOT', false) "
"FROM peers p WHERE p.account_id = $1 AND p.peer_id = ids.chat_id) AS is_bot, "
"(SELECT COALESCE((p.raw->>'is_contact')::bool, (p.raw->>'contact')::bool, "
"false) FROM peers p "
"WHERE p.account_id = $1 AND p.peer_id = ids.chat_id) AS is_contact, "
"(SELECT COALESCE(ch.raw->'chat'->>'type', ch.raw->>'type') "
"= 'ChatType.CHANNEL' FROM chat_history ch "
"WHERE ch.account_id = $1 AND ch.chat_id = ids.chat_id "
"AND COALESCE(ch.raw->'chat'->>'type', ch.raw->>'type') IS NOT NULL "
"ORDER BY ch.ts DESC LIMIT 1) AS is_broadcast, "
"(SELECT lm.text FROM messages lm "
"WHERE lm.account_id = $1 AND lm.chat_id = ids.chat_id "
"ORDER BY lm.date DESC, lm.message_id DESC LIMIT 1) AS last_text, "
"(SELECT lm.sender_id FROM messages lm "
"WHERE lm.account_id = $1 AND lm.chat_id = ids.chat_id "
"ORDER BY lm.date DESC, lm.message_id DESC LIMIT 1) AS last_sender_id "
"FROM ids LEFT JOIN agg ON agg.chat_id = ids.chat_id "
"ORDER BY last_date DESC NULLS LAST, ids.chat_id DESC LIMIT $2 OFFSET $3",
account_id,
page.capped_limit,
page.offset,
params: list[object] = [account_id, page.capped_limit, page.offset]
rows_sql = _CHAT_ROWS.format(ids=_ALL_IDS)
clauses: list[str] = []
if folder is not None:
clauses.append(_folder_filter(len(params)))
params.extend(_folder_params(folder))
if search:
params.append(f"%{search}%")
clauses.append(f"chat.title ILIKE ${len(params)}")
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
query = (
f"SELECT chat.* FROM ({rows_sql}) chat{where} " # noqa: S608
"ORDER BY last_date DESC NULLS LAST, chat_id DESC LIMIT $2 OFFSET $3"
)
items = []
for row in rows:
title = row["group_title"] or _peer_title(
row["first_name"], row["last_name"], row["username"]
)
items.append(
ChatListItem(
chat_id=row["chat_id"],
title=title,
kind="private" if row["chat_id"] > 0 else "group",
has_avatar=row["has_avatar"],
is_bot=bool(row["is_bot"]),
is_contact=bool(row["is_contact"]),
is_broadcast=bool(row["is_broadcast"]),
message_count=row["message_count"],
last_date=row["last_date"],
last_text=row["last_text"],
last_sender_id=row["last_sender_id"],
)
)
return items
rows = await pool.fetch(query, *params)
return [_chat_item(row) for row in rows]
async def get_chat(
pool: asyncpg.Pool, account_id: int, chat_id: int
) -> ChatListItem | None:
row = await pool.fetchrow(_CHAT_ROWS.format(ids=_ONE_ID), account_id, chat_id)
return _chat_item(row) if row is not None else None
async def get_chat_history( # noqa: PLR0913