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`: сборка без тега падает с понятным текстом, а не ломается на миграции в рантайме.
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ run:
|
|||||||
timeout: 5m
|
timeout: 5m
|
||||||
tests: true
|
tests: true
|
||||||
build-tags:
|
build-tags:
|
||||||
- sqlite_cgo
|
- sqlite_fts5
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
default: none
|
default: none
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ RUN go mod download
|
|||||||
COPY cmd/ ./cmd/
|
COPY cmd/ ./cmd/
|
||||||
COPY internal/ ./internal/
|
COPY internal/ ./internal/
|
||||||
RUN CC=musl-gcc CGO_ENABLED=1 go build \
|
RUN CC=musl-gcc CGO_ENABLED=1 go build \
|
||||||
-tags "sqlite_omit_load_extension" \
|
-tags "sqlite_omit_load_extension sqlite_fts5" \
|
||||||
-trimpath \
|
-trimpath \
|
||||||
-ldflags "-s -w -linkmode external -extldflags -static -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.buildDate=${BUILD_DATE}" \
|
-ldflags "-s -w -linkmode external -extldflags -static -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.buildDate=${BUILD_DATE}" \
|
||||||
-o /out/glchat ./cmd/glchat
|
-o /out/glchat ./cmd/glchat
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ export GOPROXY ?= https://proxy.golang.org,direct
|
|||||||
export GOSUMDB ?= sum.golang.org
|
export GOSUMDB ?= sum.golang.org
|
||||||
export CGO_ENABLED ?= 1
|
export CGO_ENABLED ?= 1
|
||||||
|
|
||||||
GOFLAGS_LOCAL := -trimpath
|
# sqlite_fts5 включает полнотекстовый поиск в mattn/go-sqlite3 (AGENT.md 7.15):
|
||||||
|
# без тега поиск по сообщениям не работает, поэтому он обязателен для всех сборок.
|
||||||
|
GO_TAGS ?= sqlite_fts5
|
||||||
|
GOFLAGS_LOCAL := -trimpath -tags $(GO_TAGS)
|
||||||
GO_LDFLAGS := -s -w \
|
GO_LDFLAGS := -s -w \
|
||||||
-X main.version=$(VERSION) \
|
-X main.version=$(VERSION) \
|
||||||
-X main.commit=$(COMMIT) \
|
-X main.commit=$(COMMIT) \
|
||||||
@@ -102,12 +105,12 @@ build: ## Собрать бинарник сервера в ./build/glchat
|
|||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: ## Тесты Go с race-детектором
|
test: ## Тесты Go с race-детектором
|
||||||
go test -race -count=1 $(GO_PACKAGES)
|
go test -tags $(GO_TAGS) -race -count=1 $(GO_PACKAGES)
|
||||||
|
|
||||||
.PHONY: test-cover
|
.PHONY: test-cover
|
||||||
test-cover: ## Тесты с отчётом покрытия (./build/coverage.out)
|
test-cover: ## Тесты с отчётом покрытия (./build/coverage.out)
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
go test -race -count=1 -coverprofile=$(BUILD_DIR)/coverage.out $(GO_PACKAGES)
|
go test -tags $(GO_TAGS) -race -count=1 -coverprofile=$(BUILD_DIR)/coverage.out $(GO_PACKAGES)
|
||||||
go tool cover -func=$(BUILD_DIR)/coverage.out | tail -1
|
go tool cover -func=$(BUILD_DIR)/coverage.out | tail -1
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//go:build !sqlite_fts5
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
// Без тега sqlite_fts5 драйвер mattn/go-sqlite3 не умеет виртуальные таблицы
|
||||||
|
// FTS5, и миграция поиска падает на старте. Паникуем сразу и с понятным
|
||||||
|
// текстом: собрать инстанс без полнотекстового поиска нельзя (AGENT.md 7.15).
|
||||||
|
func init() {
|
||||||
|
panic("glchat: сборка без тега sqlite_fts5 — добавьте -tags sqlite_fts5 (см. GO_TAGS в Makefile)")
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
-- +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;
|
||||||
Reference in New Issue
Block a user