Problem
jobs/va-apple-music-url-remediation/orchestrate.ts's invalidateAlbumBatch binds its id list as a bare JS array:
WHERE "album_id" = ANY(${albumIds}) // albumIds: number[]
Drizzle expands a JS array inside a sql template into a comma-separated parameter list, so Postgres receives a row constructor rather than an array:
WHERE "album_id" = ANY(($1, $2, ... $202))
ANY requires an array on the right-hand side, so the statement is rejected at parse time — SQLSTATE 42809, op ANY/ALL (array) requires array on right side. It is dataset-independent: the shipped statement could never have invalidated a row, on any page, for any input.
The album_metadata phase of the BS#2000 remediation has therefore never once succeeded.
Evidence
Production run 2026-08-06 09:33 PDT, run_id 47dfff79-44d7-4768-8a18-3dd49278dc67:
{"level":"error","step":"write_failed","message":"album_metadata invalidation failed","after_id":0,
"error_message":"Failed query: UPDATE \"wxyc_schema\".\"album_metadata\" SET \"apple_music_url\" = NULL, ... WHERE \"album_id\" = ANY(($1, $2, ... $202)) AND \"apple_music_url\" IS NOT NULL"}
Run summary for the phase:
album_metadata: {"candidates":206,"invalidated":0,"batches":1}
206 candidates found, 0 written, the first batch aborted the phase.
The flowsheet phase of the same run completed correctly (52 rows nulled, 56 re-verified and kept) — only the album_metadata arm is broken, and it is the DJ-visible one: apps/backend/services/flowsheet.service.ts serves coalesce(album_metadata.apple_music_url, flowsheet.apple_music_url), so for a linked row the value a DJ sees is the album_metadata one. Those ~206 rows are still serving Apple Music deep-links minted by LML's pre-LML#1139 V/A-blind track matcher.
Why no test caught it
Three layers each miss it for a different reason:
- Unit tests never parse SQL.
jest.unit.config.ts maps @wxyc/database to tests/mocks/database.mock.ts, and tests/__mocks__/drizzle-orm.ts stubs the sql tag as { sql: strings, values }. Nothing serializes a statement.
- The suite's
renderSql helper is blind to parameterization. It splices bound values inline, so ANY(${[7, 8]}) and ANY(${'{7,8}'}::int[]) are indistinguishable in its output. The existing invalidateAlbumBatch SQL test asserts the SET clause and the IS NOT NULL guard and stayed green through the entire broken run. (In fact renderSql renders the defective statement as ANY() — the array collapses to an empty string — and nobody looked.)
- The existing integration spec pins the candidate net, not the write.
tests/integration/va-apple-music-url-remediation-net.spec.js covers the fold_artist_name regex superset only.
Compounding this: npm run typecheck does not cover jobs/**, and a bare number[] in a sql template is well-typed anyway — the defect is invisible to the type system by construction.
The idiom it diverged from
At least six other jobs already bind a PG array literal string with an explicit cast, and one of them names the trap:
jobs/album-critic-reviews-etl/antijoin.ts:52-61 — comment calls it "the BS#1068/BS#1071 trap"
jobs/flowsheet-ghost-row-sweep/orchestrate.ts:241-244
jobs/flowsheet-metadata-backfill/orchestrate.ts:748,782,815
jobs/flowsheet-etl/job.ts:118
jobs/album-reviews-etl/link.ts:137
jobs/artist-unicode-dedup/merge.ts:313 (job-local intArrayLiteral helper)
const idArrayLiteral = `{${albumIds.join(',')}}`;
... WHERE "album_id" = ANY(${idArrayLiteral}::int[])
End state
Revised during review (the original text specified the array-literal-plus-::int[] fix, which review superseded). invalidateAlbumBatch sends a VALUES-join UPDATE, mirroring applyFlowsheetBatch in the same file: each id and each observed url is bound as its own parameter, so no JS array reaches the driver at all and nothing has to hand-roll a PG array literal. Review also required a compare-and-set on the observed apple_music_url (the album arm had none, only IS NOT NULL), which needs the per-row payload the VALUES join already carries.
Regression coverage:
- an integration spec that
requires the compiled dist/orchestrate.cjs and runs the real invalidateAlbumBatch against Postgres — a hand-mirrored copy of the statement would pass with the defect restored, so it must not be one;
- unit assertions on the compare-and-set, the
updated_at stamp, and the absence of any bare-array bind, scoped honestly to what the mock can observe.
Files
jobs/va-apple-music-url-remediation/orchestrate.ts (the fix, line ~378)
tests/integration/va-apple-music-url-remediation-invalidate.spec.js (new)
tests/unit/jobs/va-apple-music-url-remediation/orchestrate.test.ts
Constraints
- The integration runner is babel-jest with no TypeScript support, so an integration spec cannot import the TS job — the statement must be mirrored in SQL, as the sibling net spec already does and documents.
- Do not change the phase ordering (flowsheet before
album_metadata), the compare-and-set UPDATE on the flowsheet arm, or the rescue-rate detector.
Acceptance criteria
Related
Problem
jobs/va-apple-music-url-remediation/orchestrate.ts'sinvalidateAlbumBatchbinds its id list as a bare JS array:Drizzle expands a JS array inside a
sqltemplate into a comma-separated parameter list, so Postgres receives a row constructor rather than an array:ANYrequires an array on the right-hand side, so the statement is rejected at parse time — SQLSTATE42809,op ANY/ALL (array) requires array on right side. It is dataset-independent: the shipped statement could never have invalidated a row, on any page, for any input.The
album_metadataphase of the BS#2000 remediation has therefore never once succeeded.Evidence
Production run 2026-08-06 09:33 PDT,
run_id47dfff79-44d7-4768-8a18-3dd49278dc67:{"level":"error","step":"write_failed","message":"album_metadata invalidation failed","after_id":0, "error_message":"Failed query: UPDATE \"wxyc_schema\".\"album_metadata\" SET \"apple_music_url\" = NULL, ... WHERE \"album_id\" = ANY(($1, $2, ... $202)) AND \"apple_music_url\" IS NOT NULL"}Run summary for the phase:
album_metadata: {"candidates":206,"invalidated":0,"batches":1}206 candidates found, 0 written, the first batch aborted the phase.
The flowsheet phase of the same run completed correctly (52 rows nulled, 56 re-verified and kept) — only the
album_metadataarm is broken, and it is the DJ-visible one:apps/backend/services/flowsheet.service.tsservescoalesce(album_metadata.apple_music_url, flowsheet.apple_music_url), so for a linked row the value a DJ sees is thealbum_metadataone. Those ~206 rows are still serving Apple Music deep-links minted by LML's pre-LML#1139 V/A-blind track matcher.Why no test caught it
Three layers each miss it for a different reason:
jest.unit.config.tsmaps@wxyc/databasetotests/mocks/database.mock.ts, andtests/__mocks__/drizzle-orm.tsstubs thesqltag as{ sql: strings, values }. Nothing serializes a statement.renderSqlhelper is blind to parameterization. It splices bound values inline, soANY(${[7, 8]})andANY(${'{7,8}'}::int[])are indistinguishable in its output. The existinginvalidateAlbumBatch SQLtest asserts the SET clause and theIS NOT NULLguard and stayed green through the entire broken run. (In factrenderSqlrenders the defective statement asANY()— the array collapses to an empty string — and nobody looked.)tests/integration/va-apple-music-url-remediation-net.spec.jscovers thefold_artist_nameregex superset only.Compounding this:
npm run typecheckdoes not coverjobs/**, and a barenumber[]in asqltemplate is well-typed anyway — the defect is invisible to the type system by construction.The idiom it diverged from
At least six other jobs already bind a PG array literal string with an explicit cast, and one of them names the trap:
jobs/album-critic-reviews-etl/antijoin.ts:52-61— comment calls it "the BS#1068/BS#1071 trap"jobs/flowsheet-ghost-row-sweep/orchestrate.ts:241-244jobs/flowsheet-metadata-backfill/orchestrate.ts:748,782,815jobs/flowsheet-etl/job.ts:118jobs/album-reviews-etl/link.ts:137jobs/artist-unicode-dedup/merge.ts:313(job-localintArrayLiteralhelper)End state
Regression coverage:
requires the compileddist/orchestrate.cjsand runs the realinvalidateAlbumBatchagainst Postgres — a hand-mirrored copy of the statement would pass with the defect restored, so it must not be one;updated_atstamp, and the absence of any bare-array bind, scoped honestly to what the mock can observe.Files
jobs/va-apple-music-url-remediation/orchestrate.ts(the fix, line ~378)tests/integration/va-apple-music-url-remediation-invalidate.spec.js(new)tests/unit/jobs/va-apple-music-url-remediation/orchestrate.test.tsConstraints
album_metadata), the compare-and-set UPDATE on the flowsheet arm, or the rescue-rate detector.Acceptance criteria
invalidateAlbumBatch.42809 op ANY/ALL (array) requires array on right side.updated_atstamp is asserted — migration 0084's trigger is flowsheet-only (review addition).jobs/va-apple-music-url-remediation/has the same defect. (Audited:ANY(appears exactly once in the job; every other binding inorchestrate.tsis a scalar. No other file in the job contains SQL.)album_metadataphase can be re-run and actually invalidates its candidates.Related
album_metadataarm has never run successfully; this bug blocks its completion.