Refactor snapshot management: integrate logging, enhance concurrency with mutex, add clean extraction option, and update gRPC ListSnapshots with ListOptions.
This commit is contained in:
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
// Не должен возвращать ошибку, если снапшот не найден.
|
||||
|
Reference in New Issue
Block a user