Files
yellowjacket/backend/explore/artifactimport_test.go
yonluandClaude Opus 5 b3737d30af feat(explore): carry multi-artist credits in the catalog
A track credited to more than one artist has exactly one navigable
artist in this app and the rest are punctuation. `primaryArtist()`
string-parses the credit, strips a " feat. " clause and discards the
guest; it deliberately does not split on "&", "with" or "," because
those live inside real artist names.

Measured on a real 26,069-file library plus an 80+80 MusicBrainz
sample: 13% of recordings are multi-artist upstream, while only 0.86%
of files carry any structured multi-artist tag — mp3 carries zero
files with multiple MUSICBRAINZ_ARTISTID across 19,840. Of 1,286 files
saying "feat.", 90% have nothing structured behind it, and a sample of
80 such files was multi-artist in MB 80 times out of 80.

CLAUDE.md justified plan 013's removal of the credit tables with "3
credits of 2,823 listed more than one artist". That measured our own
*writer* — cachedLinkArtist was called once per credit, so a
collaboration could never have been recorded. Dropping the join table
was still right on cost; the evidence for "multi-artist is rare" was
not.

A credit is ordered parts and the credit string is derived from them,
so join phrases are assembly instructions, not disassembly ones.
Nothing here reconstructs a credit by searching a name inside a credit
string: the stored text may come from tags while the parts come from
the catalog, and those disagree for ~1 in 3 multi-artist credits.

Where it comes from, after two dead ends: the canonical dump CI
already streams has no join phrases and no as-credited names, and the
JSON dumps cover 153,691 recordings of ~35M with *zero* overlap
against a real library. So mbdump.tar.bz2 — 7.1 GB, ~13.7 min in
pure-Go bzip2, whose members are alphabetical, which is what lets one
pass resolve an entity's credit without buffering 35M recordings.

- artist_credit_part / artist_credit_ref, multi-artist credits only:
  a single-artist credit is already explore_index's own artist_name.
- Column layouts verified against the real 20260815 export;
  ErrDumpShape makes a wrong guess a failed build, not a wrong catalog.
- The pass runs on every mode, not just a build. The job picks its mode
  from the index's own state, and a complete import means "refresh",
  which never enters the importer — so credits could otherwise only
  arrive via a rebuild that re-downloads ~205 GB. It reports whether it
  populated anything, which is what flips `changed` and republishes.
- The importer asks whether an artifact carries the tables, on the
  writer where `core` is attached, so the artifact already published
  still imports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfVYUVExXsx1nSWrXN8mAh
2026-08-17 08:25:36 -04:00

751 lines
21 KiB
Go

package explore
import (
"context"
"database/sql"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
"yellowjacket/backend/database"
)
// artifactRow is one catalog row written into a test artifact.
type artifactRow struct {
entityType string
mbid string
title string
artistName string
artistMBID string
popularity int
}
// writeTestArtifact builds an artifact file matching what cmd/indexexport
// produces: catalog columns only, no FTS, no triggers.
func writeTestArtifact(
t *testing.T, meta map[string]string, rows []artifactRow,
) string {
t.Helper()
path := filepath.Join(t.TempDir(), "core-index.db")
db, err := sql.Open("sqlite", "file:"+path)
if err != nil {
t.Fatalf("open artifact: %v", err)
}
defer func() { _ = db.Close() }()
for _, stmt := range []string{
`CREATE TABLE explore_index (
entity_type TEXT NOT NULL,
mbid TEXT NOT NULL,
title TEXT NOT NULL,
artist_name TEXT NOT NULL,
artist_mbid TEXT NOT NULL,
aliases TEXT NOT NULL DEFAULT '',
popularity INTEGER NOT NULL DEFAULT 0,
listener_count INTEGER NOT NULL DEFAULT 0,
duration INTEGER NOT NULL DEFAULT 0,
caa_release_mbid TEXT NOT NULL DEFAULT '',
release_name TEXT NOT NULL DEFAULT '',
primary_type TEXT NOT NULL DEFAULT '',
secondary_types TEXT NOT NULL DEFAULT '',
release_date TEXT NOT NULL DEFAULT '',
artist_type TEXT NOT NULL DEFAULT '',
country TEXT NOT NULL DEFAULT '',
disambiguation TEXT NOT NULL DEFAULT '',
sort_name TEXT NOT NULL DEFAULT '',
discog_fetched INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (mbid)
) WITHOUT ROWID`,
`CREATE TABLE artifact_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)`,
} {
if _, err := db.Exec(stmt); err != nil {
t.Fatalf("create artifact schema: %v", err)
}
}
for k, v := range meta {
if _, err := db.Exec(
`INSERT INTO artifact_meta (key, value) VALUES (?, ?)`, k, v,
); err != nil {
t.Fatalf("stamp artifact meta: %v", err)
}
}
for _, r := range rows {
if _, err := db.Exec(`
INSERT INTO explore_index
(entity_type, mbid, title, artist_name, artist_mbid, popularity)
VALUES (?, ?, ?, ?, ?, ?)`,
r.entityType, r.mbid, r.title, r.artistName, r.artistMBID, r.popularity,
); err != nil {
t.Fatalf("insert artifact row: %v", err)
}
}
return path
}
// validMeta is the artifact_meta a well-formed artifact carries.
func validMeta() map[string]string {
return map[string]string{
"artifact_version": supportedArtifactVersion,
"built_at": "2026-07-17T03:53:43Z",
"listens_applied_series": "2593",
"source_rows": "2052168",
}
}
func TestImportCoreArtifactMergesCatalog(t *testing.T) {
db := database.NewTestDB(t)
si := NewSearchIndex(db, nil, nil, testLogger())
path := writeTestArtifact(t, validMeta(), []artifactRow{
{"artist", artA, "Artist A", "Artist A", artA, 5000},
{"artist", artB, "Artist B", "Artist B", artB, 4000},
{"release_group", rgA, "Album A", "Artist A", artA, 3000},
{"recording", recA, "Song A", "Artist A", artA, 2000},
})
if err := si.importCoreArtifact(context.Background(), path); err != nil {
t.Fatalf("importCoreArtifact: %v", err)
}
var got int
if err := db.QueryRowWriter(
"SELECT COUNT(*) FROM explore_index",
).Scan(&got); err != nil {
t.Fatalf("count rows: %v", err)
}
if got != 4 {
t.Errorf("merged %d rows, want 4", got)
}
// The merge must leave the index searchable: the FTS rebuild that
// closes the bulk-load window is the only thing populating it, since
// the triggers were dropped for the duration.
var hits int
if err := db.QueryRowWriter(
`SELECT COUNT(*) FROM explore_index_fts WHERE explore_index_fts MATCH ?`,
"Song",
).Scan(&hits); err != nil {
t.Fatalf("query fts: %v", err)
}
if hits == 0 {
t.Error("FTS index is empty after merge; search would return nothing")
}
}
func TestImportCoreArtifactStampsMeta(t *testing.T) {
db := database.NewTestDB(t)
si := NewSearchIndex(db, nil, nil, testLogger())
path := writeTestArtifact(t, validMeta(), []artifactRow{
{"artist", artA, "Artist A", "Artist A", artA, 5000},
})
if err := si.importCoreArtifact(context.Background(), path); err != nil {
t.Fatalf("importCoreArtifact: %v", err)
}
if !si.hasMeta(dumpImportDoneKey) {
t.Error("dump_import_done not stamped; a full dump import would run anyway")
}
// Without this the incremental refresh refuses to run and the
// shipped popularity never updates again.
if series, ok := si.metaInt(listensAppliedSeriesKey); !ok || series != 2593 {
t.Errorf("listens_applied_series = %d (ok=%v), want 2593", series, ok)
}
if !si.hasMeta(coreArtifactVersionKey) {
t.Error("core_artifact_version not stamped")
}
}
// A merge must never downgrade what the index already holds, because a
// user's own library rows and any lazily-fetched detail predate it.
func TestImportCoreArtifactPreservesLocalData(t *testing.T) {
db := database.NewTestDB(t)
si := NewSearchIndex(db, nil, nil, testLogger())
si.upsertBatch([]SearchIndexResult{{
EntityType: "recording",
MBID: recA,
Title: "Song A",
ArtistName: "Artist A",
ArtistMBID: artA,
Popularity: 9999,
Duration: 210000,
InLibrary: true,
DiscogFetched: true,
}})
path := writeTestArtifact(t, validMeta(), []artifactRow{
// Lower popularity and no duration: both must lose.
{"recording", recA, "Song A", "Artist A", artA, 10},
})
if err := si.importCoreArtifact(context.Background(), path); err != nil {
t.Fatalf("importCoreArtifact: %v", err)
}
var popularity, duration, inLibrary, discogFetched int
if err := db.QueryRowWriter(`
SELECT popularity, duration, in_library, discog_fetched
FROM explore_index WHERE mbid = ?`, dbMBID(recA),
).Scan(&popularity, &duration, &inLibrary, &discogFetched); err != nil {
t.Fatalf("read merged row: %v", err)
}
if popularity != 9999 {
t.Errorf("popularity = %d, want 9999 (higher must win)", popularity)
}
if duration != 210000 {
t.Errorf("duration = %d, want 210000 (artifact carries none)", duration)
}
if inLibrary != 1 {
t.Error("in_library was cleared; the artifact must not touch personal columns")
}
if discogFetched != 1 {
t.Error("discog_fetched was cleared by the merge")
}
}
// The batch walk is the part most likely to drop or duplicate rows, so
// it is exercised across many batch boundaries rather than one.
func TestImportCoreArtifactBatchWalkCoversAllRows(t *testing.T) {
db := database.NewTestDB(t)
si := NewSearchIndex(db, nil, nil, testLogger())
// Shrink the batch rather than growing the fixture: what matters is
// crossing several boundaries, including a short final one.
original := artifactMergeBatch
artifactMergeBatch = 100
t.Cleanup(func() { artifactMergeBatch = original })
const total = 337
rows := make([]artifactRow, 0, total)
for i := range total {
rows = append(rows, artifactRow{
entityType: "recording",
mbid: syntheticMBID(i),
title: "Song",
artistName: "Artist",
artistMBID: artA,
popularity: i,
})
}
path := writeTestArtifact(t, validMeta(), rows)
if err := si.importCoreArtifact(context.Background(), path); err != nil {
t.Fatalf("importCoreArtifact: %v", err)
}
var got int
if err := db.QueryRowWriter(
"SELECT COUNT(*) FROM explore_index",
).Scan(&got); err != nil {
t.Fatalf("count rows: %v", err)
}
if got != total {
t.Errorf("merged %d rows, want %d", got, total)
}
}
func TestImportCoreArtifactRejectsBadArtifacts(t *testing.T) {
tests := []struct {
name string
meta map[string]string
rows []artifactRow
want error
}{
{
name: "version mismatch",
meta: map[string]string{"artifact_version": "99"},
rows: []artifactRow{{"artist", artA, "A", "A", artA, 1}},
want: ErrArtifactVersion,
},
{
name: "no version",
meta: map[string]string{},
rows: []artifactRow{{"artist", artA, "A", "A", artA, 1}},
want: ErrArtifactVersion,
},
{
name: "empty catalog",
meta: validMeta(),
rows: nil,
want: ErrArtifactUnusable,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := database.NewTestDB(t)
si := NewSearchIndex(db, nil, nil, testLogger())
path := writeTestArtifact(t, tt.meta, tt.rows)
err := si.importCoreArtifact(context.Background(), path)
if err == nil {
t.Fatal("expected rejection, got nil")
}
if !strings.Contains(err.Error(), tt.want.Error()) {
t.Errorf("error = %v, want it to wrap %v", err, tt.want)
}
// A rejected artifact must not leave the index claiming it
// has a catalog, or the real build would never run.
if si.hasMeta(dumpImportDoneKey) {
t.Error("rejected artifact still stamped dump_import_done")
}
})
}
}
func TestInspectArtifactRejectsNonArtifactFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "garbage.db")
if err := os.WriteFile(path, []byte("not a database"), 0o644); err != nil {
t.Fatalf("write file: %v", err)
}
if _, err := inspectArtifact(path); err == nil {
t.Error("expected rejection of a non-artifact file")
}
}
// syntheticMBID produces distinct, well-formed MBIDs for bulk fixtures.
func syntheticMBID(i int) string {
const hex = "0123456789abcdef"
buf := []byte("00000000-0000-0000-0000-000000000000")
for pos := len(buf) - 1; pos >= 0 && i > 0; pos-- {
if buf[pos] == '-' {
continue
}
buf[pos] = hex[i%16]
i /= 16
}
return string(buf)
}
// resolveArtistName falls back to the MBID when no name is available.
// That value must never reach the index: the upsert no longer defends
// against it, so the writer is the only thing standing in the way.
func TestAddFromCacheNeverStoresMBIDAsName(t *testing.T) {
db := database.NewTestDB(t)
si := NewSearchIndex(db, nil, nil, testLogger())
si.AddFromCache(artA, artA, []MBReleaseGroup{
{MBID: rgA, Title: "Album A", PrimaryType: "Album"},
})
var artistRows int
if err := db.QueryRowWriter(
`SELECT COUNT(*) FROM explore_index
WHERE entity_type = 'artist' AND title = mbid`,
).Scan(&artistRows); err != nil {
t.Fatalf("count artist rows: %v", err)
}
if artistRows != 0 {
t.Errorf("%d artist rows stored the MBID as their title", artistRows)
}
var artistName string
if err := db.QueryRowWriter(
`SELECT artist_name FROM explore_index WHERE mbid = ?`, dbMBID(rgA),
).Scan(&artistName); err != nil {
t.Fatalf("read release group: %v", err)
}
if artistName == artA {
t.Error("release group stored the artist MBID as its artist_name")
}
// A real name arriving later must still win over the empty one.
si.AddFromCache("Real Artist", artA, []MBReleaseGroup{
{MBID: rgA, Title: "Album A", PrimaryType: "Album"},
})
if err := db.QueryRowWriter(
`SELECT artist_name FROM explore_index WHERE mbid = ?`, dbMBID(rgA),
).Scan(&artistName); err != nil {
t.Fatalf("re-read release group: %v", err)
}
if artistName != "Real Artist" {
t.Errorf("artist_name = %q, want it filled in once known", artistName)
}
}
// The importer names the artifact's columns independently of the
// exporter that writes them. If the two lists drift, the merge either
// fails outright or silently shifts values into the wrong columns, so
// they are compared directly against cmd/indexexport's source.
func TestArtifactColumnsMatchExporter(t *testing.T) {
src, err := os.ReadFile("../../cmd/indexexport/main.go")
if err != nil {
t.Fatalf("read exporter: %v", err)
}
const marker = "const catalogColumns = `"
i := strings.Index(string(src), marker)
if i < 0 {
t.Fatalf("catalogColumns not found in cmd/indexexport/main.go")
}
rest := string(src)[i+len(marker):]
j := strings.Index(rest, "`")
if j < 0 {
t.Fatal("unterminated catalogColumns literal")
}
normalise := func(s string) string {
out := make([]string, 0, 32)
for _, f := range strings.Split(s, ",") {
out = append(out, strings.Join(strings.Fields(f), ""))
}
return strings.Join(out, ",")
}
exporter := normalise(rest[:j])
importer := normalise(artifactCatalogColumns)
if exporter != importer {
t.Errorf("column lists have drifted:\n exporter: %s\n importer: %s",
exporter, importer)
}
}
// TestImportCoreArtifactAcceptsBothEncodings is the compatibility half
// of the storage change.
//
// The catalog stores an MBID as 16 raw bytes and an entity type as a
// code, which took the table and its indexes from 677 MB to 389 MB. A
// published artifact carries whichever form the exporter that built it
// used, and there is one already out there in the older text form — so
// the importer decides by asking the artifact, not by trusting a
// version number, and both must land identically.
func TestImportCoreArtifactAcceptsBothEncodings(t *testing.T) {
compact := filepath.Join(t.TempDir(), "core-index.db")
db, err := sql.Open("sqlite", "file:"+compact)
if err != nil {
t.Fatalf("open artifact: %v", err)
}
if _, err := db.Exec(`CREATE TABLE explore_index (
entity_type INTEGER NOT NULL,
mbid BLOB NOT NULL,
title TEXT NOT NULL,
artist_name TEXT NOT NULL,
artist_mbid BLOB NOT NULL,
aliases TEXT NOT NULL DEFAULT '',
popularity INTEGER NOT NULL DEFAULT 0,
listener_count INTEGER NOT NULL DEFAULT 0,
duration INTEGER NOT NULL DEFAULT 0,
caa_release_mbid BLOB NOT NULL DEFAULT x'',
release_name TEXT NOT NULL DEFAULT '',
primary_type TEXT NOT NULL DEFAULT '',
secondary_types TEXT NOT NULL DEFAULT '',
release_date TEXT NOT NULL DEFAULT '',
artist_type TEXT NOT NULL DEFAULT '',
country TEXT NOT NULL DEFAULT '',
disambiguation TEXT NOT NULL DEFAULT '',
sort_name TEXT NOT NULL DEFAULT '',
discog_fetched INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (mbid)
)`); err != nil {
t.Fatalf("create artifact table: %v", err)
}
if _, err := db.Exec(
`CREATE TABLE artifact_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)`,
); err != nil {
t.Fatalf("create artifact meta: %v", err)
}
for k, v := range validMeta() {
if _, err := db.Exec(
"INSERT INTO artifact_meta (key, value) VALUES (?, ?)", k, v,
); err != nil {
t.Fatalf("write artifact meta: %v", err)
}
}
if _, err := db.Exec(`
INSERT INTO explore_index (entity_type, mbid, title, artist_name, artist_mbid, popularity)
VALUES (1, ?, 'Artist A', 'Artist A', ?, 5000)`,
mbidBytes(artA), mbidBytes(artA),
); err != nil {
t.Fatalf("write artifact row: %v", err)
}
_ = db.Close()
live := database.NewTestDB(t)
si := NewSearchIndex(live, nil, nil, testLogger())
if err := si.importCoreArtifact(context.Background(), compact); err != nil {
t.Fatalf("importCoreArtifact (compact): %v", err)
}
got := si.LookupArtistByMBID(artA)
if got == nil {
t.Fatal("artist from a compact artifact was not imported")
}
if got.Title != "Artist A" || got.MBID != artA {
t.Errorf("imported %+v, want Artist A / %s", got, artA)
}
}
// TestImportCoreArtifactReadsTotalsWhenPresent covers both halves of the
// denominator's arrival: an artifact that carries total_tracks imports
// it, and one built before the column existed still imports at all.
//
// The second half is the one worth a test. Adding a column to the
// importer's SELECT list is how you break every artifact already
// published - "no such column: total_tracks", on a file nobody can
// re-cut retroactively - so the importer asks the artifact what it has,
// the same way it asks which encoding it uses.
func TestImportCoreArtifactReadsTotalsWhenPresent(t *testing.T) {
path := writeTestArtifact(t, validMeta(), []artifactRow{
{"release_group", rgA, "Big Album", "Solo Star", artA, 5000},
})
// The exporter writes the column; writeTestArtifact builds the older
// shape, so add it here rather than changing every other test's
// fixture to carry a value they do not use.
artifact, err := sql.Open("sqlite", "file:"+path)
if err != nil {
t.Fatalf("reopen artifact: %v", err)
}
for _, stmt := range []string{
`ALTER TABLE explore_index ADD COLUMN total_tracks INTEGER NOT NULL DEFAULT 0`,
`UPDATE explore_index SET total_tracks = 12`,
} {
if _, err := artifact.Exec(stmt); err != nil {
t.Fatalf("add total_tracks: %v", err)
}
}
_ = artifact.Close()
live := database.NewTestDB(t)
si := NewSearchIndex(live, nil, nil, testLogger())
if err := si.importCoreArtifact(context.Background(), path); err != nil {
t.Fatalf("importCoreArtifact: %v", err)
}
rg := si.LookupReleaseGroupByMBID(rgA)
if rg == nil {
t.Fatal("release group was not imported")
}
if rg.TotalTracks != 12 {
t.Errorf("TotalTracks = %d, want 12", rg.TotalTracks)
}
// And the shape that predates the column: the same import, from an
// artifact that has no total_tracks at all.
older := writeTestArtifact(t, validMeta(), []artifactRow{
{"release_group", rgB, "Duet Album", "Solo Star", artA, 4000},
})
live2 := database.NewTestDB(t)
si2 := NewSearchIndex(live2, nil, nil, testLogger())
if err := si2.importCoreArtifact(context.Background(), older); err != nil {
t.Fatalf("importCoreArtifact (no total_tracks column): %v", err)
}
old := si2.LookupReleaseGroupByMBID(rgB)
if old == nil {
t.Fatal("release group from a column-less artifact was not imported")
}
if old.TotalTracks != 0 {
t.Errorf("TotalTracks = %d, want 0 (the catalog does not say)", old.TotalTracks)
}
}
// addArtifactCredits gives an artifact file the credit tables the
// exporter now writes, so the import path can be exercised against one
// that has them.
func addArtifactCredits(t *testing.T, path string) {
t.Helper()
db, err := sql.Open("sqlite", "file:"+path)
if err != nil {
t.Fatalf("open artifact: %v", err)
}
defer func() { _ = db.Close() }()
for _, stmt := range []string{
`CREATE TABLE artist_credit_part (
credit_id INTEGER NOT NULL,
position INTEGER NOT NULL,
artist_mbid BLOB NOT NULL,
credited_name TEXT NOT NULL,
join_phrase TEXT NOT NULL DEFAULT '',
PRIMARY KEY (credit_id, position)
) WITHOUT ROWID`,
`CREATE TABLE artist_credit_ref (
mbid BLOB NOT NULL PRIMARY KEY,
credit_id INTEGER NOT NULL
) WITHOUT ROWID`,
} {
if _, err := db.Exec(stmt); err != nil {
t.Fatalf("create credit tables: %v", err)
}
}
// The packed form the catalog stores. uuid16/parseUUID live behind
// the indexbuild tag, so this file decodes for itself.
pack := func(mbid string) []byte {
raw, err := hex.DecodeString(strings.ReplaceAll(mbid, "-", ""))
if err != nil || len(raw) != 16 {
t.Fatalf("fixture MBID %q is not a UUID: %v", mbid, err)
}
return raw
}
a, b, rec := pack(artA), pack(artB), pack(recA)
for _, part := range [][]any{
{7, 0, a, "Artist A", " feat. "},
{7, 1, b, "Artist B", ""},
} {
if _, err := db.Exec(`INSERT INTO artist_credit_part
(credit_id, position, artist_mbid, credited_name, join_phrase)
VALUES (?, ?, ?, ?, ?)`, part...); err != nil {
t.Fatalf("insert part: %v", err)
}
}
if _, err := db.Exec(
"INSERT INTO artist_credit_ref (mbid, credit_id) VALUES (?, ?)", rec, 7,
); err != nil {
t.Fatalf("insert ref: %v", err)
}
}
// TestImportCoreArtifactMergesCredits is the positive half of the
// compatibility pair: an artifact that carries credits delivers them,
// rendering back to the credit string they decompose.
func TestImportCoreArtifactMergesCredits(t *testing.T) {
db := database.NewTestDB(t)
si := NewSearchIndex(db, nil, nil, testLogger())
path := writeTestArtifact(t, validMeta(), []artifactRow{
{"recording", recA, "Song A", "Artist A feat. Artist B", artA, 2000},
})
addArtifactCredits(t, path)
if err := si.importCoreArtifact(context.Background(), path); err != nil {
t.Fatalf("importCoreArtifact: %v", err)
}
rows, err := db.QueryContext(
`SELECT p.credited_name, p.join_phrase
FROM artist_credit_ref r
JOIN artist_credit_part p ON p.credit_id = r.credit_id
ORDER BY p.position`,
)
if err != nil {
t.Fatalf("query credits: %v", err)
}
defer func() { _ = rows.Close() }()
var rendered strings.Builder
for rows.Next() {
var name, join string
if err := rows.Scan(&name, &join); err != nil {
t.Fatalf("scan: %v", err)
}
rendered.WriteString(name)
rendered.WriteString(join)
}
if got := rendered.String(); got != "Artist A feat. Artist B" {
t.Errorf("rendered credit = %q, want %q", got, "Artist A feat. Artist B")
}
}
// TestImportCoreArtifactWithoutCredits is the regression that matters
// most here: an artifact published before credits existed cannot be
// re-cut retroactively, so it must import as a catalog that declines to
// answer rather than failing outright. writeTestArtifact deliberately
// builds one without the tables.
func TestImportCoreArtifactWithoutCredits(t *testing.T) {
db := database.NewTestDB(t)
si := NewSearchIndex(db, nil, nil, testLogger())
path := writeTestArtifact(t, validMeta(), []artifactRow{
{"recording", recA, "Song A", "Artist A", artA, 2000},
})
if err := si.importCoreArtifact(context.Background(), path); err != nil {
t.Fatalf("an artifact without credit tables must still import: %v", err)
}
var rows int
if err := db.QueryRowWriter(
"SELECT COUNT(*) FROM explore_index",
).Scan(&rows); err != nil {
t.Fatalf("count: %v", err)
}
if rows != 1 {
t.Errorf("catalog rows = %d, want 1", rows)
}
var refs int
if err := db.QueryRowWriter(
"SELECT COUNT(*) FROM artist_credit_ref",
).Scan(&refs); err != nil {
t.Fatalf("count refs: %v", err)
}
if refs != 0 {
t.Errorf("credit refs = %d, want 0", refs)
}
}