kevo/pkg/transaction/transaction_test.go
Jeremy Tregunna 6fc3be617d
Some checks failed
Go Tests / Run Tests (1.24.2) (push) Has been cancelled
feat: Initial release of kevo storage engine.
Adds a complete LSM-based storage engine with these features:
- Single-writer based architecture for the storage engine
- WAL for durability, and hey it's configurable
- MemTable with skip list implementation for fast read/writes
- SSTable with block-based structure for on-disk level-based storage
- Background compaction with tiered strategy
- ACID transactions
- Good documentation (I hope)
2025-04-20 14:06:50 -06:00

323 lines
8.3 KiB
Go

package transaction
import (
"bytes"
"os"
"testing"
"github.com/jer/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)
}
}