Skip to content

Add lyric-line service with line-centric model and song notes - #65

Merged
RebelliousSmile merged 5 commits into
mainfrom
claude/db-lyric-line-structure-7vkcup
Jul 30, 2026
Merged

Add lyric-line service with line-centric model and song notes#65
RebelliousSmile merged 5 commits into
mainfrom
claude/db-lyric-line-structure-7vkcup

Conversation

@RebelliousSmile

Copy link
Copy Markdown
Owner

Summary

Introduces a new line-centric data model for songs, replacing the legacy parallel-array structure with normalized lyric_lines and lyric_line_translations tables. The service lazily backfills lines from the legacy arrays on first read, guarded by a migration flag to prevent resurrection of deleted lines. Also adds support for free-form song notes.

Key Changes

Backend (Rust)

  • New lyric_lines service module with full CRUD operations for lines and translations
    • get_song_lines() — fetch all lines of a song (ordered, with translations)
    • create_line(), update_line(), delete_line() — line lifecycle
    • reorder_lines() — reorder a song's lines with atomic position updates
    • set_line_translation(), remove_line_translation() — manage per-language translations
    • get_song_notes(), create_note(), update_note(), delete_note() — song notes CRUD
  • New lyric_line model with LyricLine, LineKind (lyric/silence), LineTranslation, and SongNote types
  • Database schema additions:
    • lyric_lines table (id, song_id, position, kind, content, phonetic, singers, timestamps)
    • lyric_line_translations table (line_id, lang, text, phonetic)
    • song_notes table (id, song_id, position, content, timestamps)
    • lines_migrated flag on songs table to guard backfill idempotency
    • Unique index on (song_id, position) to enforce contiguous line ordering
  • Backfill logic (ensure_backfilled()) explodes legacy lyrics, phonetic_lyrics, translations, and line_singers arrays into normalized rows on first read
  • Position reindexing (reindex()) uses two-pass negative offset to avoid unique constraint violations during reordering
  • Comprehensive test suite covering backfill, CRUD, reordering, translations, and notes

Frontend (TypeScript/Tauri)

  • New Tauri command bindings for all line and note operations
  • TypeScript types: LineKind, LineTranslation, LyricLine, SongNote
  • API wrapper functions in tauri-api.ts for lines and notes
  • Type exports in types/index.ts

Tauri App Integration

  • Registered all new commands in lib.rs

Implementation Details

  • Lazy backfill: Lines are created from legacy arrays only when first accessed via get_song_lines(), minimizing migration overhead
  • Idempotency: The lines_migrated flag ensures backfill runs exactly once per song; subsequent reads skip the migration
  • Position safety: Reordering uses a two-pass algorithm (park at negatives, then assign finals) to avoid transient unique constraint violations
  • Cascading deletes: Foreign keys with ON DELETE CASCADE ensure translations and notes are cleaned up when lines or songs are deleted
  • Partial updates: UpdateLineData uses Option<T> to allow selective field updates without overwriting unspecified fields
  • Translations as child table: Allows songs to gain new languages without schema changes; each line can have translations in any language

https://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG

claude added 5 commits July 29, 2026 22:42
Introduce the lyric line as a first-class, ordered, multilingual,
singer-attributed unit, layered additively over the existing songs row.

Schema (rust-backend/src/db/sqlite.rs):
- lyric_lines(id, song_id, position, kind, content, phonetic, singers)
  with kind = 'lyric' | 'silence' and a UNIQUE(song_id, position) index
- lyric_line_translations(line_id, lang, text, phonetic) — one row per
  (line, language), so a song gains a language without a schema change
- song_notes(id, song_id, position, content) — free-form notes attached
  to the whole song, outside the lyric sequence
- songs.lines_migrated flag + idempotent migration

Model (models/lyric_line.rs): LyricLine, LineKind, LineTranslation,
SongNote, CreateLineData, UpdateLineData.

Service (services/lyric_lines.rs): lazy backfill from the legacy parallel
arrays on first read (guarded by lines_migrated so deleted lines are not
resurrected), line CRUD with position re-packing, safe two-pass reorder,
per-language translation upsert/remove, and song-note CRUD. 12 tests.

Tauri commands + frontend TS bindings (types + invoke wrappers) added.

The songs parallel arrays remain the current source of truth; migrating
the practice UI to LyricLine and dropping the redundant columns is a
later phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG
Frontend (CI Frontend job was failing at `npm ci`):
- Regenerate package-lock.json — it predated vitest and most
  devDependencies, so `npm ci` aborted on missing lockfile entries.
- Add the `test:unit` script the workflow invokes (alias of `vitest run`);
  it was referenced by ci-frontend.yml but absent from package.json.

Backend (CI Rust `cargo test` step):
- Gate `test_generate_phonetic_returns_original_in_stub` and
  `test_create_song_supported_language_populates_phonetic_lyrics` behind
  `#[cfg(not(feature = "python"))]`. Both assert the stub behaviour
  (original text echoed); under the default `python` feature the backend
  romanizes (or errors when the Python packages are absent), so they only
  hold for the no-python build. The python path stays covered by the
  `#[ignore]`d tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG
Phase 3 (partial): make the line model the source of truth on the read
path, without changing behaviour for existing (backfilled) songs.

- useLinesStore (stores/lines.ts): fetch (TTL cache) + full mutations for
  lines, translations and notes over the cmd_*_line / cmd_*_note commands.
  12 vitest specs.
- projectLinesToSong (lib/line-projection.ts): pure, tested bridge that
  renders a song's lines back into the legacy parallel-array Song shape the
  practice modes still expect — silences excluded, line edits reflected. 8
  vitest specs.
- SongDetailView: fetch lines + notes on mount; feed the practice modes
  (Karaoke/FillBlank/Mcq/Oral) the projected song so their data now flows
  from the line store; render a Notes section. The mode components are left
  untouched.

Remaining tail (not in this commit): migrate the standalone practice views
(PracticeOralView/QuizView/…, separate inline implementations) to the line
store, then drop the redundant songs parallel columns once every consumer
reads lines. Both need runtime QA of the desktop app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG
Migrate the four standalone practice views (Oral/Quiz/Karaoke/FillBlanks)
off `song.lyrics` and onto the line store: each now fetches the song's
lines on mount and derives its `lyrics` list from `linesStore.lyricLinesFor`
(silences excluded). Behaviour is unchanged for existing songs — backfilled
lines reproduce the original lyric lines exactly — and silences added via
the line API are now correctly skipped in practice.

With this, every practice surface (both the mode components inside
SongDetailView and these standalone views) sources its sequence from the
line model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG
…umns

Complete the line-centric migration: the lyric sequence now lives only in
lyric_lines / lyric_line_translations. The redundant songs columns
(lyrics, phonetic_lyrics, translations, line_singers) are dropped.

- Migration (sqlite.rs): explode every un-migrated song's legacy arrays into
  lines, then ALTER TABLE ... DROP COLUMN the four parallel columns. Guarded
  by a column-existence check so it's a no-op once applied. `songs.singers`
  (the song's singer roster) is kept — it is not per-line data.
- songs service: save_song / update_song persist the sequence via
  lyric_lines::write_song_lines; get_song(s) reconstruct the parallel arrays
  from lines via load_song_arrays + hydrate. The Song API shape is unchanged,
  so the Tauri commands and the whole frontend are unaffected.
- lyric_lines: ensure_backfilled is now a plain existence check (no lazy
  column read); write_song_lines (Song → lines) and load_song_arrays
  (lines → Song arrays) are the round-trip bridge.

The full backend suite (151 tests, incl. a new legacy-migration test that
backfills then drops columns and checks idempotence) passes with the default
`python` feature; clippy -D warnings is clean.

Caveat: a legacy song update rebuilds the whole line set, so silences added
via the line API are not preserved across a cmd_update_song call. No UI adds
silences through that path today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG
@RebelliousSmile
RebelliousSmile merged commit 5885bfe into main Jul 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants