From 1eaf7921b1d49c09b373f708f7da9fac2c35843b Mon Sep 17 00:00:00 2001 From: tendinginfinity24 Date: Fri, 5 Sep 2025 13:17:38 +0530 Subject: [PATCH 1/2] Implement basic key-value store with persistence and tests --- .../key_value_store.go | 66 ++++++++++- .../key_value_store_test.go | 109 ++++++------------ 2 files changed, 100 insertions(+), 75 deletions(-) diff --git a/internal/exercises/templates/35_basic_key_value_store/key_value_store.go b/internal/exercises/templates/35_basic_key_value_store/key_value_store.go index 18eaf71..1685659 100644 --- a/internal/exercises/templates/35_basic_key_value_store/key_value_store.go +++ b/internal/exercises/templates/35_basic_key_value_store/key_value_store.go @@ -1,6 +1,10 @@ package basic_key_value_store import ( + "bufio" + "fmt" + "os" + "strings" "sync" ) @@ -27,29 +31,83 @@ func (e *StoreError) Error() string { func NewKeyValueStore(filepath string) *KeyValueStore { // TODO: initialize the store - return &KeyValueStore{} + return &KeyValueStore{ + data: make(map[string]string), + filepath: filepath, + } } func (s *KeyValueStore) Load() error { // TODO: load key/value pairs from file if present - return nil + s.mu.Lock() + defer s.mu.Unlock() + + file, err := os.Open(s.filepath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + s.data[parts[0]] = parts[1] + } + } + + return scanner.Err() } func (s *KeyValueStore) Save() error { // TODO: write pairs to file - return nil + s.mu.Lock() + defer s.mu.Unlock() + file, err := os.Create(s.filepath) + if err != nil { + return err + } + defer file.Close() + + writer := bufio.NewWriter(file) + for k, v := range s.data { + _, err := fmt.Fprintf(writer, "%s=%s\n", k, v) + if err != nil { + return err + } + } + return writer.Flush() } func (s *KeyValueStore) Set(key, value string) { // TODO: set key to value + s.mu.Lock() + defer s.mu.Unlock() + s.data[key] = value } func (s *KeyValueStore) Get(key string) (string, *StoreError) { // TODO: get value or return error when missing - return "", nil + s.mu.RLock() + defer s.mu.RUnlock() + val, ok := s.data[key] + if !ok { + return "", &StoreError{Message: "key not found"} + } + return val, nil } func (s *KeyValueStore) Delete(key string) *StoreError { // TODO: delete key or return error when missing + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.data[key]; !ok { + return &StoreError{Message: "key not found"} + } + delete(s.data, key) return nil } diff --git a/internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go b/internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go index 004ecd4..d41bb32 100644 --- a/internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go +++ b/internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go @@ -5,94 +5,61 @@ import ( "testing" ) -func TestNewKeyValueStore(t *testing.T) { - filepath := "test_kv_store.txt" - s := NewKeyValueStore(filepath) - if s == nil { - t.Errorf("Expected a new KeyValueStore, got nil") - } - if s.filepath != filepath { - t.Errorf("Expected filepath %s, got %s", filepath, s.filepath) - } - if s.data == nil { - t.Errorf("Expected data map to be initialized, got nil") - } +// helper to clean up after tests +func cleanup(filename string) { + _ = os.Remove(filename) } -func TestSetAndGet(t *testing.T) { - filepath := "test_set_get.txt" - defer os.Remove(filepath) - - s := NewKeyValueStore(filepath) - s.Set("name", "Alice") - s.Set("age", "30") +func TestLoadAndSave(t *testing.T) { + filename := "test_store.db" + defer cleanup(filename) - val, err := s.Get("name") - if err != nil || val != "Alice" { - t.Errorf("Expected 'Alice', got %q, error: %v", val, err) - } + store := NewKeyValueStore(filename) - val, err = s.Get("age") - if err != nil || val != "30" { - t.Errorf("Expected '30', got %q, error: %v", val, err) - } + // Save data + store.Set("city", "New York") + store.Set("country", "USA") - _, err = s.Get("nonexistent") - if err == nil { - t.Errorf("Expected error for non-existent key, got nil") + // Persist to file + if err := store.Save(); err != nil { + t.Fatalf("failed to save: %v", err) } -} -func TestDelete(t *testing.T) { - filepath := "test_delete.txt" - defer os.Remove(filepath) + // Create a new store instance to load from file + store2 := NewKeyValueStore(filename) - s := NewKeyValueStore(filepath) - s.Set("key1", "value1") - - err := s.Delete("key1") - if err != nil { - t.Errorf("Expected no error, got %v", err) + // Reload data + if err := store2.Load(); err != nil { + t.Fatalf("failed to load: %v", err) } - _, err = s.Get("key1") - if err == nil { - t.Errorf("Expected error for deleted key, got nil") + // Verify city + val, err := store2.Get("city") + if err != nil { + t.Errorf("unexpected error: %v", err) } - - err = s.Delete("nonexistent") - if err == nil { - t.Errorf("Expected error for deleting non-existent key, got nil") + if val != "New York" { + t.Errorf("Expected %q, got %q", "New York", val) } -} - -func TestLoadAndSave(t *testing.T) { - filepath := "test_load_save.txt" - defer os.Remove(filepath) - // Create a store and save it - s1 := NewKeyValueStore(filepath) - s1.Set("city", "New York") - s1.Set("country", "USA") - err := s1.Save() + // Verify country + val, err = store2.Get("country") if err != nil { - t.Fatalf("Failed to save store: %v", err) + t.Errorf("unexpected error: %v", err) } - - // Load into a new store - s2 := NewKeyValueStore(filepath) - err = s2.Load() - if err != nil { - t.Fatalf("Failed to load store: %v", err) + if val != "USA" { + t.Errorf("Expected %q, got %q", "USA", val) } +} - val, err := s2.Get("city") - if err != nil || val != "New York" { - t.Errorf("Expected 'New York', got %q, error: %v", val, err) - } +func TestMissingKey(t *testing.T) { + filename := "test_store_missing.db" + defer cleanup(filename) + + store := NewKeyValueStore(filename) - val, err = s2.Get("country") - if err != nil || val != "USA" { - t.Errorf("Expected 'USA', got %q, error: %v", val, err) + _, err := store.Get("notthere") + if err == nil { + t.Errorf("expected error for missing key, got nil") } } From 43d7dfb05f34d2b0ef7bc9f8ea48b87e625e1b92 Mon Sep 17 00:00:00 2001 From: tendinginfinity24 Date: Fri, 5 Sep 2025 14:25:32 +0530 Subject: [PATCH 2/2] fix: define ErrKeyNotFound and implement key-value store logic --- .../key_value_store.go | 87 ++++++++++++------- .../key_value_store_test.go | 85 ++++++++++-------- 2 files changed, 106 insertions(+), 66 deletions(-) diff --git a/internal/exercises/templates/35_basic_key_value_store/key_value_store.go b/internal/exercises/templates/35_basic_key_value_store/key_value_store.go index 1685659..0a6f854 100644 --- a/internal/exercises/templates/35_basic_key_value_store/key_value_store.go +++ b/internal/exercises/templates/35_basic_key_value_store/key_value_store.go @@ -4,17 +4,12 @@ import ( "bufio" "fmt" "os" + "path/filepath" "strings" "sync" ) -// TODO: -// - Implement a basic persistent key-value store: -// - In-memory map protected with RWMutex. -// - Load: read key=value pairs from a file if present. -// - Save: write all pairs to a file. -// - Set/Get/Delete operations; Get/Delete error on missing keys. - +// KeyValueStore implements a simple thread-safe persistent key-value store. type KeyValueStore struct { data map[string]string filepath string @@ -29,19 +24,19 @@ func (e *StoreError) Error() string { return e.Message } +// Sentinel error for missing keys. +var ErrKeyNotFound = &StoreError{Message: "key not found"} + +// NewKeyValueStore initializes a new key-value store at the given filepath. func NewKeyValueStore(filepath string) *KeyValueStore { - // TODO: initialize the store return &KeyValueStore{ data: make(map[string]string), filepath: filepath, } } +// Load replaces the current store with the contents of the file. func (s *KeyValueStore) Load() error { - // TODO: load key/value pairs from file if present - s.mu.Lock() - defer s.mu.Unlock() - file, err := os.Open(s.filepath) if err != nil { if os.IsNotExist(err) { @@ -52,61 +47,95 @@ func (s *KeyValueStore) Load() error { defer file.Close() scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 0), 1024*1024) // allow long lines up to 1MB + tmp := make(map[string]string) for scanner.Scan() { line := scanner.Text() + if line == "" { + continue + } parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { - s.data[parts[0]] = parts[1] + k := strings.TrimSpace(parts[0]) + v := strings.TrimSpace(parts[1]) + tmp[k] = v } } + if err := scanner.Err(); err != nil { + return err + } - return scanner.Err() + s.mu.Lock() + s.data = tmp + s.mu.Unlock() + return nil } +// Save writes the store atomically to disk. func (s *KeyValueStore) Save() error { - // TODO: write pairs to file - s.mu.Lock() - defer s.mu.Unlock() - file, err := os.Create(s.filepath) + // Snapshot under read lock + s.mu.RLock() + snapshot := make(map[string]string, len(s.data)) + for k, v := range s.data { + snapshot[k] = v + } + s.mu.RUnlock() + + dir := filepath.Dir(s.filepath) + tmp, err := os.CreateTemp(dir, ".kvtmp-*") if err != nil { return err } - defer file.Close() - writer := bufio.NewWriter(file) - for k, v := range s.data { - _, err := fmt.Fprintf(writer, "%s=%s\n", k, v) - if err != nil { + writer := bufio.NewWriter(tmp) + for k, v := range snapshot { + if _, err := fmt.Fprintf(writer, "%s=%s\n", k, v); err != nil { + tmp.Close() + _ = os.Remove(tmp.Name()) return err } } - return writer.Flush() + if err := writer.Flush(); err != nil { + tmp.Close() + _ = os.Remove(tmp.Name()) + return err + } + if err := tmp.Sync(); err != nil { + tmp.Close() + _ = os.Remove(tmp.Name()) + return err + } + if err := tmp.Close(); err != nil { + _ = os.Remove(tmp.Name()) + return err + } + return os.Rename(tmp.Name(), s.filepath) } +// Set updates or inserts a key. func (s *KeyValueStore) Set(key, value string) { - // TODO: set key to value s.mu.Lock() defer s.mu.Unlock() s.data[key] = value } +// Get retrieves a value or ErrKeyNotFound. func (s *KeyValueStore) Get(key string) (string, *StoreError) { - // TODO: get value or return error when missing s.mu.RLock() defer s.mu.RUnlock() val, ok := s.data[key] if !ok { - return "", &StoreError{Message: "key not found"} + return "", ErrKeyNotFound } return val, nil } +// Delete removes a key or returns ErrKeyNotFound. func (s *KeyValueStore) Delete(key string) *StoreError { - // TODO: delete key or return error when missing s.mu.Lock() defer s.mu.Unlock() if _, ok := s.data[key]; !ok { - return &StoreError{Message: "key not found"} + return ErrKeyNotFound } delete(s.data, key) return nil diff --git a/internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go b/internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go index d41bb32..50a8ac5 100644 --- a/internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go +++ b/internal/exercises/templates/35_basic_key_value_store/key_value_store_test.go @@ -1,65 +1,76 @@ package basic_key_value_store import ( - "os" + "errors" + "path/filepath" "testing" ) -// helper to clean up after tests -func cleanup(filename string) { - _ = os.Remove(filename) -} - func TestLoadAndSave(t *testing.T) { - filename := "test_store.db" - defer cleanup(filename) + dir := t.TempDir() + filename := filepath.Join(dir, "test_store.db") store := NewKeyValueStore(filename) - - // Save data store.Set("city", "New York") store.Set("country", "USA") - // Persist to file if err := store.Save(); err != nil { - t.Fatalf("failed to save: %v", err) - } - - // Create a new store instance to load from file - store2 := NewKeyValueStore(filename) - - // Reload data - if err := store2.Load(); err != nil { - t.Fatalf("failed to load: %v", err) + t.Fatalf("unexpected save error: %v", err) } - // Verify city - val, err := store2.Get("city") - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if val != "New York" { - t.Errorf("Expected %q, got %q", "New York", val) + // load into a new store + other := NewKeyValueStore(filename) + if err := other.Load(); err != nil { + t.Fatalf("unexpected load error: %v", err) } - // Verify country - val, err = store2.Get("country") - if err != nil { - t.Errorf("unexpected error: %v", err) + if v, err := other.Get("city"); v != "New York" || err != nil { + t.Errorf("expected New York, got %q, err=%v", v, err) } - if val != "USA" { - t.Errorf("Expected %q, got %q", "USA", val) + if v, err := other.Get("country"); v != "USA" || err != nil { + t.Errorf("expected USA, got %q, err=%v", v, err) } } func TestMissingKey(t *testing.T) { - filename := "test_store_missing.db" - defer cleanup(filename) + dir := t.TempDir() + filename := filepath.Join(dir, "test_store_missing.db") store := NewKeyValueStore(filename) + store.Set("name", "Alice") _, err := store.Get("notthere") - if err == nil { - t.Errorf("expected error for missing key, got nil") + if !errors.Is(err, ErrKeyNotFound) { + t.Errorf("expected ErrKeyNotFound, got %v", err) + } +} + +func TestDelete(t *testing.T) { + t.Parallel() + dir := t.TempDir() + filename := filepath.Join(dir, "test_store.db") + + store := NewKeyValueStore(filename) + store.Set("k", "v") + + if err := store.Delete("k"); err != nil { + t.Fatalf("unexpected delete error: %v", err) + } + if _, err := store.Get("k"); !errors.Is(err, ErrKeyNotFound) { + t.Fatalf("expected ErrKeyNotFound after delete, got %v", err) + } + if err := store.Delete("k"); !errors.Is(err, ErrKeyNotFound) { + t.Fatalf("expected ErrKeyNotFound on second delete, got %v", err) + } +} + +func TestLoadMissingFileIsNoop(t *testing.T) { + t.Parallel() + dir := t.TempDir() + filename := filepath.Join(dir, "does_not_exist.db") + + store := NewKeyValueStore(filename) + if err := store.Load(); err != nil { + t.Fatalf("expected nil error on missing file, got %v", err) } }