Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 103 additions & 16 deletions internal/exercises/templates/35_basic_key_value_store/key_value_store.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package basic_key_value_store

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
Expand All @@ -25,31 +24,119 @@ 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{}
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
file, err := os.Open(s.filepath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
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 {
k := strings.TrimSpace(parts[0])
v := strings.TrimSpace(parts[1])
tmp[k] = v
}
}
if err := scanner.Err(); err != nil {
return 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
return nil
// 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
}

Comment on lines +84 to +89
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Create parent directory before atomic write.

Save fails if s.filepath’s directory doesn’t exist. Ensure dir is created.

 dir := filepath.Dir(s.filepath)
+if err := os.MkdirAll(dir, 0o755); err != nil {
+  return err
+}
 tmp, err := os.CreateTemp(dir, ".kvtmp-*")
 if err != nil {
   return err
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dir := filepath.Dir(s.filepath)
tmp, err := os.CreateTemp(dir, ".kvtmp-*")
if err != nil {
return err
}
dir := filepath.Dir(s.filepath)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, ".kvtmp-*")
if err != nil {
return err
}
🤖 Prompt for AI Agents
In internal/exercises/templates/35_basic_key_value_store/key_value_store.go
around lines 84 to 89, the code attempts to create a temp file in dir but fails
when the parent directory for s.filepath does not exist; call os.MkdirAll(dir,
0o755) (and check/return its error) before os.CreateTemp to ensure the parent
directory exists, then proceed with creating the temp file and the existing
atomic write flow.

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
}
}
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
return "", nil
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.data[key]
if !ok {
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 ErrKeyNotFound
}
delete(s.data, key)
return nil
}
Original file line number Diff line number Diff line change
@@ -1,98 +1,76 @@
package basic_key_value_store

import (
"os"
"errors"
"path/filepath"
"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")
}
}

func TestSetAndGet(t *testing.T) {
filepath := "test_set_get.txt"
defer os.Remove(filepath)
func TestLoadAndSave(t *testing.T) {
dir := t.TempDir()
filename := filepath.Join(dir, "test_store.db")

s := NewKeyValueStore(filepath)
s.Set("name", "Alice")
s.Set("age", "30")
store := NewKeyValueStore(filename)
store.Set("city", "New York")
store.Set("country", "USA")

val, err := s.Get("name")
if err != nil || val != "Alice" {
t.Errorf("Expected 'Alice', got %q, error: %v", val, err)
if err := store.Save(); err != nil {
t.Fatalf("unexpected save error: %v", err)
}

val, err = s.Get("age")
if err != nil || val != "30" {
t.Errorf("Expected '30', got %q, error: %v", val, err)
// load into a new store
other := NewKeyValueStore(filename)
if err := other.Load(); err != nil {
t.Fatalf("unexpected load error: %v", err)
}

_, err = s.Get("nonexistent")
if err == nil {
t.Errorf("Expected error for non-existent key, got nil")
if v, err := other.Get("city"); v != "New York" || err != nil {
t.Errorf("expected New York, got %q, err=%v", v, err)
}
if v, err := other.Get("country"); v != "USA" || err != nil {
t.Errorf("expected USA, got %q, err=%v", v, err)
}
}

func TestDelete(t *testing.T) {
filepath := "test_delete.txt"
defer os.Remove(filepath)
func TestMissingKey(t *testing.T) {
dir := t.TempDir()
filename := filepath.Join(dir, "test_store_missing.db")

s := NewKeyValueStore(filepath)
s.Set("key1", "value1")

err := s.Delete("key1")
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
store := NewKeyValueStore(filename)
store.Set("name", "Alice")

_, err = s.Get("key1")
if err == nil {
t.Errorf("Expected error for deleted key, got nil")
}

err = s.Delete("nonexistent")
if err == nil {
t.Errorf("Expected error for deleting non-existent key, got nil")
_, err := store.Get("notthere")
if !errors.Is(err, ErrKeyNotFound) {
t.Errorf("expected ErrKeyNotFound, got %v", err)
}
}

func TestLoadAndSave(t *testing.T) {
filepath := "test_load_save.txt"
defer os.Remove(filepath)
func TestDelete(t *testing.T) {
t.Parallel()
dir := t.TempDir()
filename := filepath.Join(dir, "test_store.db")

// Create a store and save it
s1 := NewKeyValueStore(filepath)
s1.Set("city", "New York")
s1.Set("country", "USA")
err := s1.Save()
if err != nil {
t.Fatalf("Failed to save store: %v", err)
}
store := NewKeyValueStore(filename)
store.Set("k", "v")

// Load into a new store
s2 := NewKeyValueStore(filepath)
err = s2.Load()
if err != nil {
t.Fatalf("Failed to load store: %v", err)
if err := store.Delete("k"); err != nil {
t.Fatalf("unexpected delete error: %v", err)
}

val, err := s2.Get("city")
if err != nil || val != "New York" {
t.Errorf("Expected 'New York', got %q, error: %v", val, 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")

val, err = s2.Get("country")
if err != nil || val != "USA" {
t.Errorf("Expected 'USA', got %q, error: %v", val, err)
store := NewKeyValueStore(filename)
if err := store.Load(); err != nil {
t.Fatalf("expected nil error on missing file, got %v", err)
}
}