package sqlite import ( "context" "database/sql" "encoding/json" "errors" "fmt" "gitea.unprism.ru/KRBL/Agate" "gitea.unprism.ru/KRBL/Agate/store" _ "github.com/mattn/go-sqlite3" "os" "path/filepath" "time" ) const ( createSnapshotsTableSQL = ` CREATE TABLE IF NOT EXISTS snapshots ( id TEXT PRIMARY KEY, name TEXT NOT NULL, parent_id TEXT, creation_time DATETIME NOT NULL, files_json TEXT -- Храним список файлов как JSON blob );` ) // sqliteStore реализует интерфейс store.MetadataStore с использованием SQLite. type sqliteStore struct { db *sql.DB } // NewSQLiteStore создает и инициализирует новое хранилище метаданных на SQLite. func NewSQLiteStore(dbPath string) (store.MetadataStore, error) { // Убедимся, что директория для файла БД существует dir := filepath.Dir(dbPath) if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("failed to create directory for sqlite db %s: %w", dir, err) } // Открываем или создаем файл БД SQLite // Опции _journal=WAL и _busy_timeout=5000 могут улучшить производительность при конкурентном доступе dsn := fmt.Sprintf("file:%s?_journal=WAL&_busy_timeout=5000&_foreign_keys=on", dbPath) db, err := sql.Open("sqlite3", dsn) if err != nil { return nil, fmt.Errorf("failed to open sqlite database %s: %w", dbPath, err) } // Проверяем соединение if err := db.PingContext(context.Background()); err != nil { // Используем Background т.к. это инициализация db.Close() return nil, fmt.Errorf("failed to ping sqlite database %s: %w", dbPath, err) } // Применяем схему БД if err := createSchema(context.Background(), db); err != nil { // Используем Background т.к. это инициализация db.Close() return nil, fmt.Errorf("failed to apply sqlite schema: %w", err) } return &sqliteStore{db: db}, nil } // createSchema создает необходимые таблицы, если они не существуют. func createSchema(ctx context.Context, db *sql.DB) error { _, err := db.ExecContext(ctx, createSnapshotsTableSQL) if err != nil { return fmt.Errorf("failed to execute schema creation: %w", err) } return nil } // Close закрывает соединение с БД. func (s *sqliteStore) Close() error { if s.db != nil { return s.db.Close() } return nil } // SaveSnapshotMetadata сохраняет метаданные снапшота в БД. func (s *sqliteStore) SaveSnapshotMetadata(ctx context.Context, snap store.Snapshot) error { // Сериализуем список файлов в JSON filesJSON, err := json.Marshal(snap.Files) if err != nil { return fmt.Errorf("failed to marshal files to JSON for snapshot %s: %w", snap.ID, err) } // Используем INSERT OR REPLACE для атомарной вставки или обновления query := ` INSERT OR REPLACE INTO snapshots (id, name, parent_id, creation_time, files_json) VALUES (?, ?, ?, ?, ?); ` _, err = s.db.ExecContext(ctx, query, snap.ID, snap.Name, sql.NullString{String: snap.ParentID, Valid: snap.ParentID != ""}, // Используем NullString для опционального parent_id snap.CreationTime.UTC(), // Сохраняем в UTC string(filesJSON), ) if err != nil { return fmt.Errorf("failed to save snapshot %s metadata: %w", snap.ID, err) } return nil } // GetSnapshotMetadata извлекает метаданные снапшота из БД. func (s *sqliteStore) GetSnapshotMetadata(ctx context.Context, snapshotID string) (*store.Snapshot, error) { query := ` SELECT id, name, parent_id, creation_time, files_json FROM snapshots WHERE id = ?; ` row := s.db.QueryRowContext(ctx, query, snapshotID) var snap store.Snapshot var parentID sql.NullString var filesJSON string var creationTimeStr string // Читаем время как строку, потом парсим err := row.Scan( &snap.ID, &snap.Name, &parentID, &creationTimeStr, &filesJSON, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { // Если запись не найдена, возвращаем кастомную ошибку return nil, agate.ErrNotFound } return nil, fmt.Errorf("failed to query snapshot %s: %w", snapshotID, err) } // Устанавливаем ParentID, если он не NULL snap.ParentID = parentID.String // Парсим время из строки (SQLite хранит DATETIME как текст) // Используем формат, совместимый с SQLite и Go's time.RFC3339Nano, или стандартный SQLite формат // 'YYYY-MM-DD HH:MM:SS' - требует парсинга с нужным layout // Проще сохранять и читать как RFC3339 или Unix timestamp. Мы сохраняли как UTC(). // Попробуем стандартный формат SQLite const sqliteLayout = "2006-01-02 15:04:05" // Стандартный формат SQLite DATETIME без таймзоны t, parseErr := time.Parse(sqliteLayout, creationTimeStr) if parseErr != nil { // Попробуем формат с долями секунд, если первый не сработал const sqliteLayoutWithMs = "2006-01-02 15:04:05.999999999" t, parseErr = time.Parse(sqliteLayoutWithMs, creationTimeStr) if parseErr != nil { // Попробуем RFC3339, если сохраняли как UTC().Format(time.RFC3339) t, parseErr = time.Parse(time.RFC3339, creationTimeStr) if parseErr != nil { return nil, fmt.Errorf("failed to parse creation time '%s' for snapshot %s: %w", creationTimeStr, snapshotID, parseErr) } } } snap.CreationTime = t.UTC() // Сохраняем как UTC // Десериализуем список файлов из JSON if err := json.Unmarshal([]byte(filesJSON), &snap.Files); err != nil { return nil, fmt.Errorf("failed to unmarshal files JSON for snapshot %s: %w", snapshotID, err) } return &snap, nil } // ListSnapshotsMetadata извлекает краткую информацию обо всех снапшотах. func (s *sqliteStore) ListSnapshotsMetadata(ctx context.Context) ([]store.SnapshotInfo, error) { // Simplified implementation to debug the issue fmt.Println("ListSnapshotsMetadata called") // Get all snapshot IDs first query := `SELECT id FROM snapshots ORDER BY creation_time DESC;` fmt.Println("Executing query:", query) rows, err := s.db.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("failed to query snapshot IDs: %w", err) } defer rows.Close() var snapshots []store.SnapshotInfo // For each ID, get the full snapshot details for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, fmt.Errorf("failed to scan snapshot ID: %w", err) } // Get the full snapshot details snapshot, err := s.GetSnapshotMetadata(ctx, id) if err != nil { return nil, fmt.Errorf("failed to get snapshot details for ID %s: %w", id, err) } // Convert to SnapshotInfo info := store.SnapshotInfo{ ID: snapshot.ID, Name: snapshot.Name, ParentID: snapshot.ParentID, CreationTime: snapshot.CreationTime, } snapshots = append(snapshots, info) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating snapshot IDs: %w", err) } // If no snapshots found, return an empty slice if len(snapshots) == 0 { fmt.Println("No snapshots found") return []store.SnapshotInfo{}, nil } fmt.Printf("Found %d snapshots\n", len(snapshots)) return snapshots, nil } // DeleteSnapshotMetadata удаляет метаданные снапшота. func (s *sqliteStore) DeleteSnapshotMetadata(ctx context.Context, snapshotID string) error { query := `DELETE FROM snapshots WHERE id = ?;` result, err := s.db.ExecContext(ctx, query, snapshotID) if err != nil { return fmt.Errorf("failed to delete snapshot %s metadata: %w", snapshotID, err) } // Проверяем, была ли запись реально удалена (опционально) _, err = result.RowsAffected() if err != nil { // Не критично, если не можем получить количество удаленных строк fmt.Printf("Warning: could not get rows affected after deleting snapshot %s: %v\n", snapshotID, err) } // fmt.Printf("Deleted %d metadata rows for snapshot %s\n", rowsAffected, snapshotID) return nil // Не возвращаем ошибку, если запись не найдена }