Files
glchat/internal/store/store.go
T

113 lines
3.7 KiB
Go
Raw Normal View History

// Package store содержит доступ к данным glchat: модели и запросы поверх SQLite.
// Вся работа с БД идёт только через этот пакет (AGENT.md §12).
package store
import (
"context"
"database/sql"
"errors"
"fmt"
"math"
"time"
"glchat/internal/database"
)
var (
ErrNotFound = errors.New("record not found")
ErrConflict = errors.New("record already exists")
ErrPermissionDenied = errors.New("permission denied")
ErrLastAdminRole = errors.New("guild must keep a role with MANAGE_ROLES")
ErrLastAdministrator = errors.New("guild must keep administrator permissions")
)
// Store — точка доступа к данным. Writer используется для мутаций,
// Reader — для чтения (один писатель и пул чтения, AGENT.md §9.1).
type Store struct {
writer *sql.DB
reader *sql.DB
ids *Snowflake
now func() time.Time
}
func New(db *database.DB) *Store {
return &Store{
writer: db.Writer,
reader: db.Reader,
ids: NewSnowflake(0),
now: func() time.Time { return time.Now().UTC() },
}
}
// Reader возвращает соединение для чтения (доступно другим пакетам,
// которым нужны собственные запросы, например permissions).
func (s *Store) Reader() *sql.DB { return s.reader }
// Writer возвращает соединение для записи.
func (s *Store) Writer() *sql.DB { return s.writer }
// NextID выдаёт новый идентификатор (Snowflake, AGENT.md 6.4).
func (s *Store) NextID() uint64 { return s.ids.Next() }
// Timestamp форматирует время в ISO 8601 UTC с миллисекундами.
func (s *Store) Timestamp(t time.Time) string {
return t.UTC().Format("2006-01-02T15:04:05.000Z")
}
func (s *Store) Now() string { return s.Timestamp(s.now()) }
// InTx выполняет функцию в транзакции с откатом при ошибке.
func (s *Store) InTx(ctx context.Context, fn func(tx *sql.Tx) error) error {
tx, err := s.writer.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
if err := fn(tx); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil && !errors.Is(rollbackErr, sql.ErrTxDone) {
return fmt.Errorf("%w (rollback: %w)", err, rollbackErr)
}
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit transaction: %w", err)
}
return nil
}
func mapError(err error) error {
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
return err
}
// idToInt переводит uint64-идентификатор в int64 для SQLite. Идентификаторы
// Snowflake формируются приложением, поэтому заведомо меньше MaxInt64;
// проверка диапазона сохраняет корректность при ручных данных.
func idToInt(id uint64) (int64, error) {
if id > math.MaxInt64 {
return 0, fmt.Errorf("identifier %d exceeds the supported range", id)
}
return int64(id), nil
}
// intToID переводит значение SQLite в uint64-идентификатор.
func intToID(value int64) uint64 {
if value < 0 {
return 0
}
return uint64(value)
}
// boolToInt превращает Go-флаг в значение SQLite (0/1).
func boolToInt(value bool) int {
if value {
return 1
}
return 0
}
// mustID конвертирует идентификатор, который уже проверен вызывающим кодом
// (значения Snowflake всегда в диапазоне int64).
func mustID(id uint64) int64 { return int64(id) }