// 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) }