feat: play-count measurement store — identifiers, snapshots, measurements, ledger + queue
Merged
SW
4mo ago+102+102+012345678901234567890123456789-0-0-0123456789
What
Five tables for the play-count measurement store (recoupable/chat#1791), built on songs.isrc as the canonical key — same FK pattern as catalog_songs/song_artists, written in the current lean migration style (no grant boilerplate, inline constraints, RLS enabled):
song_identifiers— external platform ID ↔ ISRC mapping (platform,identifier_type,value; unique reverse lookup). The Apify actor takes Spotify album URLs in and returns Spotify track IDs out, andsongsholds no platform IDs today.playcount_snapshots— snapshot jobs: mints thesnapshot_idthatPOST /api/research/snapshots(docs#238) returns, tracksstate/schedule/album_count/estimated_cost_usd;(account, created_at)index backs the per-org monthly cap (429).song_measurements— append-only metric captures, the system of record. Unique(song, platform, metric, captured_at)(fetch-once), series index(song, platform, metric, captured_at DESC),CHECK (value >= 0), nullablesnapshotFK for lineage.data_sourcematches the published contract field. Noset_updated_attrigger: rows are never updated or deleted by application code.songstats_quota_ledger— every Songstats hit recorded;sum(hits)over the rolling 30-day window enforces the ~1,000-hit budget. NullableaccountFK +(account, spent_at)index attribute spend per org.songstats_backfill_queue— value-ranked work list (rank_score= all-time count,CHECK >= 0), drain index(status, rank_score DESC)forFOR UPDATE SKIP LOCKEDclaims.
Review changes (0d741cb)
All review comments addressed: grants removed (KISS, per review), quota ledger gained account attribution (per review), cubic's two CHECK constraints applied, immutability comment reconciled. Branch merged up past database#31.
Merge order
Last database step of the #1791 train (cleanup ✅ → docs ✅ #238 → database → api). The api PRs depend on this (regenerated database.types.ts).
🤖 Generated with Claude Code