mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
216 lines
7.3 KiB
PL/PgSQL
216 lines
7.3 KiB
PL/PgSQL
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
|
room_id BIGINT NOT NULL,
|
|
user_id BIGINT,
|
|
content TEXT NOT NULL,
|
|
message_type SMALLINT NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id, created_at),
|
|
FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE RESTRICT
|
|
) PARTITION BY RANGE (created_at);
|
|
|
|
COMMENT ON TABLE chat_messages IS 'Persistent chat messages (partitioned by day, retention configurable)';
|
|
COMMENT ON COLUMN chat_messages.id IS 'numeric identity ID';
|
|
COMMENT ON COLUMN chat_messages.content IS 'Message content';
|
|
|
|
CREATE OR REPLACE FUNCTION create_chat_message_partition(
|
|
partition_date DATE DEFAULT CURRENT_DATE
|
|
) RETURNS JSON AS $$
|
|
DECLARE
|
|
partition_name TEXT;
|
|
start_date DATE;
|
|
end_date DATE;
|
|
index_count INTEGER := 0;
|
|
BEGIN
|
|
start_date := DATE_TRUNC('day', partition_date);
|
|
end_date := start_date + INTERVAL '1 day';
|
|
partition_name := 'chat_messages_' || TO_CHAR(start_date, 'YYYY_MM_DD');
|
|
|
|
EXECUTE format(
|
|
'CREATE TABLE IF NOT EXISTS %I PARTITION OF chat_messages
|
|
FOR VALUES FROM (%L) TO (%L)',
|
|
partition_name, start_date, end_date
|
|
);
|
|
|
|
EXECUTE format(
|
|
'CREATE INDEX IF NOT EXISTS %I ON %I(room_id, created_at DESC, user_id)',
|
|
partition_name || '_idx_room_pagination', partition_name
|
|
);
|
|
index_count := index_count + 1;
|
|
|
|
EXECUTE format(
|
|
'CREATE INDEX IF NOT EXISTS %I ON %I(user_id, created_at DESC)',
|
|
partition_name || '_idx_user_created', partition_name
|
|
);
|
|
index_count := index_count + 1;
|
|
|
|
EXECUTE format(
|
|
'CREATE INDEX IF NOT EXISTS %I ON %I(created_at DESC)',
|
|
partition_name || '_idx_created_at', partition_name
|
|
);
|
|
index_count := index_count + 1;
|
|
|
|
RETURN json_build_object(
|
|
'partition_name', partition_name,
|
|
'start_date', start_date,
|
|
'end_date', end_date,
|
|
'indexes_created', index_count,
|
|
'status', 'success'
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION create_chat_message_partition(DATE) IS
|
|
'Create a single chat message partition with indexes (idempotent, daily granularity). Parameter: partition date (default: current date)';
|
|
|
|
CREATE OR REPLACE FUNCTION create_chat_message_partitions(
|
|
days_ahead INTEGER DEFAULT 30
|
|
) RETURNS JSON AS $$
|
|
DECLARE
|
|
i INTEGER;
|
|
partition_date DATE;
|
|
result JSON;
|
|
partitions JSONB := '[]'::JSONB;
|
|
success_count INTEGER := 0;
|
|
BEGIN
|
|
partition_date := DATE_TRUNC('day', CURRENT_DATE);
|
|
|
|
FOR i IN 0..days_ahead LOOP
|
|
result := create_chat_message_partition(partition_date);
|
|
partitions := partitions || result::JSONB;
|
|
success_count := success_count + 1;
|
|
partition_date := partition_date + INTERVAL '1 day';
|
|
END LOOP;
|
|
|
|
RETURN json_build_object(
|
|
'status', 'completed',
|
|
'total_requested', days_ahead + 1,
|
|
'success_count', success_count,
|
|
'partitions', partitions
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION create_chat_message_partitions(INTEGER) IS
|
|
'Batch-create chat message partitions for current day + N days ahead (daily granularity). Parameter: days ahead (default: 30)';
|
|
|
|
CREATE OR REPLACE FUNCTION drop_old_chat_message_partitions(
|
|
keep_days INTEGER DEFAULT 90
|
|
) RETURNS JSON AS $$
|
|
DECLARE
|
|
cutoff_date DATE;
|
|
cutoff_name TEXT;
|
|
partition_record RECORD;
|
|
dropped JSONB := '[]'::JSONB;
|
|
drop_count INTEGER := 0;
|
|
BEGIN
|
|
cutoff_date := CURRENT_DATE - (keep_days || ' days')::INTERVAL;
|
|
cutoff_name := 'chat_messages_' || TO_CHAR(cutoff_date, 'YYYY_MM_DD');
|
|
|
|
FOR partition_record IN
|
|
SELECT tablename
|
|
FROM pg_tables
|
|
WHERE schemaname = 'public'
|
|
AND tablename LIKE 'chat_messages_%'
|
|
AND tablename ~ '^chat_messages_[0-9]{4}_[0-9]{2}_[0-9]{2}$'
|
|
AND tablename < cutoff_name
|
|
ORDER BY tablename
|
|
LOOP
|
|
EXECUTE format('DROP TABLE IF EXISTS %I', partition_record.tablename);
|
|
dropped := dropped || jsonb_build_array(
|
|
jsonb_build_object('partition', partition_record.tablename)
|
|
);
|
|
drop_count := drop_count + 1;
|
|
|
|
RAISE NOTICE 'Dropped chat partition: %', partition_record.tablename;
|
|
END LOOP;
|
|
|
|
RETURN json_build_object(
|
|
'status', 'success',
|
|
'dropped_count', drop_count,
|
|
'keep_days', keep_days,
|
|
'cutoff_date', cutoff_date,
|
|
'dropped_partitions', dropped
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION drop_old_chat_message_partitions(INTEGER) IS
|
|
'Drop chat message partitions older than N days (daily granularity). Parameter: days to keep (default: 90)';
|
|
|
|
CREATE OR REPLACE FUNCTION check_chat_message_partitions(
|
|
days_ahead INTEGER DEFAULT 30
|
|
) RETURNS JSON AS $$
|
|
DECLARE
|
|
partition_date DATE;
|
|
expected_name TEXT;
|
|
expected_partitions TEXT[] := ARRAY[]::TEXT[];
|
|
missing_partitions JSONB := '[]'::JSONB;
|
|
total_partitions INTEGER := 0;
|
|
total_size BIGINT := 0;
|
|
partition_record RECORD;
|
|
BEGIN
|
|
partition_date := DATE_TRUNC('day', CURRENT_DATE);
|
|
|
|
FOR i IN 0..days_ahead LOOP
|
|
expected_name := 'chat_messages_' || TO_CHAR(partition_date, 'YYYY_MM_DD');
|
|
expected_partitions := array_append(expected_partitions, expected_name);
|
|
partition_date := partition_date + INTERVAL '1 day';
|
|
END LOOP;
|
|
|
|
FOREACH expected_name IN ARRAY expected_partitions
|
|
LOOP
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_tables
|
|
WHERE schemaname = 'public' AND tablename = expected_name
|
|
) THEN
|
|
missing_partitions := missing_partitions || jsonb_build_array(
|
|
jsonb_build_object(
|
|
'partition_name', expected_name,
|
|
'status', 'missing'
|
|
)
|
|
);
|
|
END IF;
|
|
END LOOP;
|
|
|
|
FOR partition_record IN
|
|
SELECT
|
|
tablename,
|
|
pg_total_relation_size(format('%I.%I', schemaname, tablename)) as size
|
|
FROM pg_tables
|
|
WHERE schemaname = 'public'
|
|
AND tablename LIKE 'chat_messages_%'
|
|
AND tablename ~ '^chat_messages_[0-9]{4}_[0-9]{2}_[0-9]{2}$'
|
|
ORDER BY tablename DESC
|
|
LOOP
|
|
total_partitions := total_partitions + 1;
|
|
total_size := total_size + partition_record.size;
|
|
END LOOP;
|
|
|
|
RETURN json_build_object(
|
|
'status', 'checked',
|
|
'total_partitions', total_partitions,
|
|
'total_size_mb', ROUND(total_size::NUMERIC / 1024 / 1024, 2),
|
|
'missing_partitions', missing_partitions,
|
|
'missing_count', jsonb_array_length(missing_partitions),
|
|
'health_status', CASE
|
|
WHEN jsonb_array_length(missing_partitions) = 0 THEN 'healthy'
|
|
ELSE 'warning'
|
|
END
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION check_chat_message_partitions(INTEGER) IS
|
|
'Check health of chat message partitions';
|
|
|
|
CREATE TABLE IF NOT EXISTS chat_messages_default PARTITION OF chat_messages DEFAULT;
|
|
CREATE INDEX IF NOT EXISTS chat_messages_default_idx_room_pagination
|
|
ON chat_messages_default(room_id, created_at DESC, user_id);
|
|
CREATE INDEX IF NOT EXISTS chat_messages_default_idx_user_created
|
|
ON chat_messages_default(user_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS chat_messages_default_idx_created_at
|
|
ON chat_messages_default(created_at DESC);
|
|
|
|
SELECT create_chat_message_partitions(30) AS initial_partitions;
|