Files
yellowjacket/.planning/milestones/v1.0-phases/05-database-library-tests/05-01-PLAN.md
T
yonlu 6ce0661fca chore: complete v1.0 Consolidation milestone
Archive milestone artifacts:
- milestones/v1.0-ROADMAP.md (full roadmap archive)
- milestones/v1.0-REQUIREMENTS.md (26/26 requirements complete)
- milestones/v1.0-phases/ (8 phase directories with plans, summaries, verifications)

Updated:
- PROJECT.md: full evolution review, all consolidation requirements validated
- ROADMAP.md: collapsed to milestone summary with archive link
- STATE.md: reset for next milestone
- MILESTONES.md: created with stats and accomplishments
- RETROSPECTIVE.md: created with lessons learned

Deleted:
- REQUIREMENTS.md (archived, fresh for next milestone)

8 phases, 17 plans, 34 tasks, 84 tests added, 6 days
2026-03-05 09:34:43 -05:00

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
05-database-library-tests 01 execute 1
backend/database/search_test.go
true
TEST-03
truths artifacts key_links
SearchFTS returns correct results for basic term queries
SearchFTS returns nil for empty queries
SearchFTS handles special characters (quotes, slashes like AC/DC) without error
SearchFTS multi-word queries match across title/artist/album columns
SearchFTSByFilename scopes search to file_path column only
SearchFTSTracks returns full 16-column track metadata
FTS5 search ranking produces consistent BM25 ordering for known data
Diacritics search works (Beyonce finds Beyoncé)
RebuildSearchIndex repopulates the index from audio_files data
tokeniseForFTS and buildFTSQuery produce correct FTS5 query syntax
Schema migrations run successfully on a fresh database
All tests pass with -race flag
path provides min_lines
backend/database/search_test.go FTS5 search tests, pure helper tests, migration tests, rebuild tests 300
from to via pattern
backend/database/search_test.go backend/database/search.go direct function calls (same package) SearchFTS|SearchFTSByFilename|SearchFTSTracks|tokeniseForFTS|buildFTSQuery|stripExtForSearch
from to via pattern
backend/database/search_test.go backend/database/testhelper.go NewTestDB(t) NewTestDB
Write unit tests for the database package covering FTS5 search queries (SearchFTS, SearchFTSByFilename, SearchFTSTracks), pure helper functions (tokeniseForFTS, buildFTSQuery, stripExtForSearch), search index operations (InsertSearchIndex, DeleteSearchIndex, ClearSearchIndex, RebuildSearchIndex), and schema migration verification.

Purpose: Lock down FTS5 search behavior before Phase 6's VIEW consolidation — these tests become the safety net that proves the VIEW doesn't break search ranking or result mapping. Output: backend/database/search_test.go with ~12-15 tests, all passing with -race.

<execution_context> @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/05-database-library-tests/05-CONTEXT.md @.planning/phases/03-test-infrastructure/03-01-SUMMARY.md @.planning/phases/04-queue-config-player-tests/04-01-SUMMARY.md

From backend/database/database.go:

type DB struct {
	db      *sql.DB
	Ctx     context.Context
	Queries *sqlcgen.Queries
	logger  *slog.Logger
}

func (d *DB) ExecContext(query string, args ...any) (sql.Result, error)
func (d *DB) QueryContext(query string, args ...any) (*sql.Rows, error)
func (d *DB) BeginTx() (*sql.Tx, error)

From backend/database/testhelper.go:

func NewTestDB(t *testing.T) *DB

From backend/database/search.go:

type SearchRow struct {
	FilePath           string
	LengthMilliseconds int64
	Title              string
	Artist             string
	Album              string
}

type SearchTrackRow struct {
	FilePath           string
	LengthMilliseconds int64
	Title              string
	ArtistName         string
	TrackNumber        sql.NullInt64
	DiscNumber         sql.NullInt64
	Album              string
	Genre              string
	Year               int64
	Composer           string
	FileType           string
	SampleRate         int64
	BitDepth           int64
	Channels           int64
	Bitrate            int64
	FileSize           int64
}

func (d *DB) SearchFTS(query string, limit int) ([]SearchRow, error)
func (d *DB) SearchFTSByFilename(basename string, limit int) ([]SearchRow, error)
func (d *DB) SearchFTSTracks(query string, limit int) ([]SearchTrackRow, error)
func (d *DB) InsertSearchIndex(rowid int64, filePath, title, artist, album string) error
func (d *DB) DeleteSearchIndex(rowid int64) error
func (d *DB) ClearSearchIndex() error
func (d *DB) RebuildSearchIndex() error

// Unexported (same package, accessible in tests):
func buildFTSQuery(query string) string
func tokeniseForFTS(s string) []string
func stripExtForSearch(s string) string

SQL schema — search_index (FTS5 contentless table):

CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
    file_path, title, artist, album,
    content='',
    tokenize='unicode61 remove_diacritics 2'
);

SQL schema — audio_files:

CREATE TABLE IF NOT EXISTS audio_files (
  id integer PRIMARY KEY,
  file_path text NOT NULL UNIQUE,
  length_milliseconds int NOT NULL,
  file_type_id int NOT NULL,
  recording_id int NOT NULL,
  sample_rate int NOT NULL DEFAULT 0,
  bit_depth int NOT NULL DEFAULT 0,
  channels int NOT NULL DEFAULT 0,
  bitrate int NOT NULL DEFAULT 0,
  file_size int NOT NULL DEFAULT 0,
  basename text NOT NULL DEFAULT '',
  FOREIGN KEY(file_type_id) REFERENCES file_types(id),
  FOREIGN KEY(recording_id) REFERENCES recordings(id)
);

SQL schema — recordings:

CREATE TABLE IF NOT EXISTS recordings (
  id INTEGER PRIMARY KEY, name TEXT NOT NULL,
  artist_credit_id INTEGER NOT NULL, track_number INTEGER,
  disc_number INTEGER, year INTEGER, genre TEXT, composer TEXT,
  lyrics TEXT, comment TEXT,
  FOREIGN KEY(artist_credit_id) REFERENCES artist_credit(id)
);

SQL schema — artist_credit:

CREATE TABLE IF NOT EXISTS artist_credit (id INTEGER PRIMARY KEY, text TEXT NOT NULL UNIQUE);

SQL schema — release_groups:

CREATE TABLE IF NOT EXISTS release_groups (
  id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE,
  cover_art_id INTEGER, album_artist_credit_id INTEGER,
  year INTEGER, total_tracks INTEGER, total_discs INTEGER,
  FOREIGN KEY(cover_art_id) REFERENCES cover_art(id),
  FOREIGN KEY(album_artist_credit_id) REFERENCES artist_credit(id)
);

SQL schema — release_group_recordings:

CREATE TABLE IF NOT EXISTS release_group_recordings (
  id INTEGER PRIMARY KEY, release_group_id INTEGER NOT NULL,
  recording_id INTEGER NOT NULL, track_number INTEGER, disc_number INTEGER,
  FOREIGN KEY(release_group_id) REFERENCES release_groups(id),
  FOREIGN KEY(recording_id) REFERENCES recordings(id)
);

Existing test pattern from queue package (seedAudioFiles):

// Creates FK chain: artist_credit → recordings → audio_files
_, err := db.ExecContext(
    "INSERT OR IGNORE INTO artist_credit (id, text) VALUES (1, 'Test Artist')",
)
_, err = db.ExecContext(
    "INSERT OR IGNORE INTO recordings (id, name, artist_credit_id) VALUES (?, ?, 1)",
    recID, fmt.Sprintf("Track %d", i+1),
)
_, err = db.ExecContext(
    "INSERT OR IGNORE INTO audio_files (id, file_path, length_milliseconds, file_type_id, recording_id) VALUES (?, ?, 180000, 0, ?)",
    afID, fp, recID,
)
Task 1: Pure helper function tests + seed helper backend/database/search_test.go Create `backend/database/search_test.go` (package database — internal tests, access unexported functions).

Seed helper function: Create seedSearchData(t *testing.T, db *DB) that inserts ~6-8 tracks with the full FK chain needed for FTS5 search:

  • artist_credit rows (e.g., "Queen", "Beyoncé", "AC/DC", "Pink Floyd")
  • recordings with varied metadata (title, track_number, disc_number, year, genre, composer)
  • audio_files with file_path, length_milliseconds, file_type_id=0, recording_id
  • release_groups with album names (e.g., "A Night at the Opera", "Lemonade", "Back in Black", "The Dark Side of the Moon")
  • release_group_recordings linking recordings to release_groups
  • search_index entries via InsertSearchIndex() for each audio file (rowid must match audio_files.id)

Use realistic music metadata per CONTEXT.md decision: "Bohemian Rhapsody" by "Queen" on "A Night at the Opera", "Halo" by "Beyoncé" on "Lemonade", "Back in Black" by "AC/DC" on "Back in Black", "Comfortably Numb" by "Pink Floyd" on "The Dark Side of the Moon", "Another One Bites the Dust" by "Queen" on "The Game", etc.

Pure helper tests (no DB needed):

  1. TestTokeniseForFTS — table-driven subtests:

    • Simple word: "hello" → ["\"hello\""]
    • Multiple words: "hello world" → ["\"hello\"" "\"world\""]
    • Hyphens split: "rock-pop" → ["\"rock\"" "\"pop\""]
    • Slashes split: "AC/DC" → ["\"AC\"" "\"DC\""]
    • Dots split: "01.track" → ["\"01\"" "\"track\""]
    • Underscores split: "my_song" → ["\"my\"" "\"song\""]
    • Double quotes escaped: he"llo["\"he\"\"llo\""]
    • Empty string: "" → nil or empty slice
    • Only separators: "---" → nil or empty slice
  2. TestBuildFTSQuery — table-driven subtests:

    • Single word: "queen" → "\"queen\""
    • Multi-word: "bohemian rhapsody" → "\"bohemian\" \"rhapsody\""
    • Empty string returns the original (empty)
  3. TestStripExtForSearch — table-driven subtests:

    • "song.mp3" → "song"
    • "my.song.flac" → "my.song"
    • "noextension" → "noextension"
    • ".hidden" → ".hidden" (dot at position 0 is not stripped)

Follow established patterns: t.Parallel(), t.Run() subtests, standard library testing (no testify), TestFunctionName_Scenario naming convention. cd backend && go test -race -run "TestTokeniseForFTS|TestBuildFTSQuery|TestStripExtForSearch|seedSearchData" ./database/ -v -count=1 Pure helper tests pass: tokeniseForFTS handles all separator types and quote escaping, buildFTSQuery produces correct FTS5 syntax, stripExtForSearch handles edge cases. seedSearchData helper function creates full entity graph for search tests.

Task 2: FTS5 search + index operation + migration tests backend/database/search_test.go Add to the existing `backend/database/search_test.go` file created in Task 1.

FTS5 Search tests (use seedSearchData + NewTestDB):

  1. TestSearchFTS_BasicTerm — search for "queen", verify returns "Bohemian Rhapsody" and "Another One Bites the Dust" (both Queen tracks). Assert len >= 2, check FilePath and Title fields.

  2. TestSearchFTS_EmptyQuery — search for "", verify returns nil (not an error). Also test whitespace-only " ".

  3. TestSearchFTS_SpecialCharacters — search for "AC/DC", verify returns the AC/DC track. The tokeniser splits on /, so "AC" and "DC" both match. Also test a query with double quotes.

  4. TestSearchFTS_MultiWord — search for "bohemian rhapsody", verify returns the Queen track as top result. Multi-word queries use implicit AND.

  5. TestSearchFTS_Diacritics — search for "Beyonce" (no accent), verify returns the Beyoncé track. This tests unicode61 remove_diacritics 2 tokeniser config.

  6. TestSearchFTS_Ranking — seed data with specific artist/title combos where one track should rank higher. Search a term that appears in both title and artist of one track vs. only artist of another. Assert the more-relevant result comes first (lower BM25 rank = first). Use exact result ordering assertion per CONTEXT.md decision.

  7. TestSearchFTSByFilename — search by basename "bohemian_rhapsody.mp3", verify matches. The search strips extension and scopes to file_path column. Also test empty basename returns nil.

  8. TestSearchFTSTracks — search for "queen", verify returns SearchTrackRow with all 16 fields populated (FilePath, LengthMilliseconds, Title, ArtistName, TrackNumber, DiscNumber, Album, Genre, Year, Composer, FileType, SampleRate, BitDepth, Channels, Bitrate, FileSize). This is the safety net for the full-metadata search path.

Search index operation tests:

  1. TestInsertAndDeleteSearchIndex — insert a search_index entry, verify SearchFTS finds it, delete it, verify SearchFTS no longer finds it.

  2. TestRebuildSearchIndex — seed audio_files + recordings + artist_credit + release_groups + release_group_recordings (without search_index entries), call RebuildSearchIndex(), verify SearchFTS now returns results.

  3. TestClearSearchIndex — seed search data, call ClearSearchIndex(), verify SearchFTS returns empty.

Migration test:

  1. TestMigrationsApplied — call NewTestDB(t), verify user_version PRAGMA is >= 3 (all 3 migrations applied). Verify the artist_credit_artist UNIQUE index exists by attempting a duplicate insert and checking for UNIQUE violation error.

Each test gets its own NewTestDB(t) call + seedSearchData(t, db) where needed. Use t.Parallel() for all tests. Follow established Phase 4 patterns (table-driven subtests where appropriate, descriptive assertions with t.Errorf). cd backend && go test -race ./database/ -v -count=1 12+ database tests pass with -race: FTS5 search works for basic terms, empty queries, special characters (AC/DC), multi-word, diacritics (Beyonce→Beyoncé), ranking order is deterministic. SearchFTSByFilename scopes to file_path column. SearchFTSTracks returns full 16-column metadata. Insert/Delete/Clear/Rebuild index operations work correctly. Migrations verified applied.

```bash # All database package tests pass with race detector cd backend && go test -race ./database/ -v -count=1

Verify test count is in target range (10-15)

cd backend && go test ./database/ -v -count=1 2>&1 | grep -c "=== RUN"

</verification>

<success_criteria>
- backend/database/search_test.go exists with 12-15 tests
- All search functions tested independently: SearchFTS, SearchFTSByFilename, SearchFTSTracks
- Pure helpers tested: tokeniseForFTS, buildFTSQuery, stripExtForSearch
- Index operations tested: InsertSearchIndex, DeleteSearchIndex, ClearSearchIndex, RebuildSearchIndex
- Diacritics behavior verified (Beyonce → Beyoncé)
- Special characters handled (AC/DC, quotes)
- Search ranking produces consistent ordering
- Migrations verified (user_version >= 3, UNIQUE index works)
- All tests pass with `go test -race`
</success_criteria>

<output>
After completion, create `.planning/phases/05-database-library-tests/05-01-SUMMARY.md`
</output>