Files
grendervill 4550bea6cc feat(db): схема Фазы 2 — сообщения, реакции, read states, файлы, инвайты, FTS5
Миграция 00004: таблицы `messages`, `message_reactions`,
`channel_read_states`, `files`, `invites` и виртуальная таблица `messages_fts`
с триггерами синхронизации (AGENT.md 6.1, 7.6, 7.7, 7.9, 7.15, 7.16).

Полнотекстовый поиск требует сборки с тегом `sqlite_fts5`:
- `GO_TAGS ?= sqlite_fts5` в Makefile (build/test/cover) и в Dockerfile;
- build-tags в golangci-lint;
- страж `internal/database/fts5_guard.go`: сборка без тега падает с понятным
  текстом, а не ломается на миграции в рантайме.
2026-09-19 23:29:39 +03:00

115 lines
5.4 KiB
SQL

-- +goose Up
-- Фаза 2: сообщения, реакции, закрепления, read states и полнотекстовый поиск
-- (AGENT.md 6.1, 7.6, 7.15).
CREATE TABLE messages (
id INTEGER PRIMARY KEY,
channel_id INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
author_id INTEGER REFERENCES users (id) ON DELETE SET NULL,
content TEXT NOT NULL DEFAULT '',
reply_to_id INTEGER REFERENCES messages (id) ON DELETE SET NULL,
type TEXT NOT NULL DEFAULT 'default',
-- Вложения хранятся списком метаданных: сами файлы лежат на диске (7.7).
attachments_json TEXT NOT NULL DEFAULT '[]',
mentions_json TEXT NOT NULL DEFAULT '[]',
edited_at TEXT,
pinned INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX messages_channel_idx ON messages (channel_id, id DESC);
CREATE INDEX messages_author_idx ON messages (author_id, id DESC);
CREATE INDEX messages_pinned_idx ON messages (channel_id, id) WHERE pinned = 1;
CREATE TABLE message_reactions (
message_id INTEGER NOT NULL REFERENCES messages (id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
emoji TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (message_id, user_id, emoji)
);
CREATE INDEX message_reactions_emoji_idx ON message_reactions (message_id, emoji);
-- Состояние прочтения на пользователя и комнату: синхронизируется между
-- устройствами через Gateway (AGENT.md 7.16).
CREATE TABLE channel_read_states (
user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
channel_id INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
last_message_id INTEGER NOT NULL DEFAULT 0,
mention_count INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (user_id, channel_id)
);
CREATE INDEX channel_read_states_channel_idx ON channel_read_states (channel_id);
-- Вложения: файл регистрируется отдельно от сообщения, чтобы работали
-- предпросмотры и очистка сирот (AGENT.md 7.7).
CREATE TABLE files (
id INTEGER PRIMARY KEY,
uploader_id INTEGER REFERENCES users (id) ON DELETE SET NULL,
guild_id INTEGER REFERENCES guilds (id) ON DELETE CASCADE,
channel_id INTEGER REFERENCES channels (id) ON DELETE CASCADE,
message_id INTEGER REFERENCES messages (id) ON DELETE CASCADE,
filename TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
size_bytes INTEGER NOT NULL DEFAULT 0,
width INTEGER NOT NULL DEFAULT 0,
height INTEGER NOT NULL DEFAULT 0,
storage_path TEXT NOT NULL DEFAULT '',
sha256 TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX files_message_idx ON files (message_id);
-- Полнотекстовый поиск по сообщениям (AGENT.md 7.15): индексируем содержимое,
-- права проверяются при запросе.
CREATE VIRTUAL TABLE messages_fts USING fts5 (
content,
content = 'messages',
content_rowid = 'id',
tokenize = 'unicode61 remove_diacritics 2'
);
-- Триггеры содержат `;` внутри тела: goose требует явных границ оператора.
-- +goose StatementBegin
CREATE TRIGGER messages_fts_insert AFTER INSERT ON messages BEGIN
INSERT INTO messages_fts (rowid, content) VALUES (new.id, new.content);
END;
-- +goose StatementEnd
-- +goose StatementBegin
CREATE TRIGGER messages_fts_delete AFTER DELETE ON messages BEGIN
INSERT INTO messages_fts (messages_fts, rowid, content) VALUES ('delete', old.id, old.content);
END;
-- +goose StatementEnd
-- +goose StatementBegin
CREATE TRIGGER messages_fts_update AFTER UPDATE OF content ON messages BEGIN
INSERT INTO messages_fts (messages_fts, rowid, content) VALUES ('delete', old.id, old.content);
INSERT INTO messages_fts (rowid, content) VALUES (new.id, new.content);
END;
-- +goose StatementEnd
-- Приглашения (AGENT.md 7.9): код, лимит использований и срок жизни.
CREATE TABLE invites (
code TEXT PRIMARY KEY,
guild_id INTEGER NOT NULL REFERENCES guilds (id) ON DELETE CASCADE,
channel_id INTEGER REFERENCES channels (id) ON DELETE SET NULL,
creator_id INTEGER REFERENCES users (id) ON DELETE SET NULL,
max_uses INTEGER NOT NULL DEFAULT 0,
uses INTEGER NOT NULL DEFAULT 0,
max_age_sec INTEGER NOT NULL DEFAULT 0,
expires_at TEXT,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX invites_guild_idx ON invites (guild_id, created_at DESC);
CREATE INDEX invites_expires_idx ON invites (expires_at);
-- +goose Down
DROP TABLE invites;
DROP TRIGGER messages_fts_update;
DROP TRIGGER messages_fts_delete;
DROP TRIGGER messages_fts_insert;
DROP TABLE messages_fts;
DROP TABLE files;
DROP TABLE channel_read_states;
DROP TABLE message_reactions;
DROP TABLE messages;