diff --git a/backend/internal/store/rows.go b/backend/internal/store/rows.go index 8b9544a..183e95e 100644 --- a/backend/internal/store/rows.go +++ b/backend/internal/store/rows.go @@ -682,6 +682,30 @@ func (db *DB) CompanyNames(tickers []string) (map[string]string, error) { return out, rows.Err() } +// SetCompanyNames bulk-upserts company_name for tickers (from top-changes +// snapshots). Only non-empty names are written; existing names win (skip +// blank overwrite) so a later richer source doesn't clobber a good one. +func (db *DB) SetCompanyNames(names map[string]string) error { + if len(names) == 0 { + return nil + } + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + for t, nm := range names { + nm = strings.TrimSpace(nm) + if nm == "" || strings.EqualFold(nm, t) { + continue + } + if _, err := tx.Exec(`UPDATE universe SET company_name = ? WHERE ticker = ? AND company_name = ''`, nm, t); err != nil { + return err + } + } + return tx.Commit() +} + // TickerHit is one local search result over the universe table. type TickerHit struct { Symbol string diff --git a/backend/internal/store/rows_extra_test.go b/backend/internal/store/rows_extra_test.go index eecf514..4852c79 100644 --- a/backend/internal/store/rows_extra_test.go +++ b/backend/internal/store/rows_extra_test.go @@ -155,3 +155,47 @@ func TestUniverseSearch(t *testing.T) { t.Fatal("TLKM has no name, must be omitted") } } + +// SetCompanyNames fills blank names; never overwrites existing ones; skips +// blanks and ticker-equal names. +func TestSetCompanyNames(t *testing.T) { + db, err := Open(t.TempDir() + "/names.db") + if err != nil { + t.Fatal(err) + } + defer db.Close() + uni := []UniverseRow{ + {Symbol: "BBCA.JK", Close: 100, Date: "2026-09-16", CompanyName: "PT Bank Central Asia Tbk"}, + {Symbol: "TLKM.JK", Close: 90, Date: "2026-09-16"}, + {Symbol: "BBRI.JK", Close: 80, Date: "2026-09-16"}, + } + if err := db.SaveUniverse(uni); err != nil { + t.Fatal(err) + } + // Fill TLKM + BBRI; BBCA already has a name (must NOT be clobbered); + // "UNVR" would be a new row (INSERT OR IGNORE means it's skipped). + err = db.SetCompanyNames(map[string]string{ + "TLKM": "PT Telkom Indonesia (Persero) Tbk", + "BBRI": "PT Bank Rakyat Indonesia (Persero) Tbk", + "BBCA": "SHOULD NOT OVERWRITE", + "UNVR": "PT Unilever Indonesia Tbk", + "EMTK": "PT Elang Mahkota Teknologi Tbk", // no universe row -> no-op + "BLAH": "BLAH", // ticker-equal -> skip + }) + if err != nil { + t.Fatal(err) + } + names, err := db.CompanyNames([]string{"BBCA", "TLKM", "BBRI", "UNVR"}) + if err != nil { + t.Fatal(err) + } + if names["BBCA"] != "PT Bank Central Asia Tbk" { + t.Fatalf("BBCA clobbered: %q", names["BBCA"]) + } + if names["TLKM"] != "PT Telkom Indonesia (Persero) Tbk" || names["BBRI"] != "PT Bank Rakyat Indonesia (Persero) Tbk" { + t.Fatalf("names = %+v, want TLKM+BBRI filled", names) + } + if _, ok := names["UNVR"]; ok { + t.Fatal("UNVR not in universe must be absent") + } +}