Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,13 +695,15 @@ they can't tell you:
`check:splitby-perf`) are stages of `scripts/gate.sh` of their own, and the boundary suite is in
`npm test`.
`npm run report:coverage` — the unit suites' coverage (CI shows it on the reports job).
**Every view kind's output is pinned** by `test/view-snapshots.test.ts` (in `npm test`, no
**Every view kind's output is pinned** by the view snapshot tests (`test/view-snapshots*.test.ts`,
four parallel shards of `test/support/view-snapshots.ts`; in `npm test`, no
database): one gallery example per kind runs through the whole engine against warehouse answers
recorded in `test/snapshots/answers/`, and its compiled SQL and queries, computed result, figure
and results text are compared with `test/snapshots/<kind>.{sql,json,svg,txt}`. A diff there is a
change to review, not noise: `UPDATE_SNAPSHOTS=1` accepts output changes, and a changed query
needs `RECORD_SNAPSHOTS=1` with `.env.local` (public warehouse only; the answers hold public
studies' patient-level rows). A new view kind adds its case there. Each case also runs with its
needs `RECORD_SNAPSHOTS=1 node --env-file=.env.local --test test/view-snapshots*.test.ts`
(public warehouse only; the answers hold public studies' patient-level rows). A new view kind
adds its case to `CASES` there (a check fails until it does). Each case also runs with its
answers delivered in reverse, and must not change: a runner combines what it asked for after
every answer is in, in the view's order (`askEachSite` in `lib/views/shared.ts`), never inside
the callback as each one lands.
Expand Down
3 changes: 2 additions & 1 deletion lib/ch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ export interface ChStatement { conn: ChConn; format: string; query: string; exte
/**
* Answers statements instead of the database, or watches them on their way to it (`send`, which
* takes extra ClickHouse settings for that statement, e.g. "max_threads=1"). Only
* test/view-snapshots.test.ts sets one: it records the warehouse's answers, then replays them.
* the view snapshot tests (test/support/view-snapshots.ts) set one: they record the warehouse's
* answers, then replay them.
*/
export type ChTransport = (st: ChStatement, send: (settings?: string) => Promise<string>) => Promise<string>;
let transport: ChTransport | null = null;
Expand Down
2 changes: 1 addition & 1 deletion lib/views/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export async function studyCatalogs(c: ViewRun, studies: string[]): Promise<Stud
* Asks every (cohort, study) the view draws that `resolvers` holds, concurrently, and returns the
* answers in the view's order (its cohorts, then each cohort's studies), never in the order they
* finish. Combine them after this returns: combining inside `ask` as each answer lands makes the
* result depend on which query finished first (test/view-snapshots.test.ts checks for that).
* result depend on which query finished first (test/support/view-snapshots.ts checks for that).
*/
export async function askEachSite<R, T>(ctx: ViewRun, resolvers: Record<string, R>,
ask: (cohort: string, study: string, r: R) => Promise<T>): Promise<{ cohort: string; study: string; answer: T }[]> {
Expand Down
189 changes: 189 additions & 0 deletions test/snapshots/acquired.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
{
"study": "lung_msk_pdx",
"cohorts": {
"all patients": {
"count": 172,
"text": "all patients in the study",
"studies": [
"lung_msk_pdx"
]
}
},
"views": [
{
"kind": "acquired",
"cohorts": [
"all patients"
],
"acquired": {
"anchoredTo": "Treatment [SUBTYPE=Systemic therapy]",
"preWindowDays": null,
"minGapDays": 30,
"sampleEvents": [
"Sample acquisition",
"Sequencing",
"Specimen"
],
"sampleKey": "SAMPLE_ID",
"measure": "driver",
"scan": {
"top": 12,
"minPost": 3
},
"panels": [
{
"cohort": "all patients",
"anchored": 142,
"dated": 142,
"withPre": 67,
"withPost": 107,
"patients": 33,
"preDays": 27,
"postDays": 279,
"gapDays": {
"q1": 154,
"median": 301,
"q3": 514
},
"genes": [
{
"gene": "CDKN2B",
"pairs": 33,
"shared": 2,
"lost": 1,
"acquired": 8,
"neither": 22,
"p": 0.03906250000000003,
"q": 0.5000000000000001
},
{
"gene": "MYC",
"pairs": 33,
"shared": 1,
"lost": 0,
"acquired": 4,
"neither": 28,
"p": 0.125,
"q": 0.5000000000000001
},
{
"gene": "CARD11",
"pairs": 33,
"shared": 0,
"lost": 0,
"acquired": 4,
"neither": 29,
"p": 0.125,
"q": 0.5000000000000001
},
{
"gene": "DNMT3B",
"pairs": 33,
"shared": 0,
"lost": 0,
"acquired": 4,
"neither": 29,
"p": 0.125,
"q": 0.5000000000000001
},
{
"gene": "NKX3-1",
"pairs": 33,
"shared": 0,
"lost": 0,
"acquired": 4,
"neither": 29,
"p": 0.125,
"q": 0.5000000000000001
},
{
"gene": "NTRK1",
"pairs": 33,
"shared": 0,
"lost": 0,
"acquired": 4,
"neither": 29,
"p": 0.125,
"q": 0.5000000000000001
},
{
"gene": "RAC1",
"pairs": 33,
"shared": 0,
"lost": 0,
"acquired": 4,
"neither": 29,
"p": 0.125,
"q": 0.5000000000000001
},
{
"gene": "KEAP1",
"pairs": 33,
"shared": 0,
"lost": 1,
"acquired": 5,
"neither": 27,
"p": 0.21875000000000017,
"q": 0.5000000000000001
},
{
"gene": "ETV1",
"pairs": 33,
"shared": 1,
"lost": 0,
"acquired": 3,
"neither": 29,
"p": 0.25000000000000006,
"q": 0.5000000000000001
},
{
"gene": "NKX2-1",
"pairs": 33,
"shared": 1,
"lost": 0,
"acquired": 3,
"neither": 29,
"p": 0.25000000000000006,
"q": 0.5000000000000001
},
{
"gene": "DDR2",
"pairs": 33,
"shared": 0,
"lost": 0,
"acquired": 3,
"neither": 30,
"p": 0.25000000000000006,
"q": 0.5000000000000001
},
{
"gene": "INHBA",
"pairs": 31,
"shared": 0,
"lost": 0,
"acquired": 3,
"neither": 28,
"p": 0.25000000000000006,
"q": 0.5000000000000001
}
],
"tested": 30
}
]
},
"participation": {
"studies": [
{
"study": "lung_msk_pdx",
"mode": "patient-level",
"instance": "primary",
"source": 0
}
],
"requiresPatientLevel": false,
"aggregateOnly": []
},
"caveat": "Patients with a sample dated on or before their first Treatment [SUBTYPE=Systemic therapy] and another at least 30 days after it; each side is one sample — the last before and the first after — dated by its first Sample acquisition/Sequencing/Specimen timeline event (attribute SAMPLE_ID). Funnel: all patients — 142 started, 67 with a sample before, 107 with one after, 33 patients with both (median 27 days before and 279 after; samples a median 301 days apart, IQR 154–514). Per gene, counting only patients whose before- AND after-sample panels both cover it: an OncoKB oncogenic alteration (v7.5) after but not before (acquired), before but not after (lost), on both (shared). The exact McNemar test asks whether acquired and lost differ; q is Benjamini–Hochberg across the genes tested in each cohort — every gene altered after treatment in at least 3 patients (all patients: 30 genes), the 12 smallest p drawn. These are few patients, and a second biopsy is clinically indicated — typically taken at progression — so they are a selected group, and \"acquired\" means seen after and not before, not caused by the treatment: a subclone the first biopsy missed, a different lesion, or a lower-purity sample under-calling one side reads the same. Which treatment ran between the samples beyond the anchor is not considered."
}
]
}
119 changes: 119 additions & 0 deletions test/snapshots/acquired.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
-- cohort all patients
-- lung_msk_pdx
SELECT DISTINCT replaceOne(patient_unique_id, 'lung_msk_pdx_', '') AS pid FROM snapshot_db.sample_derived
WHERE cancer_study_identifier = 'lung_msk_pdx'

-- view 0 acquired: acquired:funnel:all patients · lung_msk_pdx
WITH cohort AS (SELECT DISTINCT replaceOne(patient_unique_id, 'lung_msk_pdx_', '') AS pid FROM snapshot_db.sample_derived
WHERE cancer_study_identifier = 'lung_msk_pdx'),
anch AS (
SELECT p.stable_id AS pid, min(ce.start_date) AS day
FROM snapshot_db.clinical_event ce
JOIN snapshot_db.patient p ON p.internal_id = ce.patient_id
JOIN snapshot_db.cancer_study cs ON cs.cancer_study_id = p.cancer_study_id
WHERE cs.cancer_study_identifier = 'lung_msk_pdx' AND lowerUTF8(trimBoth(ce.event_type)) = lowerUTF8(trimBoth('Treatment'))
AND ce.clinical_event_id IN (SELECT clinical_event_id FROM snapshot_db.clinical_event_data WHERE lowerUTF8(trimBoth(key)) = lowerUTF8(trimBoth('SUBTYPE')) AND lowerUTF8(trimBoth(value)) = lowerUTF8(trimBoth('Systemic therapy')))
AND p.stable_id IN (SELECT pid FROM cohort)
GROUP BY pid
),
tl AS (
SELECT p.stable_id AS pid, d.value AS sample, min(ce.start_date) AS day
FROM snapshot_db.clinical_event ce
JOIN snapshot_db.patient p ON p.internal_id = ce.patient_id
JOIN snapshot_db.cancer_study cs ON cs.cancer_study_id = p.cancer_study_id
JOIN snapshot_db.clinical_event_data d ON d.clinical_event_id = ce.clinical_event_id AND lowerUTF8(trimBoth(d.key)) = lowerUTF8(trimBoth('SAMPLE_ID'))
WHERE cs.cancer_study_identifier = 'lung_msk_pdx' AND lowerUTF8(trimBoth(ce.event_type)) IN (lowerUTF8(trimBoth('Sample acquisition')), lowerUTF8(trimBoth('Sequencing')), lowerUTF8(trimBoth('Specimen')))
AND p.stable_id IN (SELECT pid FROM anch)
GROUP BY pid, sample
),
dated AS (
SELECT tl.pid AS pid, sd.sample_unique_id AS suid, tl.day - anch.day AS rel
FROM tl INNER JOIN anch ON anch.pid = tl.pid
INNER JOIN snapshot_db.sample_derived sd ON sd.cancer_study_identifier = 'lung_msk_pdx' AND sd.sample_stable_id = tl.sample AND sd.patient_stable_id = tl.pid
),
pick AS (
SELECT pid, countIf(rel <= 0) AS n_pre, countIf(rel >= 30) AS n_post,
argMaxIf(suid, (rel, suid), rel <= 0) AS pre, argMinIf(suid, (rel, suid), rel >= 30) AS post,
maxIf(rel, rel <= 0) AS pre_rel, minIf(rel, rel >= 30) AS post_rel
FROM dated GROUP BY pid
),
ps AS (
SELECT pre AS suid, pid, 'a' AS side FROM pick WHERE n_pre > 0 AND n_post > 0
UNION ALL SELECT post AS suid, pid, 'b' AS side FROM pick WHERE n_pre > 0 AND n_post > 0
)
SELECT (SELECT count() FROM anch) AS anchored, (SELECT uniqExact(pid) FROM tl) AS with_events, count() AS dated,
countIf(n_pre > 0) AS with_pre, countIf(n_post > 0) AS with_post, countIf(n_pre > 0 AND n_post > 0) AS paired,
quantileExactIf(0.5)(-pre_rel, n_pre > 0 AND n_post > 0) AS pre_median, quantileExactIf(0.5)(post_rel, n_pre > 0 AND n_post > 0) AS post_median,
quantileExactIf(0.25)(post_rel - pre_rel, n_pre > 0 AND n_post > 0) AS gap_q1, quantileExactIf(0.5)(post_rel - pre_rel, n_pre > 0 AND n_post > 0) AS gap_median,
quantileExactIf(0.75)(post_rel - pre_rel, n_pre > 0 AND n_post > 0) AS gap_q3
FROM pick

-- view 0 acquired: acquired:genes:all patients · lung_msk_pdx
WITH cohort AS (SELECT DISTINCT replaceOne(patient_unique_id, 'lung_msk_pdx_', '') AS pid FROM snapshot_db.sample_derived
WHERE cancer_study_identifier = 'lung_msk_pdx'),
anch AS (
SELECT p.stable_id AS pid, min(ce.start_date) AS day
FROM snapshot_db.clinical_event ce
JOIN snapshot_db.patient p ON p.internal_id = ce.patient_id
JOIN snapshot_db.cancer_study cs ON cs.cancer_study_id = p.cancer_study_id
WHERE cs.cancer_study_identifier = 'lung_msk_pdx' AND lowerUTF8(trimBoth(ce.event_type)) = lowerUTF8(trimBoth('Treatment'))
AND ce.clinical_event_id IN (SELECT clinical_event_id FROM snapshot_db.clinical_event_data WHERE lowerUTF8(trimBoth(key)) = lowerUTF8(trimBoth('SUBTYPE')) AND lowerUTF8(trimBoth(value)) = lowerUTF8(trimBoth('Systemic therapy')))
AND p.stable_id IN (SELECT pid FROM cohort)
GROUP BY pid
),
tl AS (
SELECT p.stable_id AS pid, d.value AS sample, min(ce.start_date) AS day
FROM snapshot_db.clinical_event ce
JOIN snapshot_db.patient p ON p.internal_id = ce.patient_id
JOIN snapshot_db.cancer_study cs ON cs.cancer_study_id = p.cancer_study_id
JOIN snapshot_db.clinical_event_data d ON d.clinical_event_id = ce.clinical_event_id AND lowerUTF8(trimBoth(d.key)) = lowerUTF8(trimBoth('SAMPLE_ID'))
WHERE cs.cancer_study_identifier = 'lung_msk_pdx' AND lowerUTF8(trimBoth(ce.event_type)) IN (lowerUTF8(trimBoth('Sample acquisition')), lowerUTF8(trimBoth('Sequencing')), lowerUTF8(trimBoth('Specimen')))
AND p.stable_id IN (SELECT pid FROM anch)
GROUP BY pid, sample
),
dated AS (
SELECT tl.pid AS pid, sd.sample_unique_id AS suid, tl.day - anch.day AS rel
FROM tl INNER JOIN anch ON anch.pid = tl.pid
INNER JOIN snapshot_db.sample_derived sd ON sd.cancer_study_identifier = 'lung_msk_pdx' AND sd.sample_stable_id = tl.sample AND sd.patient_stable_id = tl.pid
),
pick AS (
SELECT pid, countIf(rel <= 0) AS n_pre, countIf(rel >= 30) AS n_post,
argMaxIf(suid, (rel, suid), rel <= 0) AS pre, argMinIf(suid, (rel, suid), rel >= 30) AS post,
maxIf(rel, rel <= 0) AS pre_rel, minIf(rel, rel >= 30) AS post_rel
FROM dated GROUP BY pid
),
ps AS (
SELECT pre AS suid, pid, 'a' AS side FROM pick WHERE n_pre > 0 AND n_post > 0
UNION ALL SELECT post AS suid, pid, 'b' AS side FROM pick WHERE n_pre > 0 AND n_post > 0
),
alt AS (
SELECT g.hugo_gene_symbol AS gene, ps.pid AS pid, ps.side AS side, max(ann.is_driver) AS hit
FROM snapshot_db.genomic_event_derived g
LEFT JOIN oncokb_annotation.variant_annotation ann
ON ann.oncokb_version = 'v7.5' AND ann.is_driver
AND ann.hugo_gene_symbol = g.hugo_gene_symbol
AND ((g.variant_type = 'mutation' AND ann.alteration_type = 'MUTATION' AND ann.alteration = g.mutation_variant)
OR (g.variant_type = 'cna' AND ann.alteration_type = 'CNA' AND ann.alteration = if(g.cna_alteration >= 2, 'AMPLIFICATION', 'DELETION'))
OR (g.variant_type = 'structural_variant' AND ann.alteration_type = 'SV' AND ann.alteration = 'Fusion'))
INNER JOIN ps ON ps.suid = g.sample_unique_id
WHERE g.cancer_study_identifier = 'lung_msk_pdx' AND (g.variant_type != 'cna' OR abs(g.cna_alteration) >= 2)
AND g.sample_unique_id IN (SELECT suid FROM ps)
GROUP BY gene, pid, side
HAVING hit > 0
),
cov AS (
SELECT DISTINCT pg.gene AS gene, ps.pid AS pid, ps.side AS side
FROM snapshot_db.sample_to_gene_panel_derived sp
JOIN snapshot_db.gene_panel_to_gene_derived pg ON pg.gene_panel_id = sp.gene_panel_id AND pg.gene IN (SELECT DISTINCT gene FROM alt)
INNER JOIN ps ON ps.suid = sp.sample_unique_id
WHERE sp.cancer_study_identifier = 'lung_msk_pdx'
AND sp.alteration_type IN ('MUTATION_EXTENDED','COPY_NUMBER_ALTERATION','STRUCTURAL_VARIANT')
),
per AS (
SELECT gene, pid, max(side = 'a') AS pa, max(side = 'b') AS pb,
max(side = 'a' AND hit > 0) AS aa, max(side = 'b' AND hit > 0) AS ab
FROM (SELECT gene, pid, side, hit FROM alt UNION ALL SELECT gene, pid, side, 0 AS hit FROM cov)
GROUP BY gene, pid
)
SELECT gene, count() AS pairs, countIf(aa AND ab) AS both, countIf(aa AND NOT ab) AS a_only, countIf(ab AND NOT aa) AS b_only
FROM per WHERE pa AND pb GROUP BY gene HAVING both + a_only + b_only > 0
Loading
Loading