package database import ( "context" "database/sql" "fmt" "testing" "time" ) func TestRepositoriesTransaction(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() repos := NewRepositories(db) // Test successful transaction with multiple operations err := repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error { // Create a file file := &File{ Path: "/test/tx_file.txt", MTime: time.Now().Truncate(time.Second), CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, GID: 1000, } if err := repos.Files.Create(ctx, tx, file); err != nil { return err } // Create chunks chunk1 := &Chunk{ ChunkHash: "tx_chunk1", SHA256: "tx_sha1", Size: 512, } if err := repos.Chunks.Create(ctx, tx, chunk1); err != nil { return err } chunk2 := &Chunk{ ChunkHash: "tx_chunk2", SHA256: "tx_sha2", Size: 512, } if err := repos.Chunks.Create(ctx, tx, chunk2); err != nil { return err } // Map chunks to file fc1 := &FileChunk{ FileID: file.ID, Idx: 0, ChunkHash: chunk1.ChunkHash, } if err := repos.FileChunks.Create(ctx, tx, fc1); err != nil { return err } fc2 := &FileChunk{ FileID: file.ID, Idx: 1, ChunkHash: chunk2.ChunkHash, } if err := repos.FileChunks.Create(ctx, tx, fc2); err != nil { return err } // Create blob blob := &Blob{ ID: "tx-blob-id-1", Hash: "tx_blob1", CreatedTS: time.Now().Truncate(time.Second), } if err := repos.Blobs.Create(ctx, tx, blob); err != nil { return err } // Map chunks to blob bc1 := &BlobChunk{ BlobID: blob.ID, ChunkHash: chunk1.ChunkHash, Offset: 0, Length: 512, } if err := repos.BlobChunks.Create(ctx, tx, bc1); err != nil { return err } bc2 := &BlobChunk{ BlobID: blob.ID, ChunkHash: chunk2.ChunkHash, Offset: 512, Length: 512, } if err := repos.BlobChunks.Create(ctx, tx, bc2); err != nil { return err } return nil }) if err != nil { t.Fatalf("transaction failed: %v", err) } // Verify all data was committed file, err := repos.Files.GetByPath(ctx, "/test/tx_file.txt") if err != nil { t.Fatalf("failed to get file: %v", err) } if file == nil { t.Error("expected file after transaction") } chunks, err := repos.FileChunks.GetByFile(ctx, "/test/tx_file.txt") if err != nil { t.Fatalf("failed to get file chunks: %v", err) } if len(chunks) != 2 { t.Errorf("expected 2 file chunks, got %d", len(chunks)) } blob, err := repos.Blobs.GetByHash(ctx, "tx_blob1") if err != nil { t.Fatalf("failed to get blob: %v", err) } if blob == nil { t.Error("expected blob after transaction") } } func TestRepositoriesTransactionRollback(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() repos := NewRepositories(db) // Test transaction rollback err := repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error { // Create a file file := &File{ Path: "/test/rollback_file.txt", MTime: time.Now().Truncate(time.Second), CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, GID: 1000, } if err := repos.Files.Create(ctx, tx, file); err != nil { return err } // Create a chunk chunk := &Chunk{ ChunkHash: "rollback_chunk", SHA256: "rollback_sha", Size: 1024, } if err := repos.Chunks.Create(ctx, tx, chunk); err != nil { return err } // Return error to trigger rollback return fmt.Errorf("intentional rollback") }) if err == nil || err.Error() != "intentional rollback" { t.Fatalf("expected rollback error, got: %v", err) } // Verify nothing was committed file, err := repos.Files.GetByPath(ctx, "/test/rollback_file.txt") if err != nil { t.Fatalf("error checking for file: %v", err) } if file != nil { t.Error("file should not exist after rollback") } chunk, err := repos.Chunks.GetByHash(ctx, "rollback_chunk") if err != nil { t.Fatalf("error checking for chunk: %v", err) } if chunk != nil { t.Error("chunk should not exist after rollback") } } func TestRepositoriesReadTransaction(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() repos := NewRepositories(db) // First, create some data file := &File{ Path: "/test/read_file.txt", MTime: time.Now().Truncate(time.Second), CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, GID: 1000, } err := repos.Files.Create(ctx, nil, file) if err != nil { t.Fatalf("failed to create file: %v", err) } // Test read-only transaction var retrievedFile *File err = repos.WithReadTx(ctx, func(ctx context.Context, tx *sql.Tx) error { var err error retrievedFile, err = repos.Files.GetByPathTx(ctx, tx, "/test/read_file.txt") if err != nil { return err } // Try to write in read-only transaction (should fail) _ = repos.Files.Create(ctx, tx, &File{ Path: "/test/should_fail.txt", MTime: time.Now(), CTime: time.Now(), Size: 0, Mode: 0644, UID: 1000, GID: 1000, }) // SQLite might not enforce read-only at this level, but we test the pattern return nil }) if err != nil { t.Fatalf("read transaction failed: %v", err) } if retrievedFile == nil { t.Error("expected to retrieve file in read transaction") } }