package transaction import ( "bytes" "os" "testing" "github.com/KevoDB/kevo/pkg/engine" ) func setupTestEngine(t *testing.T) (*engine.Engine, string) { // Create a temporary directory for the test tempDir, err := os.MkdirTemp("", "transaction_test_*") if err != nil { t.Fatalf("Failed to create temp directory: %v", err) } // Create a new engine eng, err := engine.NewEngine(tempDir) if err != nil { os.RemoveAll(tempDir) t.Fatalf("Failed to create engine: %v", err) } return eng, tempDir } func TestReadOnlyTransaction(t *testing.T) { eng, tempDir := setupTestEngine(t) defer os.RemoveAll(tempDir) defer eng.Close() // Add some data directly to the engine if err := eng.Put([]byte("key1"), []byte("value1")); err != nil { t.Fatalf("Failed to put key1: %v", err) } if err := eng.Put([]byte("key2"), []byte("value2")); err != nil { t.Fatalf("Failed to put key2: %v", err) } // Create a read-only transaction tx, err := NewTransaction(eng, ReadOnly) if err != nil { t.Fatalf("Failed to create read-only transaction: %v", err) } // Test Get functionality value, err := tx.Get([]byte("key1")) if err != nil { t.Fatalf("Failed to get key1: %v", err) } if !bytes.Equal(value, []byte("value1")) { t.Errorf("Expected 'value1' but got '%s'", value) } // Test read-only constraints err = tx.Put([]byte("key3"), []byte("value3")) if err != ErrReadOnlyTransaction { t.Errorf("Expected ErrReadOnlyTransaction but got: %v", err) } err = tx.Delete([]byte("key1")) if err != ErrReadOnlyTransaction { t.Errorf("Expected ErrReadOnlyTransaction but got: %v", err) } // Test iterator iter := tx.NewIterator() count := 0 for iter.SeekToFirst(); iter.Valid(); iter.Next() { count++ } if count != 2 { t.Errorf("Expected 2 keys but found %d", count) } // Test commit (which for read-only just releases resources) if err := tx.Commit(); err != nil { t.Errorf("Failed to commit read-only transaction: %v", err) } // Transaction should be closed now _, err = tx.Get([]byte("key1")) if err != ErrTransactionClosed { t.Errorf("Expected ErrTransactionClosed but got: %v", err) } } func TestReadWriteTransaction(t *testing.T) { eng, tempDir := setupTestEngine(t) defer os.RemoveAll(tempDir) defer eng.Close() // Add initial data if err := eng.Put([]byte("key1"), []byte("value1")); err != nil { t.Fatalf("Failed to put key1: %v", err) } // Create a read-write transaction tx, err := NewTransaction(eng, ReadWrite) if err != nil { t.Fatalf("Failed to create read-write transaction: %v", err) } // Add more data through the transaction if err := tx.Put([]byte("key2"), []byte("value2")); err != nil { t.Fatalf("Failed to put key2: %v", err) } if err := tx.Put([]byte("key3"), []byte("value3")); err != nil { t.Fatalf("Failed to put key3: %v", err) } // Delete a key if err := tx.Delete([]byte("key1")); err != nil { t.Fatalf("Failed to delete key1: %v", err) } // Verify the changes are visible in the transaction but not in the engine yet // Check via transaction value, err := tx.Get([]byte("key2")) if err != nil { t.Errorf("Failed to get key2 from transaction: %v", err) } if !bytes.Equal(value, []byte("value2")) { t.Errorf("Expected 'value2' but got '%s'", value) } // Check deleted key _, err = tx.Get([]byte("key1")) if err == nil { t.Errorf("key1 should be deleted in transaction") } // Check directly in engine - changes shouldn't be visible yet value, err = eng.Get([]byte("key2")) if err == nil { t.Errorf("key2 should not be visible in engine yet") } value, err = eng.Get([]byte("key1")) if err != nil { t.Errorf("key1 should still be visible in engine: %v", err) } // Commit the transaction if err := tx.Commit(); err != nil { t.Fatalf("Failed to commit transaction: %v", err) } // Now check engine again - changes should be visible value, err = eng.Get([]byte("key2")) if err != nil { t.Errorf("key2 should be visible in engine after commit: %v", err) } if !bytes.Equal(value, []byte("value2")) { t.Errorf("Expected 'value2' but got '%s'", value) } // Deleted key should be gone value, err = eng.Get([]byte("key1")) if err == nil { t.Errorf("key1 should be deleted in engine after commit") } // Transaction should be closed _, err = tx.Get([]byte("key2")) if err != ErrTransactionClosed { t.Errorf("Expected ErrTransactionClosed but got: %v", err) } } func TestTransactionRollback(t *testing.T) { eng, tempDir := setupTestEngine(t) defer os.RemoveAll(tempDir) defer eng.Close() // Add initial data if err := eng.Put([]byte("key1"), []byte("value1")); err != nil { t.Fatalf("Failed to put key1: %v", err) } // Create a read-write transaction tx, err := NewTransaction(eng, ReadWrite) if err != nil { t.Fatalf("Failed to create read-write transaction: %v", err) } // Add and modify data if err := tx.Put([]byte("key2"), []byte("value2")); err != nil { t.Fatalf("Failed to put key2: %v", err) } if err := tx.Delete([]byte("key1")); err != nil { t.Fatalf("Failed to delete key1: %v", err) } // Rollback the transaction if err := tx.Rollback(); err != nil { t.Fatalf("Failed to rollback transaction: %v", err) } // Changes should not be visible in the engine value, err := eng.Get([]byte("key1")) if err != nil { t.Errorf("key1 should still exist after rollback: %v", err) } if !bytes.Equal(value, []byte("value1")) { t.Errorf("Expected 'value1' but got '%s'", value) } // key2 should not exist _, err = eng.Get([]byte("key2")) if err == nil { t.Errorf("key2 should not exist after rollback") } // Transaction should be closed _, err = tx.Get([]byte("key1")) if err != ErrTransactionClosed { t.Errorf("Expected ErrTransactionClosed but got: %v", err) } } func TestTransactionIterator(t *testing.T) { eng, tempDir := setupTestEngine(t) defer os.RemoveAll(tempDir) defer eng.Close() // Add initial data if err := eng.Put([]byte("key1"), []byte("value1")); err != nil { t.Fatalf("Failed to put key1: %v", err) } if err := eng.Put([]byte("key3"), []byte("value3")); err != nil { t.Fatalf("Failed to put key3: %v", err) } if err := eng.Put([]byte("key5"), []byte("value5")); err != nil { t.Fatalf("Failed to put key5: %v", err) } // Create a read-write transaction tx, err := NewTransaction(eng, ReadWrite) if err != nil { t.Fatalf("Failed to create read-write transaction: %v", err) } // Add and modify data in transaction if err := tx.Put([]byte("key2"), []byte("value2")); err != nil { t.Fatalf("Failed to put key2: %v", err) } if err := tx.Put([]byte("key4"), []byte("value4")); err != nil { t.Fatalf("Failed to put key4: %v", err) } if err := tx.Delete([]byte("key3")); err != nil { t.Fatalf("Failed to delete key3: %v", err) } // Use iterator to check order and content iter := tx.NewIterator() expected := []struct { key string value string }{ {"key1", "value1"}, {"key2", "value2"}, {"key4", "value4"}, {"key5", "value5"}, } i := 0 for iter.SeekToFirst(); iter.Valid(); iter.Next() { if i >= len(expected) { t.Errorf("Too many keys in iterator") break } if !bytes.Equal(iter.Key(), []byte(expected[i].key)) { t.Errorf("Expected key '%s' but got '%s'", expected[i].key, string(iter.Key())) } if !bytes.Equal(iter.Value(), []byte(expected[i].value)) { t.Errorf("Expected value '%s' but got '%s'", expected[i].value, string(iter.Value())) } i++ } if i != len(expected) { t.Errorf("Expected %d keys but found %d", len(expected), i) } // Test range iterator rangeIter := tx.NewRangeIterator([]byte("key2"), []byte("key5")) expected = []struct { key string value string }{ {"key2", "value2"}, {"key4", "value4"}, } i = 0 for rangeIter.SeekToFirst(); rangeIter.Valid(); rangeIter.Next() { if i >= len(expected) { t.Errorf("Too many keys in range iterator") break } if !bytes.Equal(rangeIter.Key(), []byte(expected[i].key)) { t.Errorf("Expected key '%s' but got '%s'", expected[i].key, string(rangeIter.Key())) } if !bytes.Equal(rangeIter.Value(), []byte(expected[i].value)) { t.Errorf("Expected value '%s' but got '%s'", expected[i].value, string(rangeIter.Value())) } i++ } if i != len(expected) { t.Errorf("Expected %d keys in range but found %d", len(expected), i) } // Commit and verify results if err := tx.Commit(); err != nil { t.Fatalf("Failed to commit transaction: %v", err) } }