Skip to content

fix(tracks): match ISRC ignoring dashes and case, add functional index#832

Merged
raymondjacobson merged 2 commits into
mainfrom
claude/mystifying-bartik-9526e1
May 19, 2026
Merged

fix(tracks): match ISRC ignoring dashes and case, add functional index#832
raymondjacobson merged 2 commits into
mainfrom
claude/mystifying-bartik-9526e1

Conversation

@raymondjacobson
Copy link
Copy Markdown
Member

@raymondjacobson raymondjacobson commented May 19, 2026

Summary

ISRC lookup via GET /v1/tracks?isrc=… failed when the stored value and the query disagreed on dash placement. Partner report (Thomas):

Fixed purely on the search/query side (no normalization at write time):

  • Normalize the incoming isrc query param in Go: uppercase + strip non-alphanumeric characters before passing to the DB.
  • In the SQL, compare against regexp_replace(upper(isrc), '[^A-Z0-9]', '', 'g') so the stored value is normalized at compare time.

Net result: any combination of dashes/spaces/casing in either the request or the stored row matches. US-ANG-21-03742, USANG2103742, usang2103742, and US-ANG2103742 all resolve to the same row.

Index

Added tracks_isrc_normalized_idx, a partial functional index matching the comparison expression so the new query can index-scan instead of seq-scanning all of tracks:

create index concurrently if not exists tracks_isrc_normalized_idx
    on public.tracks ((regexp_replace(upper(isrc), '[^A-Z0-9]'::text, ''::text, 'g')))
    where isrc is not null;

Verified planner picks it on a 50k-row scratch table:

Bitmap Heap Scan on scratch_tracks  (cost=16.43..248.18 rows=500 width=4)
  Recheck Cond: (regexp_replace(upper(isrc), '[^A-Z0-9]'::text, ''::text, 'g'::text) = ANY (...))
  ->  Bitmap Index Scan on scratch_tracks_isrc_norm_idx  (cost=0.00..16.30 rows=500 width=0)
        Index Cond: (regexp_replace(upper(isrc), '[^A-Z0-9]'::text, ''::text, 'g'::text) = ANY (...))

Index DDL uses CREATE INDEX CONCURRENTLY (no BEGIN/COMMIT wrapper) so the build does not take an ACCESS EXCLUSIVE lock on tracks. IF NOT EXISTS keeps the migration idempotent. Partial on isrc IS NOT NULL since most rows have no ISRC.

Heads-up given #830: that PR moved 0201 away from CONCURRENTLY because the legacy Python Celery task on challenge_disbursements was keeping ~3-minute transactions open continuously, blocking the build's virtualxid wait indefinitely. tracks is written by the Go indexer and isn't subject to that long-transaction pattern, so CONCURRENTLY should complete normally here. If a stuck build needs to be aborted, drop the invalid index with DROP INDEX IF EXISTS tracks_isrc_normalized_idx; and re-run.

ISWC

There is no iswc query-param lookup today (column exists and is exposed in API responses, but no handler or sqlc query reads it). Nothing to mirror — left as-is.

Test plan

  • TestGetTracksByISRC covers: stored-with-dashes queried undashed, queried with same dashes, queried lowercased; stored-without-dashes queried as-is and queried with inserted dashes.
  • Existing tracks tests (TestTracksEndpoint, TestGetTracksByPermalink, TestGetTracksExcludesAccessAuthorities, Test200UnAuthed) still pass.
  • Migration applies cleanly and is idempotent (re-running prints relation \"tracks_isrc_normalized_idx\" already exists, skipping).
  • EXPLAIN on a seeded scratch table shows Bitmap Index Scan on tracks_isrc_normalized_idx.
  • After deploy: verify SELECT indexrelid::regclass, indisvalid FROM pg_index WHERE indexrelid::regclass::text = 'tracks_isrc_normalized_idx'; returns indisvalid = true.

🤖 Generated with Claude Code

@raymondjacobson raymondjacobson changed the title fix(tracks): match ISRC ignoring dashes and case fix(tracks): match ISRC ignoring dashes and case, add functional index May 19, 2026
raymondjacobson and others added 2 commits May 19, 2026 13:44
Normalize both the incoming ?isrc= query param (uppercase, strip
non-alphanumerics) and the stored value in the SQL comparison so a row
stored as "US-ANG-21-03742" matches a query of "USANG2103742" and vice
versa.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a partial functional index matching the
regexp_replace(upper(isrc), '[^A-Z0-9]', '', 'g') comparison so dash/case-
insensitive ?isrc= lookups can index-scan instead of seq-scanning the
tracks table. EXPLAIN on a 50k-row scratch table confirms the planner
picks the index.

Uses CREATE INDEX CONCURRENTLY (no transaction wrapper) to avoid taking
an ACCESS EXCLUSIVE lock on tracks during the build. IF NOT EXISTS keeps
the migration idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@raymondjacobson raymondjacobson force-pushed the claude/mystifying-bartik-9526e1 branch from 058c565 to e95cb02 Compare May 19, 2026 20:44
@raymondjacobson raymondjacobson merged commit 09d3e48 into main May 19, 2026
5 checks passed
@raymondjacobson raymondjacobson deleted the claude/mystifying-bartik-9526e1 branch May 19, 2026 21:06
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.

1 participant