From 7315e37c15955992343201c5e39d9bc87a52d656 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 2 Aug 2026 20:11:03 -0400 Subject: [PATCH] =?UTF-8?q?fix(db):=20apply=20sqlc's=20actual=20output=20f?= =?UTF-8?q?or=20candidate=5Fartist=5Ftags=20=E2=80=94=20#2376?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three divergences in the hand-written generated file, all caught by verify-generate on the first run. Two are sqlc rules I had wrong: 1. When a query's SELECT list exactly matches a table's columns in order, sqlc REUSES the model struct rather than emitting a bespoke Row type. So ListCandidateArtistTagsForMbids returns []CandidateArtistTag, and ListCandidateArtistTagsForMbidsRow should never have existed. 2. models.go is ordered by GO STRUCT NAME, not table name. Table order would put candidate_artist_tag_state before candidate_artist_tags; sqlc emits CandidateArtistTag before CandidateArtistTagState. The earlier slice-3 observation ("ordered by table name") was consistent with both orderings and so never discriminated — this case does. 3. sqlc smart-quotes a doubled '' inside a promoted comment into a typographic ”. Reworded the prose to say "the empty string" instead of encoding a mangling into the source. Note the integration lane PASSED on the broken push while this failed. That is #2380's lesson landing again, and the reason the check exists: valid SQL executing against real Postgres proves nothing about whether the committed Go matches its source. Co-Authored-By: Claude Opus 5 (1M context) --- internal/db/dbq/candidate_artist_tags.sql.go | 21 +++++++------------ internal/db/dbq/models.go | 12 +++++------ internal/db/queries/candidate_artist_tags.sql | 9 ++++---- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/internal/db/dbq/candidate_artist_tags.sql.go b/internal/db/dbq/candidate_artist_tags.sql.go index 29ec71a9..a733e4a2 100644 --- a/internal/db/dbq/candidate_artist_tags.sql.go +++ b/internal/db/dbq/candidate_artist_tags.sql.go @@ -110,23 +110,17 @@ SELECT candidate_mbid, tag, weight WHERE candidate_mbid = ANY($1::text[]) ` -type ListCandidateArtistTagsForMbidsRow struct { - CandidateMbid string - Tag string - Weight float64 -} - // Cached tags for a set of candidates, for slice 6's taste-overlap ranking. // One row per (candidate, tag). -func (q *Queries) ListCandidateArtistTagsForMbids(ctx context.Context, dollar_1 []string) ([]ListCandidateArtistTagsForMbidsRow, error) { +func (q *Queries) ListCandidateArtistTagsForMbids(ctx context.Context, dollar_1 []string) ([]CandidateArtistTag, error) { rows, err := q.db.Query(ctx, listCandidateArtistTagsForMbids, dollar_1) if err != nil { return nil, err } defer rows.Close() - var items []ListCandidateArtistTagsForMbidsRow + var items []CandidateArtistTag for rows.Next() { - var i ListCandidateArtistTagsForMbidsRow + var i CandidateArtistTag if err := rows.Scan(&i.CandidateMbid, &i.Tag, &i.Weight); err != nil { return nil, err } @@ -186,10 +180,11 @@ type ListCandidateArtistsMissingTagsRow struct { // Already-in-library candidates are skipped: they have an artists row, so // their tags belong in track_tags, and the suggestion query filters them out // anyway. $1 = current tag_sources_version, $2 = limit. -// candidate_name is coalesced to '' so it lands non-nullable in Go: the name is -// only a Last.fm lookup key, and empty simply means "MBID-keyed providers only", -// which the provider chain already handles. max() is an arbitrary-but- -// deterministic pick when several seeds spell the same MBID differently. +// candidate_name is coalesced to the empty string so it lands non-nullable in +// Go: the name is only a Last.fm lookup key, and empty simply means "MBID-keyed +// providers only", which the provider chain already handles. max() is an +// arbitrary-but-deterministic pick when several seeds spell one MBID +// differently. func (q *Queries) ListCandidateArtistsMissingTags(ctx context.Context, arg ListCandidateArtistsMissingTagsParams) ([]ListCandidateArtistsMissingTagsRow, error) { rows, err := q.db.Query(ctx, listCandidateArtistsMissingTags, arg.TagSourcesVersion, arg.Limit) if err != nil { diff --git a/internal/db/dbq/models.go b/internal/db/dbq/models.go index d4ec39f7..524bb6a9 100644 --- a/internal/db/dbq/models.go +++ b/internal/db/dbq/models.go @@ -240,6 +240,12 @@ type AuditLog struct { CreatedAt pgtype.Timestamptz } +type CandidateArtistTag struct { + CandidateMbid string + Tag string + Weight float64 +} + type CandidateArtistTagState struct { CandidateMbid string TagSource string @@ -247,12 +253,6 @@ type CandidateArtistTagState struct { UpdatedAt pgtype.Timestamptz } -type CandidateArtistTag struct { - CandidateMbid string - Tag string - Weight float64 -} - type ContextualLike struct { ID pgtype.UUID UserID pgtype.UUID diff --git a/internal/db/queries/candidate_artist_tags.sql b/internal/db/queries/candidate_artist_tags.sql index 150b0637..4403689f 100644 --- a/internal/db/queries/candidate_artist_tags.sql +++ b/internal/db/queries/candidate_artist_tags.sql @@ -20,10 +20,11 @@ -- Already-in-library candidates are skipped: they have an artists row, so -- their tags belong in track_tags, and the suggestion query filters them out -- anyway. $1 = current tag_sources_version, $2 = limit. --- candidate_name is coalesced to '' so it lands non-nullable in Go: the name is --- only a Last.fm lookup key, and empty simply means "MBID-keyed providers only", --- which the provider chain already handles. max() is an arbitrary-but- --- deterministic pick when several seeds spell the same MBID differently. +-- candidate_name is coalesced to the empty string so it lands non-nullable in +-- Go: the name is only a Last.fm lookup key, and empty simply means "MBID-keyed +-- providers only", which the provider chain already handles. max() is an +-- arbitrary-but-deterministic pick when several seeds spell one MBID +-- differently. SELECT u.candidate_mbid, coalesce(max(u.candidate_name), '')::text AS candidate_name, sum(u.score)::float8 AS total_score