package store import ( "reflect" "testing" ) func TestRubyReadingsUpsertAndRead(t *testing.T) { s, _ := openTemp(t) rows := []RubyReading{ {BookID: "b", Base: "漢字", Reading: "かんじ", FirstChapter: 2, Occurrences: 5}, {BookID: "b", Base: "東京", Reading: "とうきょう", FirstChapter: 1, Occurrences: 3}, {BookID: "other", Base: "京", Reading: "きょう", FirstChapter: 1, Occurrences: 1}, } for _, rr := range rows { if err := s.UpsertRubyReading(rr); err != nil { t.Fatal(err) } } got, err := s.RubyReadingsForBook("b") if err != nil { t.Fatal(err) } // Ordered by (first_chapter, base, reading): 東京(ch1) before 漢字(ch2). want := []RubyReading{ {BookID: "b", Base: "東京", Reading: "とうきょう", FirstChapter: 1, Occurrences: 3}, {BookID: "b", Base: "漢字", Reading: "かんじ", FirstChapter: 2, Occurrences: 5}, } if !reflect.DeepEqual(got, want) { t.Fatalf("read = %#v, want %#v (other book must not leak)", got, want) } } func TestRubyReadingsUpsertIsIdempotentAndConverges(t *testing.T) { s, _ := openTemp(t) rr := RubyReading{BookID: "b", Base: "漢字", Reading: "かんじ", FirstChapter: 3, Occurrences: 4} if err := s.UpsertRubyReading(rr); err != nil { t.Fatal(err) } // Re-ingesting the IDENTICAL full-book aggregate is a no-op (idempotent): one row, // same values, occurrences never doubled. if err := s.UpsertRubyReading(rr); err != nil { t.Fatal(err) } got, err := s.RubyReadingsForBook("b") if err != nil { t.Fatal(err) } if len(got) != 1 || got[0].FirstChapter != 3 || got[0].Occurrences != 4 { t.Fatalf("identical re-upsert must be a no-op: %#v", got) } // A re-ingest after a source edit (e.g. a foreword inserted) moves the pair's // first appearance to ch 4 and changes the count. persistRuby recomputes the // authoritative full-book aggregate, so the store must REPLACE BOTH columns — // it must NOT MIN-pin first_chapter to the stale-early 3 where the pair no // longer occurs (self-review finding). if err := s.UpsertRubyReading(RubyReading{BookID: "b", Base: "漢字", Reading: "かんじ", FirstChapter: 4, Occurrences: 2}); err != nil { t.Fatal(err) } got, err = s.RubyReadingsForBook("b") if err != nil { t.Fatal(err) } if len(got) != 1 || got[0].FirstChapter != 4 || got[0].Occurrences != 2 { t.Fatalf("re-ingest must REPLACE (converge) both columns, not MIN-pin: %#v", got) } } func TestRubyReadingsVariantReadingsAreDistinctRows(t *testing.T) { // The same base with two readings (variant yomi) are two rows, not a collision. s, _ := openTemp(t) for _, rr := range []RubyReading{ {BookID: "b", Base: "海", Reading: "うみ", FirstChapter: 1, Occurrences: 2}, {BookID: "b", Base: "海", Reading: "かい", FirstChapter: 4, Occurrences: 1}, } { if err := s.UpsertRubyReading(rr); err != nil { t.Fatal(err) } } got, err := s.RubyReadingsForBook("b") if err != nil { t.Fatal(err) } if len(got) != 2 { t.Fatalf("variant readings must be distinct rows, got %#v", got) } }