From 2ca09749d90c7e2e1a2396e2d198030e5f05e899 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 1 May 2026 05:50:22 -0400 Subject: [PATCH] feat(db): add artist_similarity_unmatched schema (migration 0012) --- internal/db/dbq/models.go | 9 ++++++ internal/db/dbq/similarity.sql.go | 32 +++++++++++++++++++ .../0012_artist_similarity_unmatched.down.sql | 2 ++ .../0012_artist_similarity_unmatched.up.sql | 17 ++++++++++ internal/db/queries/similarity.sql | 12 +++++++ internal/dbtest/reset.go | 1 + 6 files changed, 73 insertions(+) create mode 100644 internal/db/migrations/0012_artist_similarity_unmatched.down.sql create mode 100644 internal/db/migrations/0012_artist_similarity_unmatched.up.sql diff --git a/internal/db/dbq/models.go b/internal/db/dbq/models.go index 052bf95f..be014e20 100644 --- a/internal/db/dbq/models.go +++ b/internal/db/dbq/models.go @@ -216,6 +216,15 @@ type ArtistSimilarity struct { FetchedAt pgtype.Timestamptz } +type ArtistSimilarityUnmatched struct { + SeedArtistID pgtype.UUID + CandidateMbid string + CandidateName string + Score float64 + Source string + FetchedAt pgtype.Timestamptz +} + type ContextualLike struct { ID pgtype.UUID UserID pgtype.UUID diff --git a/internal/db/dbq/similarity.sql.go b/internal/db/dbq/similarity.sql.go index 2673985c..5fff165f 100644 --- a/internal/db/dbq/similarity.sql.go +++ b/internal/db/dbq/similarity.sql.go @@ -172,6 +172,38 @@ func (q *Queries) UpsertArtistSimilarity(ctx context.Context, arg UpsertArtistSi return err } +const upsertArtistSimilarityUnmatched = `-- name: UpsertArtistSimilarityUnmatched :exec +INSERT INTO artist_similarity_unmatched ( + seed_artist_id, candidate_mbid, candidate_name, score, source +) VALUES ($1, $2, $3, $4, $5) +ON CONFLICT (seed_artist_id, candidate_mbid, source) DO UPDATE SET + candidate_name = EXCLUDED.candidate_name, + score = EXCLUDED.score, + fetched_at = now() +` + +type UpsertArtistSimilarityUnmatchedParams struct { + SeedArtistID pgtype.UUID + CandidateMbid string + CandidateName string + Score float64 + Source string +} + +// Persists an out-of-library similar-artist MBID. Idempotent on +// (seed_artist_id, candidate_mbid, source) — re-fetches refresh the +// name/score and bump fetched_at. +func (q *Queries) UpsertArtistSimilarityUnmatched(ctx context.Context, arg UpsertArtistSimilarityUnmatchedParams) error { + _, err := q.db.Exec(ctx, upsertArtistSimilarityUnmatched, + arg.SeedArtistID, + arg.CandidateMbid, + arg.CandidateName, + arg.Score, + arg.Source, + ) + return err +} + const upsertTrackSimilarity = `-- name: UpsertTrackSimilarity :exec INSERT INTO track_similarity (track_a_id, track_b_id, score, source, fetched_at) VALUES ($1, $2, $3, 'listenbrainz', now()) diff --git a/internal/db/migrations/0012_artist_similarity_unmatched.down.sql b/internal/db/migrations/0012_artist_similarity_unmatched.down.sql new file mode 100644 index 00000000..29d52c9e --- /dev/null +++ b/internal/db/migrations/0012_artist_similarity_unmatched.down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS artist_similarity_unmatched_seed_score_idx; +DROP TABLE IF EXISTS artist_similarity_unmatched; diff --git a/internal/db/migrations/0012_artist_similarity_unmatched.up.sql b/internal/db/migrations/0012_artist_similarity_unmatched.up.sql new file mode 100644 index 00000000..0d7cf1cb --- /dev/null +++ b/internal/db/migrations/0012_artist_similarity_unmatched.up.sql @@ -0,0 +1,17 @@ +-- M5c: persist unmatched-similar-artist MBIDs that the M4b worker would +-- otherwise discard. Mirrors artist_similarity shape: same composite PK +-- with source, same (seed_id, score DESC) index, same source enum check. +-- The candidate side is text + name (no FK) — that's the whole point. + +CREATE TABLE artist_similarity_unmatched ( + seed_artist_id uuid NOT NULL REFERENCES artists(id) ON DELETE CASCADE, + candidate_mbid text NOT NULL, + candidate_name text NOT NULL, + score DOUBLE PRECISION NOT NULL, + source text NOT NULL CHECK (source IN ('listenbrainz', 'musicbrainz_tag', 'user_cooccurrence')), + fetched_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (seed_artist_id, candidate_mbid, source) +); + +CREATE INDEX artist_similarity_unmatched_seed_score_idx + ON artist_similarity_unmatched (seed_artist_id, score DESC); diff --git a/internal/db/queries/similarity.sql b/internal/db/queries/similarity.sql index 15a76b58..c9688c00 100644 --- a/internal/db/queries/similarity.sql +++ b/internal/db/queries/similarity.sql @@ -48,3 +48,15 @@ INSERT INTO artist_similarity (artist_a_id, artist_b_id, score, source, fetched_ VALUES ($1, $2, $3, 'listenbrainz', now()) ON CONFLICT (artist_a_id, artist_b_id, source) DO UPDATE SET score = EXCLUDED.score, fetched_at = EXCLUDED.fetched_at; + +-- name: UpsertArtistSimilarityUnmatched :exec +-- Persists an out-of-library similar-artist MBID. Idempotent on +-- (seed_artist_id, candidate_mbid, source) — re-fetches refresh the +-- name/score and bump fetched_at. +INSERT INTO artist_similarity_unmatched ( + seed_artist_id, candidate_mbid, candidate_name, score, source +) VALUES ($1, $2, $3, $4, $5) +ON CONFLICT (seed_artist_id, candidate_mbid, source) DO UPDATE SET + candidate_name = EXCLUDED.candidate_name, + score = EXCLUDED.score, + fetched_at = now(); diff --git a/internal/dbtest/reset.go b/internal/dbtest/reset.go index 65c72225..3395d19a 100644 --- a/internal/dbtest/reset.go +++ b/internal/dbtest/reset.go @@ -40,6 +40,7 @@ const TestUserPrefix = "test-" var dataTables = []string{ "artist_similarity", "track_similarity", + "artist_similarity_unmatched", // M5c "scrobble_queue", "contextual_likes", "general_likes_albums",