Refactor snapshot management: integrate logging, enhance concurrency with mutex, add clean extraction option, and update gRPC ListSnapshots with ListOptions.

This commit is contained in:
2025-07-07 20:03:40 +03:00
parent 19c02d3573
commit 223a63ee6d
14 changed files with 977 additions and 253 deletions

View File

@ -167,58 +167,83 @@ func (s *sqliteStore) GetSnapshotMetadata(ctx context.Context, snapshotID string
return &snap, nil
}
// ListSnapshotsMetadata извлекает краткую информацию обо всех снапшотах.
func (s *sqliteStore) ListSnapshotsMetadata(ctx context.Context) ([]store.SnapshotInfo, error) {
// Simplified implementation to debug the issue
fmt.Println("ListSnapshotsMetadata called")
// 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{}
// Get all snapshot IDs first
query := `SELECT id FROM snapshots ORDER BY creation_time DESC;`
fmt.Println("Executing query:", query)
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`
}
rows, err := s.db.QueryContext(ctx, query)
// 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 snapshot IDs: %w", err)
return nil, fmt.Errorf("failed to query snapshots: %w", err)
}
defer rows.Close()
var snapshots []store.SnapshotInfo
// For each ID, get the full snapshot details
// Iterate through the results
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, fmt.Errorf("failed to scan snapshot ID: %w", err)
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)
}
// 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)
// Set parent ID if not NULL
if parentID.Valid {
info.ParentID = parentID.String
}
// Convert to SnapshotInfo
info := store.SnapshotInfo{
ID: snapshot.ID,
Name: snapshot.Name,
ParentID: snapshot.ParentID,
CreationTime: snapshot.CreationTime,
// 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 IDs: %w", err)
return nil, fmt.Errorf("error iterating snapshot rows: %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
}

View File

@ -121,7 +121,7 @@ func TestListSnapshotsMetadata(t *testing.T) {
// Create test snapshots
ctx := context.Background()
now := time.Now().UTC().Truncate(time.Second)
testSnapshots := []store.Snapshot{
{
ID: "snapshot-1",
@ -154,8 +154,8 @@ func TestListSnapshotsMetadata(t *testing.T) {
}
}
// List the snapshots
snapshots, err := s.ListSnapshotsMetadata(ctx)
// List the snapshots with empty options
snapshots, err := s.ListSnapshotsMetadata(ctx, store.ListOptions{})
if err != nil {
t.Fatalf("Failed to list snapshots: %v", err)
}
@ -189,6 +189,164 @@ func TestListSnapshotsMetadata(t *testing.T) {
}
}
func TestListSnapshotsMetadata_WithOptions(t *testing.T) {
// Create a temporary directory for tests
tempDir, err := os.MkdirTemp("", "agate-test-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir) // Clean up after test
// Create a new store
dbPath := filepath.Join(tempDir, "test.db")
s, err := NewSQLiteStore(dbPath)
if err != nil {
t.Fatalf("Failed to create SQLite store: %v", err)
}
defer s.Close()
// Create test snapshots with different names
ctx := context.Background()
now := time.Now().UTC().Truncate(time.Second)
testSnapshots := []store.Snapshot{
{
ID: "alpha-1",
Name: "alpha-1",
ParentID: "",
CreationTime: now.Add(-3 * time.Hour),
Files: []store.FileInfo{},
},
{
ID: "alpha-2",
Name: "alpha-2",
ParentID: "alpha-1",
CreationTime: now.Add(-2 * time.Hour),
Files: []store.FileInfo{},
},
{
ID: "beta-1",
Name: "beta-1",
ParentID: "",
CreationTime: now.Add(-1 * time.Hour),
Files: []store.FileInfo{},
},
}
// Save the snapshots
for _, snap := range testSnapshots {
err = s.SaveSnapshotMetadata(ctx, snap)
if err != nil {
t.Fatalf("Failed to save snapshot metadata: %v", err)
}
}
// Test different ListOptions scenarios
t.Run("FilterByName", func(t *testing.T) {
// Filter snapshots by name "alpha"
opts := store.ListOptions{
FilterByName: "alpha",
}
snapshots, err := s.ListSnapshotsMetadata(ctx, opts)
if err != nil {
t.Fatalf("Failed to list snapshots with filter: %v", err)
}
// Should return 2 snapshots (alpha-1 and alpha-2)
if len(snapshots) != 2 {
t.Errorf("Wrong number of snapshots returned: got %d, want %d", len(snapshots), 2)
}
// Check that only alpha snapshots are returned
for _, snap := range snapshots {
if snap.ID != "alpha-1" && snap.ID != "alpha-2" {
t.Errorf("Unexpected snapshot ID in filtered results: %s", snap.ID)
}
}
})
t.Run("Limit", func(t *testing.T) {
// Limit to 1 snapshot (should return the newest one)
opts := store.ListOptions{
Limit: 1,
}
snapshots, err := s.ListSnapshotsMetadata(ctx, opts)
if err != nil {
t.Fatalf("Failed to list snapshots with limit: %v", err)
}
// Should return 1 snapshot
if len(snapshots) != 1 {
t.Errorf("Wrong number of snapshots returned: got %d, want %d", len(snapshots), 1)
}
// The newest snapshot should be beta-1
if snapshots[0].ID != "beta-1" {
t.Errorf("Wrong snapshot returned with limit: got %s, want %s", snapshots[0].ID, "beta-1")
}
})
t.Run("Offset", func(t *testing.T) {
// Limit to 1 snapshot with offset 1 (should return the second newest)
opts := store.ListOptions{
Limit: 1,
Offset: 1,
}
snapshots, err := s.ListSnapshotsMetadata(ctx, opts)
if err != nil {
t.Fatalf("Failed to list snapshots with offset: %v", err)
}
// Should return 1 snapshot
if len(snapshots) != 1 {
t.Errorf("Wrong number of snapshots returned: got %d, want %d", len(snapshots), 1)
}
// The second newest snapshot should be alpha-2
if snapshots[0].ID != "alpha-2" {
t.Errorf("Wrong snapshot returned with offset: got %s, want %s", snapshots[0].ID, "alpha-2")
}
})
t.Run("FilterAndPagination", func(t *testing.T) {
// Filter by "alpha" with limit 1
opts := store.ListOptions{
FilterByName: "alpha",
Limit: 1,
}
snapshots, err := s.ListSnapshotsMetadata(ctx, opts)
if err != nil {
t.Fatalf("Failed to list snapshots with filter and pagination: %v", err)
}
// Should return 1 snapshot
if len(snapshots) != 1 {
t.Errorf("Wrong number of snapshots returned: got %d, want %d", len(snapshots), 1)
}
// The newest alpha snapshot should be alpha-2
if snapshots[0].ID != "alpha-2" {
t.Errorf("Wrong snapshot returned with filter and limit: got %s, want %s", snapshots[0].ID, "alpha-2")
}
})
t.Run("NoResults", func(t *testing.T) {
// Filter by a name that doesn't exist
opts := store.ListOptions{
FilterByName: "gamma",
}
snapshots, err := s.ListSnapshotsMetadata(ctx, opts)
if err != nil {
t.Fatalf("Failed to list snapshots with non-matching filter: %v", err)
}
// Should return 0 snapshots
if len(snapshots) != 0 {
t.Errorf("Expected 0 snapshots, got %d", len(snapshots))
}
})
}
func TestDeleteSnapshotMetadata(t *testing.T) {
// Create a temporary directory for tests
tempDir, err := os.MkdirTemp("", "agate-test-*")
@ -238,4 +396,4 @@ func TestDeleteSnapshotMetadata(t *testing.T) {
if err != nil {
t.Fatalf("DeleteSnapshotMetadata returned an error for non-existent snapshot: %v", err)
}
}
}

View File

@ -31,6 +31,13 @@ type SnapshotInfo struct {
CreationTime time.Time // Время создания
}
// ListOptions provides options for filtering and paginating snapshot lists
type ListOptions struct {
FilterByName string // Filter snapshots by name (substring match)
Limit int // Maximum number of snapshots to return
Offset int // Number of snapshots to skip
}
// MetadataStore определяет интерфейс для хранения и извлечения метаданных снапшотов.
type MetadataStore interface {
// SaveSnapshotMetadata сохраняет полные метаданные снапшота, включая список файлов.
@ -41,8 +48,8 @@ type MetadataStore interface {
// Возвращает agate.ErrNotFound, если снапшот не найден.
GetSnapshotMetadata(ctx context.Context, snapshotID string) (*Snapshot, error)
// ListSnapshotsMetadata извлекает краткую информацию обо всех снапшотах.
ListSnapshotsMetadata(ctx context.Context) ([]SnapshotInfo, error)
// ListSnapshotsMetadata извлекает краткую информацию о снапшотах с фильтрацией и пагинацией.
ListSnapshotsMetadata(ctx context.Context, opts ListOptions) ([]SnapshotInfo, error)
// DeleteSnapshotMetadata удаляет метаданные снапшота по его ID.
// Не должен возвращать ошибку, если снапшот не найден.