Files
yellowjacket/backend/library/scan_fixtures_test.go
T
yonlu 7a1412383f perf(library): send the track list as a dictionary-encoded column table
GetTracks answered with one JSON object per track: 20.5 MB at 26k
tracks, ~350 bytes a row of repeated key names, four cover URLs that
are identical across an album, and the album, artist and genre strings
repeated on every track. Encoding it was ~170 MB of transient Go
allocation, and parsing it the WebView's memory peak.

GetTrackTable replaces it: one array per column, every repeated
string stored once and sent as an index, genre lists interned, and only
the fields the Tracks view reads. LastPlayed and the three larger cover
tiers are left to the details dialog, which now fetches whole tracks by
path. Each row still goes through trackFromRow, so this is an encoding
of the one projection, not a second one. 167 bytes a track against 929
in the test library; a Go test holds it under a quarter of the object
encoding, and a Vitest test reads the Go columns and the generated
Track interface and fails if they drift.

An empty library is now an empty table rather than the "no tracks in
library" error the old binding returned.

Closes #281
2026-10-05 23:30:37 -04:00

104 lines
3.2 KiB
Go

package library
import (
"path/filepath"
"testing"
"yellowjacket/backend/database/sql/sqlcgen"
)
// TestScan_FixtureLibraryLeavesNothingBehind runs a real scan over the
// generated fixture library and asserts the invariant the file-shaped
// schema exists for: every row is a file's, and nothing outlives one.
//
// The old schema could not state this. A scan wrote a recording, an
// artist credit, a credit-artist link and a release-group link per
// file, all of which survived the file's deletion, and a real library
// accumulated 812 recordings, 216 release groups and 260 artists with
// nothing behind them - which is what made "do I own this" unanswerable.
func TestScan_FixtureLibraryLeavesNothingBehind(t *testing.T) {
t.Parallel()
lib, db := setupTestLibrary(t)
root, err := filepath.Abs("../../test_data/music_library_test")
if err != nil {
t.Fatalf("resolve fixture path: %v", err)
}
if _, err := filepath.Glob(filepath.Join(root, "*")); err != nil {
t.Skipf("fixture library not generated (make testdata): %v", err)
}
library, err := db.Queries.CreateLibrary(lib.ctx, sqlcgen.CreateLibraryParams{
Name: "Fixtures",
Path: root,
})
if err != nil {
t.Fatalf("create library: %v", err)
}
if metrics := lib.scanInternal(library.ID, library.Name, library.Path); metrics == nil {
t.Fatal("scanInternal returned nil metrics")
}
count := func(query string) int64 {
t.Helper()
var n int64
if err := db.QueryRowWriter(query).Scan(&n); err != nil {
t.Fatalf("%s: %v", query, err)
}
return n
}
files := count("SELECT COUNT(*) FROM audio_files")
if files == 0 {
t.Skip("fixture library is empty; run make testdata")
}
table, err := lib.GetTrackTable(0)
if err != nil {
t.Fatalf("GetTrackTable: %v", err)
}
// One track per file: the projection cannot multiply rows, because
// there is no join table left to multiply them.
if int64(len(table.FilePath)) != files {
t.Errorf("GetTrackTable returned %d rows for %d files", len(table.FilePath), files)
}
// Nothing shared outlives what refers to it.
for _, c := range []struct {
what string
query string
}{
{"albums with no file", `SELECT COUNT(*) FROM albums al
WHERE NOT EXISTS (SELECT 1 FROM audio_files af WHERE af.album_id = al.id)`},
{"artists nothing refers to", `SELECT COUNT(*) FROM artists a
WHERE NOT EXISTS (SELECT 1 FROM audio_files af WHERE af.artist_id = a.id)
AND NOT EXISTS (SELECT 1 FROM albums al WHERE al.artist_id = a.id)`},
{"genre links with no file", `SELECT COUNT(*) FROM file_genres fg
WHERE NOT EXISTS (SELECT 1 FROM audio_files af WHERE af.id = fg.audio_file_id)`},
} {
if n := count(c.query); n != 0 {
t.Errorf("%s = %d, want 0", c.what, n)
}
}
// And the scan actually filed things: albums, artists and genres
// all resolved, with the tags on the files that named them.
if albums, err := lib.GetAlbums(0); err != nil || len(albums) == 0 {
t.Errorf("GetAlbums = %d albums, err %v; want some", len(albums), err)
}
if artists, err := lib.GetArtists(0); err != nil || len(artists) == 0 {
t.Errorf("GetArtists = %d artists, err %v; want some", len(artists), err)
}
if genres, err := lib.GetGenres(0); err != nil || len(genres) == 0 {
t.Errorf("GetGenres = %d genres, err %v; want some", len(genres), err)
}
}