package storage import ( "context" "database/sql" "errors" "fmt" "log/slog" "os" "path/filepath" _ "github.com/mattn/go-sqlite3" // Драйвер для SQLite ) var ( // ErrNotFound возвращается, когда запись не найдена в хранилище. ErrNotFound = errors.New("not found") ) // SQLiteStorage реализует интерфейс interfaces.TrackStorage для SQLite. type SQLiteStorage struct { db *sql.DB } // NewSQLiteStorage создает и инициализирует новое хранилище SQLite. // Он также проверяет и создает таблицу, если она не существует. func NewSQLiteStorage(ctx context.Context, dbPath string) (*SQLiteStorage, error) { // Убедимся, что директория для файла БД существует dir := filepath.Dir(dbPath) if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("failed to create data directory: %w", err) } db, err := sql.Open("sqlite3", dbPath) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } if err := db.PingContext(ctx); err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } // Устанавливаем 1 соединение, т.к. SQLite плохо работает с конкурентной записью. // Для наших целей этого более чем достаточно. db.SetMaxOpenConns(1) storage := &SQLiteStorage{db: db} if err := storage.initSchema(ctx); err != nil { return nil, fmt.Errorf("failed to initialize schema: %w", err) } slog.Info("SQLite storage initialized successfully", "path", dbPath) return storage, nil } // initSchema создает таблицу для кэша, если она еще не существует. func (s *SQLiteStorage) initSchema(ctx context.Context) error { const ddl = ` CREATE TABLE IF NOT EXISTS tracks_cache ( yandex_track_id TEXT PRIMARY KEY, telegram_file_id TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );` _, err := s.db.ExecContext(ctx, ddl) return err } // Get получает telegram_file_id по yandex_track_id. // Возвращает ErrNotFound, если запись не найдена. func (s *SQLiteStorage) Get(ctx context.Context, yandexTrackID string) (string, error) { const op = "storage.sqlite.Get" stmt, err := s.db.PrepareContext(ctx, "SELECT telegram_file_id FROM tracks_cache WHERE yandex_track_id = ?") if err != nil { return "", fmt.Errorf("%s: %w", op, err) } defer stmt.Close() var fileID string err = stmt.QueryRowContext(ctx, yandexTrackID).Scan(&fileID) if err != nil { if errors.Is(err, sql.ErrNoRows) { return "", ErrNotFound } return "", fmt.Errorf("%s: %w", op, err) } return fileID, nil } // Set сохраняет новую запись в кэш. func (s *SQLiteStorage) Set(ctx context.Context, yandexTrackID, telegramFileID string) error { const op = "storage.sqlite.Set" stmt, err := s.db.PrepareContext(ctx, "INSERT INTO tracks_cache(yandex_track_id, telegram_file_id) VALUES(?, ?)") if err != nil { return fmt.Errorf("%s: %w", op, err) } defer stmt.Close() _, err = stmt.ExecContext(ctx, yandexTrackID, telegramFileID) if err != nil { return fmt.Errorf("%s: %w", op, err) } return nil } // Count возвращает общее количество записей в кэше. func (s *SQLiteStorage) Count(ctx context.Context) (int, error) { const op = "storage.sqlite.Count" var count int err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM tracks_cache").Scan(&count) if err != nil { return 0, fmt.Errorf("%s: %w", op, err) } return count, nil } // Close закрывает соединение с базой данных. func (s *SQLiteStorage) Close() error { return s.db.Close() }