directory: move all storage to the database, optimize performance (#6287)

* core: add custom indexed columns to groups and contacts

* directory: use database (TODO search/listing)

* triggers to maintain current member count

* update simplexmq, fix tests, use summary from GroupInfo

* fix all directory tests

* remove acceptance fields from group reg

* enable all tests

* clean up

* postgres migrations, fixes

* query plans

* use function in postgres triggers, improve sqlite query

* fix export/import

* update schema

* prevent admins from promoting groups when approving

* update listing every 5 minutes
This commit is contained in:
Evgeny
2025-09-28 15:11:49 +01:00
committed by GitHub
parent 53d6d057c6
commit 015d5de364
40 changed files with 1897 additions and 1054 deletions
@@ -17,6 +17,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20250729_member_contact_requests
import Simplex.Chat.Store.Postgres.Migrations.M20250801_via_group_link_uri
import Simplex.Chat.Store.Postgres.Migrations.M20250802_chat_peer_type
import Simplex.Chat.Store.Postgres.Migrations.M20250813_delivery_tasks
import Simplex.Chat.Store.Postgres.Migrations.M20250919_group_summary
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -33,7 +34,8 @@ schemaMigrations =
("20250729_member_contact_requests", m20250729_member_contact_requests, Just down_m20250729_member_contact_requests),
("20250801_via_group_link_uri", m20250801_via_group_link_uri, Just down_m20250801_via_group_link_uri),
("20250802_chat_peer_type", m20250802_chat_peer_type, Just down_m20250802_chat_peer_type),
("20250813_delivery_tasks", m20250813_delivery_tasks, Just down_m20250813_delivery_tasks)
("20250813_delivery_tasks", m20250813_delivery_tasks, Just down_m20250813_delivery_tasks),
("20250919_group_summary", m20250919_group_summary, Just down_m20250919_group_summary)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,116 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20250919_group_summary where
import Data.Text (Text)
import qualified Data.Text as T
import Text.RawString.QQ (r)
m20250919_group_summary :: Text
m20250919_group_summary =
T.pack
[r|
ALTER TABLE groups ADD COLUMN summary_current_members_count BIGINT NOT NULL DEFAULT 0;
CREATE INDEX idx_groups_summary_current_members_count ON groups(summary_current_members_count);
CREATE FUNCTION is_current_member(p_status TEXT) RETURNS BOOLEAN
LANGUAGE plpgsql AS $$
BEGIN
RETURN p_status IN (
'introduced',
'intro-inv',
'accepted',
'announced',
'connected',
'complete',
'creator'
);
END;
$$;
UPDATE groups g
SET summary_current_members_count = COALESCE(c.cnt, 0)
FROM (
SELECT group_id, COUNT(group_member_id) AS cnt
FROM group_members
WHERE is_current_member(member_status) = TRUE
GROUP BY group_id
) c
WHERE g.group_id = c.group_id;
CREATE FUNCTION on_group_members_insert_update_summary() RETURNS TRIGGER
LANGUAGE plpgsql AS $$
BEGIN
IF is_current_member(NEW.member_status) THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count + 1
WHERE group_id = NEW.group_id;
END IF;
RETURN NEW;
END;
$$;
CREATE FUNCTION on_group_members_delete_update_summary() RETURNS TRIGGER
LANGUAGE plpgsql AS $$
BEGIN
IF is_current_member(OLD.member_status) THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count - 1
WHERE group_id = OLD.group_id;
END IF;
RETURN OLD;
END;
$$;
CREATE FUNCTION on_group_members_update_update_summary() RETURNS TRIGGER
LANGUAGE plpgsql AS $$
DECLARE
was_active BOOLEAN;
is_active BOOLEAN;
BEGIN
was_active := is_current_member(OLD.member_status);
is_active := is_current_member(NEW.member_status);
IF was_active != is_active THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count +
(CASE WHEN is_active THEN 1 ELSE -1 END)
WHERE group_id = NEW.group_id;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER tr_group_members_insert_update_summary
AFTER INSERT ON group_members
FOR EACH ROW
EXECUTE FUNCTION on_group_members_insert_update_summary();
CREATE TRIGGER tr_group_members_delete_update_summary
AFTER DELETE ON group_members
FOR EACH ROW
EXECUTE FUNCTION on_group_members_delete_update_summary();
CREATE TRIGGER tr_group_members_update_update_summary
AFTER UPDATE ON group_members
FOR EACH ROW
EXECUTE FUNCTION on_group_members_update_update_summary();
|]
down_m20250919_group_summary :: Text
down_m20250919_group_summary =
T.pack
[r|
DROP TRIGGER tr_group_members_insert_update_summary ON group_members;
DROP TRIGGER tr_group_members_delete_update_summary ON group_members;
DROP TRIGGER tr_group_members_update_update_summary ON group_members;
DROP FUNCTION on_group_members_insert_update_summary;
DROP FUNCTION on_group_members_delete_update_summary;
DROP FUNCTION on_group_members_update_update_summary;
DROP FUNCTION is_current_member;
DROP INDEX idx_groups_summary_current_members_count;
ALTER TABLE groups DROP COLUMN summary_current_members_count;
|]
@@ -15,6 +15,76 @@ SET row_security = off;
CREATE SCHEMA test_chat_schema;
CREATE FUNCTION test_chat_schema.is_current_member(p_status text) RETURNS boolean
LANGUAGE plpgsql
AS $$
BEGIN
RETURN p_status IN (
'introduced',
'intro-inv',
'accepted',
'announced',
'connected',
'complete',
'creator'
);
END;
$$;
CREATE FUNCTION test_chat_schema.on_group_members_delete_update_summary() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF is_current_member(OLD.member_status) THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count - 1
WHERE group_id = OLD.group_id;
END IF;
RETURN OLD;
END;
$$;
CREATE FUNCTION test_chat_schema.on_group_members_insert_update_summary() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF is_current_member(NEW.member_status) THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count + 1
WHERE group_id = NEW.group_id;
END IF;
RETURN NEW;
END;
$$;
CREATE FUNCTION test_chat_schema.on_group_members_update_update_summary() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
was_active BOOLEAN;
is_active BOOLEAN;
BEGIN
was_active := is_current_member(OLD.member_status);
is_active := is_current_member(NEW.member_status);
IF was_active != is_active THEN
UPDATE groups
SET summary_current_members_count = summary_current_members_count +
(CASE WHEN is_active THEN 1 ELSE -1 END)
WHERE group_id = NEW.group_id;
END IF;
RETURN NEW;
END;
$$;
SET default_table_access_method = heap;
@@ -716,7 +786,8 @@ CREATE TABLE test_chat_schema.groups (
welcome_shared_msg_id bytea,
request_shared_msg_id bytea,
conn_link_prepared_connection smallint DEFAULT 0 NOT NULL,
via_group_link_uri bytea
via_group_link_uri bytea,
summary_current_members_count bigint DEFAULT 0 NOT NULL
);
@@ -2064,6 +2135,10 @@ CREATE INDEX idx_groups_inv_queue_info ON test_chat_schema.groups USING btree (i
CREATE INDEX idx_groups_summary_current_members_count ON test_chat_schema.groups USING btree (summary_current_members_count);
CREATE INDEX idx_groups_via_group_link_uri_hash ON test_chat_schema.groups USING btree (user_id, via_group_link_uri_hash);
@@ -2248,6 +2323,18 @@ CREATE INDEX note_folders_user_id ON test_chat_schema.note_folders USING btree (
CREATE TRIGGER tr_group_members_delete_update_summary AFTER DELETE ON test_chat_schema.group_members FOR EACH ROW EXECUTE FUNCTION test_chat_schema.on_group_members_delete_update_summary();
CREATE TRIGGER tr_group_members_insert_update_summary AFTER INSERT ON test_chat_schema.group_members FOR EACH ROW EXECUTE FUNCTION test_chat_schema.on_group_members_insert_update_summary();
CREATE TRIGGER tr_group_members_update_update_summary AFTER UPDATE ON test_chat_schema.group_members FOR EACH ROW EXECUTE FUNCTION test_chat_schema.on_group_members_update_update_summary();
ALTER TABLE ONLY test_chat_schema.calls
ADD CONSTRAINT calls_chat_item_id_fkey FOREIGN KEY (chat_item_id) REFERENCES test_chat_schema.chat_items(chat_item_id) ON DELETE CASCADE;