Files
Agate/store/sqlite/sqlite.go

278 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sqlite
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"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, store.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 retrieves basic information about snapshots with filtering and pagination.
func (s *sqliteStore) ListSnapshotsMetadata(ctx context.Context, opts store.ListOptions) ([]store.SnapshotInfo, error) {
// Build the query with optional filtering
var query string
var args []interface{}
if opts.FilterByName != "" {
query = `SELECT id, name, parent_id, creation_time FROM snapshots WHERE name LIKE ? ORDER BY creation_time DESC`
args = append(args, "%"+opts.FilterByName+"%")
} else {
query = `SELECT id, name, parent_id, creation_time FROM snapshots ORDER BY creation_time DESC`
}
// Add pagination if specified
if opts.Limit > 0 {
query += " LIMIT ?"
args = append(args, opts.Limit)
if opts.Offset > 0 {
query += " OFFSET ?"
args = append(args, opts.Offset)
}
}
// Execute the query
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query snapshots: %w", err)
}
defer rows.Close()
var snapshots []store.SnapshotInfo
// Iterate through the results
for rows.Next() {
var info store.SnapshotInfo
var parentID sql.NullString
var creationTimeStr string
if err := rows.Scan(&info.ID, &info.Name, &parentID, &creationTimeStr); err != nil {
return nil, fmt.Errorf("failed to scan snapshot row: %w", err)
}
// Set parent ID if not NULL
if parentID.Valid {
info.ParentID = parentID.String
}
// Parse creation time
const sqliteLayout = "2006-01-02 15:04:05" // Standard SQLite DATETIME format without timezone
t, parseErr := time.Parse(sqliteLayout, creationTimeStr)
if parseErr != nil {
// Try format with milliseconds if the first one didn't work
const sqliteLayoutWithMs = "2006-01-02 15:04:05.999999999"
t, parseErr = time.Parse(sqliteLayoutWithMs, creationTimeStr)
if parseErr != nil {
// Try RFC3339 if saved as 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, info.ID, parseErr)
}
}
}
info.CreationTime = t.UTC() // Store as UTC
snapshots = append(snapshots, info)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating snapshot rows: %w", err)
}
// If no snapshots found, return an empty slice
if len(snapshots) == 0 {
return []store.SnapshotInfo{}, nil
}
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 // Не возвращаем ошибку, если запись не найдена
}
// UpdateSnapshotParentID обновляет ParentID для указанного снапшота.
func (s *sqliteStore) UpdateSnapshotParentID(ctx context.Context, snapshotID, newParentID string) error {
query := `UPDATE snapshots SET parent_id = ? WHERE id = ?;`
_, err := s.db.ExecContext(ctx, query, newParentID, snapshotID)
if err != nil {
return fmt.Errorf("failed to update parent ID for snapshot %s: %w", snapshotID, err)
}
return nil
}