package nstore import ( "slices" "testing" "time" _ "github.com/mattn/go-sqlite3" ) func TestSaveFetch(t *testing.T) { value := exampleTestPerson() db, err := OpenDebug(":memory:") if err != nil { t.Fatal(err) } defer db.Close() IDs, err := db.Save(&value) if err != nil { t.Fatal(err) } if len(IDs) != 1 || IDs[0] == 0 { t.Error("database save yields no row ID") } value.Age = 40 updateIDs, err := db.Save(&value) if err != nil { t.Fatal(err) } if len(updateIDs) != 1 || updateIDs[0] != IDs[0] { t.Errorf("update id %d does not match original id %d", updateIDs[0], IDs[0]) } fetchedValue, err := db.FetchOne(&testPerson{}, IDs[0]) if err != nil { t.Fatal(err) } fetchedPerson := fetchedValue.(*testPerson) if fetchedPerson.Name != value.Name || fetchedPerson.Age != value.Age { t.Errorf("fetched value does not match saved value") } // Check that Friends slice is populated if len(fetchedPerson.Friends) != 1 { t.Errorf("expected 1 friend, got %d", len(fetchedPerson.Friends)) } else { friend := fetchedPerson.Friends[0] if friend.Name != "Sam" || friend.Age != 33 || friend.FavoriteColor != "red" { t.Errorf("friend data mismatch: got %+v", friend) } // Friend's Friends should be empty due to recursion depth limit if len(friend.Friends) != 0 { t.Errorf("expected friend's Friends to be empty due to depth limit, got %d", len(friend.Friends)) } } } func TestSaveFetchPrimitiveSlice(t *testing.T) { db, err := OpenDebug(":memory:") if err != nil { t.Fatal(err) } defer db.Close() book := testBook{ Name: "The Go Programming Language", Year: 2015, Authors: []string{"Alan A. A. Donovan", "Brian W. Kernighan"}, } IDs, err := db.Save(&book) if err != nil { t.Fatal(err) } if len(IDs) != 1 || IDs[0] == 0 { t.Error("database save yields no row ID") } fetched, err := db.FetchOne(&testBook{}, IDs[0]) if err != nil { t.Fatal(err) } fetchedBook := fetched.(*testBook) if fetchedBook.Name != book.Name || fetchedBook.Year != book.Year { t.Errorf("fetched book mismatch: got %+v", fetchedBook) } if !slices.Equal(fetchedBook.Authors, book.Authors) { t.Errorf("authors mismatch: got %v, want %v", fetchedBook.Authors, book.Authors) } } func TestSaveFetchTime(t *testing.T) { db, err := OpenDebug(":memory:") if err != nil { t.Fatal(err) } defer db.Close() now := time.Now().Truncate(time.Second) // Truncate to seconds for comparison car := testCarType{ CreatedAt: now, Name: "Model S", Make: "Tesla", Model: "P100D", } IDs, err := db.Save(&car) if err != nil { t.Fatal(err) } if len(IDs) != 1 || IDs[0] == 0 { t.Error("database save yields no row ID") } fetched, err := db.FetchOne(&testCarType{}, IDs[0]) if err != nil { t.Fatal(err) } fetchedCar := fetched.(*testCarType) if fetchedCar.Name != car.Name || fetchedCar.Make != car.Make || fetchedCar.Model != car.Model { t.Errorf("fetched car mismatch: got %+v", fetchedCar) } // Compare times with tolerance (SQLite stores as string, may lose monotonic clock) if !fetchedCar.CreatedAt.Equal(car.CreatedAt) { t.Errorf("createdAt mismatch: got %v, want %v", fetchedCar.CreatedAt, car.CreatedAt) } } func TestSaveFetchCircularReference(t *testing.T) { type Node struct { ID int Name string Next *Node } db, err := OpenDebug(":memory:") if err != nil { t.Fatal(err) } defer db.Close() node1 := &Node{ID: 1, Name: "First"} node2 := &Node{ID: 2, Name: "Second"} node1.Next = node2 node2.Next = node1 // circular reference IDs, err := db.Save(node1, node2) if err != nil { t.Fatal(err) } if len(IDs) != 2 { t.Errorf("expected 2 IDs, got %d", len(IDs)) } // Fetch node1 fetched1, err := db.FetchOne(&Node{}, IDs[0]) if err != nil { t.Fatal(err) } fetchedNode1 := fetched1.(*Node) if fetchedNode1.ID != 1 { t.Errorf("node1 ID mismatch: got %d, want 1", fetchedNode1.ID) return } if fetchedNode1.Name != "First" { t.Errorf("node1 name mismatch: got %s", fetchedNode1.Name) return } // Fetch node2 fetched2, err := db.FetchOne(&Node{}, IDs[1]) if err != nil { t.Fatal(err) return } fetchedNode2 := fetched2.(*Node) if fetchedNode2.ID != 2 { t.Errorf("node2 ID mismatch: got %d, want 2", fetchedNode2.ID) return } if fetchedNode2.Name != "Second" { t.Errorf("node2 name mismatch: got %s", fetchedNode2.Name) return } // Next should point to node2 (depth 1) if fetchedNode1.Next == nil { t.Error("node1.Next is nil") return } else if fetchedNode1.Next.Name != "Second" { t.Errorf("node1.Next.Name mismatch: got %s", fetchedNode1.Next.Name) return } // Due to recursion depth limit (3), we populate relations up to depth 2. // depth 0: node1 -> node2 (depth 1) // depth 1: node2 -> node1 (depth 2) // depth 2: node1 -> node2 (depth 3, but relations skipped) // depth 3: node2 -> nil (relations skipped) // So we should have a chain of 4 nodes, with the last one's Next = nil. // Verify we don't have infinite recursion. if fetchedNode1.Next == nil { t.Error("node1.Next is nil") return } if fetchedNode1.Next.Next == nil { t.Error("node1.Next.Next is nil") return } if fetchedNode1.Next.Next.Next == nil { t.Error("node1.Next.Next.Next is nil, expected node2 at depth 3") return } // This should be nil due to depth limit if fetchedNode1.Next.Next.Next.Next != nil { t.Errorf("expected node1.Next.Next.Next.Next to be nil due to depth limit, got %v", fetchedNode1.Next.Next.Next.Next) } } func TestSaveFetchMultiple(t *testing.T) { db, err := OpenDebug(":memory:") if err != nil { t.Fatal(err) } defer db.Close() persons := []testPerson{ {Name: "Alice", Age: 25, FavoriteColor: "green"}, {Name: "Bob", Age: 30, FavoriteColor: "blue"}, {Name: "Charlie", Age: 35, FavoriteColor: "red"}, } // Save each individually var ids []int64 for _, p := range persons { IDs, err := db.Save(&p) if err != nil { t.Fatal(err) } ids = append(ids, IDs[0]) } // Fetch all seq, err := db.FetchAll(&testPerson{}) if err != nil { t.Fatal(err) } count := 0 for id, val := range seq { _ = id p := val.(*testPerson) // Find matching person found := false for _, orig := range persons { if p.Name == orig.Name && p.Age == orig.Age && p.FavoriteColor == orig.FavoriteColor { found = true break } } if !found { t.Errorf("unexpected person fetched: %+v", p) } count++ } if count != 3 { t.Errorf("expected 3 persons, got %d", count) } } func TestDelete(t *testing.T) { db, err := OpenDebug(":memory:") if err != nil { t.Fatal(err) } defer db.Close() person := testPerson{ Name: "ToDelete", Age: 99, FavoriteColor: "black", } IDs, err := db.Save(&person) if err != nil { t.Fatal(err) } id := IDs[0] // Fetch to ensure it's there fetched, err := db.FetchOne(&testPerson{}, id) if err != nil { t.Fatal(err) } if fetched.(*testPerson).Name != "ToDelete" { t.Error("fetch after save failed") } // Delete err = db.Delete(&person) if err != nil { t.Fatal(err) } // Fetch again should return error _, err = db.FetchOne(&testPerson{}, id) if err == nil { t.Error("expected error after delete") } }