From 4c3772377a3931e62cabe4e699b6d00a2c9fe81c Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sat, 11 Jul 2026 06:03:08 -0700 Subject: [PATCH 01/58] transitioning to sqlite instead of Rda --- .Rbuildignore | 1 + .gitignore | 1 + DESCRIPTION | 5 +- NAMESPACE | 6 + R/data_comp.R | 225 +++++++++++-------------------- R/gambl_mutations_db.R | 57 ++++++++ R/get_cn_segments_from_db.R | 25 ++++ R/get_genes_db.R | 37 +++++ R/get_ssm_from_db.R | 70 ++++++++++ R/get_sv_from_db.R | 41 ++++++ R/sample_metadata.R | 10 ++ data-raw/assemble_bundled_data.R | 55 ++++++-- data-raw/build_mutations_db.R | 26 ++++ data-raw/build_reference_db.R | 156 +++++++++++++++++++++ data-raw/make_sample_metadata.R | 16 +++ data-raw/test_gambl_db.R | 108 +++++++++++++++ data-raw/write_mutations_db.R | 84 ++++++++++++ data/sample_metadata.rda | Bin 0 -> 13444 bytes inst/extdata/gambl_reference.db | Bin 0 -> 454656 bytes man/gambl_mutations_db.Rd | 37 +++++ man/gambl_reference_db.Rd | 33 +++++ man/get_cn_segments_from_db.Rd | 24 ++++ man/get_genes.Rd | 39 ++++-- man/get_ssm_from_db.Rd | 51 +++++++ man/get_sv_from_db.Rd | 40 ++++++ man/sample_metadata.Rd | 20 +++ 26 files changed, 998 insertions(+), 169 deletions(-) create mode 100644 R/gambl_mutations_db.R create mode 100644 R/get_cn_segments_from_db.R create mode 100644 R/get_genes_db.R create mode 100644 R/get_ssm_from_db.R create mode 100644 R/get_sv_from_db.R create mode 100644 R/sample_metadata.R create mode 100644 data-raw/build_mutations_db.R create mode 100644 data-raw/build_reference_db.R create mode 100644 data-raw/make_sample_metadata.R create mode 100644 data-raw/test_gambl_db.R create mode 100644 data-raw/write_mutations_db.R create mode 100644 data/sample_metadata.rda create mode 100644 inst/extdata/gambl_reference.db create mode 100644 man/gambl_mutations_db.Rd create mode 100644 man/gambl_reference_db.Rd create mode 100644 man/get_cn_segments_from_db.Rd create mode 100644 man/get_ssm_from_db.Rd create mode 100644 man/get_sv_from_db.Rd create mode 100644 man/sample_metadata.Rd diff --git a/.Rbuildignore b/.Rbuildignore index b7dc945..1203c55 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -2,3 +2,4 @@ ^\.Rproj\.user$ ^LICENSE\.md$ ^data-raw$ +^gambl_mutations\.db$ diff --git a/.gitignore b/.gitignore index cd67eac..cad1feb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .Rproj.user +gambl_mutations.db diff --git a/DESCRIPTION b/DESCRIPTION index 286baf9..17d0a8c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -25,13 +25,16 @@ Description: The package contains manually curated data for the genomic Analysis License: MIT + file LICENSE Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.2 +RoxygenNote: 7.3.3 Depends: R (>= 3.5) LazyData: true Imports: + DBI, + dbplyr, dplyr, ggplot2, + RSQLite, tidyr LazyDataCompression: xz Suggests: diff --git a/NAMESPACE b/NAMESPACE index 6381e6b..a791c72 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,11 @@ # Generated by roxygen2: do not edit by hand +export(gambl_mutations_db) +export(gambl_reference_db) +export(get_cn_segments_from_db) +export(get_genes) +export(get_ssm_from_db) +export(get_sv_from_db) import(dplyr) import(ggplot2) import(tidyr) diff --git a/R/data_comp.R b/R/data_comp.R index a9d3d58..ae31fdf 100644 --- a/R/data_comp.R +++ b/R/data_comp.R @@ -3,197 +3,124 @@ #' #' @description Retrieve gene names from bundled lymphoma gene lists. #' -#' @details Complete lists of genes described as significantly mutated in large -#' lymphoma studies have been curated and provided with this package. +#' @details Gene lists are read from the bundled normalized reference database +#' (`inst/extdata/gambl_reference.db`), which is built from the LLMPP curated +#' lists. Confidence tier and data version are columns in that database, so the +#' previous per-entity, per-version `.rda` objects are no longer required. #' #' @param entities Optional vector specifying one or more lymphoma entities #' e.g. MCL, DLBCL, BL. -#' @param curated_only Specify FALSE to retrieve all genes or leave default -#' for the curated subset. +#' @param curated_only When TRUE (default) return the curated set (Tier 1 and +#' Tier 2) and drop retired Tier 3 genes. Set FALSE to include all tiers. +#' Ignored when `tier` is supplied. +#' @param tier Optional integer vector of confidence tiers to keep, e.g. +#' `tier = 1` for high-confidence genes only, or `tier = c(1, 2)`. When +#' supplied it takes precedence over `curated_only`. #' @param gene_format Specify what to return as output. Can be one of: #' * "symbol" (the default): list of gene symbols -#' * "ensembl": list of ENSEMBLE IDs -#' * "data.frame": data frame with column Gene and per-entity gene status -#' @param version Specify which version to return. Currently supported versions -#' are 0.0 (legacy version from original GAMBLR), 0.1, and _latest. The -#' latter will always point to the highest numeric version of the genes. +#' * "ensembl": list of Ensembl IDs +#' * "data.frame": one row per gene with a `_Tier` column per +#' requested entity (the gene's tier there, or NA if absent). Filter +#' these columns to focus on tiers, e.g. +#' `dplyr::filter(df, DLBCL_Tier == 1, FL_Tier == 1) |> dplyr::pull(Gene)`. +#' @param version Data version to return. Defaults to "_latest", the current +#' LLMPP-sourced version in the reference database. Legacy numeric versions +#' (0.0, 0.1) are no longer bundled. #' -#' @return A character vector of gene symbol or Ensembl IDs or a data frame. +#' @return A character vector of gene symbols or Ensembl IDs, or a data frame. #' #' @import dplyr tidyr #' +#' @export +#' #' @examples -#' all_dlbcl_genes <- get_genes(entities = "DLBCL", curated_only = FALSE) +#' # high-confidence (Tier 1) DLBCL genes +#' dlbcl_tier1 <- get_genes(entities = "DLBCL", tier = 1) +#' +#' # genes that are Tier 1 in DLBCL, FL and BL simultaneously +#' gene_df <- get_genes(entities = c("DLBCL", "FL", "BL"), gene_format = "data.frame") +#' shared_tier1 <- dplyr::filter(gene_df, DLBCL_Tier == 1, FL_Tier == 1, BL_Tier == 1) +#' #' all_curated_genes <- get_genes() get_genes <- function( entities = c("DLBCL", "MCL", "BL"), curated_only = TRUE, + tier = NULL, gene_format = "symbol", version = "_latest" ) { - # We need to handle the legacy version (0.0) separately - # because it was not pathology-specific - if (version == "0.0") { + # Gene lists are sourced from the normalized reference database (built from + # the LLMPP curated lists) rather than per-entity, per-version .rda objects. + # "version" and "tier" are columns, so lymphoma_genes_*_v0.1/v0.2/v_latest + # are no longer needed. + if (!version %in% c("_latest", "latest")) { message( paste( - "You have requested the legacy version of the lymphoma genes.", - "It is only provided here for backwards compatibility.", - "We recommend you use the most recent version by keeping the", - "default value of the argument version. Use at your own risk." + "Legacy per-version gene lists (e.g. 0.0, 0.1) are no longer", + "bundled. Data is now sourced from the LLMPP curated lists;", + "returning the current version." ) ) - - legacy_lymphoma_genes <- eval( - parse( - text = paste0( - "GAMBLR.data::", - "lymphoma_genes_lymphoma_genes_v0.0") - ) - ) - - legacy_lymphoma_genes <- legacy_lymphoma_genes %>% - dplyr::select( - Gene, ensembl_gene_id, - intersect( - colnames(.), - entities - ) - ) - if (curated_only) { - # drop any row where all pathologies have FALSE - legacy_lymphoma_genes <- legacy_lymphoma_genes %>% - dplyr::filter( - ! if_all(3:ncol(.), ~ . == "FALSE") - ) - } - - if (gene_format == "symbol") { - return(legacy_lymphoma_genes$Gene) - } else if (gene_format == "ensembl") { - return(legacy_lymphoma_genes$ensembl_gene_id) - } else if (gene_format == "data.frame") { - return(legacy_lymphoma_genes) - } else { - stop( - "You requested output format that is not supported." - ) - } } - #construct file name using entity - entities <- tolower(entities) - - r_objects <- entities %>% - paste0( - "lymphoma_genes_", - ., - "_v", - version - ) - - # check for unsupported gene sets - all_files <- system.file( - "extdata", - package = "GAMBLR.data" - ) %>% - list.files( - recursive = TRUE, - full.names = TRUE - ) - - all_files <- gsub( - ".*extdata/", - "", - all_files - ) - - all_files <- all_files[grepl("lymphoma_genes", all_files)] - - all_files <- all_files[grepl(version, all_files)] - - available_entities <- gsub("(.*/\\s*(.*$))", "\\2", all_files) + # `tier`, when supplied, selects exactly those confidence tiers (e.g. + # tier = 1 for high-confidence genes only) and takes precedence over + # `curated_only`. Otherwise curated_only == TRUE keeps the curated set + # (Tier 1 + Tier 2) and drops retired Tier 3 genes; FALSE returns all tiers. + keep_tiers <- if (!is.null(tier)) { + as.integer(tier) + } else if (curated_only) { + c(1L, 2L) + } else { + c(1L, 2L, 3L) + } - available_entities <- gsub(".tsv", "", available_entities) + con <- gambl_reference_db() + entities_u <- toupper(entities) - missing_sets <- setdiff( - entities, - available_entities - ) + available <- dplyr::tbl(con, "gene_entity") %>% + dplyr::distinct(entity) %>% + dplyr::pull(entity) + missing_sets <- setdiff(entities_u, available) if (length(missing_sets) > 0) { warning( paste( "The gene set for the entity", - missing_sets, + paste(missing_sets, collapse = ", "), "is not available and will not be returned." ) ) - r_objects <- r_objects[ - grepl( - paste( - available_entities, - collapse="|" - ), - r_objects - ) - ] - entities <- entities[entities %in% available_entities] + entities_u <- intersect(entities_u, available) } - # Combine all lists into one df - # Do it in a way that when GAMBLR.data is ot imported, the data objects - # are still available - all_entities_data <- list() - - for (i in seq_along(r_objects)) { - all_entities_data[[i]] <- eval( - parse( - text = paste0( - "GAMBLR.data::", - r_objects[i] - ) - ) - ) - } - - all_entities_data <- all_entities_data %>% - # only select necessary columns - lapply( - ., - `[`, - , - c("ensembl_gene_id", "Gene", "curated") - ) - - names(all_entities_data) <- toupper(entities) - - all_entities_data <- all_entities_data %>% - bind_rows(.id = "entity") - - if (curated_only) { - # drop any row where curated is FALSE - all_entities_data <- all_entities_data %>% - dplyr::filter( - curated == "TRUE" - ) - } + dat <- dplyr::tbl(con, "gene_entity") %>% + dplyr::filter(entity %in% entities_u, tier %in% keep_tiers) %>% + dplyr::select(entity, ensembl_gene_id, Gene = gene, tier) %>% + dplyr::collect() if (gene_format == "symbol") { - return(all_entities_data$Gene %>% unique %>% sort) + return(dat$Gene %>% unique %>% sort) } else if (gene_format == "ensembl") { - return(all_entities_data$ensembl_gene_id %>% unique %>% sort) + return(dat$ensembl_gene_id %>% unique %>% sort) } else if (gene_format == "data.frame") { + # one row per gene; one _Tier column per requested entity + # holding the gene's tier there (NA if absent), so callers can do + # filter(df, DLBCL_Tier == 1, FL_Tier == 1, ...) %>% pull(Gene) return( - all_entities_data %>% - select(-curated) %>% - mutate(is_gene = "TRUE") %>% - tidyr::pivot_wider( - names_from = "entity", - values_from = "is_gene" - ) %>% - replace(is.na(.), "FALSE") + dat %>% + dplyr::distinct(Gene, ensembl_gene_id, entity, tier) %>% + tidyr::pivot_wider( + id_cols = c(Gene, ensembl_gene_id), + names_from = "entity", + values_from = "tier", + names_glue = "{entity}_Tier" + ) %>% + dplyr::select(Gene, ensembl_gene_id, + dplyr::any_of(paste0(entities_u, "_Tier"))) %>% + dplyr::arrange(Gene) ) } else { stop( diff --git a/R/gambl_mutations_db.R b/R/gambl_mutations_db.R new file mode 100644 index 0000000..755f7d0 --- /dev/null +++ b/R/gambl_mutations_db.R @@ -0,0 +1,57 @@ +#' @title Connect to the GAMBL mutations database. +#' +#' @description Returns a (cached) read-only DBI connection to `gambl_mutations.db`, +#' the SQLite database holding the large sample-level frames (MAF, aSHM, seg, +#' bedpe) built from `sample_data` by `data-raw/build_mutations_db.R`. +#' +#' @details Unlike the small bundled reference database, this file is large +#' (~1 GB) and is NOT shipped inside the package. It is distributed as a cached +#' release asset. The path is resolved in this order: +#' \enumerate{ +#' \item the `db_path` argument; +#' \item `getOption("GAMBLR.data.mutations_db")`; +#' \item the `GAMBLR_MUTATIONS_DB` environment variable; +#' \item the user cache dir, `tools::R_user_dir("GAMBLR.data", "cache")`; +#' \item `gambl_mutations.db` in the working directory (development). +#' } +#' +#' @param db_path Optional explicit path to the .db file. +#' +#' @return A read-only DBIConnection. +#' +#' @examples +#' \dontrun{ +#' con <- gambl_mutations_db() +#' DBI::dbListTables(con) +#' } +#' @export +gambl_mutations_db <- function(db_path = NULL) { + if (is.null(db_path)) { + opt <- getOption("GAMBLR.data.mutations_db", default = NULL) + env <- Sys.getenv("GAMBLR_MUTATIONS_DB", unset = "") + candidates <- c( + opt, + if (nzchar(env)) env, + file.path(tools::R_user_dir("GAMBLR.data", "cache"), "gambl_mutations.db"), + "gambl_mutations.db" + ) + candidates <- candidates[!is.null(candidates)] + hit <- candidates[file.exists(candidates)] + if (length(hit) == 0) { + stop("gambl_mutations.db not found. Set options(GAMBLR.data.mutations_db = \"/path\"), ", + "the GAMBLR_MUTATIONS_DB env var, or place it in ", + tools::R_user_dir("GAMBLR.data", "cache"), call. = FALSE) + } + db_path <- hit[1] + } + stopifnot(file.exists(db_path)) + + cached <- getOption("gamblr.mutations.con") + if (!is.null(cached) && DBI::dbIsValid(cached) && + identical(normalizePath(cached@dbname), normalizePath(db_path))) { + return(cached) + } + con <- DBI::dbConnect(RSQLite::SQLite(), db_path, flags = RSQLite::SQLITE_RO) + options(gamblr.mutations.con = con) + con +} diff --git a/R/get_cn_segments_from_db.R b/R/get_cn_segments_from_db.R new file mode 100644 index 0000000..3546fc5 --- /dev/null +++ b/R/get_cn_segments_from_db.R @@ -0,0 +1,25 @@ +#' @title Query copy-number segments from the mutations database. +#' +#' @description Data-access helper that retrieves copy-number segments from the +#' `seg` table of `gambl_mutations.db`. Backs the GAMBLR.open `get_cn_segments` +#' and `get_sample_cn_segments` accessors. +#' +#' @param projection Genome build / `genome_build` column value. Default "grch37". +#' @param sample_ids Optional character vector of segment `ID`s (sample ids) to keep. +#' @param con Optional DBI connection (defaults to [gambl_mutations_db()]). +#' +#' @return A data frame of seg rows (the `genome_build` helper column is dropped; +#' chromosome-prefix handling is left to the caller, matching the bundled seg). +#' +#' @export +get_cn_segments_from_db <- function(projection = "grch37", + sample_ids = NULL, + con = NULL) { + if (is.null(con)) con <- gambl_mutations_db() + q <- dplyr::tbl(con, "seg") %>% + dplyr::filter(genome_build == projection) + if (!is.null(sample_ids)) q <- dplyr::filter(q, ID %in% sample_ids) + res <- dplyr::collect(q) + res$genome_build <- NULL + res +} diff --git a/R/get_genes_db.R b/R/get_genes_db.R new file mode 100644 index 0000000..9f797eb --- /dev/null +++ b/R/get_genes_db.R @@ -0,0 +1,37 @@ +#' @title Connect to the bundled GAMBLR reference database. +#' +#' @description Returns a (cached) read-only DBI connection to the normalized +#' SQLite reference database built from the LLMPP curated gene lists. This is the +#' shared backing store used by [get_genes()] (and intended for the Lymphopedia +#' wiki / data API), replacing the versioned `lymphoma_genes_*` .rda objects. +#' +#' @details The database is rebuilt from source by `data-raw/build_reference_db.R`. +#' Tables: `gene_entity` (one row per gene x entity, with tier / aSHM / citekey / +#' ensembl_gene_id / source_version), `ashm_regions`, and `build_info`. +#' +#' @param db_path Optional explicit path to the .db file. Defaults to the copy +#' installed with the package; falls back to `inst/extdata/gambl_reference.db` +#' during development. +#' +#' @return A DBIConnection. +#' +#' @examples +#' \dontrun{ +#' con <- gambl_reference_db() +#' dplyr::tbl(con, "gene_entity") +#' } +#' @export +gambl_reference_db <- function(db_path = NULL) { + if (is.null(db_path)) { + db_path <- system.file("extdata", "gambl_reference.db", package = "GAMBLR.data") + if (!nzchar(db_path) || !file.exists(db_path)) { + db_path <- file.path("inst", "extdata", "gambl_reference.db") + } + } + stopifnot(file.exists(db_path)) + cache <- getOption("gamblr.reference.con") + if (!is.null(cache) && DBI::dbIsValid(cache)) return(cache) + con <- DBI::dbConnect(RSQLite::SQLite(), db_path, flags = RSQLite::SQLITE_RO) + options(gamblr.reference.con = con) + con +} diff --git a/R/get_ssm_from_db.R b/R/get_ssm_from_db.R new file mode 100644 index 0000000..07dc110 --- /dev/null +++ b/R/get_ssm_from_db.R @@ -0,0 +1,70 @@ +#' @title Query SSMs from the mutations database. +#' +#' @description Shared data-access helper that retrieves simple somatic mutations +#' (SSM, MAF format) from `gambl_mutations.db`, pushing the common filters down to +#' indexed SQL. This is the single query path behind the GAMBLR.open SSM +#' accessors (get_coding_ssm, get_ssm_by_samples, get_ssm_by_region(s), ...). +#' +#' @param projection Genome build / `genome_build` column value. Default "grch37". +#' @param sample_ids Optional character vector of `Tumor_Sample_Barcode` to keep. +#' @param tool_name Pipeline to keep (matched case-insensitively). Default +#' "slms-3"; set NULL to skip the Pipeline filter. +#' @param include_ashm When TRUE, also query the `ashm` table and row-bind it. +#' @param coding_only When TRUE, keep only coding `Variant_Classification`s. +#' @param include_silent When FALSE (and `coding_only`), drop Silent mutations. +#' @param min_read_support Keep only variants with `t_alt_count` >= this value. +#' @param this_study Optional single `Study` to restrict to. +#' @param regions Optional data frame with columns `chrom`, `start`, `end`; rows +#' are OR-ed (each region is an indexed range scan). +#' @param con Optional DBI connection (defaults to [gambl_mutations_db()]). +#' +#' @return A data frame of MAF rows (the `genome_build` helper column is dropped). +#' +#' @export +get_ssm_from_db <- function(projection = "grch37", + sample_ids = NULL, + tool_name = "slms-3", + include_ashm = FALSE, + coding_only = FALSE, + include_silent = TRUE, + min_read_support = 0, + this_study = NULL, + regions = NULL, + con = NULL) { + if (is.null(con)) con <- gambl_mutations_db() + cc <- if (include_silent) coding_class else coding_class[coding_class != "Silent"] + tn <- if (!is.null(tool_name)) tolower(tool_name) else NULL + + # apply the filters that are common to a single (table, region) query + base_query <- function(table_name, region = NULL) { + q <- dplyr::tbl(con, table_name) %>% + dplyr::filter(genome_build == projection) + if (!is.null(tn)) q <- dplyr::filter(q, tolower(Pipeline) == tn) + if (!is.null(this_study)) q <- dplyr::filter(q, Study == this_study) + if (coding_only) q <- dplyr::filter(q, Variant_Classification %in% cc) + if (min_read_support > 0) q <- dplyr::filter(q, t_alt_count >= min_read_support) + if (!is.null(sample_ids)) q <- dplyr::filter(q, Tumor_Sample_Barcode %in% sample_ids) + if (!is.null(region)) { + rc <- region$chrom; rs <- region$start; re <- region$end + q <- dplyr::filter(q, Chromosome == rc & + Start_Position > rs & Start_Position < re) + } + dplyr::collect(q) + } + + # one query per region (each uses the (genome_build, Chromosome, Start_Position) + # index), otherwise a single unrestricted query + gather <- function(table_name) { + if (is.null(regions) || nrow(regions) == 0) return(base_query(table_name)) + dplyr::bind_rows(lapply(seq_len(nrow(regions)), function(i) + base_query(table_name, region = list( + chrom = as.character(regions$chrom[i]), + start = as.numeric(regions$start[i]), + end = as.numeric(regions$end[i]))))) + } + + res <- gather("maf") + if (include_ashm) res <- dplyr::bind_rows(res, gather("ashm")) + res$genome_build <- NULL + res +} diff --git a/R/get_sv_from_db.R b/R/get_sv_from_db.R new file mode 100644 index 0000000..2b2544b --- /dev/null +++ b/R/get_sv_from_db.R @@ -0,0 +1,41 @@ +#' @title Query structural variants (SV) from the mutations database. +#' +#' @description Data-access helper that retrieves Manta SV breakpoints (BEDPE +#' format) from the `bedpe` table of `gambl_mutations.db`, pushing the common +#' filters down to SQL. Backs the GAMBLR.open `get_manta_sv` accessor. +#' +#' @param projection Genome build / `genome_build` column value. Default "grch37". +#' @param sample_ids Optional character vector of `tumour_sample_id` to keep. +#' @param min_vaf Minimum `VAF_tumour`. Default 0. +#' @param min_score Minimum Manta `SCORE`. Default 0. +#' @param pass_only When TRUE, keep only `FILTER == "PASS"` breakpoints. +#' @param region Optional list with `chrom`, `start`, `end`; keeps breakpoints +#' whose A- or B-end falls in the range (positions compared numerically). +#' @param con Optional DBI connection (defaults to [gambl_mutations_db()]). +#' +#' @return A data frame of BEDPE rows (the `genome_build` helper column is dropped). +#' +#' @export +get_sv_from_db <- function(projection = "grch37", + sample_ids = NULL, + min_vaf = 0, + min_score = 0, + pass_only = FALSE, + region = NULL, + con = NULL) { + if (is.null(con)) con <- gambl_mutations_db() + q <- dplyr::tbl(con, "bedpe") %>% + dplyr::filter(genome_build == projection, + VAF_tumour >= min_vaf, + SCORE >= min_score) + if (!is.null(sample_ids)) q <- dplyr::filter(q, tumour_sample_id %in% sample_ids) + if (pass_only) q <- dplyr::filter(q, FILTER == "PASS") + if (!is.null(region)) { + rc <- region$chrom; rs <- as.numeric(region$start); re <- as.numeric(region$end) + q <- dplyr::filter(q, (CHROM_A == rc & START_A >= rs & START_A <= re) | + (CHROM_B == rc & START_B >= rs & START_B <= re)) + } + res <- dplyr::collect(q) + res$genome_build <- NULL + res +} diff --git a/R/sample_metadata.R b/R/sample_metadata.R new file mode 100644 index 0000000..f328d61 --- /dev/null +++ b/R/sample_metadata.R @@ -0,0 +1,10 @@ +#' Lightweight sample metadata for the bundled GAMBL samples. +#' +#' The per-sample metadata table (formerly `sample_data$meta`), split into its +#' own object so that loading metadata does not pull the large MAF/seg/bedpe +#' frames of `sample_data` into memory. The heavy mutation frames now live in +#' the separate `gambl_mutations.db` SQLite database (see +#' `data-raw/build_mutations_db.R`). Rebuilt by `data-raw/make_sample_metadata.R`. +#' +#' @format A data frame with 3344 rows and 31 columns, keyed by `sample_id`. +"sample_metadata" diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 597a7a9..d90ca2b 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -1,10 +1,25 @@ -setwd("~/my_dir/repos/GAMBLR.data/") +# --- Working directory & config ------------------------------------------- +# Run from the GAMBLR.data checkout (holds inst/extdata and data/); override +# with the GAMBLR_DATA_ROOT env var. No hard-coded paths. +PKG_ROOT <- Sys.getenv("GAMBLR_DATA_ROOT", unset = getwd()) +stopifnot(dir.exists(PKG_ROOT)) +setwd(PKG_ROOT) library(readxl) library(GAMBLR) library(parallel) library(tidyverse) +# GAMBLR.results locates GSC data via config.yml (repo_base / project_base). +# check_config_and_value() looks first in the working directory, then falls back +# to the copy shipped in GAMBLR.results/inst/extdata. As a last resort (and to +# cover any direct config::get() calls), point R_CONFIG_FILE at that shipped +# config when nothing else provides one. +if (!file.exists("config.yml") && !nzchar(Sys.getenv("R_CONFIG_FILE"))) { + Sys.setenv(R_CONFIG_FILE = system.file("extdata", "config.yml", + package = "GAMBLR.results")) +} + # Global variables definition colnames_for_bundled_meta <- c( "patient_id", @@ -278,7 +293,8 @@ reddy_meta <- read_excel( COO_consensus ) -setwd("/projects/rmorin/projects/gambl-repos/gambl-kdreval/") +# (config is resolved via config.yml / the GAMBLR.results fallback; no chdir to +# a gambl repo is needed to locate data — repo_base is absolute) reddy_meta_gambl <- get_gambl_metadata() %>% dplyr::filter(cohort == "dlbcl_reddy") %>% @@ -719,7 +735,7 @@ sample_data$hg38$maf <- get_ssm_by_samples( ) -setwd("~/my_dir/repos/GAMBLR.data/") +setwd(PKG_ROOT) # Add data from Arthur paper arthur_maf <- read_tsv( @@ -764,7 +780,8 @@ trios_samples <- read_xlsx( ) %>% drop_na(DNAseq_sample_id) -setwd("/projects/rmorin/projects/gambl-repos/gambl-kdreval/") +# (config is resolved via config.yml / the GAMBLR.results fallback; no chdir to +# a gambl repo is needed to locate data — repo_base is absolute) trios_meta <- get_gambl_metadata() %>% filter( @@ -938,7 +955,7 @@ sample_data$hg38$ashm <- bind_rows( trios_ashm_hg38 ) %>% distinct -setwd("~/my_dir/repos/GAMBLR.data/") +setwd(PKG_ROOT) # Add data from Reddy paper reddy_original_maf <- read_tsv( @@ -955,12 +972,34 @@ sample_data$grch37$maf <- bind_rows( ) ) -usethis::use_data( +# --- Persist the assembled data ------------------------------------------- +# Previously this bundled the multi-GB `sample_data.rda`. Instead we now write: +# 1. a lightweight, bundled `sample_metadata` object (metadata only), and +# 2. the large per-sample frames to gambl_mutations.db (a release asset, +# NOT shipped in the package tarball). +# This is built straight from the in-memory `sample_data` above, so there is no +# sample_data.rda round-trip. + +# 1. lightweight bundled metadata (replaces sample_data$meta lookups) +sample_metadata <- sample_data$meta +usethis::use_data(sample_metadata, overwrite = TRUE, compress = "xz") + +# 2. large sample-level frames -> SQLite +source("data-raw/write_mutations_db.R") +write_mutations_db( sample_data, - overwrite = TRUE, - compress = "xz" + out_db = "gambl_mutations.db", + source_desc = paste0("assemble_bundled_data.R @ ", format(Sys.Date())) ) +# During the transition you may still want the legacy monolithic object +# (e.g. until other GAMBLR packages are audited off sample_data). Flip to TRUE +# to also write data/sample_data.rda. +WRITE_LEGACY_SAMPLE_DATA <- FALSE +if (WRITE_LEGACY_SAMPLE_DATA) { + usethis::use_data(sample_data, overwrite = TRUE, compress = "xz") +} + library(data.tree) tree <- FromListSimple(sample_data) diff --git a/data-raw/build_mutations_db.R b/data-raw/build_mutations_db.R new file mode 100644 index 0000000..53f7262 --- /dev/null +++ b/data-raw/build_mutations_db.R @@ -0,0 +1,26 @@ +# build_mutations_db.R +# +# ONE-TIME adapter: build gambl_mutations.db from an EXISTING sample_data.rda, +# without re-running the (GSC-dependent) assembly. Use this only to convert a +# pre-existing bundled object. +# +# The ONGOING build writes the DB directly from source at the end of +# data-raw/assemble_bundled_data.R via the same write_mutations_db() helper, so +# there is no sample_data.rda round-trip in the normal pipeline. +# +# Usage: +# Rscript data-raw/build_mutations_db.R [SAMPLE_DATA_RDA] [OUT_DB] + +source("data-raw/write_mutations_db.R") + +args <- commandArgs(trailingOnly = TRUE) +rda <- if (length(args) >= 1) args[[1]] else "data/sample_data.rda" +out <- if (length(args) >= 2) args[[2]] else "gambl_mutations.db" + +message("Loading ", rda, " ...") +e <- new.env() +load(rda, envir = e) +sample_data <- get("sample_data", envir = e) + +write_mutations_db(sample_data, out_db = out, + source_desc = paste("one-time adapter from", rda)) diff --git a/data-raw/build_reference_db.R b/data-raw/build_reference_db.R new file mode 100644 index 0000000..3a3e62c --- /dev/null +++ b/data-raw/build_reference_db.R @@ -0,0 +1,156 @@ +# build_reference_db.R +# +# Proof-of-concept: build a single normalized SQLite reference database from the +# canonical LLMPP curated gene lists (NOT the versioned lymphoma_genes_*.rda +# objects). This one file is intended to become the shared backing store for +# GAMBLR.data accessors, the Lymphopedia wiki, and the Lymphopedia JSON/SQL API. +# +# Usage: +# Rscript data-raw/build_reference_db.R [LLMPP_CURATED_DIR] [OUT_DB] +# +# Design notes: +# * The per-entity curated TSVs have heterogeneous schemas. We normalize the +# common core into one long `gene_entity` table (one row per gene x entity) +# and stash the entity-specific columns as JSON in `extra` so nothing is lost. +# * "Which version" becomes a column (source_version), not a filename +# convention -- this collapses the lymphoma_genes_*_v0.1/v0.2/v_latest soup. + +suppressMessages({ + library(dplyr) + library(readr) + library(tidyr) + library(DBI) + library(jsonlite) +}) + +args <- commandArgs(trailingOnly = TRUE) +curated_dir <- ifelse(length(args) >= 1, args[[1]], + path.expand("~/git/LLMPP/resources/curated")) +out_db <- ifelse(length(args) >= 2, args[[2]], + file.path("inst", "extdata", "gambl_reference.db")) + +stopifnot(dir.exists(curated_dir)) +dir.create(dirname(out_db), recursive = TRUE, showWarnings = FALSE) + +# LLMPP source version -> stamped into every row so the data is self-describing +llmpp_version <- tryCatch( + system(paste("git -C", shQuote(dirname(dirname(curated_dir))), + "describe --always --dirty"), intern = TRUE), + error = function(e) NA_character_ +) +if (length(llmpp_version) == 0 || is.na(llmpp_version[1])) { + llmpp_version <- paste0("LLMPP-", format(Sys.Date())) +} else { + llmpp_version <- paste0("LLMPP@", llmpp_version[1]) +} + +# entity -> curated file +entity_files <- c( + DLBCL = "dlbcl_genes.tsv", + BL = "bl_genes.tsv", + FL = "fl_genes.tsv", + PMBL = "pmbl_genes.tsv", + MCL = "mcl_genes.tsv", + MZL = "mzl_genes.tsv" +) + +# pull a column by any of several candidate names (case-insensitive); NA if absent +pick <- function(df, candidates) { + hit <- names(df)[tolower(names(df)) %in% tolower(candidates)] + if (length(hit) == 0) return(rep(NA_character_, nrow(df))) + as.character(df[[hit[1]]]) +} + +core_cols <- c("gene", "tier", "ashm", "qc", "citekey", "pmid", "mutation_effect") + +harmonize_one <- function(entity, file) { + df <- suppressWarnings(read_tsv(file, col_types = cols(.default = "c"), + name_repair = "unique_quiet")) + core <- tibble( + gene = pick(df, c("Gene", "gene", "Hugo_Symbol")), + entity = entity, + tier = pick(df, c("Tier")), + ashm = pick(df, c("aSHM")), + qc = pick(df, c("QC")), + citekey = pick(df, c("citekey")), + pmid = pick(df, c("PMID")), + mutation_effect = pick(df, c("MutationEffect")) + ) |> filter(!is.na(gene), gene != "") + # keep everything else as JSON so entity-specific fields are not lost + used <- c("Gene", "gene", "Hugo_Symbol", "Tier", "aSHM", "QC", "citekey", + "PMID", "MutationEffect") + extra_df <- df[, !tolower(names(df)) %in% tolower(used), drop = FALSE] + extra_df <- extra_df[, names(extra_df) != "" & !grepl("^\\.\\.\\.", names(extra_df)), + drop = FALSE] + core$extra <- if (ncol(extra_df) == 0) NA_character_ else + vapply(seq_len(nrow(core)), function(i) + toJSON(as.list(extra_df[i, , drop = FALSE]), auto_unbox = TRUE, na = "null"), + character(1)) + core +} + +# symbol -> ensembl map, reused from GAMBLR.data's own gencode_to_symbol object, +# so gene_format = "ensembl" keeps working from the DB. One ensembl id per symbol. +sym2ens <- local({ + e <- new.env(); load(file.path("data", "gencode_to_symbol.rda"), envir = e) + get("gencode_to_symbol", envir = e) |> + dplyr::filter(!is.na(hgnc_symbol), hgnc_symbol != "") |> + dplyr::distinct(hgnc_symbol, .keep_all = TRUE) |> + dplyr::select(gene = hgnc_symbol, ensembl_gene_id) +}) + +gene_entity <- bind_rows( + Map(function(e, f) harmonize_one(e, file.path(curated_dir, f)), + names(entity_files), entity_files) +) |> + left_join(sym2ens, by = "gene") |> + mutate( + tier = suppressWarnings(as.integer(tier)), + source_version = llmpp_version + ) |> + relocate(ensembl_gene_id, .after = gene) |> + relocate(source_version, .after = last_col()) + +# --- second reference table: aSHM regions (both genome builds) ---------------- +read_ashm <- function(file, build) { + df <- suppressWarnings(read_tsv(file, col_types = cols(.default = "c"), + name_repair = "unique_quiet")) + tibble( + gene = pick(df, c("gene")), + chrom = pick(df, c("chr_name", "chrom", "chr")), + start = as.integer(df[[2]]), + end = as.integer(df[[3]]), + region = pick(df, c("region")), + regulatory_comment = pick(df, c("regulatory_comment")), + build = build, + source_version = llmpp_version + ) |> filter(!is.na(gene)) +} +ashm_dir <- file.path(curated_dir, "aSHM") +ashm_regions <- bind_rows( + read_ashm(file.path(ashm_dir, "somatic_hypermutation_locations_GRCh37.txt"), "grch37"), + read_ashm(file.path(ashm_dir, "somatic_hypermutation_locations_hg38.txt"), "hg38") +) + +# --- write the database ------------------------------------------------------- +if (file.exists(out_db)) file.remove(out_db) +con <- dbConnect(RSQLite::SQLite(), out_db) +on.exit(dbDisconnect(con)) + +dbWriteTable(con, "gene_entity", as.data.frame(gene_entity), overwrite = TRUE) +dbWriteTable(con, "ashm_regions", as.data.frame(ashm_regions), overwrite = TRUE) +dbWriteTable(con, "build_info", data.frame( + key = c("source", "source_version", "built_at", "builder"), + value = c("LLMPP resources/curated", llmpp_version, + format(Sys.time(), tz = "UTC", usetz = TRUE), + "data-raw/build_reference_db.R") +), overwrite = TRUE) + +dbExecute(con, "CREATE INDEX idx_ge_gene ON gene_entity(gene)") +dbExecute(con, "CREATE INDEX idx_ge_entity ON gene_entity(entity)") +dbExecute(con, "CREATE INDEX idx_ge_tier ON gene_entity(tier)") +dbExecute(con, "CREATE INDEX idx_ashm_gene ON ashm_regions(gene)") + +cat(sprintf("Wrote %s\n gene_entity : %d rows (%d genes, %d entities)\n ashm_regions: %d rows\n version : %s\n", + out_db, nrow(gene_entity), dplyr::n_distinct(gene_entity$gene), + dplyr::n_distinct(gene_entity$entity), nrow(ashm_regions), llmpp_version)) diff --git a/data-raw/make_sample_metadata.R b/data-raw/make_sample_metadata.R new file mode 100644 index 0000000..bfe14ce --- /dev/null +++ b/data-raw/make_sample_metadata.R @@ -0,0 +1,16 @@ +# make_sample_metadata.R +# +# Extract the lightweight sample metadata (sample_data$meta) into its own bundled +# object, `sample_metadata`, so that loading metadata does NOT drag the multi-GB +# MAF frames of `sample_data` into memory. The heavy mutation frames now live in +# gambl_mutations.db (see data-raw/build_mutations_db.R). + +e <- new.env() +load(file.path("data", "sample_data.rda"), envir = e) +sample_metadata <- get("sample_data", envir = e)$meta + +message(sprintf("sample_metadata: %d rows x %d cols (%.0f KB)", + nrow(sample_metadata), ncol(sample_metadata), + as.numeric(object.size(sample_metadata)) / 1024)) + +save(sample_metadata, file = file.path("data", "sample_metadata.rda"), compress = "xz") diff --git a/data-raw/test_gambl_db.R b/data-raw/test_gambl_db.R new file mode 100644 index 0000000..b5c9acd --- /dev/null +++ b/data-raw/test_gambl_db.R @@ -0,0 +1,108 @@ +# test_gambl_db.R +# +# Simple, dependency-light sanity checks for a freshly built gambl_mutations.db. +# Run after building the DB (on the GSC side) to confirm the output is usable by +# the GAMBLR.open accessors. +# +# Usage: +# Rscript data-raw/test_gambl_db.R [OUT_DB] [SAMPLE_METADATA_RDA] [SAMPLE_DATA_RDA] +# +# Exits non-zero if any critical check fails. The optional SAMPLE_DATA_RDA +# enables a regression check (DB row counts vs the in-memory frames). + +suppressMessages({ library(DBI); library(RSQLite) }) + +args <- commandArgs(trailingOnly = TRUE) +db <- if (length(args) >= 1) args[[1]] else "gambl_mutations.db" +meta <- if (length(args) >= 2) args[[2]] else "data/sample_metadata.rda" +rda <- if (length(args) >= 3) args[[3]] else "data/sample_data.rda" + +fails <- 0L +check <- function(cond, msg) { + status <- if (isTRUE(cond)) "PASS" else { fails <<- fails + 1L; "FAIL" } + cat(sprintf(" [%s] %s\n", status, msg)) +} + +stopifnot(file.exists(db)) +con <- dbConnect(SQLite(), db, flags = SQLITE_RO) +on.exit(dbDisconnect(con)) +tbls <- dbListTables(con) +n <- function(t) dbGetQuery(con, sprintf("SELECT COUNT(*) n FROM %s", t))$n +cols <- function(t) dbListFields(con, t) +builds_in <- function(t) sort(dbGetQuery(con, sprintf("SELECT DISTINCT genome_build g FROM %s", t))$g) + +cat("== tables present ==\n") +for (t in c("maf","ashm","seg","bedpe","sample_meta","build_info")) + check(t %in% tbls, sprintf("table %s exists", t)) + +cat("\n== row counts (all > 0) ==\n") +for (t in c("maf","ashm","seg","bedpe","sample_meta")) { + cnt <- if (t %in% tbls) n(t) else 0 + check(cnt > 0, sprintf("%-11s has %d rows", t, cnt)) +} + +cat("\n== both genome builds present ==\n") +for (t in c("maf","ashm","seg","bedpe")) + if (t %in% tbls) + check(all(c("grch37","hg38") %in% builds_in(t)), + sprintf("%-6s has builds: %s", t, paste(builds_in(t), collapse=", "))) + +cat("\n== required columns present (what the accessors filter on) ==\n") +req <- list( + maf = c("Chromosome","Start_Position","End_Position","Tumor_Sample_Barcode", + "Pipeline","Hugo_Symbol","Study","Variant_Classification","t_alt_count","genome_build"), + seg = c("ID","chrom","start","end","CN","genome_build"), + bedpe = c("tumour_sample_id","CHROM_A","START_A","CHROM_B","START_B","VAF_tumour","SCORE","FILTER","genome_build"), + sample_meta = c("sample_id","Tumor_Sample_Barcode","seq_type","study") +) +for (t in names(req)) if (t %in% tbls) { + missing <- setdiff(req[[t]], cols(t)) + check(length(missing) == 0, + sprintf("%-11s columns present%s", t, + if (length(missing)) paste0(" (MISSING: ", paste(missing, collapse=", "), ")") else "")) +} + +cat("\n== indexes present ==\n") +idx <- dbGetQuery(con, "SELECT name FROM sqlite_master WHERE type='index'")$name +for (i in c("idx_maf_pos","idx_maf_sample","idx_maf_gene","idx_seg_sample","idx_bedpe_sample")) + check(i %in% idx, sprintf("index %s", i)) + +cat("\n== pipelines present ==\n") +pipes <- dbGetQuery(con, "SELECT DISTINCT Pipeline p FROM maf")$p +for (p in c("SLMS-3","Publication")) + check(p %in% pipes, sprintf("Pipeline '%s' present (all: %s)", p, paste(pipes, collapse=", "))) + +cat("\n== spot query: MYC locus, grch37, slms-3 returns variants ==\n") +myc <- dbGetQuery(con, "SELECT COUNT(*) n FROM maf WHERE genome_build='grch37' + AND Chromosome='8' AND Start_Position>128723128 AND Start_Position<128774067 + AND lower(Pipeline)='slms-3'")$n +check(myc > 0, sprintf("MYC slms-3 variants found: %d", myc)) + +cat("\n== build_info counts match actual table counts ==\n") +if ("build_info" %in% tbls) { + bi <- dbGetQuery(con, "SELECT key, value FROM build_info") + getbi <- function(k) as.numeric(bi$value[bi$key == k]) + for (t in c("maf","ashm","seg","bedpe")) + check(isTRUE(getbi(paste0("n_", t)) == n(t)), + sprintf("build_info n_%s == COUNT(%s)", t, t)) +} + +cat("\n== sample_metadata.rda matches sample_meta table ==\n") +if (file.exists(meta)) { + e <- new.env(); load(meta, envir = e); sm <- get("sample_metadata", envir = e) + check(nrow(sm) == n("sample_meta"), + sprintf("sample_metadata rows (%d) == sample_meta table (%d)", nrow(sm), n("sample_meta"))) +} else cat(" [skip] no", meta, "\n") + +cat("\n== regression vs sample_data.rda (optional) ==\n") +if (file.exists(rda)) { + e <- new.env(); load(rda, envir = e); sd <- get("sample_data", envir = e) + for (t in c("maf","ashm","seg","bedpe")) { + expect <- sum(vapply(c("grch37","hg38"), + function(b) { x <- sd[[b]][[t]]; if (is.null(x)) 0L else nrow(x) }, integer(1))) + check(expect == n(t), sprintf("%-6s DB rows (%d) == sample_data rows (%d)", t, n(t), expect)) + } +} else cat(" [skip] no", rda, "(fresh GSC build has no legacy .rda — expected)\n") + +cat(sprintf("\n==== %s: %d check(s) failed ====\n", if (fails==0) "ALL PASSED" else "FAILURES", fails)) +if (fails > 0) quit(status = 1) diff --git a/data-raw/write_mutations_db.R b/data-raw/write_mutations_db.R new file mode 100644 index 0000000..abf294d --- /dev/null +++ b/data-raw/write_mutations_db.R @@ -0,0 +1,84 @@ +# write_mutations_db.R +# +# Reusable build-time helper: given the in-memory `sample_data` list (as +# assembled by data-raw/assemble_bundled_data.R), write the normalized +# `gambl_mutations.db` SQLite database with indexes matching the GAMBLR.open +# accessors. This is the ONGOING path that replaces bundling `sample_data.rda`: +# assemble_bundled_data.R -> sample_data (in memory) -> write_mutations_db() +# +# `source()` this file (it defines the function; it does not run anything). +# Requires the DBI and RSQLite packages (Imports of GAMBLR.data). + +write_mutations_db <- function(sample_data, + out_db = "gambl_mutations.db", + builds = c("grch37", "hg38"), + source_desc = "assemble_bundled_data.R") { + stopifnot(is.list(sample_data), !is.null(sample_data$meta)) + + if (file.exists(out_db)) file.remove(out_db) + con <- DBI::dbConnect(RSQLite::SQLite(), out_db) + on.exit(DBI::dbDisconnect(con)) + # speed up the bulk load + DBI::dbExecute(con, "PRAGMA journal_mode = OFF") + DBI::dbExecute(con, "PRAGMA synchronous = OFF") + + # sample metadata (single table, no genome build) + DBI::dbWriteTable(con, "sample_meta", as.data.frame(sample_data$meta), + overwrite = TRUE) + + # genome-build-stamped frames; append per build to keep peak memory low + write_element <- function(elem) { + first <- TRUE + total <- 0L + for (b in builds) { + x <- sample_data[[b]][[elem]] + if (is.null(x) || !nrow(x)) next + x <- as.data.frame(x) + x$genome_build <- b + DBI::dbWriteTable(con, elem, x, append = !first, overwrite = first) + total <- total + nrow(x) + first <- FALSE + } + total + } + counts <- vapply(c("maf", "ashm", "seg", "bedpe"), write_element, integer(1)) + + # indexes mirroring how the GAMBLR.open accessors query the data. + # wrapped in try() so a build missing an optional table/column is non-fatal. + idx <- c( + "CREATE INDEX idx_maf_pos ON maf(genome_build, Chromosome, Start_Position)", + "CREATE INDEX idx_maf_sample ON maf(Tumor_Sample_Barcode)", + "CREATE INDEX idx_maf_gene ON maf(Hugo_Symbol)", + "CREATE INDEX idx_maf_pipe ON maf(Pipeline)", + "CREATE INDEX idx_maf_study ON maf(Study)", + "CREATE INDEX idx_ashm_pos ON ashm(genome_build, Chromosome, Start_Position)", + "CREATE INDEX idx_ashm_sample ON ashm(Tumor_Sample_Barcode)", + "CREATE INDEX idx_seg_sample ON seg(genome_build, ID)", + "CREATE INDEX idx_seg_pos ON seg(genome_build, chrom, start)", + "CREATE INDEX idx_bedpe_sample ON bedpe(tumour_sample_id)", + "CREATE INDEX idx_meta_sample ON sample_meta(sample_id)", + "CREATE INDEX idx_meta_barcode ON sample_meta(Tumor_Sample_Barcode)" + ) + for (stmt in idx) try(DBI::dbExecute(con, stmt), silent = TRUE) + + # self-describing provenance / expected-counts table (used by the tests) + DBI::dbWriteTable(con, "build_info", data.frame( + key = c("source", "built_at", "builder", + "n_maf", "n_ashm", "n_seg", "n_bedpe", "n_samples"), + value = c(source_desc, + format(Sys.time(), tz = "UTC", usetz = TRUE), + "write_mutations_db", + counts[["maf"]], counts[["ashm"]], counts[["seg"]], + counts[["bedpe"]], nrow(sample_data$meta)), + stringsAsFactors = FALSE + ), overwrite = TRUE) + + DBI::dbExecute(con, "VACUUM") + DBI::dbExecute(con, "ANALYZE") + + message(sprintf("Wrote %s (%.0f MB) maf=%d ashm=%d seg=%d bedpe=%d samples=%d", + out_db, file.info(out_db)$size / 1024^2, + counts[["maf"]], counts[["ashm"]], counts[["seg"]], + counts[["bedpe"]], nrow(sample_data$meta))) + invisible(out_db) +} diff --git a/data/sample_metadata.rda b/data/sample_metadata.rda new file mode 100644 index 0000000000000000000000000000000000000000..968b9a9b6b9ceee336901c33441555cf2cd670b2 GIT binary patch literal 13444 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 literal 0 HcmV?d00001 diff --git a/inst/extdata/gambl_reference.db b/inst/extdata/gambl_reference.db new file mode 100644 index 0000000000000000000000000000000000000000..8f44e11b445aa143427d5de8f45f3f129ec52a9f GIT binary patch literal 454656 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

ZaaZd!5Hu9u++GGKp+uCw+SP5L) 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 literal 0 HcmV?d00001 diff --git a/man/gambl_mutations_db.Rd b/man/gambl_mutations_db.Rd new file mode 100644 index 0000000..aa4df18 --- /dev/null +++ b/man/gambl_mutations_db.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/gambl_mutations_db.R +\name{gambl_mutations_db} +\alias{gambl_mutations_db} +\title{Connect to the GAMBL mutations database.} +\usage{ +gambl_mutations_db(db_path = NULL) +} +\arguments{ +\item{db_path}{Optional explicit path to the .db file.} +} +\value{ +A read-only DBIConnection. +} +\description{ +Returns a (cached) read-only DBI connection to \code{gambl_mutations.db}, +the SQLite database holding the large sample-level frames (MAF, aSHM, seg, +bedpe) built from \code{sample_data} by \code{data-raw/build_mutations_db.R}. +} +\details{ +Unlike the small bundled reference database, this file is large +(~1 GB) and is NOT shipped inside the package. It is distributed as a cached +release asset. The path is resolved in this order: +\enumerate{ +\item the \code{db_path} argument; +\item \code{getOption("GAMBLR.data.mutations_db")}; +\item the \code{GAMBLR_MUTATIONS_DB} environment variable; +\item the user cache dir, \code{tools::R_user_dir("GAMBLR.data", "cache")}; +\item \code{gambl_mutations.db} in the working directory (development). +} +} +\examples{ +\dontrun{ +con <- gambl_mutations_db() +DBI::dbListTables(con) +} +} diff --git a/man/gambl_reference_db.Rd b/man/gambl_reference_db.Rd new file mode 100644 index 0000000..189f4cb --- /dev/null +++ b/man/gambl_reference_db.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get_genes_db.R +\name{gambl_reference_db} +\alias{gambl_reference_db} +\title{Connect to the bundled GAMBLR reference database.} +\usage{ +gambl_reference_db(db_path = NULL) +} +\arguments{ +\item{db_path}{Optional explicit path to the .db file. Defaults to the copy +installed with the package; falls back to \code{inst/extdata/gambl_reference.db} +during development.} +} +\value{ +A DBIConnection. +} +\description{ +Returns a (cached) read-only DBI connection to the normalized +SQLite reference database built from the LLMPP curated gene lists. This is the +shared backing store used by \code{\link[=get_genes]{get_genes()}} (and intended for the Lymphopedia +wiki / data API), replacing the versioned \verb{lymphoma_genes_*} .rda objects. +} +\details{ +The database is rebuilt from source by \code{data-raw/build_reference_db.R}. +Tables: \code{gene_entity} (one row per gene x entity, with tier / aSHM / citekey / +ensembl_gene_id / source_version), \code{ashm_regions}, and \code{build_info}. +} +\examples{ +\dontrun{ +con <- gambl_reference_db() +dplyr::tbl(con, "gene_entity") +} +} diff --git a/man/get_cn_segments_from_db.Rd b/man/get_cn_segments_from_db.Rd new file mode 100644 index 0000000..9118654 --- /dev/null +++ b/man/get_cn_segments_from_db.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get_cn_segments_from_db.R +\name{get_cn_segments_from_db} +\alias{get_cn_segments_from_db} +\title{Query copy-number segments from the mutations database.} +\usage{ +get_cn_segments_from_db(projection = "grch37", sample_ids = NULL, con = NULL) +} +\arguments{ +\item{projection}{Genome build / \code{genome_build} column value. Default "grch37".} + +\item{sample_ids}{Optional character vector of segment \code{ID}s (sample ids) to keep.} + +\item{con}{Optional DBI connection (defaults to \code{\link[=gambl_mutations_db]{gambl_mutations_db()}}).} +} +\value{ +A data frame of seg rows (the \code{genome_build} helper column is dropped; +chromosome-prefix handling is left to the caller, matching the bundled seg). +} +\description{ +Data-access helper that retrieves copy-number segments from the +\code{seg} table of \code{gambl_mutations.db}. Backs the GAMBLR.open \code{get_cn_segments} +and \code{get_sample_cn_segments} accessors. +} diff --git a/man/get_genes.Rd b/man/get_genes.Rd index 247de33..7be853a 100644 --- a/man/get_genes.Rd +++ b/man/get_genes.Rd @@ -7,6 +7,7 @@ get_genes( entities = c("DLBCL", "MCL", "BL"), curated_only = TRUE, + tier = NULL, gene_format = "symbol", version = "_latest" ) @@ -15,29 +16,45 @@ get_genes( \item{entities}{Optional vector specifying one or more lymphoma entities e.g. MCL, DLBCL, BL.} -\item{curated_only}{Specify FALSE to retrieve all genes or leave default -for the curated subset.} +\item{curated_only}{When TRUE (default) return the curated set (Tier 1 and +Tier 2) and drop retired Tier 3 genes. Set FALSE to include all tiers. +Ignored when \code{tier} is supplied.} + +\item{tier}{Optional integer vector of confidence tiers to keep, e.g. +\code{tier = 1} for high-confidence genes only, or \code{tier = c(1, 2)}. When +supplied it takes precedence over \code{curated_only}.} \item{gene_format}{Specify what to return as output. Can be one of: * "symbol" (the default): list of gene symbols -* "ensembl": list of ENSEMBLE IDs -* "data.frame": data frame with column Gene and per-entity gene status} +* "ensembl": list of Ensembl IDs +* "data.frame": one row per gene with a \verb{_Tier} column per +requested entity (the gene's tier there, or NA if absent). Filter +these columns to focus on tiers, e.g. +\code{dplyr::filter(df, DLBCL_Tier == 1, FL_Tier == 1) |> dplyr::pull(Gene)}.} -\item{version}{Specify which version to return. Currently supported versions -are 0.0 (legacy version from original GAMBLR), 0.1, and _latest. The -latter will always point to the highest numeric version of the genes.} +\item{version}{Data version to return. Defaults to "_latest", the current +LLMPP-sourced version in the reference database. Legacy numeric versions +(0.0, 0.1) are no longer bundled.} } \value{ -A character vector of gene symbol or Ensembl IDs or a data frame. +A character vector of gene symbols or Ensembl IDs, or a data frame. } \description{ Retrieve gene names from bundled lymphoma gene lists. } \details{ -Complete lists of genes described as significantly mutated in large -lymphoma studies have been curated and provided with this package. +Gene lists are read from the bundled normalized reference database +(\code{inst/extdata/gambl_reference.db}), which is built from the LLMPP curated +lists. Confidence tier and data version are columns in that database, so the +previous per-entity, per-version \code{.rda} objects are no longer required. } \examples{ -all_dlbcl_genes <- get_genes(entities = "DLBCL", curated_only = FALSE) +# high-confidence (Tier 1) DLBCL genes +dlbcl_tier1 <- get_genes(entities = "DLBCL", tier = 1) + +# genes that are Tier 1 in DLBCL, FL and BL simultaneously +gene_df <- get_genes(entities = c("DLBCL", "FL", "BL"), gene_format = "data.frame") +shared_tier1 <- dplyr::filter(gene_df, DLBCL_Tier == 1, FL_Tier == 1, BL_Tier == 1) + all_curated_genes <- get_genes() } diff --git a/man/get_ssm_from_db.Rd b/man/get_ssm_from_db.Rd new file mode 100644 index 0000000..80dcfba --- /dev/null +++ b/man/get_ssm_from_db.Rd @@ -0,0 +1,51 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get_ssm_from_db.R +\name{get_ssm_from_db} +\alias{get_ssm_from_db} +\title{Query SSMs from the mutations database.} +\usage{ +get_ssm_from_db( + projection = "grch37", + sample_ids = NULL, + tool_name = "slms-3", + include_ashm = FALSE, + coding_only = FALSE, + include_silent = TRUE, + min_read_support = 0, + this_study = NULL, + regions = NULL, + con = NULL +) +} +\arguments{ +\item{projection}{Genome build / \code{genome_build} column value. Default "grch37".} + +\item{sample_ids}{Optional character vector of \code{Tumor_Sample_Barcode} to keep.} + +\item{tool_name}{Pipeline to keep (matched case-insensitively). Default +"slms-3"; set NULL to skip the Pipeline filter.} + +\item{include_ashm}{When TRUE, also query the \code{ashm} table and row-bind it.} + +\item{coding_only}{When TRUE, keep only coding \code{Variant_Classification}s.} + +\item{include_silent}{When FALSE (and \code{coding_only}), drop Silent mutations.} + +\item{min_read_support}{Keep only variants with \code{t_alt_count} >= this value.} + +\item{this_study}{Optional single \code{Study} to restrict to.} + +\item{regions}{Optional data frame with columns \code{chrom}, \code{start}, \code{end}; rows +are OR-ed (each region is an indexed range scan).} + +\item{con}{Optional DBI connection (defaults to \code{\link[=gambl_mutations_db]{gambl_mutations_db()}}).} +} +\value{ +A data frame of MAF rows (the \code{genome_build} helper column is dropped). +} +\description{ +Shared data-access helper that retrieves simple somatic mutations +(SSM, MAF format) from \code{gambl_mutations.db}, pushing the common filters down to +indexed SQL. This is the single query path behind the GAMBLR.open SSM +accessors (get_coding_ssm, get_ssm_by_samples, get_ssm_by_region(s), ...). +} diff --git a/man/get_sv_from_db.Rd b/man/get_sv_from_db.Rd new file mode 100644 index 0000000..bd8f74c --- /dev/null +++ b/man/get_sv_from_db.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get_sv_from_db.R +\name{get_sv_from_db} +\alias{get_sv_from_db} +\title{Query structural variants (SV) from the mutations database.} +\usage{ +get_sv_from_db( + projection = "grch37", + sample_ids = NULL, + min_vaf = 0, + min_score = 0, + pass_only = FALSE, + region = NULL, + con = NULL +) +} +\arguments{ +\item{projection}{Genome build / \code{genome_build} column value. Default "grch37".} + +\item{sample_ids}{Optional character vector of \code{tumour_sample_id} to keep.} + +\item{min_vaf}{Minimum \code{VAF_tumour}. Default 0.} + +\item{min_score}{Minimum Manta \code{SCORE}. Default 0.} + +\item{pass_only}{When TRUE, keep only \code{FILTER == "PASS"} breakpoints.} + +\item{region}{Optional list with \code{chrom}, \code{start}, \code{end}; keeps breakpoints +whose A- or B-end falls in the range (positions compared numerically).} + +\item{con}{Optional DBI connection (defaults to \code{\link[=gambl_mutations_db]{gambl_mutations_db()}}).} +} +\value{ +A data frame of BEDPE rows (the \code{genome_build} helper column is dropped). +} +\description{ +Data-access helper that retrieves Manta SV breakpoints (BEDPE +format) from the \code{bedpe} table of \code{gambl_mutations.db}, pushing the common +filters down to SQL. Backs the GAMBLR.open \code{get_manta_sv} accessor. +} diff --git a/man/sample_metadata.Rd b/man/sample_metadata.Rd new file mode 100644 index 0000000..d61ca49 --- /dev/null +++ b/man/sample_metadata.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/sample_metadata.R +\docType{data} +\name{sample_metadata} +\alias{sample_metadata} +\title{Lightweight sample metadata for the bundled GAMBL samples.} +\format{ +A data frame with 3344 rows and 31 columns, keyed by \code{sample_id}. +} +\usage{ +sample_metadata +} +\description{ +The per-sample metadata table (formerly \code{sample_data$meta}), split into its +own object so that loading metadata does not pull the large MAF/seg/bedpe +frames of \code{sample_data} into memory. The heavy mutation frames now live in +the separate \code{gambl_mutations.db} SQLite database (see +\code{data-raw/build_mutations_db.R}). Rebuilt by \code{data-raw/make_sample_metadata.R}. +} +\keyword{datasets} From da83baf2f7267fbdfdbfed580ab56a6fa4528e6f Mon Sep 17 00:00:00 2001 From: Ryan Morin Date: Sat, 11 Jul 2026 13:02:52 -0700 Subject: [PATCH 02/58] improvements and efficiencies --- data-raw/assemble_bundled_data.R | 57 ++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index d90ca2b..1b01eac 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -242,7 +242,8 @@ pull_data <- function( slms3 <- get_ssm_by_samples( these_samples_metadata = pull_meta, basic_columns = FALSE, - projection = pull_projection + projection = pull_projection, + subset_from_merge = FALSE ) %>% select( all_of(all_cols) @@ -345,7 +346,10 @@ all_capture_meta <- bind_rows( # warning: this is very slow! all_capture_grch37_ssm_to_bundle <- pull_data(all_capture_meta) +print("Done collecting grch37") all_capture_hg38_ssm_to_bundle <- pull_data(all_capture_meta, "hg38") +print("Done collecting hg38") + # Importing DLBCL cell lines cell_lines_data <- list() @@ -384,15 +388,17 @@ cell_lines_data$hg38$ssm_to_bundle <- get_ssm_by_samples( basic_columns = FALSE ) %>% select(all_of(all_cols)) -cell_lines_data$grch37$cnv_to_bundle <- get_sample_cn_segments( - these_sample_ids = cell_lines_data$meta$sample_id -) +cell_lines_data$grch37$cnv_to_bundle <- get_cn_segments( + these_samples_metadata = cell_lines_data$meta, + projection="grch37" +) %>% + dplyr::select(all_of(c("ID","chrom","start","end","LOH_flag","log.ratio","CN","seg_seq_type"))) -cell_lines_data$hg38$cnv_to_bundle <- get_sample_cn_segments( - these_sample_ids = cell_lines_data$meta$sample_id, - projection = "hg38", - with_chr_prefix = TRUE -) +cell_lines_data$hg38$cnv_to_bundle <- get_cn_segments( + these_samples_metadata = cell_lines_data$meta, + projection="hg38" +) %>% + dplyr::select(all_of(c("ID","chrom","start","end","LOH_flag","log.ratio","CN","seg_seq_type"))) cell_lines_data$grch37$sv_to_bundle <- get_manta_sv( these_samples_metadata = cell_lines_data$meta, @@ -521,13 +527,13 @@ sample_data$hg38$maf <- bind_rows( sample_data$grch37$seg <- bind_rows( fl_data$cnv_to_bundle, - cell_lines_data$grch37$cnv + cell_lines_data$grch37$cnv_to_bundle ) sample_data$hg38$seg <- bind_rows( bl_data$cnv_to_bundle, dlbcl_data$cnv_to_bundle, - cell_lines_data$hg38$cnv + cell_lines_data$hg38$cnv_to_bundle ) #add SVs @@ -618,20 +624,21 @@ sample_data$grch37$maf <- bind_rows( ) ) +regions_bed_grch37 = GAMBLR.utils::create_bed_data( + GAMBLR.data::grch37_ashm_regions , + fix_names = "concat", + concat_cols = c("gene","region"),sep="-" +) # Add aSHM mutations for the already released samples grch37_ashm <- get_ssm_by_regions( these_samples_metadata = sample_data$meta, - regions_bed = GAMBLR.utils::create_bed_data( - GAMBLR.data::grch37_ashm_regions, - fix_names = "concat", - concat_cols = c("gene","region"),sep="-" - ), + regions_bed = regions_bed_grch37, streamlined = FALSE, basic_columns = FALSE ) %>% select( - any_of(c(colnames(sample_data$grch37$maf), maf_columns_to_keep)) + any_of(c(colnames(sample_data$grch37$maf), maf_columns_to_keep)) ) grch37_ashm <- grch37_ashm %>% @@ -679,6 +686,7 @@ hg38_ashm <- left_join( sample_data$grch37$ashm <- grch37_ashm sample_data$hg38$ashm <- hg38_ashm +print("done extracting aSHM mutations from GAMBLR.results") # Now add the SLMS-3 calls in both projections for those samples that # are bundled as publication data @@ -696,11 +704,13 @@ publication_samples <- c( publication_samples_grch37, publication_samples_hg38 ) +print("extracting grch37 mutations in lymphoma genes with GAMBLR.results") sample_data$grch37$maf <- get_ssm_by_samples( these_samples_metadata = get_gambl_metadata() %>% filter(sample_id %in% publication_samples), - basic_columns = FALSE) %>% + basic_columns = FALSE, + subset_from_merge = FALSE) %>% filter( Hugo_Symbol %in% all_lymphoma_genes ) %>% @@ -714,12 +724,14 @@ sample_data$grch37$maf <- get_ssm_by_samples( ., sample_data$grch37$maf ) +print("extracting hg38 mutations in lymphoma genes with GAMBLR.results") sample_data$hg38$maf <- get_ssm_by_samples( these_samples_metadata = get_gambl_metadata() %>% filter(sample_id %in% publication_samples), projection = "hg38", - basic_columns = FALSE) %>% + basic_columns = FALSE, + subset_from_merge = FALSE) %>% filter( Hugo_Symbol %in% all_lymphoma_genes ) %>% @@ -733,7 +745,7 @@ sample_data$hg38$maf <- get_ssm_by_samples( ., sample_data$hg38$maf ) - +print("done extracting all mutations in lymphoma genes with GAMBLR.results") setwd(PKG_ROOT) @@ -831,7 +843,7 @@ genome_trios_ssm_grch37 <- get_ssm_by_samples( Study = "Hilton" ) %>% select(all_of(colnames(sample_data$grch37$maf))) - +print("extracting mutations for Trios cohort") capture_trios_ssm_grch37 <- get_ssm_by_samples( these_samples_metadata = trios_meta %>% filter(seq_type == "capture"), @@ -955,6 +967,8 @@ sample_data$hg38$ashm <- bind_rows( trios_ashm_hg38 ) %>% distinct +print("Done getting aSHM from Trios") + setwd(PKG_ROOT) # Add data from Reddy paper @@ -984,6 +998,7 @@ sample_data$grch37$maf <- bind_rows( sample_metadata <- sample_data$meta usethis::use_data(sample_metadata, overwrite = TRUE, compress = "xz") +print("Starting sqlite build") # 2. large sample-level frames -> SQLite source("data-raw/write_mutations_db.R") write_mutations_db( From dc3c3e9387f0116c754dc06faa0f5f0a626ea667 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sat, 11 Jul 2026 13:28:04 -0700 Subject: [PATCH 03/58] Document gambl_reference.db / gambl_mutations.db table schemas Add full table/column/join-key reference to ?gambl_reference_db and ?gambl_mutations_db (rendered as markdown tables), plus a short "Bundled databases" pointer in the README, so the schema isn't only in the build scripts. Co-Authored-By: Claude Sonnet 5 --- R/gambl_mutations_db.R | 43 ++++++++++++++++++++++++++++++++---- R/get_genes_db.R | 23 +++++++++++++++++--- README.md | 14 ++++++++++++ man/gambl_mutations_db.Rd | 46 +++++++++++++++++++++++++++++++++++---- man/gambl_reference_db.Rd | 25 ++++++++++++++++++--- 5 files changed, 137 insertions(+), 14 deletions(-) diff --git a/R/gambl_mutations_db.R b/R/gambl_mutations_db.R index 755f7d0..f0d59e5 100644 --- a/R/gambl_mutations_db.R +++ b/R/gambl_mutations_db.R @@ -2,11 +2,13 @@ #' #' @description Returns a (cached) read-only DBI connection to `gambl_mutations.db`, #' the SQLite database holding the large sample-level frames (MAF, aSHM, seg, -#' bedpe) built from `sample_data` by `data-raw/build_mutations_db.R`. +#' bedpe) built from the in-memory `sample_data` object assembled by +#' `data-raw/assemble_bundled_data.R`, via the shared `write_mutations_db()` +#' helper (`data-raw/write_mutations_db.R`). #' -#' @details Unlike the small bundled reference database, this file is large -#' (~1 GB) and is NOT shipped inside the package. It is distributed as a cached -#' release asset. The path is resolved in this order: +#' @details Unlike the small bundled [gambl_reference_db()], this file is large +#' (several hundred MB) and is NOT shipped inside the package. It is +#' distributed as a cached release asset. The path is resolved in this order: #' \enumerate{ #' \item the `db_path` argument; #' \item `getOption("GAMBLR.data.mutations_db")`; @@ -15,6 +17,33 @@ #' \item `gambl_mutations.db` in the working directory (development). #' } #' +#' # Table reference +#' +#' Both genome builds are stacked into the same table (not split across +#' separate tables); filter on `genome_build` (`"grch37"` / `"hg38"`) to select +#' one. `maf` and `ashm` share the same (curated, 49-column) schema: a reduced +#' column set, not the full upstream flatfile MAF. +#' +#' | Table | Rows (typical) | Grain | Key columns | +#' | --- | --- | --- | --- | +#' | `maf` | ~3.6M | one somatic mutation call | `Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline, Study, genome_build` (+ ~35 more MAF-standard columns) | +#' | `ashm` | ~135k | one mutation call in an aSHM region | same schema as `maf` | +#' | `seg` | ~126k | one copy-number segment | `ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build` | +#' | `bedpe` | ~900 | one Manta structural-variant breakpoint pair | `CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build` | +#' | `sample_meta` | ~3.3k | one sample | `patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...` (31 columns total; its own `genome_build` column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) | +#' | `build_info` | 8 | key/value | provenance (`source`, `built_at`, `builder`) and expected row counts (`n_maf`, `n_ashm`, `n_seg`, `n_bedpe`, `n_samples`) used by the `data-raw/test_gambl_db.R` regression checks | +#' +#' ## Join keys +#' `maf`, `ashm`, and `bedpe` (via `tumour_sample_id`) key to +#' `sample_meta$Tumor_Sample_Barcode` / `sample_meta$sample_id`; `seg$ID` keys +#' to `sample_meta$sample_id`. +#' +#' ## Indexes +#' `maf`/`ashm`: `(genome_build, Chromosome, Start_Position)`, +#' `Tumor_Sample_Barcode`, `Hugo_Symbol` (maf only), `Pipeline`, `Study` (maf +#' only). `seg`: `(genome_build, ID)`, `(genome_build, chrom, start)`. `bedpe`: +#' `tumour_sample_id`. `sample_meta`: `sample_id`, `Tumor_Sample_Barcode`. +#' #' @param db_path Optional explicit path to the .db file. #' #' @return A read-only DBIConnection. @@ -23,6 +52,12 @@ #' \dontrun{ #' con <- gambl_mutations_db() #' DBI::dbListTables(con) +#' +#' # coding mutations in TP53, grch37, one pipeline +#' dplyr::tbl(con, "maf") |> +#' dplyr::filter(genome_build == "grch37", Hugo_Symbol == "TP53", +#' Pipeline == "SLMS-3") |> +#' dplyr::collect() #' } #' @export gambl_mutations_db <- function(db_path = NULL) { diff --git a/R/get_genes_db.R b/R/get_genes_db.R index 9f797eb..b291f8f 100644 --- a/R/get_genes_db.R +++ b/R/get_genes_db.R @@ -5,9 +5,21 @@ #' shared backing store used by [get_genes()] (and intended for the Lymphopedia #' wiki / data API), replacing the versioned `lymphoma_genes_*` .rda objects. #' -#' @details The database is rebuilt from source by `data-raw/build_reference_db.R`. -#' Tables: `gene_entity` (one row per gene x entity, with tier / aSHM / citekey / -#' ensembl_gene_id / source_version), `ashm_regions`, and `build_info`. +#' @details The database is rebuilt from source by `data-raw/build_reference_db.R` +#' and is small (well under 1 MB), so — unlike [gambl_mutations_db()] — it is +#' bundled inside the package (`inst/extdata/gambl_reference.db`). +#' +#' # Table reference +#' +#' | Table | Grain | Key columns | +#' | --- | --- | --- | +#' | `gene_entity` | one row per gene x lymphoma entity | `gene, ensembl_gene_id, entity, tier, ashm, qc, citekey, pmid, mutation_effect, extra` (JSON of entity-specific fields), `source_version` | +#' | `ashm_regions` | one row per gene x aSHM region x genome build | `gene, chrom, start, end, region, regulatory_comment, build, source_version` | +#' | `build_info` | key/value | provenance: `source` (LLMPP resources/curated), `source_version`, `built_at`, `builder` | +#' +#' `gene_entity$entity` takes values like `"DLBCL"`, `"BL"`, `"FL"`, `"PMBL"`, +#' `"MCL"`, `"MZL"`; `tier` is `1` (high-confidence), `2` (lower-confidence), or +#' `3` (retired). `ashm_regions$build` is `"grch37"` or `"hg38"`. #' #' @param db_path Optional explicit path to the .db file. Defaults to the copy #' installed with the package; falls back to `inst/extdata/gambl_reference.db` @@ -19,6 +31,11 @@ #' \dontrun{ #' con <- gambl_reference_db() #' dplyr::tbl(con, "gene_entity") +#' +#' # high-confidence DLBCL genes +#' dplyr::tbl(con, "gene_entity") |> +#' dplyr::filter(entity == "DLBCL", tier == 1) |> +#' dplyr::collect() #' } #' @export gambl_reference_db <- function(db_path = NULL) { diff --git a/README.md b/README.md index d2d051f..7742701 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,17 @@ Lab and without GSC access. For detailed information about how to install, bundled resources, functionality overview, tutorials, frequently asked questions and much more please visit the website [morinlab.github.io/GAMBLR.data/](https://morinlab.github.io/GAMBLR.data/) + +## Bundled databases + +Curated and sample-level data are served from two SQLite databases rather than +per-object `.rda` files. See `?gambl_reference_db` and `?gambl_mutations_db` +for the full table reference (columns, join keys, indexes). + +- **`gambl_reference_db()`** — small, bundled with the package. Curated gene + lists and aSHM regions (from LLMPP); backs [get_genes()]. +- **`gambl_mutations_db()`** — large (several hundred MB), distributed as a + cached release asset rather than shipped in the package. MAF, aSHM-region + mutation, copy-number segment, and structural-variant calls, plus sample + metadata; backs the GAMBLR.open `get_ssm_*`, `get_cn_segments*`, and + `get_manta_sv` accessors. diff --git a/man/gambl_mutations_db.Rd b/man/gambl_mutations_db.Rd index aa4df18..b8e1047 100644 --- a/man/gambl_mutations_db.Rd +++ b/man/gambl_mutations_db.Rd @@ -15,12 +15,14 @@ A read-only DBIConnection. \description{ Returns a (cached) read-only DBI connection to \code{gambl_mutations.db}, the SQLite database holding the large sample-level frames (MAF, aSHM, seg, -bedpe) built from \code{sample_data} by \code{data-raw/build_mutations_db.R}. +bedpe) built from the in-memory \code{sample_data} object assembled by +\code{data-raw/assemble_bundled_data.R}, via the shared \code{write_mutations_db()} +helper (\code{data-raw/write_mutations_db.R}). } \details{ -Unlike the small bundled reference database, this file is large -(~1 GB) and is NOT shipped inside the package. It is distributed as a cached -release asset. The path is resolved in this order: +Unlike the small bundled \code{\link[=gambl_reference_db]{gambl_reference_db()}}, this file is large +(several hundred MB) and is NOT shipped inside the package. It is +distributed as a cached release asset. The path is resolved in this order: \enumerate{ \item the \code{db_path} argument; \item \code{getOption("GAMBLR.data.mutations_db")}; @@ -29,9 +31,45 @@ release asset. The path is resolved in this order: \item \code{gambl_mutations.db} in the working directory (development). } } +\section{Table reference}{ +Both genome builds are stacked into the same table (not split across +separate tables); filter on \code{genome_build} (\code{"grch37"} / \code{"hg38"}) to select +one. \code{maf} and \code{ashm} share the same (curated, 49-column) schema: a reduced +column set, not the full upstream flatfile MAF.\tabular{llll}{ + Table \tab Rows (typical) \tab Grain \tab Key columns \cr + \code{maf} \tab ~3.6M \tab one somatic mutation call \tab \verb{Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline, Study, genome_build} (+ ~35 more MAF-standard columns) \cr + \code{ashm} \tab ~135k \tab one mutation call in an aSHM region \tab same schema as \code{maf} \cr + \code{seg} \tab ~126k \tab one copy-number segment \tab \verb{ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build} \cr + \code{bedpe} \tab ~900 \tab one Manta structural-variant breakpoint pair \tab \verb{CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build} \cr + \code{sample_meta} \tab ~3.3k \tab one sample \tab \verb{patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...} (31 columns total; its own \code{genome_build} column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) \cr + \code{build_info} \tab 8 \tab key/value \tab provenance (\code{source}, \code{built_at}, \code{builder}) and expected row counts (\code{n_maf}, \code{n_ashm}, \code{n_seg}, \code{n_bedpe}, \code{n_samples}) used by the \code{data-raw/test_gambl_db.R} regression checks \cr +} + +\subsection{Join keys}{ + +\code{maf}, \code{ashm}, and \code{bedpe} (via \code{tumour_sample_id}) key to +\code{sample_meta$Tumor_Sample_Barcode} / \code{sample_meta$sample_id}; \code{seg$ID} keys +to \code{sample_meta$sample_id}. +} + +\subsection{Indexes}{ + +\code{maf}/\code{ashm}: \verb{(genome_build, Chromosome, Start_Position)}, +\code{Tumor_Sample_Barcode}, \code{Hugo_Symbol} (maf only), \code{Pipeline}, \code{Study} (maf +only). \code{seg}: \verb{(genome_build, ID)}, \verb{(genome_build, chrom, start)}. \code{bedpe}: +\code{tumour_sample_id}. \code{sample_meta}: \code{sample_id}, \code{Tumor_Sample_Barcode}. +} +} + \examples{ \dontrun{ con <- gambl_mutations_db() DBI::dbListTables(con) + +# coding mutations in TP53, grch37, one pipeline +dplyr::tbl(con, "maf") |> + dplyr::filter(genome_build == "grch37", Hugo_Symbol == "TP53", + Pipeline == "SLMS-3") |> + dplyr::collect() } } diff --git a/man/gambl_reference_db.Rd b/man/gambl_reference_db.Rd index 189f4cb..03efd5d 100644 --- a/man/gambl_reference_db.Rd +++ b/man/gambl_reference_db.Rd @@ -21,13 +21,32 @@ shared backing store used by \code{\link[=get_genes]{get_genes()}} (and intended wiki / data API), replacing the versioned \verb{lymphoma_genes_*} .rda objects. } \details{ -The database is rebuilt from source by \code{data-raw/build_reference_db.R}. -Tables: \code{gene_entity} (one row per gene x entity, with tier / aSHM / citekey / -ensembl_gene_id / source_version), \code{ashm_regions}, and \code{build_info}. +The database is rebuilt from source by \code{data-raw/build_reference_db.R} +and is small (well under 1 MB), so — unlike \code{\link[=gambl_mutations_db]{gambl_mutations_db()}} — it is +bundled inside the package (\code{inst/extdata/gambl_reference.db}). } +\section{Table reference}{ +\tabular{lll}{ + Table \tab Grain \tab Key columns \cr + \code{gene_entity} \tab one row per gene x lymphoma entity \tab \verb{gene, ensembl_gene_id, entity, tier, ashm, qc, citekey, pmid, mutation_effect, extra} (JSON of entity-specific fields), \code{source_version} \cr + \code{ashm_regions} \tab one row per gene x aSHM region x genome build \tab \verb{gene, chrom, start, end, region, regulatory_comment, build, source_version} \cr + \code{build_info} \tab key/value \tab provenance: \code{source} (LLMPP resources/curated), \code{source_version}, \code{built_at}, \code{builder} \cr +} + + +\code{gene_entity$entity} takes values like \code{"DLBCL"}, \code{"BL"}, \code{"FL"}, \code{"PMBL"}, +\code{"MCL"}, \code{"MZL"}; \code{tier} is \code{1} (high-confidence), \code{2} (lower-confidence), or +\code{3} (retired). \code{ashm_regions$build} is \code{"grch37"} or \code{"hg38"}. +} + \examples{ \dontrun{ con <- gambl_reference_db() dplyr::tbl(con, "gene_entity") + +# high-confidence DLBCL genes +dplyr::tbl(con, "gene_entity") |> + dplyr::filter(entity == "DLBCL", tier == 1) |> + dplyr::collect() } } From 60243d3c6a1603a410ae88f4ffe46ec6df6c816e Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sat, 11 Jul 2026 13:29:24 -0700 Subject: [PATCH 04/58] Use get_ssm_by_regions + tabix -R for gene-restricted SSM pulls pull_data(), the publication-samples blocks, and the trios blocks pulled every mutation for a sample set via get_ssm_by_samples() only to filter down to all_lymphoma_genes afterward. Replace with get_ssm_by_regions() against a shared lymphoma_genes_bed_grch37/hg38 (built once via gene_to_region()), so tabix -R restricts to the gene loci up front instead of discarding everything outside them after the fact. cell_lines_data and coding_maf were left on get_ssm_by_samples since they don't filter by gene. Also wrap every get_ssm_by_samples()/get_ssm_by_regions() call site (15 total, including the pre-existing aSHM pulls) in a time_it() helper that logs wall-clock time, to compare against get_ssm_by_samples(subset_from_merge = TRUE/FALSE) on real GSC data. Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 329 +++++++++++++++++-------------- 1 file changed, 184 insertions(+), 145 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 1b01eac..28f7123 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -61,6 +61,32 @@ all_cols <- c( # restrict to the most inclusive DLBCL gene list all_lymphoma_genes <- lymphoma_genes_comprehensive$Gene +# Built once, reused by every get_ssm_by_regions() call below that wants only +# lymphoma-gene mutations. Restricting via tabix -R (region) instead of +# pulling every mutation for a sample set and filtering by Hugo_Symbol +# afterward is the whole point -- see get_ssm_by_regions(). +lymphoma_genes_bed_grch37 <- create_bed_data( + gene_to_region(gene_symbol = all_lymphoma_genes, projection = "grch37", return_as = "bed"), + genome_build = "grch37" +) +lymphoma_genes_bed_hg38 <- create_bed_data( + gene_to_region(gene_symbol = all_lymphoma_genes, projection = "hg38", return_as = "bed"), + genome_build = "hg38" +) + +# Wraps an expression, printing its wall-clock time; returns the expression's +# value unchanged. Used below to benchmark get_ssm_by_samples() +# (subset_from_merge TRUE vs FALSE, per-sample loop) against +# get_ssm_by_regions() (tabix -R) at each call site, to decide which +# approach to standardize on for each use case. +time_it <- function(label, expr) { + t0 <- Sys.time() + result <- expr + elapsed <- as.numeric(difftime(Sys.time(), t0, units = "secs")) + message(sprintf("[TIMING] %-55s %8.1fs", label, elapsed)) + result +} + # Importing BL data from Thomas et al # It has more patients and also contains sample ids, not just patient ids @@ -239,17 +265,15 @@ pull_data <- function( pull_meta, pull_projection = "grch37" ){ - slms3 <- get_ssm_by_samples( + lymphoma_genes_bed <- if(pull_projection == "grch37") lymphoma_genes_bed_grch37 else lymphoma_genes_bed_hg38 + slms3 <- get_ssm_by_regions( + regions_bed = lymphoma_genes_bed, these_samples_metadata = pull_meta, basic_columns = FALSE, - projection = pull_projection, - subset_from_merge = FALSE + projection = pull_projection ) %>% select( all_of(all_cols) - ) %>% - filter( - Hugo_Symbol %in% all_lymphoma_genes ) return(slms3) } @@ -344,10 +368,9 @@ all_capture_meta <- bind_rows( reddy_meta_gambl ) -# warning: this is very slow! -all_capture_grch37_ssm_to_bundle <- pull_data(all_capture_meta) +all_capture_grch37_ssm_to_bundle <- time_it("pull_data(all_capture_meta, grch37)", pull_data(all_capture_meta)) print("Done collecting grch37") -all_capture_hg38_ssm_to_bundle <- pull_data(all_capture_meta, "hg38") +all_capture_hg38_ssm_to_bundle <- time_it("pull_data(all_capture_meta, hg38)", pull_data(all_capture_meta, "hg38")) print("Done collecting hg38") @@ -377,16 +400,20 @@ cell_lines_data$meta_to_bundle <- data.frame( colnames(cell_lines_data$meta_to_bundle) <- colnames_for_bundled_meta -cell_lines_data$grch37$ssm_to_bundle <- get_ssm_by_samples( - these_samples_metadata = cell_lines_data$meta, - basic_columns = FALSE -) %>% select(all_of(all_cols)) +cell_lines_data$grch37$ssm_to_bundle <- time_it("cell_lines get_ssm_by_samples grch37", { + get_ssm_by_samples( + these_samples_metadata = cell_lines_data$meta, + basic_columns = FALSE + ) %>% select(all_of(all_cols)) +}) -cell_lines_data$hg38$ssm_to_bundle <- get_ssm_by_samples( - these_samples_metadata = cell_lines_data$meta, - projection = "hg38", - basic_columns = FALSE -) %>% select(all_of(all_cols)) +cell_lines_data$hg38$ssm_to_bundle <- time_it("cell_lines get_ssm_by_samples hg38", { + get_ssm_by_samples( + these_samples_metadata = cell_lines_data$meta, + projection = "hg38", + basic_columns = FALSE + ) %>% select(all_of(all_cols)) +}) cell_lines_data$grch37$cnv_to_bundle <- get_cn_segments( these_samples_metadata = cell_lines_data$meta, @@ -590,13 +617,15 @@ this_study_samples <- GAMBLR.data::sample_data$meta %>% pull(sample_id) # FLs in grch37 -coding_maf <- get_ssm_by_samples( - these_samples_metadata = get_gambl_metadata() %>% - filter(sample_id %in% this_study_samples), - basic_columns = FALSE) %>% - select( - all_of(selected_columns) - ) +coding_maf <- time_it("coding_maf get_ssm_by_samples", { + get_ssm_by_samples( + these_samples_metadata = get_gambl_metadata() %>% + filter(sample_id %in% this_study_samples), + basic_columns = FALSE) %>% + select( + all_of(selected_columns) + ) +}) fl_data$ssm_to_bundle <- fl_data$ssm_to_bundle %>% dplyr::left_join( @@ -631,15 +660,17 @@ regions_bed_grch37 = GAMBLR.utils::create_bed_data( ) # Add aSHM mutations for the already released samples -grch37_ashm <- get_ssm_by_regions( - these_samples_metadata = sample_data$meta, - regions_bed = regions_bed_grch37, - streamlined = FALSE, - basic_columns = FALSE -) %>% - select( - any_of(c(colnames(sample_data$grch37$maf), maf_columns_to_keep)) - ) +grch37_ashm <- time_it("grch37_ashm get_ssm_by_regions", { + get_ssm_by_regions( + these_samples_metadata = sample_data$meta, + regions_bed = regions_bed_grch37, + streamlined = FALSE, + basic_columns = FALSE + ) %>% + select( + any_of(c(colnames(sample_data$grch37$maf), maf_columns_to_keep)) + ) +}) grch37_ashm <- grch37_ashm %>% filter(Tumor_Sample_Barcode %in% sample_data$meta$Tumor_Sample_Barcode) @@ -659,20 +690,22 @@ grch37_ashm <- left_join( studies ) -hg38_ashm <- get_ssm_by_regions( - these_samples_metadata = sample_data$meta, - regions_bed = GAMBLR.utils::create_bed_data( - GAMBLR.data::hg38_ashm_regions, - fix_names = "concat", - concat_cols = c("gene","region"),sep="-" - ), - projection = "hg38", - streamlined = FALSE, - basic_columns = FALSE -) %>% - select( - any_of(c(colnames(sample_data$hg38$maf), maf_columns_to_keep)) - ) +hg38_ashm <- time_it("hg38_ashm get_ssm_by_regions", { + get_ssm_by_regions( + these_samples_metadata = sample_data$meta, + regions_bed = GAMBLR.utils::create_bed_data( + GAMBLR.data::hg38_ashm_regions, + fix_names = "concat", + concat_cols = c("gene","region"),sep="-" + ), + projection = "hg38", + streamlined = FALSE, + basic_columns = FALSE + ) %>% + select( + any_of(c(colnames(sample_data$hg38$maf), maf_columns_to_keep)) + ) +}) hg38_ashm <- hg38_ashm %>% filter(Tumor_Sample_Barcode %in% sample_data$meta$Tumor_Sample_Barcode) @@ -706,45 +739,43 @@ publication_samples <- c( ) print("extracting grch37 mutations in lymphoma genes with GAMBLR.results") -sample_data$grch37$maf <- get_ssm_by_samples( - these_samples_metadata = get_gambl_metadata() %>% - filter(sample_id %in% publication_samples), - basic_columns = FALSE, - subset_from_merge = FALSE) %>% - filter( - Hugo_Symbol %in% all_lymphoma_genes - ) %>% - mutate(Pipeline = "SLMS-3") %>% - left_join( - ., - studies - ) %>% - select(colnames(sample_data$grch37$maf)) %>% - bind_rows( - ., - sample_data$grch37$maf - ) +sample_data$grch37$maf <- time_it("publication-samples get_ssm_by_regions grch37", { + get_ssm_by_regions( + regions_bed = lymphoma_genes_bed_grch37, + these_samples_metadata = get_gambl_metadata() %>% + filter(sample_id %in% publication_samples), + basic_columns = FALSE) %>% + mutate(Pipeline = "SLMS-3") %>% + left_join( + ., + studies + ) %>% + select(colnames(sample_data$grch37$maf)) %>% + bind_rows( + ., + sample_data$grch37$maf + ) +}) print("extracting hg38 mutations in lymphoma genes with GAMBLR.results") -sample_data$hg38$maf <- get_ssm_by_samples( - these_samples_metadata = get_gambl_metadata() %>% - filter(sample_id %in% publication_samples), - projection = "hg38", - basic_columns = FALSE, - subset_from_merge = FALSE) %>% - filter( - Hugo_Symbol %in% all_lymphoma_genes - ) %>% - mutate(Pipeline = "SLMS-3") %>% - left_join( - ., - studies - ) %>% - select(colnames(sample_data$hg38$maf)) %>% - bind_rows( - ., - sample_data$hg38$maf - ) +sample_data$hg38$maf <- time_it("publication-samples get_ssm_by_regions hg38", { + get_ssm_by_regions( + regions_bed = lymphoma_genes_bed_hg38, + these_samples_metadata = get_gambl_metadata() %>% + filter(sample_id %in% publication_samples), + projection = "hg38", + basic_columns = FALSE) %>% + mutate(Pipeline = "SLMS-3") %>% + left_join( + ., + studies + ) %>% + select(colnames(sample_data$hg38$maf)) %>% + bind_rows( + ., + sample_data$hg38$maf + ) +}) print("done extracting all mutations in lymphoma genes with GAMBLR.results") setwd(PKG_ROOT) @@ -831,31 +862,33 @@ sample_data$meta <- fix ### end metadata fixing # trios grch37 ssm -genome_trios_ssm_grch37 <- get_ssm_by_samples( - these_samples_metadata = trios_meta %>% - filter(seq_type == "genome"), - basic_columns = FALSE, - subset_from_merge = TRUE -) %>% - filter(Hugo_Symbol %in% all_lymphoma_genes) %>% - mutate( - Pipeline = "SLMS-3", - Study = "Hilton" +genome_trios_ssm_grch37 <- time_it("trios get_ssm_by_regions genome grch37", { + get_ssm_by_regions( + regions_bed = lymphoma_genes_bed_grch37, + these_samples_metadata = trios_meta %>% + filter(seq_type == "genome"), + basic_columns = FALSE ) %>% - select(all_of(colnames(sample_data$grch37$maf))) + mutate( + Pipeline = "SLMS-3", + Study = "Hilton" + ) %>% + select(all_of(colnames(sample_data$grch37$maf))) +}) print("extracting mutations for Trios cohort") -capture_trios_ssm_grch37 <- get_ssm_by_samples( - these_samples_metadata = trios_meta %>% - filter(seq_type == "capture"), - basic_columns = FALSE, - subset_from_merge = TRUE -) %>% - filter(Hugo_Symbol %in% all_lymphoma_genes) %>% - mutate( - Pipeline = "SLMS-3", - Study = "Hilton" +capture_trios_ssm_grch37 <- time_it("trios get_ssm_by_regions capture grch37", { + get_ssm_by_regions( + regions_bed = lymphoma_genes_bed_grch37, + these_samples_metadata = trios_meta %>% + filter(seq_type == "capture"), + basic_columns = FALSE ) %>% - select(all_of(colnames(sample_data$grch37$maf))) + mutate( + Pipeline = "SLMS-3", + Study = "Hilton" + ) %>% + select(all_of(colnames(sample_data$grch37$maf))) +}) trios_ssm_grch37 <- bind_rows( genome_trios_ssm_grch37, @@ -863,33 +896,35 @@ trios_ssm_grch37 <- bind_rows( ) # trios hg38 ssm -genome_trios_ssm_hg38 <- get_ssm_by_samples( - these_samples_metadata = trios_meta %>% - filter(seq_type == "genome"), - basic_columns = FALSE, - subset_from_merge = TRUE, - projection = "hg38" -) %>% - filter(Hugo_Symbol %in% all_lymphoma_genes) %>% - mutate( - Pipeline = "SLMS-3", - Study = "Hilton" +genome_trios_ssm_hg38 <- time_it("trios get_ssm_by_regions genome hg38", { + get_ssm_by_regions( + regions_bed = lymphoma_genes_bed_hg38, + these_samples_metadata = trios_meta %>% + filter(seq_type == "genome"), + basic_columns = FALSE, + projection = "hg38" ) %>% - select(all_of(colnames(sample_data$hg38$maf))) - -capture_trios_ssm_hg38 <- get_ssm_by_samples( - these_samples_metadata = trios_meta %>% - filter(seq_type == "capture"), - basic_columns = FALSE, - subset_from_merge = TRUE, - projection = "hg38" -) %>% - filter(Hugo_Symbol %in% all_lymphoma_genes) %>% - mutate( - Pipeline = "SLMS-3", - Study = "Hilton" + mutate( + Pipeline = "SLMS-3", + Study = "Hilton" + ) %>% + select(all_of(colnames(sample_data$hg38$maf))) +}) + +capture_trios_ssm_hg38 <- time_it("trios get_ssm_by_regions capture hg38", { + get_ssm_by_regions( + regions_bed = lymphoma_genes_bed_hg38, + these_samples_metadata = trios_meta %>% + filter(seq_type == "capture"), + basic_columns = FALSE, + projection = "hg38" ) %>% - select(all_of(colnames(sample_data$hg38$maf))) + mutate( + Pipeline = "SLMS-3", + Study = "Hilton" + ) %>% + select(all_of(colnames(sample_data$hg38$maf))) +}) trios_ssm_hg38 <- bind_rows( genome_trios_ssm_hg38, @@ -913,12 +948,14 @@ regions_bed <- create_bed_data( sep = "-" ) -trios_ashm_grch37 <- get_ssm_by_regions( - these_samples_metadata = trios_meta, - regions_bed = regions_bed, - streamlined = FALSE, - basic_columns = FALSE -) +trios_ashm_grch37 <- time_it("trios_ashm get_ssm_by_regions grch37", { + get_ssm_by_regions( + these_samples_metadata = trios_meta, + regions_bed = regions_bed, + streamlined = FALSE, + basic_columns = FALSE + ) +}) trios_ashm_grch37 <- trios_ashm_grch37 %>% filter(Tumor_Sample_Barcode %in% trios_meta$Tumor_Sample_Barcode) @@ -945,13 +982,15 @@ regions_bed <- create_bed_data( sep = "-" ) -trios_ashm_hg38 <- get_ssm_by_regions( - these_samples_metadata = trios_meta, - regions_bed = regions_bed, - projection = "hg38", - streamlined = FALSE, - basic_columns = FALSE -) +trios_ashm_hg38 <- time_it("trios_ashm get_ssm_by_regions hg38", { + get_ssm_by_regions( + these_samples_metadata = trios_meta, + regions_bed = regions_bed, + projection = "hg38", + streamlined = FALSE, + basic_columns = FALSE + ) +}) trios_ashm_hg38 <- trios_ashm_hg38 %>% filter(Tumor_Sample_Barcode %in% trios_meta$Tumor_Sample_Barcode) From 826eefcca4ec561c655c57aae059ca65ac9e50c9 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sat, 11 Jul 2026 16:05:41 -0700 Subject: [PATCH 05/58] Scope published-study SV bundling to local sample_data$meta The Manta SV block (feeding sample_data$grch37/hg38$bedpe) filtered against GAMBLR.data::sample_data$meta$sample_id -- the currently INSTALLED package's bundled metadata, not the metadata this run is actually assembling. That made bedpe row counts depend on whatever GAMBLR.data happened to be installed at build time rather than purely the input data (e.g. 941 vs 2352 rows for the same underlying SV calls, traced to the installed sample set shifting between builds -- confirmed via nrow(GAMBLR.data::sample_data$meta) vs nrow(bundled_meta) diverging). Move the block to run after sample_data$meta is finalized and filter against that local object instead, so bedpe scope is self-consistent with the rest of the bundle for a given run. maf/ashm/seg were never affected -- only this SV block referenced the installed package. Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 132 ++++++++++++++++--------------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 28f7123..fe5296a 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -436,68 +436,10 @@ cell_lines_data$hg38$sv_to_bundle <- get_manta_sv( projection = "hg38" ) -# Adding the manta SVs for published studies -full_genome_meta <- get_gambl_metadata(seq_type_filter = "genome") - -bundled_meta <- full_genome_meta %>% - filter( - sample_id %in% GAMBLR.data::sample_data$meta$sample_id - ) - -full_sv_to_bundle <- get_manta_sv( - these_samples_metadata = bundled_meta, - projection = "hg38" - ) - -annotated_sv_to_bundle <- annotate_sv( - full_sv_to_bundle, - genome_build = "hg38" -) -annotated_sv_to_bundle <- annotated_sv_to_bundle %>% - filter(!is.na(partner)) %>% - mutate( - chrom1 = paste0("chr", chrom1), - chrom2 = paste0("chr", chrom2) - ) - -# drop all annotation columns to restore original data subset just to the putative driver SVs -annotated_sv_keep <- left_join( - full_sv_to_bundle, - annotated_sv_to_bundle, - by = c( - "CHROM_A" = "chrom1", - "CHROM_B" = "chrom2", - "START_A" = "start1", - "tumour_sample_id") - ) %>% - dplyr::filter(!is.na(partner)) %>% - select(c(1:16)) - -# Now same for the grch37 projection -full_sv_to_bundle_grch37 <- get_manta_sv( - these_samples_metadata = bundled_meta -) - -annotated_sv_to_bundle_grch37 <- annotate_sv( - full_sv_to_bundle_grch37 -) - -annotated_sv_to_bundle_grch37 <- annotated_sv_to_bundle_grch37 %>% - filter(!is.na(partner)) - -# drop all annotation columns to restore original data subset just to the putative driver SVs -annotated_sv_keep_grch37 <- left_join( - full_sv_to_bundle_grch37, - annotated_sv_to_bundle_grch37, - by = c( - "CHROM_A" = "chrom1", - "CHROM_B" = "chrom2", - "START_A" = "start1", - "tumour_sample_id") - ) %>% - filter(!is.na(partner)) %>% - select(c(1:16)) - +# Manta SVs for published studies: moved to after sample_data$meta is +# finalized below (see "Adding the manta SVs for published studies"), so the +# sample scope is the freshly-assembled local metadata for this run rather +# than whatever GAMBLR.data happens to be installed. # Combine everything together sample_data <- list() @@ -564,6 +506,72 @@ sample_data$hg38$seg <- bind_rows( ) #add SVs +# Adding the manta SVs for published studies. Scoped to sample_data$meta (the +# metadata just finalized above, for this run) rather than +# GAMBLR.data::sample_data$meta (whatever happens to be installed) so the SV +# sample scope is self-consistent with the rest of this bundle instead of +# drifting with installed-package state across runs. +full_genome_meta <- get_gambl_metadata(seq_type_filter = "genome") + +bundled_meta <- full_genome_meta %>% + filter( + sample_id %in% sample_data$meta$sample_id + ) + +full_sv_to_bundle <- get_manta_sv( + these_samples_metadata = bundled_meta, + projection = "hg38" + ) + +annotated_sv_to_bundle <- annotate_sv( + full_sv_to_bundle, + genome_build = "hg38" +) +annotated_sv_to_bundle <- annotated_sv_to_bundle %>% + filter(!is.na(partner)) %>% + mutate( + chrom1 = paste0("chr", chrom1), + chrom2 = paste0("chr", chrom2) + ) + +# drop all annotation columns to restore original data subset just to the putative driver SVs +annotated_sv_keep <- left_join( + full_sv_to_bundle, + annotated_sv_to_bundle, + by = c( + "CHROM_A" = "chrom1", + "CHROM_B" = "chrom2", + "START_A" = "start1", + "tumour_sample_id") + ) %>% + dplyr::filter(!is.na(partner)) %>% + select(c(1:16)) + +# Now same for the grch37 projection +full_sv_to_bundle_grch37 <- get_manta_sv( + these_samples_metadata = bundled_meta +) + +annotated_sv_to_bundle_grch37 <- annotate_sv( + full_sv_to_bundle_grch37 +) + +annotated_sv_to_bundle_grch37 <- annotated_sv_to_bundle_grch37 %>% + filter(!is.na(partner)) + +# drop all annotation columns to restore original data subset just to the putative driver SVs +annotated_sv_keep_grch37 <- left_join( + full_sv_to_bundle_grch37, + annotated_sv_to_bundle_grch37, + by = c( + "CHROM_A" = "chrom1", + "CHROM_B" = "chrom2", + "START_A" = "start1", + "tumour_sample_id") + ) %>% + filter(!is.na(partner)) %>% + select(c(1:16)) + sample_data$grch37$bedpe <- annotated_sv_keep_grch37 sample_data$hg38$bedpe <- annotated_sv_keep From ca04099805f4c8262dbb45bc1c205c5a68ef48d3 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sat, 11 Jul 2026 16:27:43 -0700 Subject: [PATCH 06/58] Add compare_bedpe.R diagnostic for bedpe row-count drift Compares bedpe between the bundled sample_data.rda and a fresh gambl_mutations.db build by sample set, not just row totals: unique sample overlap, and per-sample row-count delta for samples present in both. Used to check whether a bedpe count difference (e.g. after the SV-scoping fix in assemble_bundled_data.R) comes from more samples having SVs at all, or the same samples yielding more SVs than before. Co-Authored-By: Claude Sonnet 5 --- data-raw/compare_bedpe.R | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 data-raw/compare_bedpe.R diff --git a/data-raw/compare_bedpe.R b/data-raw/compare_bedpe.R new file mode 100644 index 0000000..0c1e7c1 --- /dev/null +++ b/data-raw/compare_bedpe.R @@ -0,0 +1,82 @@ +# compare_bedpe.R +# +# Compares bedpe (Manta SV) rows between the OLD bundled sample_data.rda and +# the NEW gambl_mutations.db build, to determine whether a row-count +# difference comes from more samples having SVs at all (healthy growth) or +# the same samples yielding more SVs each (possible duplication / behavior +# change) -- e.g. to diagnose why bedpe's regression check in +# test_gambl_db.R doesn't match the old snapshot as closely as maf/ashm/seg. +# +# Usage: Rscript data-raw/compare_bedpe.R [gambl_mutations.db] [old sample_data.rda] + +suppressMessages({library(DBI); library(dplyr)}) + +args <- commandArgs(trailingOnly = TRUE) +db <- if (length(args) >= 1) args[[1]] else "gambl_mutations.db" +old_rda <- if (length(args) >= 2) args[[2]] else "data/sample_data.rda" + +# --- new build (from the DB just written) ----------------------------------- +con <- DBI::dbConnect(RSQLite::SQLite(), db) +new_bedpe <- DBI::dbGetQuery(con, "SELECT tumour_sample_id, genome_build FROM bedpe") +DBI::dbDisconnect(con) + +# --- old build (from the bundled .rda used by the optional regression check) - +e <- new.env() +load(old_rda, envir = e) +old_sd <- get("sample_data", envir = e) +old_bedpe <- bind_rows( + old_sd$grch37$bedpe %>% mutate(genome_build = "grch37"), + old_sd$hg38$bedpe %>% mutate(genome_build = "hg38") +) + +cat("=== row counts ===\n") +cat("old:", nrow(old_bedpe), " new:", nrow(new_bedpe), "\n\n") + +old_samples <- unique(old_bedpe$tumour_sample_id) +new_samples <- unique(new_bedpe$tumour_sample_id) + +cat("=== unique samples with >=1 SV ===\n") +cat("old:", length(old_samples), " new:", length(new_samples), "\n\n") + +cat("=== avg rows per sample ===\n") +cat("old:", round(nrow(old_bedpe) / length(old_samples), 2), + " new:", round(nrow(new_bedpe) / length(new_samples), 2), "\n\n") + +cat("=== sample-set overlap ===\n") +cat("in BOTH: ", length(intersect(old_samples, new_samples)), "\n") +cat("ONLY in new (added): ", length(setdiff(new_samples, old_samples)), "\n") +cat("ONLY in old (dropped):", length(setdiff(old_samples, new_samples)), "\n\n") + +# For samples present in BOTH builds: did their row count change? +shared <- intersect(old_samples, new_samples) +old_per_sample <- old_bedpe %>% filter(tumour_sample_id %in% shared) %>% + count(tumour_sample_id, name = "n_old") +new_per_sample <- new_bedpe %>% filter(tumour_sample_id %in% shared) %>% + count(tumour_sample_id, name = "n_new") +cmp <- full_join(old_per_sample, new_per_sample, by = "tumour_sample_id") %>% + mutate(n_old = coalesce(n_old, 0), n_new = coalesce(n_new, 0), delta = n_new - n_old) + +cat("=== for the", length(shared), "shared samples, did per-sample SV count change? ===\n") +cat("more SVs in new build:", sum(cmp$delta > 0), "\n") +cat("fewer SVs in new build:", sum(cmp$delta < 0), "\n") +cat("unchanged: ", sum(cmp$delta == 0), "\n") +cat("extra rows from shared samples having MORE (not from new samples):", + sum(pmax(cmp$delta, 0)), "\n\n") + +cat("=== top 10 shared samples by increase ===\n") +print(cmp %>% arrange(desc(delta)) %>% head(10)) + +# Bottom line: rows added by (new_samples - old_samples) is expected growth. +# Rows added by shared samples having MORE than before is the number that +# would need a real explanation -- e.g. Manta re-run, annotate_sv() filter +# change, duplication. +new_rows_from_new_samples <- nrow(filter(new_bedpe, tumour_sample_id %in% setdiff(new_samples, old_samples))) +dropped_rows <- nrow(filter(old_bedpe, tumour_sample_id %in% setdiff(old_samples, new_samples))) +shared_delta <- sum(cmp$delta) # net change among samples present in both + +cat("\n=== attribution of the", nrow(new_bedpe) - nrow(old_bedpe), "row increase ===\n") +cat("from newly-added samples: +", new_rows_from_new_samples, "\n") +cat("from dropped samples (removed): -", dropped_rows, "\n") +cat("net change among shared samples: ", ifelse(shared_delta >= 0, "+", ""), shared_delta, "\n") +cat("sum (should equal the row increase above):", + new_rows_from_new_samples - dropped_rows + shared_delta, "\n") From 57acefb334b1710e1fae40d73d9f0e529acc0be3 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sun, 12 Jul 2026 08:08:22 -0700 Subject: [PATCH 07/58] Generalize bedpe comparison to snv/cnv/sv event-level diffing Rename compare_bedpe.R -> compare_bundle_changes.R and extend it to cover all three data types (SNV: maf+ashm, CNV: seg, SV: bedpe), each compared at the level of individual events (sample + genome_build + coordinates), not just row totals: - per-sample_id counts of gained (new) and lost (missing) events - one example gained-event row per affected sample, written to {snv,cnv,sv}_gained_examples.log for quick manual inspection - full per-sample gained/lost counts written to {snv,cnv,sv}_persample_counts.tsv Co-Authored-By: Claude Sonnet 5 --- data-raw/compare_bedpe.R | 82 ------------------ data-raw/compare_bundle_changes.R | 139 ++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 82 deletions(-) delete mode 100644 data-raw/compare_bedpe.R create mode 100644 data-raw/compare_bundle_changes.R diff --git a/data-raw/compare_bedpe.R b/data-raw/compare_bedpe.R deleted file mode 100644 index 0c1e7c1..0000000 --- a/data-raw/compare_bedpe.R +++ /dev/null @@ -1,82 +0,0 @@ -# compare_bedpe.R -# -# Compares bedpe (Manta SV) rows between the OLD bundled sample_data.rda and -# the NEW gambl_mutations.db build, to determine whether a row-count -# difference comes from more samples having SVs at all (healthy growth) or -# the same samples yielding more SVs each (possible duplication / behavior -# change) -- e.g. to diagnose why bedpe's regression check in -# test_gambl_db.R doesn't match the old snapshot as closely as maf/ashm/seg. -# -# Usage: Rscript data-raw/compare_bedpe.R [gambl_mutations.db] [old sample_data.rda] - -suppressMessages({library(DBI); library(dplyr)}) - -args <- commandArgs(trailingOnly = TRUE) -db <- if (length(args) >= 1) args[[1]] else "gambl_mutations.db" -old_rda <- if (length(args) >= 2) args[[2]] else "data/sample_data.rda" - -# --- new build (from the DB just written) ----------------------------------- -con <- DBI::dbConnect(RSQLite::SQLite(), db) -new_bedpe <- DBI::dbGetQuery(con, "SELECT tumour_sample_id, genome_build FROM bedpe") -DBI::dbDisconnect(con) - -# --- old build (from the bundled .rda used by the optional regression check) - -e <- new.env() -load(old_rda, envir = e) -old_sd <- get("sample_data", envir = e) -old_bedpe <- bind_rows( - old_sd$grch37$bedpe %>% mutate(genome_build = "grch37"), - old_sd$hg38$bedpe %>% mutate(genome_build = "hg38") -) - -cat("=== row counts ===\n") -cat("old:", nrow(old_bedpe), " new:", nrow(new_bedpe), "\n\n") - -old_samples <- unique(old_bedpe$tumour_sample_id) -new_samples <- unique(new_bedpe$tumour_sample_id) - -cat("=== unique samples with >=1 SV ===\n") -cat("old:", length(old_samples), " new:", length(new_samples), "\n\n") - -cat("=== avg rows per sample ===\n") -cat("old:", round(nrow(old_bedpe) / length(old_samples), 2), - " new:", round(nrow(new_bedpe) / length(new_samples), 2), "\n\n") - -cat("=== sample-set overlap ===\n") -cat("in BOTH: ", length(intersect(old_samples, new_samples)), "\n") -cat("ONLY in new (added): ", length(setdiff(new_samples, old_samples)), "\n") -cat("ONLY in old (dropped):", length(setdiff(old_samples, new_samples)), "\n\n") - -# For samples present in BOTH builds: did their row count change? -shared <- intersect(old_samples, new_samples) -old_per_sample <- old_bedpe %>% filter(tumour_sample_id %in% shared) %>% - count(tumour_sample_id, name = "n_old") -new_per_sample <- new_bedpe %>% filter(tumour_sample_id %in% shared) %>% - count(tumour_sample_id, name = "n_new") -cmp <- full_join(old_per_sample, new_per_sample, by = "tumour_sample_id") %>% - mutate(n_old = coalesce(n_old, 0), n_new = coalesce(n_new, 0), delta = n_new - n_old) - -cat("=== for the", length(shared), "shared samples, did per-sample SV count change? ===\n") -cat("more SVs in new build:", sum(cmp$delta > 0), "\n") -cat("fewer SVs in new build:", sum(cmp$delta < 0), "\n") -cat("unchanged: ", sum(cmp$delta == 0), "\n") -cat("extra rows from shared samples having MORE (not from new samples):", - sum(pmax(cmp$delta, 0)), "\n\n") - -cat("=== top 10 shared samples by increase ===\n") -print(cmp %>% arrange(desc(delta)) %>% head(10)) - -# Bottom line: rows added by (new_samples - old_samples) is expected growth. -# Rows added by shared samples having MORE than before is the number that -# would need a real explanation -- e.g. Manta re-run, annotate_sv() filter -# change, duplication. -new_rows_from_new_samples <- nrow(filter(new_bedpe, tumour_sample_id %in% setdiff(new_samples, old_samples))) -dropped_rows <- nrow(filter(old_bedpe, tumour_sample_id %in% setdiff(old_samples, new_samples))) -shared_delta <- sum(cmp$delta) # net change among samples present in both - -cat("\n=== attribution of the", nrow(new_bedpe) - nrow(old_bedpe), "row increase ===\n") -cat("from newly-added samples: +", new_rows_from_new_samples, "\n") -cat("from dropped samples (removed): -", dropped_rows, "\n") -cat("net change among shared samples: ", ifelse(shared_delta >= 0, "+", ""), shared_delta, "\n") -cat("sum (should equal the row increase above):", - new_rows_from_new_samples - dropped_rows + shared_delta, "\n") diff --git a/data-raw/compare_bundle_changes.R b/data-raw/compare_bundle_changes.R new file mode 100644 index 0000000..1408433 --- /dev/null +++ b/data-raw/compare_bundle_changes.R @@ -0,0 +1,139 @@ +# compare_bundle_changes.R +# +# Compares the OLD bundled sample_data.rda against a NEW gambl_mutations.db +# build at the level of individual events (not just row totals), for each of +# three data types: SNV (maf + ashm), CNV (seg), SV (bedpe). For each type: +# - counts, per sample_id, how many events are NEW (gained: present in the +# new build but not the old) and how many are LOST (missing: present in +# the old build but not the new) +# - writes one example gained-event row per affected sample to +# /{snv,cnv,sv}_gained_examples.log, for quick manual inspection +# without dumping every gained row. At most one row per sample that has +# >=1 gained event, so at most as many rows as there are samples -- in +# practice fewer, since not every sample is affected. +# - also writes the full per-sample gained/lost counts to +# /{snv,cnv,sv}_persample_counts.tsv for further analysis. +# +# "Same event" is determined by an exact match on a type-specific key (sample +# id + genome_build + genomic coordinates), NOT by row position or count -- +# two rows only count as "the same" if every key column matches exactly. +# +# Usage: Rscript data-raw/compare_bundle_changes.R [gambl_mutations.db] [old sample_data.rda] [output dir] + +suppressMessages({library(DBI); library(dplyr)}) + +args <- commandArgs(trailingOnly = TRUE) +db <- if (length(args) >= 1) args[[1]] else "gambl_mutations.db" +old_rda <- if (length(args) >= 2) args[[2]] else "data/sample_data.rda" +outdir <- if (length(args) >= 3) args[[3]] else "." +dir.create(outdir, recursive = TRUE, showWarnings = FALSE) + +con <- DBI::dbConnect(RSQLite::SQLite(), db) +on.exit(DBI::dbDisconnect(con), add = TRUE) + +e <- new.env() +load(old_rda, envir = e) +old_sd <- get("sample_data", envir = e) + +# Generic per-sample gained/lost comparison for one data type. +# old_df, new_df : data frames (already build-stamped with genome_build if +# the type spans both builds) +# sample_col : name of the sample-id column for this type +# key_cols : columns (including sample_col and genome_build, if +# present) that together define a unique "event" -- an +# exact match on all of these means "the same event" +compare_events <- function(old_df, new_df, sample_col, key_cols, label) { + key_of <- function(df) { + # ":::" separator avoids ambiguous concatenation, e.g. chrom="1", + # start=23456789 vs chrom="12",start=3456789 both -> "123456789" if + # pasted with no separator at all + do.call(paste, c(as.list(select(df, all_of(key_cols))), sep = ":::")) + } + old_df$.key <- key_of(old_df) + new_df$.key <- key_of(new_df) + + gained <- new_df %>% filter(!.key %in% old_df$.key) + lost <- old_df %>% filter(!.key %in% new_df$.key) + + gained_per_sample <- gained %>% count(.data[[sample_col]], name = "n_gained") + lost_per_sample <- lost %>% count(.data[[sample_col]], name = "n_lost") + + per_sample <- full_join(gained_per_sample, lost_per_sample, by = sample_col) %>% + mutate(n_gained = coalesce(n_gained, 0L), n_lost = coalesce(n_lost, 0L)) %>% + arrange(desc(n_gained + n_lost)) + + cat(sprintf( + "\n=== %s: %d gained rows, %d lost rows (%d samples w/ gains, %d samples w/ losses) ===\n", + label, nrow(gained), nrow(lost), + sum(per_sample$n_gained > 0), sum(per_sample$n_lost > 0) + )) + print(head(per_sample, 10)) + + list(per_sample = per_sample, gained = gained, lost = lost) +} + +# One example gained row per affected sample. +write_examples <- function(gained_df, sample_col, path) { + if (nrow(gained_df) == 0) { + message("no gained rows -- skipping ", path) + return(invisible(NULL)) + } + examples <- gained_df %>% + group_by(.data[[sample_col]]) %>% + slice_head(n = 1) %>% + ungroup() %>% + select(-.key) + write.table(examples, path, sep = "\t", quote = FALSE, row.names = FALSE) + message("wrote ", nrow(examples), " example rows to ", path) +} + +write_counts <- function(per_sample, path) { + write.table(per_sample, path, sep = "\t", quote = FALSE, row.names = FALSE) + message("wrote per-sample counts to ", path) +} + +# --- SNV: maf + ashm combined ------------------------------------------------ +new_snv <- bind_rows( + dbGetQuery(con, "SELECT * FROM maf"), + dbGetQuery(con, "SELECT * FROM ashm") +) +old_snv <- bind_rows( + old_sd$grch37$maf %>% mutate(genome_build = "grch37"), + old_sd$hg38$maf %>% mutate(genome_build = "hg38"), + old_sd$grch37$ashm %>% mutate(genome_build = "grch37"), + old_sd$hg38$ashm %>% mutate(genome_build = "hg38") +) +snv_key <- c("Tumor_Sample_Barcode", "genome_build", "Chromosome", "Start_Position", "End_Position") +snv_cmp <- compare_events(old_snv, new_snv, "Tumor_Sample_Barcode", snv_key, "SNV (maf+ashm)") +write_examples(snv_cmp$gained, "Tumor_Sample_Barcode", file.path(outdir, "snv_gained_examples.log")) +write_counts(snv_cmp$per_sample, file.path(outdir, "snv_persample_counts.tsv")) + +# --- CNV: seg ----------------------------------------------------------------- +new_cnv <- dbGetQuery(con, "SELECT * FROM seg") +old_cnv <- bind_rows( + old_sd$grch37$seg %>% mutate(genome_build = "grch37"), + old_sd$hg38$seg %>% mutate(genome_build = "hg38") +) +cnv_key <- c("ID", "genome_build", "chrom", "start", "end") +cnv_cmp <- compare_events(old_cnv, new_cnv, "ID", cnv_key, "CNV (seg)") +write_examples(cnv_cmp$gained, "ID", file.path(outdir, "cnv_gained_examples.log")) +write_counts(cnv_cmp$per_sample, file.path(outdir, "cnv_persample_counts.tsv")) + +# --- SV: bedpe ------------------------------------------------------------------ +new_sv <- dbGetQuery(con, "SELECT * FROM bedpe") +old_sv <- bind_rows( + old_sd$grch37$bedpe %>% mutate(genome_build = "grch37"), + old_sd$hg38$bedpe %>% mutate(genome_build = "hg38") +) +sv_key <- c("tumour_sample_id", "genome_build", "CHROM_A", "START_A", "CHROM_B", "START_B") +sv_cmp <- compare_events(old_sv, new_sv, "tumour_sample_id", sv_key, "SV (bedpe)") +write_examples(sv_cmp$gained, "tumour_sample_id", file.path(outdir, "sv_gained_examples.log")) +write_counts(sv_cmp$per_sample, file.path(outdir, "sv_persample_counts.tsv")) + +cat("\n=== summary ===\n") +cat(sprintf("SNV: %d samples with gains, %d samples with losses\n", + sum(snv_cmp$per_sample$n_gained > 0), sum(snv_cmp$per_sample$n_lost > 0))) +cat(sprintf("CNV: %d samples with gains, %d samples with losses\n", + sum(cnv_cmp$per_sample$n_gained > 0), sum(cnv_cmp$per_sample$n_lost > 0))) +cat(sprintf("SV: %d samples with gains, %d samples with losses\n", + sum(sv_cmp$per_sample$n_gained > 0), sum(sv_cmp$per_sample$n_lost > 0))) From 6f879d079e80000617dbbe044b5dae8f8cbff9a1 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sun, 12 Jul 2026 08:41:22 -0700 Subject: [PATCH 08/58] Add lost-event example logs to compare_bundle_changes.R write_examples() was already generic (works on any event data frame, not just "gained"); call it on cmp$lost too, per type, writing {snv,cnv,sv}_lost_examples.log alongside the existing gained-examples logs and per-sample counts. One representative lost row per affected sample, so e.g. samples that lost all their SNVs can be inspected directly instead of just counted. Co-Authored-By: Claude Sonnet 5 --- data-raw/compare_bundle_changes.R | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/data-raw/compare_bundle_changes.R b/data-raw/compare_bundle_changes.R index 1408433..e87df96 100644 --- a/data-raw/compare_bundle_changes.R +++ b/data-raw/compare_bundle_changes.R @@ -7,10 +7,12 @@ # new build but not the old) and how many are LOST (missing: present in # the old build but not the new) # - writes one example gained-event row per affected sample to -# /{snv,cnv,sv}_gained_examples.log, for quick manual inspection -# without dumping every gained row. At most one row per sample that has -# >=1 gained event, so at most as many rows as there are samples -- in -# practice fewer, since not every sample is affected. +# /{snv,cnv,sv}_gained_examples.log, and one example lost-event +# row per affected sample to /{snv,cnv,sv}_lost_examples.log -- +# for quick manual inspection without dumping every gained/lost row. At +# most one row per sample that has >=1 gained (or lost) event, so at most +# as many rows as there are samples -- in practice fewer, since not every +# sample is affected. # - also writes the full per-sample gained/lost counts to # /{snv,cnv,sv}_persample_counts.tsv for further analysis. # @@ -72,13 +74,14 @@ compare_events <- function(old_df, new_df, sample_col, key_cols, label) { list(per_sample = per_sample, gained = gained, lost = lost) } -# One example gained row per affected sample. -write_examples <- function(gained_df, sample_col, path) { - if (nrow(gained_df) == 0) { - message("no gained rows -- skipping ", path) +# One example row per affected sample, from either the gained or the lost +# data frame returned by compare_events() -- whichever is passed in. +write_examples <- function(event_df, sample_col, path) { + if (nrow(event_df) == 0) { + message("no rows -- skipping ", path) return(invisible(NULL)) } - examples <- gained_df %>% + examples <- event_df %>% group_by(.data[[sample_col]]) %>% slice_head(n = 1) %>% ungroup() %>% @@ -106,6 +109,7 @@ old_snv <- bind_rows( snv_key <- c("Tumor_Sample_Barcode", "genome_build", "Chromosome", "Start_Position", "End_Position") snv_cmp <- compare_events(old_snv, new_snv, "Tumor_Sample_Barcode", snv_key, "SNV (maf+ashm)") write_examples(snv_cmp$gained, "Tumor_Sample_Barcode", file.path(outdir, "snv_gained_examples.log")) +write_examples(snv_cmp$lost, "Tumor_Sample_Barcode", file.path(outdir, "snv_lost_examples.log")) write_counts(snv_cmp$per_sample, file.path(outdir, "snv_persample_counts.tsv")) # --- CNV: seg ----------------------------------------------------------------- @@ -117,6 +121,7 @@ old_cnv <- bind_rows( cnv_key <- c("ID", "genome_build", "chrom", "start", "end") cnv_cmp <- compare_events(old_cnv, new_cnv, "ID", cnv_key, "CNV (seg)") write_examples(cnv_cmp$gained, "ID", file.path(outdir, "cnv_gained_examples.log")) +write_examples(cnv_cmp$lost, "ID", file.path(outdir, "cnv_lost_examples.log")) write_counts(cnv_cmp$per_sample, file.path(outdir, "cnv_persample_counts.tsv")) # --- SV: bedpe ------------------------------------------------------------------ @@ -128,6 +133,7 @@ old_sv <- bind_rows( sv_key <- c("tumour_sample_id", "genome_build", "CHROM_A", "START_A", "CHROM_B", "START_B") sv_cmp <- compare_events(old_sv, new_sv, "tumour_sample_id", sv_key, "SV (bedpe)") write_examples(sv_cmp$gained, "tumour_sample_id", file.path(outdir, "sv_gained_examples.log")) +write_examples(sv_cmp$lost, "tumour_sample_id", file.path(outdir, "sv_lost_examples.log")) write_counts(sv_cmp$per_sample, file.path(outdir, "sv_persample_counts.tsv")) cat("\n=== summary ===\n") From b9c7756f300cceb2a443eb57700fd2a19214def7 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sun, 12 Jul 2026 09:44:07 -0700 Subject: [PATCH 09/58] Disable min_corrected_cov QC filter for bundle assembly get_gambl_metadata()'s coverage-based QC filter (min_corrected_cov, default 15x) can silently drop samples that were already part of a released bundle out of a fresh rebuild -- e.g. 01-16433_tumorA/B, FFPE genomes with coverage under the bar, taking all their SNVs with them in compare_bundle_changes.R's diff. Shadow get_gambl_metadata() near the top of the script with min_corrected_cov=0 baked in, so every one of the ~13 call sites picks this up in one place: assembly should not lose samples already in the released dataset due to a QC threshold applied after the fact. Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index fe5296a..9bb0cad 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -20,6 +20,17 @@ if (!file.exists("config.yml") && !nzchar(Sys.getenv("R_CONFIG_FILE"))) { package = "GAMBLR.results")) } +# get_gambl_metadata()'s min_corrected_cov QC filter (default 15x) can drop +# samples that were previously part of a released bundle out of a fresh one +# (e.g. 01-16433_tumorA/B: FFPE genomes with coverage under the bar, silently +# excluded, taking all their SNVs with them). Disable it for bundle assembly +# so a rebuild doesn't lose samples that are already in the released dataset. +# Shadows the package function for the rest of this script, so every call +# site (there are ~13) picks this up in one place. +get_gambl_metadata <- function(...) { + GAMBLR.results::get_gambl_metadata(..., min_corrected_cov = 0) +} + # Global variables definition colnames_for_bundled_meta <- c( "patient_id", From f0886b937a7c1c1fbab7d7e8c6331eeeb3445120 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sun, 12 Jul 2026 11:35:32 -0700 Subject: [PATCH 10/58] Restore Hugo_Symbol filter + pad gene regions for tabix -R SSM pulls get_ssm_by_regions() with lymphoma_genes_bed restricted by genomic coordinates alone, which don't always agree with VEP's Hugo_Symbol assignment at gene boundaries: mutations in overlapping/neighbouring genes and non-coding loci (AC/AL/AF-prefixed lncRNA transcripts, etc.) were leaking in as false "gains", confirmed via snv_gained_examples.log showing 136 genes that were never in all_lymphoma_genes. Fix has two parts, applied to all 7 gene-restricted call sites (pull_data, both publication-samples blocks, all 4 trios blocks): - filter(Hugo_Symbol %in% all_lymphoma_genes) restored after the region pull, on the now-small result (cheap, unlike filtering pre-restriction) - gene_to_region() calls building lymphoma_genes_bed_grch37/hg38 now pass pad_length = GENE_PAD_BP (2000bp) so real target-gene mutations just outside the exact gene span aren't excluded by tabix before the filter even sees them aSHM-region call sites are unaffected -- they are legitimately region- defined regardless of Hugo_Symbol and don't get this filter. Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 9bb0cad..c7db98d 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -74,14 +74,25 @@ all_lymphoma_genes <- lymphoma_genes_comprehensive$Gene # Built once, reused by every get_ssm_by_regions() call below that wants only # lymphoma-gene mutations. Restricting via tabix -R (region) instead of -# pulling every mutation for a sample set and filtering by Hugo_Symbol -# afterward is the whole point -- see get_ssm_by_regions(). +# pulling every mutation for a sample set is the performance win; every call +# site still adds `filter(Hugo_Symbol %in% all_lymphoma_genes)` afterward on +# the now-small result, because gene_to_region()'s coordinates and VEP's +# Hugo_Symbol assignment don't always agree at gene boundaries -- without the +# post-filter, mutations from overlapping/neighbouring genes and non-coding +# loci (e.g. AC/AL/AF-prefixed lncRNA transcripts) leak in as false "gains". +# GENE_PAD_BP widens the tabix window itself so real target-gene mutations +# just outside gene_to_region()'s exact span (promoter/UTR/annotation-source +# discrepancies) aren't lost before the Hugo_Symbol filter even sees them; +# the filter makes over-padding cheap (extra I/O, not incorrect inclusion). +GENE_PAD_BP <- 2000 lymphoma_genes_bed_grch37 <- create_bed_data( - gene_to_region(gene_symbol = all_lymphoma_genes, projection = "grch37", return_as = "bed"), + gene_to_region(gene_symbol = all_lymphoma_genes, projection = "grch37", + return_as = "bed", pad_length = GENE_PAD_BP), genome_build = "grch37" ) lymphoma_genes_bed_hg38 <- create_bed_data( - gene_to_region(gene_symbol = all_lymphoma_genes, projection = "hg38", return_as = "bed"), + gene_to_region(gene_symbol = all_lymphoma_genes, projection = "hg38", + return_as = "bed", pad_length = GENE_PAD_BP), genome_build = "hg38" ) @@ -283,6 +294,7 @@ pull_data <- function( basic_columns = FALSE, projection = pull_projection ) %>% + filter(Hugo_Symbol %in% all_lymphoma_genes) %>% select( all_of(all_cols) ) @@ -437,7 +449,6 @@ cell_lines_data$hg38$cnv_to_bundle <- get_cn_segments( projection="hg38" ) %>% dplyr::select(all_of(c("ID","chrom","start","end","LOH_flag","log.ratio","CN","seg_seq_type"))) - cell_lines_data$grch37$sv_to_bundle <- get_manta_sv( these_samples_metadata = cell_lines_data$meta, ) @@ -764,6 +775,7 @@ sample_data$grch37$maf <- time_it("publication-samples get_ssm_by_regions grch37 these_samples_metadata = get_gambl_metadata() %>% filter(sample_id %in% publication_samples), basic_columns = FALSE) %>% + filter(Hugo_Symbol %in% all_lymphoma_genes) %>% mutate(Pipeline = "SLMS-3") %>% left_join( ., @@ -784,6 +796,7 @@ sample_data$hg38$maf <- time_it("publication-samples get_ssm_by_regions hg38", { filter(sample_id %in% publication_samples), projection = "hg38", basic_columns = FALSE) %>% + filter(Hugo_Symbol %in% all_lymphoma_genes) %>% mutate(Pipeline = "SLMS-3") %>% left_join( ., @@ -888,6 +901,7 @@ genome_trios_ssm_grch37 <- time_it("trios get_ssm_by_regions genome grch37", { filter(seq_type == "genome"), basic_columns = FALSE ) %>% + filter(Hugo_Symbol %in% all_lymphoma_genes) %>% mutate( Pipeline = "SLMS-3", Study = "Hilton" @@ -902,6 +916,7 @@ capture_trios_ssm_grch37 <- time_it("trios get_ssm_by_regions capture grch37", { filter(seq_type == "capture"), basic_columns = FALSE ) %>% + filter(Hugo_Symbol %in% all_lymphoma_genes) %>% mutate( Pipeline = "SLMS-3", Study = "Hilton" @@ -923,6 +938,7 @@ genome_trios_ssm_hg38 <- time_it("trios get_ssm_by_regions genome hg38", { basic_columns = FALSE, projection = "hg38" ) %>% + filter(Hugo_Symbol %in% all_lymphoma_genes) %>% mutate( Pipeline = "SLMS-3", Study = "Hilton" @@ -938,6 +954,7 @@ capture_trios_ssm_hg38 <- time_it("trios get_ssm_by_regions capture hg38", { basic_columns = FALSE, projection = "hg38" ) %>% + filter(Hugo_Symbol %in% all_lymphoma_genes) %>% mutate( Pipeline = "SLMS-3", Study = "Hilton" From 33b563b370b0679f90f1da15fcc4962e8cd20f65 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sun, 12 Jul 2026 12:11:52 -0700 Subject: [PATCH 11/58] Widen gene-region padding to 10kb; add n_retained to bundle diagnostic GENE_PAD_BP=2000 was still narrower than VEP's default 5kb upstream/downstream annotation window, so real Hugo_Symbol-labeled mutations just outside the old pad (e.g. an ID3 variant 4.2kb upstream of its TSS) were dropped by the tabix -R pull before the post-filter ever saw them. 10kb gives a 2x margin; the existing Hugo_Symbol filter keeps over-padding safe. compare_bundle_changes.R now also reports n_retained per sample so a sample's total-loss vs partial-loss status is visible directly in snv/cnv/sv_persample_counts.tsv, instead of needing to be inferred. --- data-raw/assemble_bundled_data.R | 6 +++++- data-raw/compare_bundle_changes.R | 25 ++++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index c7db98d..39ad7ee 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -84,7 +84,11 @@ all_lymphoma_genes <- lymphoma_genes_comprehensive$Gene # just outside gene_to_region()'s exact span (promoter/UTR/annotation-source # discrepancies) aren't lost before the Hugo_Symbol filter even sees them; # the filter makes over-padding cheap (extra I/O, not incorrect inclusion). -GENE_PAD_BP <- 2000 +# 10kb is a 2x margin over VEP's default 5kb upstream/downstream annotation +# window (the likely source of Hugo_Symbol on these rows) -- e.g. an ID3 +# variant 4.2kb upstream of its TSS (well inside VEP's 5kb default, but +# outside a 2kb pad) was confirmed lost under the old 2000bp value. +GENE_PAD_BP <- 10000 lymphoma_genes_bed_grch37 <- create_bed_data( gene_to_region(gene_symbol = all_lymphoma_genes, projection = "grch37", return_as = "bed", pad_length = GENE_PAD_BP), diff --git a/data-raw/compare_bundle_changes.R b/data-raw/compare_bundle_changes.R index e87df96..9eb1c03 100644 --- a/data-raw/compare_bundle_changes.R +++ b/data-raw/compare_bundle_changes.R @@ -54,14 +54,25 @@ compare_events <- function(old_df, new_df, sample_col, key_cols, label) { old_df$.key <- key_of(old_df) new_df$.key <- key_of(new_df) - gained <- new_df %>% filter(!.key %in% old_df$.key) - lost <- old_df %>% filter(!.key %in% new_df$.key) - - gained_per_sample <- gained %>% count(.data[[sample_col]], name = "n_gained") - lost_per_sample <- lost %>% count(.data[[sample_col]], name = "n_lost") - + gained <- new_df %>% filter(!.key %in% old_df$.key) + lost <- old_df %>% filter(!.key %in% new_df$.key) + retained <- old_df %>% filter(.key %in% new_df$.key) + + gained_per_sample <- gained %>% count(.data[[sample_col]], name = "n_gained") + lost_per_sample <- lost %>% count(.data[[sample_col]], name = "n_lost") + retained_per_sample <- retained %>% count(.data[[sample_col]], name = "n_retained") + + # n_retained (present in both old and new) is joined in separately -- it's + # looked up only for the samples already in per_sample (those with >=1 + # gained or lost event), not full_join'd, since retained_per_sample on its + # own covers essentially every sample including untouched ones. per_sample <- full_join(gained_per_sample, lost_per_sample, by = sample_col) %>% - mutate(n_gained = coalesce(n_gained, 0L), n_lost = coalesce(n_lost, 0L)) %>% + left_join(retained_per_sample, by = sample_col) %>% + mutate( + n_gained = coalesce(n_gained, 0L), + n_lost = coalesce(n_lost, 0L), + n_retained = coalesce(n_retained, 0L) + ) %>% arrange(desc(n_gained + n_lost)) cat(sprintf( From 80b9ee9e8fc9db6ee54f9785dd361deb790ccd35 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sun, 12 Jul 2026 12:32:26 -0700 Subject: [PATCH 12/58] remove pointless code --- data-raw/assemble_bundled_data.R | 7 ------- 1 file changed, 7 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 39ad7ee..e582518 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -453,14 +453,7 @@ cell_lines_data$hg38$cnv_to_bundle <- get_cn_segments( projection="hg38" ) %>% dplyr::select(all_of(c("ID","chrom","start","end","LOH_flag","log.ratio","CN","seg_seq_type"))) -cell_lines_data$grch37$sv_to_bundle <- get_manta_sv( - these_samples_metadata = cell_lines_data$meta, -) -cell_lines_data$hg38$sv_to_bundle <- get_manta_sv( - these_samples_metadata = cell_lines_data$meta, - projection = "hg38" -) # Manta SVs for published studies: moved to after sample_data$meta is # finalized below (see "Adding the manta SVs for published studies"), so the From fbb235af7c7224301b531cd557fcb4ade70b7401 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sun, 12 Jul 2026 20:25:22 -0700 Subject: [PATCH 13/58] Expand all_lymphoma_genes to include known gene aliases Companion to GAMBLR.utils's gene_to_region()/expand_gene_aliases() fix. all_lymphoma_genes feeds 7 filter(Hugo_Symbol %in% all_lymphoma_genes) post-filters in this script; without expansion those still drop rows annotated under a gene's other name (e.g. old/new HGNC histone names) even though the region pull now finds the right coordinates. --- data-raw/assemble_bundled_data.R | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index e582518..cc49da4 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -69,8 +69,11 @@ all_cols <- c( maf_columns_to_keep ) -# restrict to the most inclusive DLBCL gene list -all_lymphoma_genes <- lymphoma_genes_comprehensive$Gene +# restrict to the most inclusive DLBCL gene list. Expanded to include known +# aliases (e.g. old/new HGNC histone names) so the Hugo_Symbol %in% +# all_lymphoma_genes filters below don't drop rows annotated under a gene's +# other name. +all_lymphoma_genes <- GAMBLR.utils::expand_gene_aliases(lymphoma_genes_comprehensive$Gene) # Built once, reused by every get_ssm_by_regions() call below that wants only # lymphoma-gene mutations. Restricting via tabix -R (region) instead of From c43c1a2680d46945775745206a9519d4497e73f9 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Sun, 12 Jul 2026 21:52:03 -0700 Subject: [PATCH 14/58] compare_bundle_changes.R: tag SNV rows with their source table maf and ashm are assembled by different code paths in assemble_bundled_data.R (gene-region-restricted pull vs aSHM-region-restricted pull), but the SNV gained/lost report merged both into one undifferentiated bucket -- there was no way to tell which code path produced a given lost/gained example without manual digging. .source (not part of the comparison key) makes this visible directly in snv_persample_counts.tsv / snv_*_examples.log. --- data-raw/compare_bundle_changes.R | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/data-raw/compare_bundle_changes.R b/data-raw/compare_bundle_changes.R index 9eb1c03..0563dec 100644 --- a/data-raw/compare_bundle_changes.R +++ b/data-raw/compare_bundle_changes.R @@ -107,15 +107,22 @@ write_counts <- function(per_sample, path) { } # --- SNV: maf + ashm combined ------------------------------------------------ +# maf and ashm are built by different code paths in assemble_bundled_data.R +# (gene-region-restricted pull vs aSHM-region-restricted pull) -- .source +# records which table each row came from so gained/lost examples can be +# traced back to the right code path instead of being lumped together as +# an undifferentiated "SNV". Not part of the comparison key: a row that +# moved from one table to the other between builds still counts as +# retained if its sample/position match. new_snv <- bind_rows( - dbGetQuery(con, "SELECT * FROM maf"), - dbGetQuery(con, "SELECT * FROM ashm") + dbGetQuery(con, "SELECT * FROM maf") %>% mutate(.source = "maf"), + dbGetQuery(con, "SELECT * FROM ashm") %>% mutate(.source = "ashm") ) old_snv <- bind_rows( - old_sd$grch37$maf %>% mutate(genome_build = "grch37"), - old_sd$hg38$maf %>% mutate(genome_build = "hg38"), - old_sd$grch37$ashm %>% mutate(genome_build = "grch37"), - old_sd$hg38$ashm %>% mutate(genome_build = "hg38") + old_sd$grch37$maf %>% mutate(genome_build = "grch37", .source = "maf"), + old_sd$hg38$maf %>% mutate(genome_build = "hg38", .source = "maf"), + old_sd$grch37$ashm %>% mutate(genome_build = "grch37", .source = "ashm"), + old_sd$hg38$ashm %>% mutate(genome_build = "hg38", .source = "ashm") ) snv_key <- c("Tumor_Sample_Barcode", "genome_build", "Chromosome", "Start_Position", "End_Position") snv_cmp <- compare_events(old_snv, new_snv, "Tumor_Sample_Barcode", snv_key, "SNV (maf+ashm)") From 2866fe4c512db09566505ce02be0f9f0a72dac6c Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 13 Jul 2026 06:08:56 -0700 Subject: [PATCH 15/58] Drop idx_maf_gene: no query path filters maf by Hugo_Symbol Confirmed against every DB-querying function in GAMBLR.open (get_ssm_by_region(s), get_ssm_by_samples, get_coding_ssm, get_all_coding_ssm) via the shared GAMBLR.data::get_ssm_from_db() helper: gene-restricted queries resolve the gene to a region first (same logic used to populate these tables) and filter on (genome_build, Chromosome, Start_Position), never Hugo_Symbol directly. The index was pure overhead -- ~55MB on disk for a column no code path queries. --- R/gambl_mutations_db.R | 10 +++++++--- data-raw/write_mutations_db.R | 1 - man/gambl_mutations_db.Rd | 10 +++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/R/gambl_mutations_db.R b/R/gambl_mutations_db.R index f0d59e5..fbd71a6 100644 --- a/R/gambl_mutations_db.R +++ b/R/gambl_mutations_db.R @@ -40,9 +40,13 @@ #' #' ## Indexes #' `maf`/`ashm`: `(genome_build, Chromosome, Start_Position)`, -#' `Tumor_Sample_Barcode`, `Hugo_Symbol` (maf only), `Pipeline`, `Study` (maf -#' only). `seg`: `(genome_build, ID)`, `(genome_build, chrom, start)`. `bedpe`: -#' `tumour_sample_id`. `sample_meta`: `sample_id`, `Tumor_Sample_Barcode`. +#' `Tumor_Sample_Barcode`, `Pipeline`, `Study` (maf only). No index on +#' `Hugo_Symbol`: gene-restricted queries resolve the gene to a region first +#' (the same logic used to populate these tables) and filter on +#' `(genome_build, Chromosome, Start_Position)` instead -- see +#' `GAMBLR.data::get_ssm_from_db()`. `seg`: `(genome_build, ID)`, +#' `(genome_build, chrom, start)`. `bedpe`: `tumour_sample_id`. +#' `sample_meta`: `sample_id`, `Tumor_Sample_Barcode`. #' #' @param db_path Optional explicit path to the .db file. #' diff --git a/data-raw/write_mutations_db.R b/data-raw/write_mutations_db.R index abf294d..117273c 100644 --- a/data-raw/write_mutations_db.R +++ b/data-raw/write_mutations_db.R @@ -48,7 +48,6 @@ write_mutations_db <- function(sample_data, idx <- c( "CREATE INDEX idx_maf_pos ON maf(genome_build, Chromosome, Start_Position)", "CREATE INDEX idx_maf_sample ON maf(Tumor_Sample_Barcode)", - "CREATE INDEX idx_maf_gene ON maf(Hugo_Symbol)", "CREATE INDEX idx_maf_pipe ON maf(Pipeline)", "CREATE INDEX idx_maf_study ON maf(Study)", "CREATE INDEX idx_ashm_pos ON ashm(genome_build, Chromosome, Start_Position)", diff --git a/man/gambl_mutations_db.Rd b/man/gambl_mutations_db.Rd index b8e1047..a2396bd 100644 --- a/man/gambl_mutations_db.Rd +++ b/man/gambl_mutations_db.Rd @@ -55,9 +55,13 @@ to \code{sample_meta$sample_id}. \subsection{Indexes}{ \code{maf}/\code{ashm}: \verb{(genome_build, Chromosome, Start_Position)}, -\code{Tumor_Sample_Barcode}, \code{Hugo_Symbol} (maf only), \code{Pipeline}, \code{Study} (maf -only). \code{seg}: \verb{(genome_build, ID)}, \verb{(genome_build, chrom, start)}. \code{bedpe}: -\code{tumour_sample_id}. \code{sample_meta}: \code{sample_id}, \code{Tumor_Sample_Barcode}. +\code{Tumor_Sample_Barcode}, \code{Pipeline}, \code{Study} (maf only). No index on +\code{Hugo_Symbol}: gene-restricted queries resolve the gene to a region first +(the same logic used to populate these tables) and filter on +\verb{(genome_build, Chromosome, Start_Position)} instead -- see +\code{GAMBLR.data::get_ssm_from_db()}. \code{seg}: \verb{(genome_build, ID)}, +\verb{(genome_build, chrom, start)}. \code{bedpe}: \code{tumour_sample_id}. +\code{sample_meta}: \code{sample_id}, \code{Tumor_Sample_Barcode}. } } From 1a8cfb6ec0fd90b71eea679add1a3157b1763b78 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 13 Jul 2026 14:49:19 -0700 Subject: [PATCH 16/58] Replace inline Study tagging with a sample_study join table; dedup SLMS-3 pulls Root cause of a real, confirmed bug: assemble_bundled_data.R pulled SLMS-3 mutation calls via separate, cohort-specific code blocks, each independently deciding which samples to pull for. A sample belonging to more than one cohort block (e.g. the Dreval FL x Hilton trios overlap) got its mutations pulled and bound multiple times -- verified directly against the previously-shipped gambl_mutations.db: 1,797 exact duplicate maf rows (same pattern in ashm), plus true duplicate rows in sample_meta for the same 5 samples. There was no deduplication safety net anywhere in the pipeline. This surfaced while replacing the Arthur cohort's raw, completely unfiltered strelka flat-file dump (2.83M rows / 65,121 distinct genes, missing the lymphoma-gene-panel filter every other block applies) with a proper SLMS-3 pull. Root cause of both problems is the same: Study/cohort membership was used to gate whether mutations get pulled, instead of being tracked as independent sample metadata. - assemble_bundled_data.R: restructured into phases -- metadata (all cohorts, including Arthur and Hilton, previously added so late that Arthur got zero aSHM coverage and Hilton needed its own dedicated aSHM block to compensate) assembled once; one consolidated SLMS-3 pull for the full deduplicated sample universe instead of ~5 separate per-cohort pulls; Publication-pipeline pulls (genuinely distinct per-paper data, not a redundant recall) kept separate. Arthur's sample_id/Tumor_Sample_Barcode are no longer overwritten to its paper's own patient-level ID (the paper's "Case ID" is now captured as study_id instead); the all-capture cohorts' patient_id was being silently dropped before reaching sample_data$meta, fixed in the same pass since the exact code region was already being touched. - write_mutations_db.R: writes the new sample_study table (sample_id, study, study_id, reference_PMID -- a many-to-many bridge table, one row per (sample_id, study) pair, so multi-study samples need no schema change); adds a write-time defensive dedup on maf/ashm keyed on the same natural mutation-call key GAMBLR.open::get_ssm_by_region() already uses at read time, as a permanent safety net against any future re-introduction of overlapping cohort blocks; drops sample_meta.reference_PMID (single-valued, already demonstrably wrong for multi-study samples) now that sample_study is the source of truth. - get_ssm_from_db.R: this_study now resolves via a join against sample_study instead of a Study column on maf/ashm (which no longer exists), reusing the existing sample_ids/Tumor_Sample_Barcode filtering machinery rather than adding new plumbing. Verified against a synthetic DB: this_study alone, this_study+tool_name, this_study+sample_ids intersection, and an unmatched study (returns zero rows, not an error) all behave correctly. - gambl_mutations_db.R: schema docs updated for the new table and the Study column removal. - test_gambl_db.R: checks updated for the schema change; also fixes a pre-existing bug (asserted idx_maf_gene, which has never existed in write_mutations_db.R -- this check failed regardless of this refactor). Both the write-time dedup and the sample_study many-to-many mechanics were verified directly against synthetic data before this commit (see session record) since the full pipeline requires GSC access to run. --- R/gambl_mutations_db.R | 23 +- R/get_ssm_from_db.R | 26 +- data-raw/assemble_bundled_data.R | 766 +++++++++++++------------------ data-raw/test_gambl_db.R | 17 +- data-raw/write_mutations_db.R | 49 +- man/gambl_mutations_db.Rd | 23 +- man/get_ssm_from_db.Rd | 9 +- 7 files changed, 438 insertions(+), 475 deletions(-) diff --git a/R/gambl_mutations_db.R b/R/gambl_mutations_db.R index fbd71a6..9f9d80b 100644 --- a/R/gambl_mutations_db.R +++ b/R/gambl_mutations_db.R @@ -26,27 +26,38 @@ #' #' | Table | Rows (typical) | Grain | Key columns | #' | --- | --- | --- | --- | -#' | `maf` | ~3.6M | one somatic mutation call | `Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline, Study, genome_build` (+ ~35 more MAF-standard columns) | +#' | `maf` | ~3.6M | one somatic mutation call | `Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline, genome_build` (+ ~35 more MAF-standard columns) | #' | `ashm` | ~135k | one mutation call in an aSHM region | same schema as `maf` | #' | `seg` | ~126k | one copy-number segment | `ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build` | #' | `bedpe` | ~900 | one Manta structural-variant breakpoint pair | `CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build` | -#' | `sample_meta` | ~3.3k | one sample | `patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...` (31 columns total; its own `genome_build` column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) | -#' | `build_info` | 8 | key/value | provenance (`source`, `built_at`, `builder`) and expected row counts (`n_maf`, `n_ashm`, `n_seg`, `n_bedpe`, `n_samples`) used by the `data-raw/test_gambl_db.R` regression checks | +#' | `sample_meta` | ~3.3k | one sample | `patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...` (its own `genome_build` column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) | +#' | `sample_study` | varies | one (sample, study) membership fact -- many-to-many, a sample belonging to N studies is N rows | `sample_id, study, study_id, reference_PMID`. `study_id` is that study's own identifier for the sample where it differs from GAMBL's `sample_id` (e.g. a paper's own case/patient ID); NA where no study-specific ID has been sourced. | +#' | `build_info` | 9 | key/value | provenance (`source`, `built_at`, `builder`) and expected row counts (`n_maf`, `n_ashm`, `n_seg`, `n_bedpe`, `n_samples`, `n_sample_study`) used by the `data-raw/test_gambl_db.R` regression checks | +#' +#' `maf`/`ashm` do NOT carry a `Study` column. Cohort/study membership is +#' tracked once per sample in `sample_study`, not once per mutation row -- +#' a mutation row's `Study` couldn't represent a sample belonging to more +#' than one study anyway, and a per-row tag caused the same sample's +#' mutations to be pulled and duplicated once per cohort-specific code path +#' that claimed it (see `GAMBLR.data::get_ssm_from_db()`'s `this_study` +#' parameter for how to filter by study post-refactor). #' #' ## Join keys #' `maf`, `ashm`, and `bedpe` (via `tumour_sample_id`) key to #' `sample_meta$Tumor_Sample_Barcode` / `sample_meta$sample_id`; `seg$ID` keys -#' to `sample_meta$sample_id`. +#' to `sample_meta$sample_id`; `sample_study$sample_id` keys to +#' `sample_meta$sample_id`. #' #' ## Indexes #' `maf`/`ashm`: `(genome_build, Chromosome, Start_Position)`, -#' `Tumor_Sample_Barcode`, `Pipeline`, `Study` (maf only). No index on +#' `Tumor_Sample_Barcode`, `Pipeline` (maf only). No index on #' `Hugo_Symbol`: gene-restricted queries resolve the gene to a region first #' (the same logic used to populate these tables) and filter on #' `(genome_build, Chromosome, Start_Position)` instead -- see #' `GAMBLR.data::get_ssm_from_db()`. `seg`: `(genome_build, ID)`, #' `(genome_build, chrom, start)`. `bedpe`: `tumour_sample_id`. -#' `sample_meta`: `sample_id`, `Tumor_Sample_Barcode`. +#' `sample_meta`: `sample_id`, `Tumor_Sample_Barcode`. `sample_study`: +#' `sample_id`, `study`. #' #' @param db_path Optional explicit path to the .db file. #' diff --git a/R/get_ssm_from_db.R b/R/get_ssm_from_db.R index 07dc110..97d6aed 100644 --- a/R/get_ssm_from_db.R +++ b/R/get_ssm_from_db.R @@ -13,7 +13,14 @@ #' @param coding_only When TRUE, keep only coding `Variant_Classification`s. #' @param include_silent When FALSE (and `coding_only`), drop Silent mutations. #' @param min_read_support Keep only variants with `t_alt_count` >= this value. -#' @param this_study Optional single `Study` to restrict to. +#' @param this_study Optional single study to restrict to, matched against +#' `sample_study$study` (not a `maf`/`ashm` column -- see +#' [gambl_mutations_db()]'s schema docs). Resolves to the set of sample_ids +#' belonging to that study, intersected with `sample_ids` if both are +#' supplied; composes with `tool_name`/Pipeline exactly as before (e.g. +#' `this_study="Reddy"` alone returns Reddy's SLMS-3 recall by default, +#' while `this_study="Reddy", tool_name="publication"` returns Reddy's +#' as-published rows only). #' @param regions Optional data frame with columns `chrom`, `start`, `end`; rows #' are OR-ed (each region is an indexed range scan). #' @param con Optional DBI connection (defaults to [gambl_mutations_db()]). @@ -35,12 +42,27 @@ get_ssm_from_db <- function(projection = "grch37", cc <- if (include_silent) coding_class else coding_class[coding_class != "Silent"] tn <- if (!is.null(tool_name)) tolower(tool_name) else NULL + # this_study resolves to sample_ids via a join against sample_study, done + # once here rather than as a per-query filter inside base_query() -- reuses + # the existing sample_ids/Tumor_Sample_Barcode machinery below instead of + # requiring maf/ashm to carry their own Study column (they don't; see + # gambl_mutations_db()'s schema docs), and keeps this_study (which + # samples) and tool_name/Pipeline (which rows for those samples) composable + # exactly as they were. + if (!is.null(this_study)) { + study_sample_ids <- dplyr::tbl(con, "sample_study") %>% + dplyr::filter(study == this_study) %>% + dplyr::distinct(sample_id) %>% + dplyr::pull(sample_id) + sample_ids <- if (is.null(sample_ids)) study_sample_ids + else intersect(sample_ids, study_sample_ids) + } + # apply the filters that are common to a single (table, region) query base_query <- function(table_name, region = NULL) { q <- dplyr::tbl(con, table_name) %>% dplyr::filter(genome_build == projection) if (!is.null(tn)) q <- dplyr::filter(q, tolower(Pipeline) == tn) - if (!is.null(this_study)) q <- dplyr::filter(q, Study == this_study) if (coding_only) q <- dplyr::filter(q, Variant_Classification %in% cc) if (min_read_support > 0) q <- dplyr::filter(q, t_alt_count >= min_read_support) if (!is.null(sample_ids)) q <- dplyr::filter(q, Tumor_Sample_Barcode %in% sample_ids) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index cc49da4..de0d9f6 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -116,8 +116,42 @@ time_it <- function(label, expr) { result } +##### Importing SLMS-3 variants +# Reused by the single consolidated SLMS-3 pull in Phase 4 below (and by +# nothing else -- every cohort-specific SLMS-3 pull that used to call this +# once per cohort block has been removed; see Phase 4). +pull_data <- function( + pull_meta, + pull_projection = "grch37" + ){ + lymphoma_genes_bed <- if(pull_projection == "grch37") lymphoma_genes_bed_grch37 else lymphoma_genes_bed_hg38 + slms3 <- get_ssm_by_regions( + regions_bed = lymphoma_genes_bed, + these_samples_metadata = pull_meta, + basic_columns = FALSE, + projection = pull_projection + ) %>% + filter(Hugo_Symbol %in% all_lymphoma_genes) %>% + select( + all_of(all_cols) + ) + return(slms3) +} + + +# ============================================================================ +# Phase 1: metadata (and, where genuinely distinct per-paper data exists, +# Publication-pipeline SSM calls) for every cohort. No SLMS-3 pulling here -- +# that used to happen per-cohort-block, which silently duplicated mutation +# rows whenever a sample belonged to more than one cohort (confirmed: e.g. +# the Dreval FL x Hilton trios overlap). Every cohort below also builds a +# small (sample_id, study, study_id, reference_PMID) frame, assembled into +# the new sample_study table in Phase 2 -- this replaces the old inline +# `Study =` tagging on mutation rows entirely, for every pipeline, so cohort +# membership is tracked once per sample instead of once per mutation row. +# ============================================================================ -# Importing BL data from Thomas et al +# --- Thomas BL (grch37, genome) -------------------------------------------- # It has more patients and also contains sample ids, not just patient ids bl_data <- list() @@ -183,8 +217,15 @@ bl_data$cnv_to_bundle <- read_xlsx( filter(ID %in% bl_data$meta_to_bundle$sample_id) %>% mutate(CN = round(2 * 2^log.ratio)) +thomas_bl_study <- data.frame( + sample_id = bl_data$meta_to_bundle$sample_id, + study = "Thomas", + study_id = bl_data$meta_to_bundle$patient_id, + reference_PMID = pmids$Thomas_BL +) + -# Importing FL data from Dreval et al +# --- Dreval FL (grch37, genome) --------------------------------------------- fl_data <- list() fl_data$meta <- read_xlsx("inst/extdata/studies/FL_Dreval.xlsx", sheet = 1) @@ -237,9 +278,15 @@ fl_data$cnv_to_bundle <- read_xlsx( ) %>% mutate(CN = round(2 * 2^log.ratio)) +dreval_study <- data.frame( + sample_id = fl_data$meta_to_bundle$sample_id, + study = "Dreval", + study_id = fl_data$meta_to_bundle$patient_id, + reference_PMID = pmids$Dreval_FL +) -# Importing DLBCL data in hg38 from Thomas et al +# --- Thomas DLBCL (hg38, genome) -------------------------------------------- dlbcl_data <- list() dlbcl_data$meta <- read_xlsx("inst/extdata/studies/BL_Thomas.xlsx", sheet = 2) @@ -289,38 +336,27 @@ dlbcl_data$cnv_to_bundle <- read_xlsx( filter(ID %in% dlbcl_data$meta_to_bundle$sample_id) %>% mutate(CN = round(2 * 2^log.ratio)) -##### Importing SLMS-3 variants -pull_data <- function( - pull_meta, - pull_projection = "grch37" - ){ - lymphoma_genes_bed <- if(pull_projection == "grch37") lymphoma_genes_bed_grch37 else lymphoma_genes_bed_hg38 - slms3 <- get_ssm_by_regions( - regions_bed = lymphoma_genes_bed, - these_samples_metadata = pull_meta, - basic_columns = FALSE, - projection = pull_projection - ) %>% - filter(Hugo_Symbol %in% all_lymphoma_genes) %>% - select( - all_of(all_cols) - ) - return(slms3) -} +thomas_dlbcl_study <- data.frame( + sample_id = dlbcl_data$meta_to_bundle$sample_id, + study = "Thomas", + study_id = dlbcl_data$meta_to_bundle$patient_id, + reference_PMID = pmids$Thomas_BL +) -# Importing DLBCL capture data +# --- DLBCL capture cohorts: Reddy, Schmitz, Chapuy, Golub ------------------- reddy_data <- list() schmitz_data <- list() chapuy_data <- list() golub_data <- list() # Importing metadata from Reddy et al and updating IDs to be consistent with GAMBL metadata -reddy_meta <- read_excel( +reddy_meta_full <- read_excel( "inst/extdata/studies/DLBCL_Reddy.xlsx", sheet = 1 ) %>% mutate( + study_id = `Sample ID`, patient_id = paste0( "Reddy_", `Sample ID` @@ -339,7 +375,18 @@ reddy_meta <- read_excel( "UNCLASS", COO_consensus ) - ) %>% + ) + +# Reddy's own paper-specific sample identifier ("Sample ID"), captured +# before it's dropped by the select() below -- see reddy_study. +reddy_study <- data.frame( + sample_id = reddy_meta_full$sample_id, + study = "Reddy", + study_id = reddy_meta_full$study_id, + reference_PMID = pmids$Reddy_DLBCL +) + +reddy_meta <- reddy_meta_full %>% dplyr::select( sample_id, patient_id, @@ -351,11 +398,14 @@ reddy_meta <- read_excel( # (config is resolved via config.yml / the GAMBLR.results fallback; no chdir to # a gambl repo is needed to locate data — repo_base is absolute) +# patient_id is deliberately retained here (unlike the original version of +# this select(), which dropped it) so all-capture samples get a real +# patient_id in sample_data$meta instead of NA. reddy_meta_gambl <- get_gambl_metadata() %>% dplyr::filter(cohort == "dlbcl_reddy") %>% dplyr::select( - sample_id, lymphgen, EBV_status_inf, cohort, pathology, seq_type, - unix_group, genome_build, pairing_status, normal_sample_id + sample_id, patient_id, lymphgen, EBV_status_inf, cohort, pathology, + seq_type, unix_group, genome_build, pairing_status, normal_sample_id ) %>% mutate(reference_PMID = pmids$Reddy_DLBCL) @@ -398,13 +448,39 @@ all_capture_meta <- bind_rows( reddy_meta_gambl ) -all_capture_grch37_ssm_to_bundle <- time_it("pull_data(all_capture_meta, grch37)", pull_data(all_capture_meta)) -print("Done collecting grch37") -all_capture_hg38_ssm_to_bundle <- time_it("pull_data(all_capture_meta, hg38)", pull_data(all_capture_meta, "hg38")) -print("Done collecting hg38") +# No paper-specific ID has been parsed anywhere in this script for these +# three cohorts (unlike Reddy/Thomas/Dreval/Arthur, they're pulled straight +# from GAMBL's own metadata, never cross-referenced against each paper's own +# supplementary sample table) -- study_id is NA until that sourcing is done. +schmitz_study <- data.frame( + sample_id = schmitz_data$meta$sample_id, + study = "Schmitz", + study_id = NA_character_, + reference_PMID = pmids$Schmitz_DLBCL +) +chapuy_study <- data.frame( + sample_id = chapuy_data$meta$sample_id, + study = "Chapuy", + study_id = NA_character_, + reference_PMID = pmids$Chapuy_DLBCL +) +golub_study <- data.frame( + sample_id = golub_data$meta$sample_id, + study = "NCI_Golub", + study_id = NA_character_, + reference_PMID = pmids$Chapuy_other +) + +# Add data from Reddy paper's own original variant calls (distinct from the +# SLMS-3 recall above -- this is the as-published set, kept as its own +# Publication-pipeline pull, not deduplicated against SLMS-3). +reddy_original_maf <- read_tsv( + "inst/extdata/studies/reddy_original_variants_with_VAF.maf.gz" +) %>% + select(any_of(all_cols)) -# Importing DLBCL cell lines +# --- DLBCL cell lines -------------------------------------------------------- cell_lines_data <- list() cell_lines_data$meta <- get_gambl_metadata(seq_type_filter = "genome") %>% @@ -430,40 +506,95 @@ cell_lines_data$meta_to_bundle <- data.frame( colnames(cell_lines_data$meta_to_bundle) <- colnames_for_bundled_meta -cell_lines_data$grch37$ssm_to_bundle <- time_it("cell_lines get_ssm_by_samples grch37", { - get_ssm_by_samples( - these_samples_metadata = cell_lines_data$meta, - basic_columns = FALSE - ) %>% select(all_of(all_cols)) -}) - -cell_lines_data$hg38$ssm_to_bundle <- time_it("cell_lines get_ssm_by_samples hg38", { - get_ssm_by_samples( - these_samples_metadata = cell_lines_data$meta, - projection = "hg38", - basic_columns = FALSE - ) %>% select(all_of(all_cols)) -}) - cell_lines_data$grch37$cnv_to_bundle <- get_cn_segments( these_samples_metadata = cell_lines_data$meta, projection="grch37" -) %>% +) %>% dplyr::select(all_of(c("ID","chrom","start","end","LOH_flag","log.ratio","CN","seg_seq_type"))) cell_lines_data$hg38$cnv_to_bundle <- get_cn_segments( these_samples_metadata = cell_lines_data$meta, projection="hg38" -) %>% +) %>% dplyr::select(all_of(c("ID","chrom","start","end","LOH_flag","log.ratio","CN","seg_seq_type"))) +# No sample_study row for cell lines -- no published cohort applies to them +# (matches the previous Study=NA behaviour for this group). + + +# --- Arthur (grch37, genome) ------------------------------------------------- +# Previously bundled from a raw, completely unfiltered flat file +# (inst/extdata/studies/DLBCL_Arthur.maf.gz, tagged Pipeline="strelka") with +# no sample-list gate and no lymphoma-gene-panel restriction at all -- +# 2.83M rows / 65,121 distinct genes, ~18,480 "mutations" per sample vs +# ~137 for a properly gene-panel-restricted SLMS-3 sample. That block is +# removed entirely; Arthur's samples now flow through the same consolidated +# SLMS-3 pull as every other cohort (Phase 4). +arthur_case_ids <- read_xlsx( + "inst/extdata/studies/DLBCL_Arthur.xlsx", + sheet = 1 +) %>% filter(`WGS data` == 1) + +# sample_id/Tumor_Sample_Barcode are GAMBL's own real values throughout -- +# never overwritten to the paper's own patient-level ID (as the removed +# code used to do). Arthur's own "Case ID" is captured separately as +# study_id in arthur_study below, joined to metadata by sample_id like any +# other study-specific identifier. +arthur_meta <- get_gambl_metadata() %>% + filter( + patient_id %in% arthur_case_ids$`Case ID`, + seq_type == "genome", + ! grepl("tumor", sample_id) + ) %>% + mutate( + cohort = "DLBCL_Arthur", + reference_PMID = pmids$Arthur_DLBCL + ) + +arthur_study <- data.frame( + sample_id = arthur_meta$sample_id, + study = "Arthur", + study_id = arthur_meta$patient_id, + reference_PMID = pmids$Arthur_DLBCL +) + + +# --- Hilton trios (genome + capture, both projections) ---------------------- +trios_samples <- read_xlsx( + "inst/extdata/studies/DLBCL_Hilton.xlsx" +) %>% +drop_na(DNAseq_sample_id) + +# (config is resolved via config.yml / the GAMBLR.results fallback; no chdir to +# a gambl repo is needed to locate data — repo_base is absolute) + +trios_meta <- get_gambl_metadata() %>% + filter( + seq_type %in% c("genome", "capture"), + sample_id %in% trios_samples$DNAseq_sample_id + ) %>% + mutate( + cohort = "DLBCL_Hilton", + reference_PMID = pmids$Hilton_DLBCL + ) + +# Unlike Thomas/Dreval/Reddy/Arthur, trios_meta comes straight from +# get_gambl_metadata(), so GAMBL's real patient_id is already correct here -- +# used directly as study_id. +hilton_study <- data.frame( + sample_id = trios_meta$sample_id, + study = "Hilton", + study_id = trios_meta$patient_id, + reference_PMID = pmids$Hilton_DLBCL +) -# Manta SVs for published studies: moved to after sample_data$meta is -# finalized below (see "Adding the manta SVs for published studies"), so the -# sample scope is the freshly-assembled local metadata for this run rather -# than whatever GAMBLR.data happens to be installed. -# Combine everything together +# ============================================================================ +# Phase 2: assemble sample_data$meta and sample_data$sample_study once, now +# that every cohort's metadata (including Arthur and Hilton, previously +# added much later -- after the main aSHM pull had already run without them) +# is available from the start. +# ============================================================================ sample_data <- list() sample_data$meta <- bind_rows( @@ -489,43 +620,46 @@ sample_data$meta <- sample_data$meta %>% sample_data$meta <- bind_rows( sample_data$meta, - all_capture_meta + all_capture_meta, + arthur_meta, + trios_meta ) -sample_data$hg38$maf <- bind_rows( - bl_data$ssm_to_bundle %>% mutate( - Pipeline = "Publication", - Study = "Thomas" - ), - dlbcl_data$ssm_to_bundle %>% mutate( - Pipeline = "Publication", - Study = "Thomas" - ), - cell_lines_data$hg38$ssm %>% mutate( - Pipeline = "SLMS-3", - Study = NA - ), - all_capture_hg38_ssm_to_bundle %>% mutate( - Pipeline = "SLMS-3", - Study = case_when( - Tumor_Sample_Barcode %in% reddy_data$meta$sample_id ~ "Reddy", - Tumor_Sample_Barcode %in% schmitz_data$meta$sample_id ~ "Schmitz", - Tumor_Sample_Barcode %in% chapuy_data$meta$sample_id ~ "Chapuy", - Tumor_Sample_Barcode %in% golub_data$meta$sample_id ~ "NCI_Golub" - ) - ) -) +### begin metadata fixing +# This preserves the original cohort column and ensures there are no duplicates +# in the metadata +fix <- sample_data$meta +fix <- fix %>% rename(study = cohort) -sample_data$grch37$seg <- bind_rows( - fl_data$cnv_to_bundle, - cell_lines_data$grch37$cnv_to_bundle +fix <- left_join( + fix, + get_gambl_metadata() %>% + select(sample_id, seq_type, cohort) ) -sample_data$hg38$seg <- bind_rows( - bl_data$cnv_to_bundle, - dlbcl_data$cnv_to_bundle, - cell_lines_data$hg38$cnv_to_bundle -) +fix <- fix %>% filter(!is.na(study)) + +fix <- distinct(fix) + +sample_data$meta <- fix +### end metadata fixing + +# sample_study: a many-to-many bridge table (one row per (sample_id, study) +# pair, not one row per sample), replacing the old inline `Study =` tagging +# on mutation rows. A sample belonging to N studies is simply N rows here -- +# no schema change needed as multi-study overlap becomes more common. +sample_data$sample_study <- bind_rows( + thomas_bl_study, + thomas_dlbcl_study, + dreval_study, + reddy_study, + schmitz_study, + chapuy_study, + golub_study, + arthur_study, + hilton_study +) %>% distinct() + #add SVs # Adding the manta SVs for published studies. Scoped to sample_data$meta (the @@ -597,7 +731,28 @@ annotated_sv_keep_grch37 <- left_join( sample_data$grch37$bedpe <- annotated_sv_keep_grch37 sample_data$hg38$bedpe <- annotated_sv_keep -# This is needed for the proteinpainter compatibility +sample_data$grch37$seg <- bind_rows( + fl_data$cnv_to_bundle, + cell_lines_data$grch37$cnv_to_bundle +) + +sample_data$hg38$seg <- bind_rows( + bl_data$cnv_to_bundle, + dlbcl_data$cnv_to_bundle, + cell_lines_data$hg38$cnv_to_bundle +) + + +# ============================================================================ +# Phase 3: Publication-pipeline SSM data. Genuinely distinct per-paper data +# (not a redundant SLMS-3 recall), kept as separate, un-deduplicated pulls -- +# tagged Pipeline="Publication" only; cohort membership lives in +# sample_data$sample_study, not an inline Study column. +# ============================================================================ + +# This is needed for the proteinpainter compatibility. Reads from the +# *installed* GAMBLR.data::sample_data (not the locally-built sample_data +# above) -- a pre-existing, out-of-scope inconsistency, left as-is. GAMBLR.data::sample_data$meta$cohort %>% table selected_columns <- c( @@ -637,9 +792,15 @@ coding_maf <- bind_rows( coding_maf_dlbcl ) -dim(GAMBLR.data::sample_data$hg38$maf) - -sample_data$hg38$maf <- sample_data$hg38$maf %>% +# hg38 Publication rows (Thomas BL + Thomas DLBCL), enriched via the +# proteinpainter-compatibility join above. This used to be applied directly +# to sample_data$hg38$maf once it existed early in the script; now it's +# applied to the Publication-only rows before Phase 6 combines them with the +# consolidated SLMS-3 pull. +hg38_publication_rows <- bind_rows( + bl_data$ssm_to_bundle %>% mutate(Pipeline = "Publication"), + dlbcl_data$ssm_to_bundle %>% mutate(Pipeline = "Publication") +) %>% left_join(coding_maf) this_study_samples <- GAMBLR.data::sample_data$meta %>% @@ -662,34 +823,56 @@ fl_data$ssm_to_bundle <- fl_data$ssm_to_bundle %>% coding_maf ) %>% distinct() - -sample_data$grch37$maf <- bind_rows( - fl_data$ssm_to_bundle %>% mutate( - Pipeline = "Publication", - Study = "Dreval" - ), - cell_lines_data$grch37$ssm %>% mutate( - Pipeline = "SLMS-3", - Study = NA - ), - all_capture_grch37_ssm_to_bundle %>% mutate( - Pipeline = "SLMS-3", - Study = case_when( - Tumor_Sample_Barcode %in% reddy_data$meta$sample_id ~ "Reddy", - Tumor_Sample_Barcode %in% schmitz_data$meta$sample_id ~ "Schmitz", - Tumor_Sample_Barcode %in% chapuy_data$meta$sample_id ~ "Chapuy", - Tumor_Sample_Barcode %in% golub_data$meta$sample_id ~ "NCI_Golub" - ) - ) + +# grch37 Publication rows (Dreval FL + Reddy's original variants). +grch37_publication_rows <- bind_rows( + fl_data$ssm_to_bundle %>% mutate(Pipeline = "Publication"), + reddy_original_maf %>% mutate(Pipeline = "Publication") ) -regions_bed_grch37 = GAMBLR.utils::create_bed_data( - GAMBLR.data::grch37_ashm_regions , +setwd(PKG_ROOT) + + +# ============================================================================ +# Phase 4: one consolidated SLMS-3 pull for every sample in the bundle, +# instead of the previous per-cohort blocks (all-capture, cell lines, Hilton +# trios, Arthur) each independently deciding which samples to pull for -- +# that pattern silently duplicated mutation rows whenever a sample belonged +# to more than one cohort block. Pulling once per (sample, genome_build), +# keyed off the now-complete deduplicated sample_data$meta, makes +# duplication structurally impossible regardless of how many cohorts a +# sample belongs to. +# ============================================================================ +all_slms3_meta <- sample_data$meta %>% + filter(seq_type %in% c("genome", "capture")) + +slms3_grch37 <- bind_rows( + time_it("SLMS-3 genome grch37", pull_data(all_slms3_meta %>% filter(seq_type == "genome"))), + time_it("SLMS-3 capture grch37", pull_data(all_slms3_meta %>% filter(seq_type == "capture"))) +) %>% mutate(Pipeline = "SLMS-3") +print("Done collecting grch37 SLMS-3") + +slms3_hg38 <- bind_rows( + time_it("SLMS-3 genome hg38", pull_data(all_slms3_meta %>% filter(seq_type == "genome"), "hg38")), + time_it("SLMS-3 capture hg38", pull_data(all_slms3_meta %>% filter(seq_type == "capture"), "hg38")) +) %>% mutate(Pipeline = "SLMS-3") +print("Done collecting hg38 SLMS-3") + + +# ============================================================================ +# Phase 5: one consolidated aSHM pull, using the now-complete sample_data$meta +# (includes Arthur + Hilton from the start). This replaces both the old main +# aSHM block (which ran before Arthur/Hilton were added to sample_data$meta, +# so neither got any aSHM coverage from it) and Hilton's separate dedicated +# aSHM block (which existed only to compensate for that gap). Arthur gets +# aSHM coverage for the first time as a result. +# ============================================================================ +regions_bed_grch37 <- create_bed_data( + grch37_ashm_regions, fix_names = "concat", - concat_cols = c("gene","region"),sep="-" + concat_cols = c("gene", "region"), sep = "-" ) -# Add aSHM mutations for the already released samples grch37_ashm <- time_it("grch37_ashm get_ssm_by_regions", { get_ssm_by_regions( these_samples_metadata = sample_data$meta, @@ -697,369 +880,56 @@ grch37_ashm <- time_it("grch37_ashm get_ssm_by_regions", { streamlined = FALSE, basic_columns = FALSE ) %>% - select( - any_of(c(colnames(sample_data$grch37$maf), maf_columns_to_keep)) - ) + select(any_of(all_cols)) }) grch37_ashm <- grch37_ashm %>% - filter(Tumor_Sample_Barcode %in% sample_data$meta$Tumor_Sample_Barcode) - -grch37_ashm <- grch37_ashm %>% mutate(Pipeline = "SLMS-3") - -studies <- bind_rows( - sample_data$grch37$maf %>% - distinct(Tumor_Sample_Barcode, Study), - sample_data$hg38$maf %>% - distinct(Tumor_Sample_Barcode, Study) -) %>% -distinct() + filter(Tumor_Sample_Barcode %in% sample_data$meta$Tumor_Sample_Barcode) %>% + mutate(Pipeline = "SLMS-3") -grch37_ashm <- left_join( - grch37_ashm, - studies +regions_bed_hg38 <- create_bed_data( + hg38_ashm_regions, + fix_names = "concat", + concat_cols = c("gene", "region"), sep = "-" ) hg38_ashm <- time_it("hg38_ashm get_ssm_by_regions", { get_ssm_by_regions( these_samples_metadata = sample_data$meta, - regions_bed = GAMBLR.utils::create_bed_data( - GAMBLR.data::hg38_ashm_regions, - fix_names = "concat", - concat_cols = c("gene","region"),sep="-" - ), + regions_bed = regions_bed_hg38, projection = "hg38", streamlined = FALSE, basic_columns = FALSE ) %>% - select( - any_of(c(colnames(sample_data$hg38$maf), maf_columns_to_keep)) - ) + select(any_of(all_cols)) }) hg38_ashm <- hg38_ashm %>% - filter(Tumor_Sample_Barcode %in% sample_data$meta$Tumor_Sample_Barcode) + filter(Tumor_Sample_Barcode %in% sample_data$meta$Tumor_Sample_Barcode) %>% + mutate(Pipeline = "SLMS-3") -hg38_ashm <- hg38_ashm %>% mutate(Pipeline = "SLMS-3") - -hg38_ashm <- left_join( - hg38_ashm, - studies -) - -sample_data$grch37$ashm <- grch37_ashm -sample_data$hg38$ashm <- hg38_ashm +sample_data$grch37$ashm <- grch37_ashm %>% distinct() +sample_data$hg38$ashm <- hg38_ashm %>% distinct() print("done extracting aSHM mutations from GAMBLR.results") -# Now add the SLMS-3 calls in both projections for those samples that -# are bundled as publication data -publication_samples_grch37 <- sample_data$grch37$maf %>% - filter(Pipeline == "Publication") %>% - pull(Tumor_Sample_Barcode) %>% - unique %>% sort - -publication_samples_hg38 <- sample_data$hg38$maf %>% - filter(Pipeline == "Publication") %>% - pull(Tumor_Sample_Barcode) %>% - unique %>% sort - -publication_samples <- c( - publication_samples_grch37, - publication_samples_hg38 -) -print("extracting grch37 mutations in lymphoma genes with GAMBLR.results") - -sample_data$grch37$maf <- time_it("publication-samples get_ssm_by_regions grch37", { - get_ssm_by_regions( - regions_bed = lymphoma_genes_bed_grch37, - these_samples_metadata = get_gambl_metadata() %>% - filter(sample_id %in% publication_samples), - basic_columns = FALSE) %>% - filter(Hugo_Symbol %in% all_lymphoma_genes) %>% - mutate(Pipeline = "SLMS-3") %>% - left_join( - ., - studies - ) %>% - select(colnames(sample_data$grch37$maf)) %>% - bind_rows( - ., - sample_data$grch37$maf - ) -}) -print("extracting hg38 mutations in lymphoma genes with GAMBLR.results") - -sample_data$hg38$maf <- time_it("publication-samples get_ssm_by_regions hg38", { - get_ssm_by_regions( - regions_bed = lymphoma_genes_bed_hg38, - these_samples_metadata = get_gambl_metadata() %>% - filter(sample_id %in% publication_samples), - projection = "hg38", - basic_columns = FALSE) %>% - filter(Hugo_Symbol %in% all_lymphoma_genes) %>% - mutate(Pipeline = "SLMS-3") %>% - left_join( - ., - studies - ) %>% - select(colnames(sample_data$hg38$maf)) %>% - bind_rows( - ., - sample_data$hg38$maf - ) -}) -print("done extracting all mutations in lymphoma genes with GAMBLR.results") - -setwd(PKG_ROOT) - -# Add data from Arthur paper -arthur_maf <- read_tsv( - "inst/extdata/studies/DLBCL_Arthur.maf.gz" -) -sample_data$grch37$maf <- bind_rows( - sample_data$grch37$maf, - arthur_maf %>% - mutate( - Pipeline = "strelka", - Study = "Arthur" - ) -) - -arthur_meta <- read_xlsx( - "inst/extdata/studies/DLBCL_Arthur.xlsx", - sheet = 1 -) %>% filter(`WGS data` == 1) - - -arthur_meta <- gambl_metadata %>% - filter( - patient_id %in% arthur_meta$`Case ID`, - seq_type == "genome", - ! grepl("tumor", sample_id) - ) %>% - mutate( - sample_id = patient_id, - Tumor_Sample_Barcode = patient_id, - cohort = "DLBCL_Arthur", - reference_PMID = pmids$Arthur_DLBCL - ) - -sample_data$meta <- bind_rows( - sample_data$meta, - arthur_meta -) - -# Add data from Hilton trios paper -trios_samples <- read_xlsx( - "inst/extdata/studies/DLBCL_Hilton.xlsx" -) %>% -drop_na(DNAseq_sample_id) - -# (config is resolved via config.yml / the GAMBLR.results fallback; no chdir to -# a gambl repo is needed to locate data — repo_base is absolute) - -trios_meta <- get_gambl_metadata() %>% - filter( - seq_type %in% c("genome", "capture"), - sample_id %in% trios_samples$DNAseq_sample_id - ) %>% - select(any_of(colnames(sample_data$meta))) %>% - mutate( - cohort = "DLBCL_Hilton", - reference_PMID = pmids$Hilton_DLBCL - ) - -sample_data$meta <- bind_rows( - sample_data$meta, - trios_meta -) - -### begin metadata fixing -# This preserves the original cohort column and ensures there are no duplicates -# in the metadata -fix <- sample_data$meta -fix <- fix %>% rename(study = cohort) - -fix <- left_join( - fix, - get_gambl_metadata() %>% - select(sample_id, seq_type, cohort) -) - -fix <- fix %>% filter(!is.na(study)) - -fix <- distinct(fix) - -sample_data$meta <- fix -### end metadata fixing - -# trios grch37 ssm -genome_trios_ssm_grch37 <- time_it("trios get_ssm_by_regions genome grch37", { - get_ssm_by_regions( - regions_bed = lymphoma_genes_bed_grch37, - these_samples_metadata = trios_meta %>% - filter(seq_type == "genome"), - basic_columns = FALSE - ) %>% - filter(Hugo_Symbol %in% all_lymphoma_genes) %>% - mutate( - Pipeline = "SLMS-3", - Study = "Hilton" - ) %>% - select(all_of(colnames(sample_data$grch37$maf))) -}) -print("extracting mutations for Trios cohort") -capture_trios_ssm_grch37 <- time_it("trios get_ssm_by_regions capture grch37", { - get_ssm_by_regions( - regions_bed = lymphoma_genes_bed_grch37, - these_samples_metadata = trios_meta %>% - filter(seq_type == "capture"), - basic_columns = FALSE - ) %>% - filter(Hugo_Symbol %in% all_lymphoma_genes) %>% - mutate( - Pipeline = "SLMS-3", - Study = "Hilton" - ) %>% - select(all_of(colnames(sample_data$grch37$maf))) -}) - -trios_ssm_grch37 <- bind_rows( - genome_trios_ssm_grch37, - capture_trios_ssm_grch37 -) - -# trios hg38 ssm -genome_trios_ssm_hg38 <- time_it("trios get_ssm_by_regions genome hg38", { - get_ssm_by_regions( - regions_bed = lymphoma_genes_bed_hg38, - these_samples_metadata = trios_meta %>% - filter(seq_type == "genome"), - basic_columns = FALSE, - projection = "hg38" - ) %>% - filter(Hugo_Symbol %in% all_lymphoma_genes) %>% - mutate( - Pipeline = "SLMS-3", - Study = "Hilton" - ) %>% - select(all_of(colnames(sample_data$hg38$maf))) -}) - -capture_trios_ssm_hg38 <- time_it("trios get_ssm_by_regions capture hg38", { - get_ssm_by_regions( - regions_bed = lymphoma_genes_bed_hg38, - these_samples_metadata = trios_meta %>% - filter(seq_type == "capture"), - basic_columns = FALSE, - projection = "hg38" - ) %>% - filter(Hugo_Symbol %in% all_lymphoma_genes) %>% - mutate( - Pipeline = "SLMS-3", - Study = "Hilton" - ) %>% - select(all_of(colnames(sample_data$hg38$maf))) -}) - -trios_ssm_hg38 <- bind_rows( - genome_trios_ssm_hg38, - capture_trios_ssm_hg38 -) +# ============================================================================ +# Phase 6: final maf assembly -- Publication rows + the consolidated SLMS-3 +# pull, per genome build. No `Study =` in any mutate() anywhere in this +# script; cohort membership lives entirely in sample_data$sample_study. +# ============================================================================ sample_data$grch37$maf <- bind_rows( - sample_data$grch37$maf, - trios_ssm_grch37 + grch37_publication_rows, + slms3_grch37 ) sample_data$hg38$maf <- bind_rows( - sample_data$hg38$maf, - trios_ssm_hg38 -) - -regions_bed <- create_bed_data( - grch37_ashm_regions, - fix_names = "concat", - concat_cols = c("gene", "region"), - sep = "-" + hg38_publication_rows, + slms3_hg38 ) -trios_ashm_grch37 <- time_it("trios_ashm get_ssm_by_regions grch37", { - get_ssm_by_regions( - these_samples_metadata = trios_meta, - regions_bed = regions_bed, - streamlined = FALSE, - basic_columns = FALSE - ) -}) - -trios_ashm_grch37 <- trios_ashm_grch37 %>% - filter(Tumor_Sample_Barcode %in% trios_meta$Tumor_Sample_Barcode) - -trios_ashm_grch37 <- trios_ashm_grch37 %>% - mutate( - Pipeline = "SLMS-3", - Study = "Hilton" - ) %>% - select(all_of(colnames(sample_data$grch37$maf))) - - -sample_data$grch37$ashm <- bind_rows( - sample_data$grch37$ashm, - trios_ashm_grch37 -) %>% distinct - - - -regions_bed <- create_bed_data( - hg38_ashm_regions, - fix_names = "concat", - concat_cols = c("gene", "region"), - sep = "-" -) - -trios_ashm_hg38 <- time_it("trios_ashm get_ssm_by_regions hg38", { - get_ssm_by_regions( - these_samples_metadata = trios_meta, - regions_bed = regions_bed, - projection = "hg38", - streamlined = FALSE, - basic_columns = FALSE - ) -}) -trios_ashm_hg38 <- trios_ashm_hg38 %>% - filter(Tumor_Sample_Barcode %in% trios_meta$Tumor_Sample_Barcode) - -trios_ashm_hg38 <- trios_ashm_hg38 %>% - mutate( - Pipeline = "SLMS-3", - Study = "Hilton" - ) %>% - select(all_of(colnames(sample_data$hg38$maf))) - -sample_data$hg38$ashm <- bind_rows( - sample_data$hg38$ashm, - trios_ashm_hg38 -) %>% distinct - -print("Done getting aSHM from Trios") - -setwd(PKG_ROOT) - -# Add data from Reddy paper -reddy_original_maf <- read_tsv( - "inst/extdata/studies/reddy_original_variants_with_VAF.maf.gz" -) %>% -select(any_of(colnames(sample_data$grch37$maf))) +print("done extracting all mutations in lymphoma genes with GAMBLR.results") -sample_data$grch37$maf <- bind_rows( - sample_data$grch37$maf, - reddy_original_maf %>% - mutate( - Pipeline = "Publication", - Study = "Reddy" - ) -) # --- Persist the assembled data ------------------------------------------- # Previously this bundled the multi-GB `sample_data.rda`. Instead we now write: diff --git a/data-raw/test_gambl_db.R b/data-raw/test_gambl_db.R index b5c9acd..d28d146 100644 --- a/data-raw/test_gambl_db.R +++ b/data-raw/test_gambl_db.R @@ -32,11 +32,11 @@ cols <- function(t) dbListFields(con, t) builds_in <- function(t) sort(dbGetQuery(con, sprintf("SELECT DISTINCT genome_build g FROM %s", t))$g) cat("== tables present ==\n") -for (t in c("maf","ashm","seg","bedpe","sample_meta","build_info")) +for (t in c("maf","ashm","seg","bedpe","sample_meta","sample_study","build_info")) check(t %in% tbls, sprintf("table %s exists", t)) cat("\n== row counts (all > 0) ==\n") -for (t in c("maf","ashm","seg","bedpe","sample_meta")) { +for (t in c("maf","ashm","seg","bedpe","sample_meta","sample_study")) { cnt <- if (t %in% tbls) n(t) else 0 check(cnt > 0, sprintf("%-11s has %d rows", t, cnt)) } @@ -50,10 +50,11 @@ for (t in c("maf","ashm","seg","bedpe")) cat("\n== required columns present (what the accessors filter on) ==\n") req <- list( maf = c("Chromosome","Start_Position","End_Position","Tumor_Sample_Barcode", - "Pipeline","Hugo_Symbol","Study","Variant_Classification","t_alt_count","genome_build"), + "Pipeline","Hugo_Symbol","Variant_Classification","t_alt_count","genome_build"), seg = c("ID","chrom","start","end","CN","genome_build"), bedpe = c("tumour_sample_id","CHROM_A","START_A","CHROM_B","START_B","VAF_tumour","SCORE","FILTER","genome_build"), - sample_meta = c("sample_id","Tumor_Sample_Barcode","seq_type","study") + sample_meta = c("sample_id","Tumor_Sample_Barcode","seq_type","study"), + sample_study = c("sample_id","study","study_id","reference_PMID") ) for (t in names(req)) if (t %in% tbls) { missing <- setdiff(req[[t]], cols(t)) @@ -63,8 +64,14 @@ for (t in names(req)) if (t %in% tbls) { } cat("\n== indexes present ==\n") +# NB: idx_maf_gene was asserted here previously but was never actually +# created by write_mutations_db.R (maf has no Hugo_Symbol index -- gene +# queries resolve to a region first, see gambl_mutations_db()'s docs), so +# this check always failed regardless of any other change. Fixed here to +# assert indexes that actually exist. idx <- dbGetQuery(con, "SELECT name FROM sqlite_master WHERE type='index'")$name -for (i in c("idx_maf_pos","idx_maf_sample","idx_maf_gene","idx_seg_sample","idx_bedpe_sample")) +for (i in c("idx_maf_pos","idx_maf_sample","idx_maf_pipe","idx_seg_sample", + "idx_bedpe_sample","idx_study_sample","idx_study_study")) check(i %in% idx, sprintf("index %s", i)) cat("\n== pipelines present ==\n") diff --git a/data-raw/write_mutations_db.R b/data-raw/write_mutations_db.R index 117273c..13eaa10 100644 --- a/data-raw/write_mutations_db.R +++ b/data-raw/write_mutations_db.R @@ -22,19 +22,53 @@ write_mutations_db <- function(sample_data, DBI::dbExecute(con, "PRAGMA journal_mode = OFF") DBI::dbExecute(con, "PRAGMA synchronous = OFF") - # sample metadata (single table, no genome build) - DBI::dbWriteTable(con, "sample_meta", as.data.frame(sample_data$meta), - overwrite = TRUE) + # sample metadata (single table, no genome build). reference_PMID is + # dropped here -- it's single-valued (one row per sample) and can't + # correctly represent a sample belonging to more than one study (each with + # its own PMID); sample_study is the source of truth for that now. + sample_meta_out <- as.data.frame(sample_data$meta) + sample_meta_out$reference_PMID <- NULL + DBI::dbWriteTable(con, "sample_meta", sample_meta_out, overwrite = TRUE) + + # sample_study: many-to-many bridge table (sample_id, study) replacing the + # old inline Study column on maf/ashm rows -- guarded so older sample_data + # objects that predate this table (e.g. via the legacy .rda adapter) don't + # hard-crash a build that simply won't have cohort-membership data. + n_sample_study <- 0L + if (!is.null(sample_data$sample_study) && nrow(sample_data$sample_study)) { + DBI::dbWriteTable(con, "sample_study", as.data.frame(sample_data$sample_study), + overwrite = TRUE) + n_sample_study <- nrow(sample_data$sample_study) + } + + # Natural mutation-call key, reused from GAMBLR.open::get_ssm_by_region() + # (which already applies the same distinct() at read time) as a write-time + # safety net: no cohort-specific pull block should ever again be able to + # introduce duplicate mutation rows for a sample that's pulled more than + # once, regardless of the reason. + dedup_keys <- list( + maf = c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2"), + ashm = c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2") + ) # genome-build-stamped frames; append per build to keep peak memory low write_element <- function(elem) { first <- TRUE total <- 0L + dk <- dedup_keys[[elem]] for (b in builds) { x <- sample_data[[b]][[elem]] if (is.null(x) || !nrow(x)) next x <- as.data.frame(x) x$genome_build <- b + if (!is.null(dk)) { + before <- nrow(x) + x <- dplyr::distinct(x, dplyr::across(dplyr::all_of(dk)), .keep_all = TRUE) + if (nrow(x) < before) { + message(sprintf("[write_mutations_db] %s/%s: dropped %d duplicate row(s)", + elem, b, before - nrow(x))) + } + } DBI::dbWriteTable(con, elem, x, append = !first, overwrite = first) total <- total + nrow(x) first <- FALSE @@ -49,26 +83,27 @@ write_mutations_db <- function(sample_data, "CREATE INDEX idx_maf_pos ON maf(genome_build, Chromosome, Start_Position)", "CREATE INDEX idx_maf_sample ON maf(Tumor_Sample_Barcode)", "CREATE INDEX idx_maf_pipe ON maf(Pipeline)", - "CREATE INDEX idx_maf_study ON maf(Study)", "CREATE INDEX idx_ashm_pos ON ashm(genome_build, Chromosome, Start_Position)", "CREATE INDEX idx_ashm_sample ON ashm(Tumor_Sample_Barcode)", "CREATE INDEX idx_seg_sample ON seg(genome_build, ID)", "CREATE INDEX idx_seg_pos ON seg(genome_build, chrom, start)", "CREATE INDEX idx_bedpe_sample ON bedpe(tumour_sample_id)", "CREATE INDEX idx_meta_sample ON sample_meta(sample_id)", - "CREATE INDEX idx_meta_barcode ON sample_meta(Tumor_Sample_Barcode)" + "CREATE INDEX idx_meta_barcode ON sample_meta(Tumor_Sample_Barcode)", + "CREATE INDEX idx_study_sample ON sample_study(sample_id)", + "CREATE INDEX idx_study_study ON sample_study(study)" ) for (stmt in idx) try(DBI::dbExecute(con, stmt), silent = TRUE) # self-describing provenance / expected-counts table (used by the tests) DBI::dbWriteTable(con, "build_info", data.frame( key = c("source", "built_at", "builder", - "n_maf", "n_ashm", "n_seg", "n_bedpe", "n_samples"), + "n_maf", "n_ashm", "n_seg", "n_bedpe", "n_samples", "n_sample_study"), value = c(source_desc, format(Sys.time(), tz = "UTC", usetz = TRUE), "write_mutations_db", counts[["maf"]], counts[["ashm"]], counts[["seg"]], - counts[["bedpe"]], nrow(sample_data$meta)), + counts[["bedpe"]], nrow(sample_data$meta), n_sample_study), stringsAsFactors = FALSE ), overwrite = TRUE) diff --git a/man/gambl_mutations_db.Rd b/man/gambl_mutations_db.Rd index a2396bd..8617064 100644 --- a/man/gambl_mutations_db.Rd +++ b/man/gambl_mutations_db.Rd @@ -37,31 +37,42 @@ separate tables); filter on \code{genome_build} (\code{"grch37"} / \code{"hg38"} one. \code{maf} and \code{ashm} share the same (curated, 49-column) schema: a reduced column set, not the full upstream flatfile MAF.\tabular{llll}{ Table \tab Rows (typical) \tab Grain \tab Key columns \cr - \code{maf} \tab ~3.6M \tab one somatic mutation call \tab \verb{Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline, Study, genome_build} (+ ~35 more MAF-standard columns) \cr + \code{maf} \tab ~3.6M \tab one somatic mutation call \tab \verb{Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline, genome_build} (+ ~35 more MAF-standard columns) \cr \code{ashm} \tab ~135k \tab one mutation call in an aSHM region \tab same schema as \code{maf} \cr \code{seg} \tab ~126k \tab one copy-number segment \tab \verb{ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build} \cr \code{bedpe} \tab ~900 \tab one Manta structural-variant breakpoint pair \tab \verb{CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build} \cr - \code{sample_meta} \tab ~3.3k \tab one sample \tab \verb{patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...} (31 columns total; its own \code{genome_build} column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) \cr - \code{build_info} \tab 8 \tab key/value \tab provenance (\code{source}, \code{built_at}, \code{builder}) and expected row counts (\code{n_maf}, \code{n_ashm}, \code{n_seg}, \code{n_bedpe}, \code{n_samples}) used by the \code{data-raw/test_gambl_db.R} regression checks \cr + \code{sample_meta} \tab ~3.3k \tab one sample \tab \verb{patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...} (its own \code{genome_build} column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) \cr + \code{sample_study} \tab varies \tab one (sample, study) membership fact -- many-to-many, a sample belonging to N studies is N rows \tab \verb{sample_id, study, study_id, reference_PMID}. \code{study_id} is that study's own identifier for the sample where it differs from GAMBL's \code{sample_id} (e.g. a paper's own case/patient ID); NA where no study-specific ID has been sourced. \cr + \code{build_info} \tab 9 \tab key/value \tab provenance (\code{source}, \code{built_at}, \code{builder}) and expected row counts (\code{n_maf}, \code{n_ashm}, \code{n_seg}, \code{n_bedpe}, \code{n_samples}, \code{n_sample_study}) used by the \code{data-raw/test_gambl_db.R} regression checks \cr } + +\code{maf}/\code{ashm} do NOT carry a \code{Study} column. Cohort/study membership is +tracked once per sample in \code{sample_study}, not once per mutation row -- +a mutation row's \code{Study} couldn't represent a sample belonging to more +than one study anyway, and a per-row tag caused the same sample's +mutations to be pulled and duplicated once per cohort-specific code path +that claimed it (see \code{GAMBLR.data::get_ssm_from_db()}'s \code{this_study} +parameter for how to filter by study post-refactor). \subsection{Join keys}{ \code{maf}, \code{ashm}, and \code{bedpe} (via \code{tumour_sample_id}) key to \code{sample_meta$Tumor_Sample_Barcode} / \code{sample_meta$sample_id}; \code{seg$ID} keys -to \code{sample_meta$sample_id}. +to \code{sample_meta$sample_id}; \code{sample_study$sample_id} keys to +\code{sample_meta$sample_id}. } \subsection{Indexes}{ \code{maf}/\code{ashm}: \verb{(genome_build, Chromosome, Start_Position)}, -\code{Tumor_Sample_Barcode}, \code{Pipeline}, \code{Study} (maf only). No index on +\code{Tumor_Sample_Barcode}, \code{Pipeline} (maf only). No index on \code{Hugo_Symbol}: gene-restricted queries resolve the gene to a region first (the same logic used to populate these tables) and filter on \verb{(genome_build, Chromosome, Start_Position)} instead -- see \code{GAMBLR.data::get_ssm_from_db()}. \code{seg}: \verb{(genome_build, ID)}, \verb{(genome_build, chrom, start)}. \code{bedpe}: \code{tumour_sample_id}. -\code{sample_meta}: \code{sample_id}, \code{Tumor_Sample_Barcode}. +\code{sample_meta}: \code{sample_id}, \code{Tumor_Sample_Barcode}. \code{sample_study}: +\code{sample_id}, \code{study}. } } diff --git a/man/get_ssm_from_db.Rd b/man/get_ssm_from_db.Rd index 80dcfba..5185858 100644 --- a/man/get_ssm_from_db.Rd +++ b/man/get_ssm_from_db.Rd @@ -33,7 +33,14 @@ get_ssm_from_db( \item{min_read_support}{Keep only variants with \code{t_alt_count} >= this value.} -\item{this_study}{Optional single \code{Study} to restrict to.} +\item{this_study}{Optional single study to restrict to, matched against +\code{sample_study$study} (not a \code{maf}/\code{ashm} column -- see +\code{\link[=gambl_mutations_db]{gambl_mutations_db()}}'s schema docs). Resolves to the set of sample_ids +belonging to that study, intersected with \code{sample_ids} if both are +supplied; composes with \code{tool_name}/Pipeline exactly as before (e.g. +\code{this_study="Reddy"} alone returns Reddy's SLMS-3 recall by default, +while \verb{this_study="Reddy", tool_name="publication"} returns Reddy's +as-published rows only).} \item{regions}{Optional data frame with columns \code{chrom}, \code{start}, \code{end}; rows are OR-ed (each region is an indexed range scan).} From 017d3db363de453926e0c78b42c012215db054c3 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 13 Jul 2026 15:00:42 -0700 Subject: [PATCH 17/58] Fix type mismatch in sample_study bind_rows (study_id chr vs dbl) Reddy's raw "Sample ID" column is read as numeric by read_excel(), while every other cohort's study_id is character -- bind_rows() can't reconcile with , confirmed by a real run on the GSC: "Can't combine ..1\$study_id and ..4\$study_id ". Coerced study_id to character at every cohort's sample_study frame construction (not just Reddy's), since any of these xlsx-sourced columns could hit the same type-inference issue depending on how the source spreadsheet happens to type that column. --- data-raw/assemble_bundled_data.R | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index de0d9f6..237834e 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -220,7 +220,7 @@ bl_data$cnv_to_bundle <- read_xlsx( thomas_bl_study <- data.frame( sample_id = bl_data$meta_to_bundle$sample_id, study = "Thomas", - study_id = bl_data$meta_to_bundle$patient_id, + study_id = as.character(bl_data$meta_to_bundle$patient_id), reference_PMID = pmids$Thomas_BL ) @@ -281,7 +281,7 @@ fl_data$cnv_to_bundle <- read_xlsx( dreval_study <- data.frame( sample_id = fl_data$meta_to_bundle$sample_id, study = "Dreval", - study_id = fl_data$meta_to_bundle$patient_id, + study_id = as.character(fl_data$meta_to_bundle$patient_id), reference_PMID = pmids$Dreval_FL ) @@ -339,7 +339,7 @@ dlbcl_data$cnv_to_bundle <- read_xlsx( thomas_dlbcl_study <- data.frame( sample_id = dlbcl_data$meta_to_bundle$sample_id, study = "Thomas", - study_id = dlbcl_data$meta_to_bundle$patient_id, + study_id = as.character(dlbcl_data$meta_to_bundle$patient_id), reference_PMID = pmids$Thomas_BL ) @@ -382,7 +382,7 @@ reddy_meta_full <- read_excel( reddy_study <- data.frame( sample_id = reddy_meta_full$sample_id, study = "Reddy", - study_id = reddy_meta_full$study_id, + study_id = as.character(reddy_meta_full$study_id), reference_PMID = pmids$Reddy_DLBCL ) @@ -554,7 +554,7 @@ arthur_meta <- get_gambl_metadata() %>% arthur_study <- data.frame( sample_id = arthur_meta$sample_id, study = "Arthur", - study_id = arthur_meta$patient_id, + study_id = as.character(arthur_meta$patient_id), reference_PMID = pmids$Arthur_DLBCL ) @@ -584,7 +584,7 @@ trios_meta <- get_gambl_metadata() %>% hilton_study <- data.frame( sample_id = trios_meta$sample_id, study = "Hilton", - study_id = trios_meta$patient_id, + study_id = as.character(trios_meta$patient_id), reference_PMID = pmids$Hilton_DLBCL ) From 6df5bd9f3a549d53eefe2cd9f9180e3b56b864d8 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 13 Jul 2026 15:21:28 -0700 Subject: [PATCH 18/58] Add PR verification notes for the sample_study refactor Tracks test_gambl_db.R results and the reasoning behind each of the 4 expected regression-check differences vs the old bundle, for use writing the eventual PR description. Has a running checklist to append further verification (compare_bundle_changes.R, Dreval/Hilton dedup spot-check, Arthur gene-panel restriction) as it's done. --- data-raw/PR_verification_notes.md | 61 +++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 data-raw/PR_verification_notes.md diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md new file mode 100644 index 0000000..c54dcc1 --- /dev/null +++ b/data-raw/PR_verification_notes.md @@ -0,0 +1,61 @@ +# Verification notes: sample↔study join table / deduplicated SLMS-3 pulls + +Working notes for the PR description covering the `assemble_bundled_data.R` / +`write_mutations_db.R` / `get_ssm_from_db.R` refactor (branch `rmorin-dev`, +commits `1a8cfb6`, `017d3db`). Append further findings (e.g. +`compare_bundle_changes.R` output) below as they come in. + +## Background + +Root cause: SLMS-3 mutation calls were pulled via separate, cohort-specific +code blocks, each independently deciding which samples to pull for. A sample +belonging to more than one cohort (e.g. the Dreval FL × Hilton trios overlap) +got its mutations pulled and bound multiple times. Confirmed directly against +the previously-shipped `gambl_mutations.db`: 1,797 exact duplicate `maf` rows +(same pattern in `ashm`), plus true duplicate rows in `sample_meta` for the +same 5 samples. + +Fix: `Study`/cohort membership moved off `maf`/`ashm` entirely into a new +`sample_study` many-to-many join table; SLMS-3 is now pulled exactly once per +deduplicated sample regardless of cohort membership. Surfaced while replacing +the Arthur cohort's raw, completely unfiltered `strelka` flat-file dump +(2.83M rows / 65,121 distinct genes, missing the lymphoma-gene-panel filter +every other block applies) with a proper SLMS-3 pull. + +## `test_gambl_db.R` run — 2026-07-14 + +All structural/integrity checks passed: + +``` +== tables present == 7/7 PASS (maf, ashm, seg, bedpe, sample_meta, sample_study, build_info) +== row counts (all > 0) == 6/6 PASS + maf 543,214 rows + ashm 134,826 rows + seg 128,075 rows + bedpe 2,352 rows + sample_meta 3,346 rows + sample_study 2,880 rows +== both genome builds present == 4/4 PASS +== required columns present == 5/5 PASS +== indexes present == 7/7 PASS (incl. new idx_study_sample, idx_study_study) +== pipelines present == 2/2 PASS -- Pipeline values: Publication, SLMS-3 (no more "strelka") +== spot query: MYC locus, grch37, slms-3 == PASS -- 4,549 variants found +== build_info counts match actual table counts == 4/4 PASS +== sample_metadata.rda matches sample_meta table == PASS (3,346 == 3,346) +``` + +### Regression vs. old `sample_data.rda` (informational, all 4 differences expected) + +| Table | Old (`sample_data.rda`) | New (`gambl_mutations.db`) | Δ | Explanation | +| --- | --- | --- | --- | --- | +| `maf` | 3,663,468 | 543,214 | −85% | Almost entirely Arthur's unfiltered `strelka` dump being removed (2,827,527 of the old rows, 65,121 distinct genes vs. the ~150-237 gene panel) + the Dreval×Hilton dedup. This is the fix working as intended, not a regression. | +| `ashm` | 134,868 | 134,826 | −0.03% | Noise-level. | +| `seg` | 125,982 | 128,075 | +1.7% | Unrelated to this refactor (CNV/seg blocks untouched) -- likely normal cohort growth in GAMBL's live metadata since the old snapshot. | +| `bedpe` | 941 | 2,352 | +150% | Bonus fix, not explicitly planned: Arthur and Hilton were previously added to `sample_data$meta` *after* the Manta SV block ran, so they got zero SV coverage -- same root cause as the aSHM gap that *was* explicitly fixed. Now that metadata assembly happens once, up front, they get SV coverage too. | + +## Still to do + +- [ ] Run `compare_bundle_changes.R` for the granular per-sample gained/lost/retained view (append results below). +- [ ] Confirm Dreval×Hilton overlap samples (`05-32150T`, `08-15460T`, `09-33003T`, `15-13383T`, `17-36275T`) show 2 `sample_study` rows each and roughly halved mutation counts vs. the old bundle. +- [ ] Confirm Arthur's `maf` rows are gene-panel-restricted (~150-237 distinct genes, not 65,121) and now have hg38 + aSHM coverage. +- [ ] Functional check: `get_ssm_from_db(this_study = "Reddy")` / `GAMBLR.open::get_ssm_by_region(this_study = "Reddy", ...)` against the real DB. From 1647e5aecfc93a7e978bcbab36c0315750a4c6a3 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 13 Jul 2026 15:33:59 -0700 Subject: [PATCH 19/58] Fix sample_study.study_id: use sample_id not patient_id (Thomas/Dreval/Hilton) patient_id is patient-level, not sample-level -- ambiguous for any cohort where a patient can have more than one sample. Confirmed as a real bug for Hilton specifically (a trios study): LY_RELY_116_tumorA and LY_RELY_116_tumorB both collapsed to study_id="LY_RELY_116", making the two sample_study rows indistinguishable. Thomas BL/DLBCL and Dreval don't currently hit this in practice but share the same underlying imprecision, fixed for consistency. All four now use sample_id, already sourced from each xlsx's own "Genome sample id"/DNAseq_sample_id column -- the closest thing to a study-native sample-level identifier available for these cohorts. Arthur (structurally protected by its !grepl("tumor", sample_id) filter) and Reddy (study_id sourced from the paper's own distinct "Sample ID" column, already 1:1 with sample_id) were left unchanged. --- data-raw/PR_verification_notes.md | 18 ++++++++++++++++++ data-raw/assemble_bundled_data.R | 24 +++++++++++++++++------- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md index c54dcc1..32f00eb 100644 --- a/data-raw/PR_verification_notes.md +++ b/data-raw/PR_verification_notes.md @@ -59,3 +59,21 @@ All structural/integrity checks passed: - [ ] Confirm Dreval×Hilton overlap samples (`05-32150T`, `08-15460T`, `09-33003T`, `15-13383T`, `17-36275T`) show 2 `sample_study` rows each and roughly halved mutation counts vs. the old bundle. - [ ] Confirm Arthur's `maf` rows are gene-panel-restricted (~150-237 distinct genes, not 65,121) and now have hg38 + aSHM coverage. - [ ] Functional check: `get_ssm_from_db(this_study = "Reddy")` / `GAMBLR.open::get_ssm_by_region(this_study = "Reddy", ...)` against the real DB. + +## Fix: `study_id` used `patient_id` instead of `sample_id` for Thomas/Dreval/Hilton + +Caught in review (not by the automated checks above): the first pass at +populating `sample_study$study_id` used `patient_id` for Thomas BL, Thomas +DLBCL, Dreval, and Hilton. For Hilton specifically this is a real bug, not +just imprecision -- Hilton is a trios study, so a single patient can have +multiple samples (e.g. `LY_RELY_116_tumorA` and `LY_RELY_116_tumorB`), and +`patient_id` collapsed both to the same `study_id`, making the two +`sample_study` rows ambiguous. Thomas/Dreval don't currently have that +ambiguity in practice, but the same fix applies for consistency: all four +now use `sample_id` (already sourced from each xlsx's own "Genome sample +id"/`DNAseq_sample_id` column, i.e. already the closest thing to a +study-native sample-level identifier) instead of `patient_id`. Arthur and +Reddy were left unchanged -- Arthur's block already excludes +`tumor`-suffixed sample_ids per patient (structurally safe from the same +ambiguity), and Reddy's `study_id` comes from the paper's own distinct +"Sample ID" column, already 1:1 with `sample_id`. diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 237834e..8321fa0 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -217,10 +217,15 @@ bl_data$cnv_to_bundle <- read_xlsx( filter(ID %in% bl_data$meta_to_bundle$sample_id) %>% mutate(CN = round(2 * 2^log.ratio)) +# study_id mirrors sample_id here (not patient_id): the xlsx's own +# "Genome sample id" column was already adopted directly as sample_id, and +# patient_id is patient-level, not sample-level -- ambiguous for any cohort +# where a patient could have more than one sample (see hilton_study below +# for a case where that ambiguity is real). thomas_bl_study <- data.frame( sample_id = bl_data$meta_to_bundle$sample_id, study = "Thomas", - study_id = as.character(bl_data$meta_to_bundle$patient_id), + study_id = as.character(bl_data$meta_to_bundle$sample_id), reference_PMID = pmids$Thomas_BL ) @@ -278,10 +283,11 @@ fl_data$cnv_to_bundle <- read_xlsx( ) %>% mutate(CN = round(2 * 2^log.ratio)) +# study_id mirrors sample_id, not patient_id -- see thomas_bl_study above. dreval_study <- data.frame( sample_id = fl_data$meta_to_bundle$sample_id, study = "Dreval", - study_id = as.character(fl_data$meta_to_bundle$patient_id), + study_id = as.character(fl_data$meta_to_bundle$sample_id), reference_PMID = pmids$Dreval_FL ) @@ -336,10 +342,11 @@ dlbcl_data$cnv_to_bundle <- read_xlsx( filter(ID %in% dlbcl_data$meta_to_bundle$sample_id) %>% mutate(CN = round(2 * 2^log.ratio)) +# study_id mirrors sample_id, not patient_id -- see thomas_bl_study above. thomas_dlbcl_study <- data.frame( sample_id = dlbcl_data$meta_to_bundle$sample_id, study = "Thomas", - study_id = as.character(dlbcl_data$meta_to_bundle$patient_id), + study_id = as.character(dlbcl_data$meta_to_bundle$sample_id), reference_PMID = pmids$Thomas_BL ) @@ -578,13 +585,16 @@ trios_meta <- get_gambl_metadata() %>% reference_PMID = pmids$Hilton_DLBCL ) -# Unlike Thomas/Dreval/Reddy/Arthur, trios_meta comes straight from -# get_gambl_metadata(), so GAMBL's real patient_id is already correct here -- -# used directly as study_id. +# study_id uses sample_id, not patient_id: Hilton is a trios study, so a +# single patient can have multiple samples (e.g. LY_RELY_116_tumorA and +# LY_RELY_116_tumorB) -- patient_id would collapse them to the same +# study_id, making the two rows ambiguous. sample_id already carries the +# distinguishing suffix and matches what the paper itself would call each +# sample. hilton_study <- data.frame( sample_id = trios_meta$sample_id, study = "Hilton", - study_id = as.character(trios_meta$patient_id), + study_id = as.character(trios_meta$sample_id), reference_PMID = pmids$Hilton_DLBCL ) From d3f55915f93bb085622a2a97f397a3d09ba19d63 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 13 Jul 2026 15:43:44 -0700 Subject: [PATCH 20/58] Record Reddy this_seq_type functional-check finding in verification notes --- data-raw/PR_verification_notes.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md index 32f00eb..122e751 100644 --- a/data-raw/PR_verification_notes.md +++ b/data-raw/PR_verification_notes.md @@ -58,7 +58,24 @@ All structural/integrity checks passed: - [ ] Run `compare_bundle_changes.R` for the granular per-sample gained/lost/retained view (append results below). - [ ] Confirm Dreval×Hilton overlap samples (`05-32150T`, `08-15460T`, `09-33003T`, `15-13383T`, `17-36275T`) show 2 `sample_study` rows each and roughly halved mutation counts vs. the old bundle. - [ ] Confirm Arthur's `maf` rows are gene-panel-restricted (~150-237 distinct genes, not 65,121) and now have hg38 + aSHM coverage. -- [ ] Functional check: `get_ssm_from_db(this_study = "Reddy")` / `GAMBLR.open::get_ssm_by_region(this_study = "Reddy", ...)` against the real DB. +- [x] Functional check: `get_ssm_by_samples()`/`get_ssm_by_patients()` against real Reddy (capture) data -- 20,480 rows returned, confirming the query layer and underlying data are correct end to end (see note below; the initial zero-result was unrelated to this refactor). + +## Non-issue: `get_ssm_by_samples()`/`get_ssm_by_patients()` initially returned 0 rows for Reddy + +Root cause turned out to be unrelated to this refactor: both functions +default `this_seq_type = "genome"` and filter `these_samples_metadata` by +it *in addition to* whatever metadata you pass in, rather than inferring it. +Reddy is a capture cohort (`seq_type == "capture"` for all 999 samples), so +the default silently dropped every row before the query ever reached the +database. Confirmed the data itself was fine throughout: a raw SQL query +against `maf` for Reddy's `Tumor_Sample_Barcode` values returned 47,626 +rows the whole time. Passing `this_seq_type = "capture"` explicitly fixed +both functions (20,480 rows each, matching after `get_ssm_from_db()`'s +Pipeline/genome_build/read-support filters narrow the raw 47,626). + +Separate, pre-existing GAMBLR.open UX gap worth considering independently +of this PR: neither function warns when the `seq_type` filter drops every +row, so this fails silently rather than with a clear message. ## Fix: `study_id` used `patient_id` instead of `sample_id` for Thomas/Dreval/Hilton From efbe48a7e04d41e08cd3cdc33c27bbc515efc6df Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 13 Jul 2026 16:09:24 -0700 Subject: [PATCH 21/58] Fix Arthur cohort: Case ID join type-safety, drop tumor-suffix exclusion compare_bundle_changes.R showed patients losing 100% of their SNVs with zero retention (e.g. patient 08-15460: 19,897 lost, 0 retained). Root-caused to two compounding bugs in the Arthur block: 1. arthur_meta matched Case ID (from DLBCL_Arthur.xlsx) against GAMBL's patient_id via `patient_id %in% arthur_case_ids$"Case ID"`. Same bug class as the study_id chr/dbl mismatch fixed earlier -- read_xlsx() can silently type a leading-zero ID like "08-15460" as numeric, and %in% fails to match across types without error. Fixed with explicit character coercion + inner_join() instead of a %in% filter. 2. `! grepl("tumor", sample_id)` excluded every sample for any patient with more than one tumor biopsy (..._tumorA/..._tumorB -- the same multi-sample-per-patient pattern Hilton has). For a patient whose only GAMBL samples are tumor-suffixed, this matched nothing, dropping their SLMS-3 coverage entirely. No other cohort in this script excludes samples this way; removed. --- data-raw/PR_verification_notes.md | 27 +++++++++++++++++++++++++++ data-raw/assemble_bundled_data.R | 25 +++++++++++++++++++------ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md index 122e751..e8dfeec 100644 --- a/data-raw/PR_verification_notes.md +++ b/data-raw/PR_verification_notes.md @@ -77,6 +77,33 @@ Separate, pre-existing GAMBLR.open UX gap worth considering independently of this PR: neither function warns when the `seq_type` filter drops every row, so this fails silently rather than with a clear message. +## Fix: Arthur's Case ID -> patient_id matching, and a `!grepl("tumor", ...)` filter that dropped multi-sample patients entirely + +`compare_bundle_changes.R` showed some patients losing 100% of their SNVs +with zero retention (e.g. patient `08-15460`: 19,897 lost, 0 retained -- +found via `egrep "Tumor_Sa|05-32150|08-15460" snv_persample_counts.tsv`). +Confirmed as an Arthur-specific bug, two compounding causes: + +1. `arthur_meta` matched Arthur's own "Case ID" (from `DLBCL_Arthur.xlsx`) + against GAMBL's `patient_id` via `patient_id %in% arthur_case_ids$\`Case ID\``. + Same class of bug as the `study_id` chr/dbl mismatch fixed earlier: + `read_xlsx()` can silently type a leading-zero ID like `08-15460` as + numeric, and a numeric-vs-character `%in%` comparison fails to match + without any error or warning. Fixed by coercing both sides to character + and using an explicit `inner_join()` instead of a `%in%` filter. +2. The block also had `! grepl("tumor", sample_id)`, which excludes every + sample for any patient with more than one tumor biopsy (e.g. + `..._tumorA`/`..._tumorB` -- the same multi-sample-per-patient pattern + Hilton has). For a patient whose *only* GAMBL samples are tumor-suffixed, + this filter matched nothing, dropping their SLMS-3 coverage entirely. No + other cohort in this script excludes samples this way, and no comment + explained the original intent -- removed. + +Still need to: rebuild with this fix and re-run `compare_bundle_changes.R` +to confirm the 100%-loss patients are resolved, and re-check whether this +also explains the `DO52686`/`07-35482`-style samples from the earlier, +larger (3.1M row) loss count. + ## Fix: `study_id` used `patient_id` instead of `sample_id` for Thomas/Dreval/Hilton Caught in review (not by the automated checks above): the first pass at diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 8321fa0..ab7a32a 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -537,22 +537,35 @@ cell_lines_data$hg38$cnv_to_bundle <- get_cn_segments( # ~137 for a properly gene-panel-restricted SLMS-3 sample. That block is # removed entirely; Arthur's samples now flow through the same consolidated # SLMS-3 pull as every other cohort (Phase 4). +# Case ID -> patient_id is done as an explicit join (not a +# `patient_id %in% arthur_case_ids$\`Case ID\`` filter) so a type mismatch +# between the xlsx's Case ID and GAMBL's patient_id (e.g. numeric vs +# character -- the same class of bug already fixed for study_id/Reddy's +# Sample ID elsewhere in this script; read_xlsx() can silently type a +# leading-zero ID like "08-15460" as numeric) can't silently drop matching +# patients from a %in% comparison. transmute() keeps only what's needed for +# the join, since nothing else from this sheet is used downstream. arthur_case_ids <- read_xlsx( "inst/extdata/studies/DLBCL_Arthur.xlsx", sheet = 1 -) %>% filter(`WGS data` == 1) +) %>% filter(`WGS data` == 1) %>% + transmute(patient_id = as.character(`Case ID`)) # sample_id/Tumor_Sample_Barcode are GAMBL's own real values throughout -- # never overwritten to the paper's own patient-level ID (as the removed # code used to do). Arthur's own "Case ID" is captured separately as # study_id in arthur_study below, joined to metadata by sample_id like any # other study-specific identifier. +# +# No `!grepl("tumor", sample_id)` filter here (the previous version of this +# block had one): it excluded every sample for any patient with more than +# one tumor biopsy (e.g. ..._tumorA/..._tumorB -- the same multi-sample- +# per-patient pattern Hilton has), dropping those patients' SLMS-3 coverage +# entirely. No other cohort in this script excludes samples this way. arthur_meta <- get_gambl_metadata() %>% - filter( - patient_id %in% arthur_case_ids$`Case ID`, - seq_type == "genome", - ! grepl("tumor", sample_id) - ) %>% + mutate(patient_id = as.character(patient_id)) %>% + inner_join(arthur_case_ids, by = "patient_id") %>% + filter(seq_type == "genome") %>% mutate( cohort = "DLBCL_Arthur", reference_PMID = pmids$Arthur_DLBCL From fd2f56ffda3ba7b374829ae0228104720b8e53bd Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 13 Jul 2026 16:40:19 -0700 Subject: [PATCH 22/58] Add relabel_to_sample_id(): fix Publication-pipeline Tumor_Sample_Barcode Root-caused the 100%-loss patients (e.g. 08-15460: 19,897 lost, 0 retained) to the now-deleted Arthur raw flat file, which used bare patient-style Tumor_Sample_Barcode values independent of arthur_meta entirely -- confirmed by reading the old file directly (grep("15460", ...) on Tumor_Sample_Barcode found "08-15460" in the raw file itself). Since that file is already deleted, this specific loss was never a bug -- it's the expected disappearance of a non-standard ID convention, correctly superseded by GAMBL's real sample IDs (confirmed present and correctly sized). Generalized into a defensive fix for the remaining Publication-pipeline pulls, which read a paper's own file directly and were trusting its Tumor_Sample_Barcode values without validation. relabel_to_sample_id() joins on sample_study$study_id and rewrites Tumor_Sample_Barcode to the real sample_id wherever they differ -- a no-op for Thomas/Dreval (study_id already mirrors sample_id there), an actual fix for Reddy's original-variants file (study_id is the paper's own distinct raw "Sample ID"). Verified against synthetic data. --- data-raw/PR_verification_notes.md | 39 ++++++++++++++++++++++++++++--- data-raw/assemble_bundled_data.R | 24 ++++++++++++++++++- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md index e8dfeec..48c76b8 100644 --- a/data-raw/PR_verification_notes.md +++ b/data-raw/PR_verification_notes.md @@ -99,10 +99,43 @@ Confirmed as an Arthur-specific bug, two compounding causes: other cohort in this script excludes samples this way, and no comment explained the original intent -- removed. +**Update**: rebuilt with this fix and re-ran -- `08-15460` (bare) still +showed the identical 100%-loss numbers, byte-for-byte. That ruled out +`arthur_meta`/`arthur_case_ids` as the source: those only ever fed +`sample_data$meta`, never `sample_data$maf` directly. Traced the real +source instead by reading the *old raw flat file itself*: +```r +arthur_raw <- readr::read_tsv("inst/extdata/studies/DLBCL_Arthur.maf.gz", + col_select = "Tumor_Sample_Barcode") +grep("15460", unique(arthur_raw$Tumor_Sample_Barcode), value = TRUE) +# [1] "08-15460" +``` +Confirmed: the deleted raw dump used bare patient-style IDs for +`Tumor_Sample_Barcode` (not GAMBL's real per-sample naming), directly and +independently of `arthur_meta`. **Conclusion: this specific finding was +never a bug.** Every "100%-loss" row keyed to `08-15460` (bare) is simply +the expected disappearance of the old file's non-standard ID convention, +now correctly superseded by GAMBL's real sample IDs (`08-15460T`: 1,515 +retained; `08-15460_tumorB`: 665 retained -- both present and correctly +sized in both old and new bundles). + +Generalized this into a defensive fix rather than leaving it Arthur-specific: +added `relabel_to_sample_id(df, study_name)`, applied to all four +Publication-pipeline pulls (Thomas BL, Thomas DLBCL, Dreval, Reddy's +original-variants file). It joins on `sample_study$study_id` and rewrites +`Tumor_Sample_Barcode` to the real `sample_id` wherever they differ -- a +no-op for Thomas/Dreval/Hilton (`study_id` already mirrors `sample_id` +there) and an actual fix for Reddy (`study_id` is the paper's own raw +"Sample ID", genuinely distinct from `sample_id`). Verified against +synthetic data: relabels when a study_id match exists, no-ops when +`Tumor_Sample_Barcode` already is the real sample_id, and passes through +unmatched values unchanged rather than dropping them. + Still need to: rebuild with this fix and re-run `compare_bundle_changes.R` -to confirm the 100%-loss patients are resolved, and re-check whether this -also explains the `DO52686`/`07-35482`-style samples from the earlier, -larger (3.1M row) loss count. +to confirm `08-15460` (bare) is gone from the loss list entirely, and +check whether the same bare-vs-suffixed pattern explains the +`DO52686`/`07-35482`-style samples from the earlier, larger (3.1M row) +loss count. ## Fix: `study_id` used `patient_id` instead of `sample_id` for Thomas/Dreval/Hilton diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index ab7a32a..45242d1 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -773,6 +773,26 @@ sample_data$hg38$seg <- bind_rows( # sample_data$sample_study, not an inline Study column. # ============================================================================ +# Publication-pipeline data is read directly from each paper's own +# supplementary file, which isn't guaranteed to use GAMBL's own +# Tumor_Sample_Barcode convention -- Arthur's now-removed raw dump was the +# clearest example, using bare patient-style IDs instead of GAMBL's real +# per-sample naming. Defensive relabel: for any row whose +# Tumor_Sample_Barcode matches a study's own identifier +# (sample_study$study_id) rather than GAMBL's real sample_id, replace it +# with the real sample_id. A no-op wherever Tumor_Sample_Barcode already IS +# the real sample_id -- true for Thomas/Dreval/Hilton, where study_id +# mirrors sample_id (see thomas_bl_study above). +relabel_to_sample_id <- function(df, study_name) { + lookup <- sample_data$sample_study %>% + filter(study == study_name, !is.na(study_id)) %>% + select(study_id, .gambl_sample_id = sample_id) + df %>% + left_join(lookup, by = c("Tumor_Sample_Barcode" = "study_id")) %>% + mutate(Tumor_Sample_Barcode = coalesce(.gambl_sample_id, Tumor_Sample_Barcode)) %>% + select(-.gambl_sample_id) +} + # This is needed for the proteinpainter compatibility. Reads from the # *installed* GAMBLR.data::sample_data (not the locally-built sample_data # above) -- a pre-existing, out-of-scope inconsistency, left as-is. @@ -824,6 +844,7 @@ hg38_publication_rows <- bind_rows( bl_data$ssm_to_bundle %>% mutate(Pipeline = "Publication"), dlbcl_data$ssm_to_bundle %>% mutate(Pipeline = "Publication") ) %>% + relabel_to_sample_id("Thomas") %>% left_join(coding_maf) this_study_samples <- GAMBLR.data::sample_data$meta %>% @@ -842,6 +863,7 @@ coding_maf <- time_it("coding_maf get_ssm_by_samples", { }) fl_data$ssm_to_bundle <- fl_data$ssm_to_bundle %>% + relabel_to_sample_id("Dreval") %>% dplyr::left_join( coding_maf ) %>% @@ -850,7 +872,7 @@ fl_data$ssm_to_bundle <- fl_data$ssm_to_bundle %>% # grch37 Publication rows (Dreval FL + Reddy's original variants). grch37_publication_rows <- bind_rows( fl_data$ssm_to_bundle %>% mutate(Pipeline = "Publication"), - reddy_original_maf %>% mutate(Pipeline = "Publication") + reddy_original_maf %>% relabel_to_sample_id("Reddy") %>% mutate(Pipeline = "Publication") ) setwd(PKG_ROOT) From b693b1da7f6a12cf8a92148f97343c05d66357ca Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 13 Jul 2026 16:42:56 -0700 Subject: [PATCH 23/58] compare_bundle_changes.R: exclude old Arthur strelka rows from SNV diff Pipeline=="strelka" was Arthur's old, now-deleted unfiltered raw-file dump -- confirmed twice (08-15460, DO52686) that these rows' Tumor_Sample_Barcode values are the raw file's own non-standard IDs, not GAMBL's real sample_id, so every one is expected to show up as "lost" regardless of anything else. Filtered out of the old baseline before the comparison so this already-understood, intentional disappearance stops drowning out genuinely new findings on every run. --- data-raw/compare_bundle_changes.R | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/data-raw/compare_bundle_changes.R b/data-raw/compare_bundle_changes.R index 0563dec..d81cc93 100644 --- a/data-raw/compare_bundle_changes.R +++ b/data-raw/compare_bundle_changes.R @@ -124,6 +124,17 @@ old_snv <- bind_rows( old_sd$grch37$ashm %>% mutate(genome_build = "grch37", .source = "ashm"), old_sd$hg38$ashm %>% mutate(genome_build = "hg38", .source = "ashm") ) + +# Pipeline == "strelka" (Arthur's old, unfiltered raw-flat-file dump) is +# intentionally and entirely gone from the new bundle -- that source file +# isn't read anymore. Confirmed (twice: 08-15460, DO52686) that its +# Tumor_Sample_Barcode values are the raw file's own non-standard IDs, not +# GAMBL's real sample_id, so every one of these rows is expected to show up +# as "lost" regardless of anything else in the pipeline. Excluded here so +# that expected, already-understood disappearance doesn't drown out +# genuinely new findings on every run. +old_snv <- old_snv %>% filter(is.na(Pipeline) | Pipeline != "strelka") + snv_key <- c("Tumor_Sample_Barcode", "genome_build", "Chromosome", "Start_Position", "End_Position") snv_cmp <- compare_events(old_snv, new_snv, "Tumor_Sample_Barcode", snv_key, "SNV (maf+ashm)") write_examples(snv_cmp$gained, "Tumor_Sample_Barcode", file.path(outdir, "snv_gained_examples.log")) From ab7061991c21fcd465ddc8c6665852954426f9e5 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 13 Jul 2026 16:56:22 -0700 Subject: [PATCH 24/58] Restore separate genome-wide SNV pull for cell lines compare_bundle_changes.R confirmed the scope reduction flagged during planning: folding cell lines into the panel-restricted consolidated SLMS-3 pull dropped their genome-wide coverage down to ~150-237 genes, showing up as 43k-65k "lost" rows per cell line vs the old bundle. Cell lines (DOHH-2, SU-DHL-10, OCI-Ly10, OCI-Ly3, SU-DHL-4) are now excluded from all_slms3_meta and get their own dedicated, unrestricted get_ssm_by_samples() pull instead, both builds -- matching their original pre-refactor behavior, bound into slms3_grch37/slms3_hg38 alongside everyone else's panel-restricted data. --- data-raw/PR_verification_notes.md | 22 +++++++++++++++++++++ data-raw/assemble_bundled_data.R | 32 ++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md index 48c76b8..2cffa98 100644 --- a/data-raw/PR_verification_notes.md +++ b/data-raw/PR_verification_notes.md @@ -137,6 +137,28 @@ check whether the same bare-vs-suffixed pattern explains the `DO52686`/`07-35482`-style samples from the earlier, larger (3.1M row) loss count. +## Fix: cell lines need a separate, genome-wide SNV pull + +After the strelka-exclusion fix, `compare_bundle_changes.R` came back clean +except for the 5 cell lines (`SU-DHL-4`, `OCI-Ly3`, `OCI-Ly10`, `SU-DHL-10`, +`DOHH-2`), each showing ~43k-65k lost rows against a few hundred/~1k +retained. This is the scope-reduction flagged during planning, now +confirmed as something to actually fix rather than accept: the original +pre-refactor cell-line pull used `get_ssm_by_samples()` with no gene-panel +filter (genome-wide), but folding cell lines into the consolidated, +panel-restricted SLMS-3 pull (Phase 4) silently reduced them to panel-only +coverage. + +Fixed by excluding cell lines from `all_slms3_meta` (so they're not also +pulled panel-restricted) and adding back a dedicated, unrestricted +`get_ssm_by_samples()` pull for them specifically, both builds, bound into +`slms3_grch37`/`slms3_hg38` alongside the panel-restricted data for +everyone else. Still tagged `Pipeline = "SLMS-3"` (same underlying pipeline, +just unrestricted scope for these 5 samples). + +Still need to: rebuild and re-run `compare_bundle_changes.R` to confirm the +cell-line losses are gone. + ## Fix: `study_id` used `patient_id` instead of `sample_id` for Thomas/Dreval/Hilton Caught in review (not by the automated checks above): the first pass at diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 45242d1..68ac360 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -888,8 +888,12 @@ setwd(PKG_ROOT) # duplication structurally impossible regardless of how many cohorts a # sample belongs to. # ============================================================================ +# Cell lines are excluded here -- they get their own separate, genome-wide +# pull below (not restricted to the lymphoma gene panel), so they must not +# also go through this panel-restricted pull or they'd be pulled twice. all_slms3_meta <- sample_data$meta %>% - filter(seq_type %in% c("genome", "capture")) + filter(seq_type %in% c("genome", "capture"), + ! sample_id %in% cell_lines_data$meta$sample_id) slms3_grch37 <- bind_rows( time_it("SLMS-3 genome grch37", pull_data(all_slms3_meta %>% filter(seq_type == "genome"))), @@ -903,6 +907,32 @@ slms3_hg38 <- bind_rows( ) %>% mutate(Pipeline = "SLMS-3") print("Done collecting hg38 SLMS-3") +# Cell lines get their own, separate, genome-WIDE SNV pull -- not restricted +# to the lymphoma gene panel like every other cohort above. This matches +# their original pre-refactor behaviour, which was lost when they were first +# folded into the panel-restricted consolidated pull above (confirmed via +# compare_bundle_changes.R: cell lines showed ~50-65k "lost" rows per sample +# against the old bundle, correctly diagnosed as a real scope reduction, not +# a bug worth working around). +cell_lines_ssm_grch37 <- time_it("cell lines get_ssm_by_samples grch37", { + get_ssm_by_samples( + these_samples_metadata = cell_lines_data$meta, + basic_columns = FALSE + ) %>% select(all_of(all_cols)) %>% mutate(Pipeline = "SLMS-3") +}) + +cell_lines_ssm_hg38 <- time_it("cell lines get_ssm_by_samples hg38", { + get_ssm_by_samples( + these_samples_metadata = cell_lines_data$meta, + projection = "hg38", + basic_columns = FALSE + ) %>% select(all_of(all_cols)) %>% mutate(Pipeline = "SLMS-3") +}) + +slms3_grch37 <- bind_rows(slms3_grch37, cell_lines_ssm_grch37) +slms3_hg38 <- bind_rows(slms3_hg38, cell_lines_ssm_hg38) +print("Done collecting cell line SLMS-3 (genome-wide)") + # ============================================================================ # Phase 5: one consolidated aSHM pull, using the now-complete sample_data$meta From 69b51cc3360b07645cacee9989e50b1f79233ac4 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 13 Jul 2026 17:28:32 -0700 Subject: [PATCH 25/58] Record cell-line fix confirmation and another bonus coverage gain --- data-raw/PR_verification_notes.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md index 2cffa98..0b72eb3 100644 --- a/data-raw/PR_verification_notes.md +++ b/data-raw/PR_verification_notes.md @@ -156,8 +156,16 @@ pulled panel-restricted) and adding back a dedicated, unrestricted everyone else. Still tagged `Pipeline = "SLMS-3"` (same underlying pipeline, just unrestricted scope for these 5 samples). -Still need to: rebuild and re-run `compare_bundle_changes.R` to confirm the -cell-line losses are gone. +**Update**: rebuilt and re-ran -- cell-line losses are gone, confirming the +fix worked. One more thing surfaced in the same run, another bonus rather +than a bug: the top of `snv_persample_counts.tsv` now shows several samples +(e.g. `15-18723_CLC02138`, `15-43891_tumorB`, `07-17613T`) with real gained +counts (64-630) but `n_lost = 0` and `n_retained = 0` -- meaning they had +*zero* rows in the old bundle at all, not a mismatch. Confirmed these were +samples left out of the last release for some reason and are now correctly +included -- consistent with the pattern already seen for Arthur/Hilton +(aSHM, SV coverage) where the old per-cohort-block gating silently excluded +samples that the consolidated pull now correctly picks up. ## Fix: `study_id` used `patient_id` instead of `sample_id` for Thomas/Dreval/Hilton From c88be133775961143905ef35aed38e1bd32af947 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 08:23:59 -0700 Subject: [PATCH 26/58] Fix Phase 4/5: never pass sample_data$meta directly to GAMBLR.results calls GAMBLR_examples_output.log surfaced a real bug: get_coding_ssm() for 183 Dreval FL patients returned zero rows, when it used to return real coding-classified data. Traced via elimination (ID matching, t_alt_count type, min_read_support all confirmed fine) to coding_only=TRUE alone zeroing out 28,867 real SLMS-3/grch37 rows -- every one of them was non-coding, which is biologically implausible across 183 real patients and the whole lymphoma gene panel. Root cause: unix_group (and likely other native GAMBL metadata columns) is NA for every cohort built by hand in this script (BL_Thomas, DLBCL_Thomas, FL_Dreval, DLBCL_cell_lines -- 878 samples) rather than pulled live from get_gambl_metadata(), and get_ssm_by_regions() apparently needs it to correctly resolve which merged flat file to read for a sample. This was specific to the two things newly introduced this session -- Phase 4's consolidated SLMS-3 pull and Phase 5's consolidated aSHM pull -- both of which passed sample_data$meta-derived subsets directly to get_ssm_by_regions(). Every other GAMBLR.results call site in this script already re-fetched fresh, complete metadata via get_gambl_metadata() filtered by sample_id first, which is why this only surfaced here. Fixed both: sample_data$meta is now used only to determine which sample_ids belong in each pull; the actual these_samples_metadata passed to get_ssm_by_regions() is always a fresh get_gambl_metadata() call filtered to that same sample_id set. --- data-raw/PR_verification_notes.md | 44 +++++++++++++++++++++++++++++++ data-raw/assemble_bundled_data.R | 23 ++++++++++++++-- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md index 0b72eb3..8db3260 100644 --- a/data-raw/PR_verification_notes.md +++ b/data-raw/PR_verification_notes.md @@ -137,6 +137,50 @@ check whether the same bare-vs-suffixed pattern explains the `DO52686`/`07-35482`-style samples from the earlier, larger (3.1M row) loss count. +## Fix: consolidated Phase 4/5 pulls passed sample_data$meta directly to get_ssm_by_regions() -- missing coding calls for hand-built cohorts + +`GAMBLR_examples_output.log`'s regenerated run showed a `get_coding_ssm()` +example for 183 Dreval FL patients (sample_id matching `"SP"`) return **zero +rows**, when it used to return real coding-classified data. Traced through +several rounds of elimination (ID matching confirmed fine, `t_alt_count` +confirmed numeric, `min_read_support` confirmed innocent) to: `coding_only += TRUE` alone reduced 28,867 real SLMS-3/grch37 rows for these patients to +zero -- every one of them was non-coding (`Intron`/`5'Flank`/`3'Flank`/ +`5'UTR`/`3'UTR`). For 183 real FL patients to have literally zero coding +mutations across the whole lymphoma gene panel is biologically implausible. + +Root cause: `unix_group` (and likely other native GAMBL metadata columns) +is `NA` for every cohort built by hand in this script rather than pulled +live from `get_gambl_metadata()` -- confirmed directly: +```r +filter(sample_data$meta, is.na(unix_group)) %>% count(study) +# BL_Thomas 234, DLBCL_Arthur 153, DLBCL_Thomas 43, DLBCL_cell_lines 5, FL_Dreval 443 +filter(sample_data$meta, !is.na(unix_group)) %>% count(study) +# DLBCL_Hilton 159, NCI_DLBCL_Golub 124, dlbcl_chapuy 233, dlbcl_reddy 999, dlbcl_schmitz 951 +``` +`get_ssm_by_regions()` apparently relies on columns like `unix_group` to +determine which underlying merged flat file to read for a sample -- with it +missing, it was silently returning an incomplete (non-coding-only) subset +for affected samples instead of erroring. This was specific to Phase 4 (the +consolidated SLMS-3 pull) and Phase 5 (the consolidated aSHM pull), both of +which passed `sample_data$meta`-derived subsets directly to +`get_ssm_by_regions()` -- every *other* GAMBLR.results call site in this +script (the SV/bedpe block, the cell-lines pull, the Publication-pipeline +enrichment steps) already re-fetched fresh, complete metadata via +`get_gambl_metadata()` filtered by `sample_id` first, which is exactly why +this only surfaced in the two things newly introduced this session. + +Fixed both: `sample_data$meta` is now used only to determine which +`sample_id`s belong in each pull; the actual `these_samples_metadata` +passed to `get_ssm_by_regions()` is always a fresh `get_gambl_metadata()` +call filtered to that same `sample_id` set. General principle going +forward, not just for these two spots: never pass `sample_data$meta` (or +anything derived from it) directly to a GAMBLR.results function. + +Still need to: rebuild and re-check the Dreval `"SP"` patient example, and +re-run `compare_bundle_changes.R` broadly in case this affected other +samples/cohorts beyond just the one example that happened to catch it. + ## Fix: cell lines need a separate, genome-wide SNV pull After the strelka-exclusion fix, `compare_bundle_changes.R` came back clean diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 68ac360..70675ba 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -891,9 +891,21 @@ setwd(PKG_ROOT) # Cell lines are excluded here -- they get their own separate, genome-wide # pull below (not restricted to the lymphoma gene panel), so they must not # also go through this panel-restricted pull or they'd be pulled twice. +# +# sample_data$meta is used ONLY to decide which sample_ids belong in this +# pull -- never passed directly to a GAMBLR.results call. Cohorts built by +# hand (BL_Thomas, DLBCL_Thomas, FL_Dreval, DLBCL_cell_lines) don't have +# every column a fresh get_gambl_metadata() pull would (e.g. unix_group is +# NA for all of them), which silently caused get_ssm_by_regions() to miss +# real coding-classified calls for those samples (confirmed: a raw pull for +# one such sample had real Missense_Mutation/Nonsense_Mutation/Silent rows +# that never made it into the assembled maf table). Re-fetching complete, +# live metadata for exactly this sample_id set avoids that entirely. all_slms3_meta <- sample_data$meta %>% filter(seq_type %in% c("genome", "capture"), ! sample_id %in% cell_lines_data$meta$sample_id) +all_slms3_meta <- get_gambl_metadata() %>% + filter(sample_id %in% all_slms3_meta$sample_id) slms3_grch37 <- bind_rows( time_it("SLMS-3 genome grch37", pull_data(all_slms3_meta %>% filter(seq_type == "genome"))), @@ -942,6 +954,13 @@ print("Done collecting cell line SLMS-3 (genome-wide)") # aSHM block (which existed only to compensate for that gap). Arthur gets # aSHM coverage for the first time as a result. # ============================================================================ +# sample_data$meta is used only to fix the sample_id set -- see the same +# rationale next to all_slms3_meta above for why a fresh get_gambl_metadata() +# pull is used for the actual GAMBLR.results call instead of sample_data$meta +# directly. +ashm_pull_meta <- get_gambl_metadata() %>% + filter(sample_id %in% sample_data$meta$sample_id) + regions_bed_grch37 <- create_bed_data( grch37_ashm_regions, fix_names = "concat", @@ -950,7 +969,7 @@ regions_bed_grch37 <- create_bed_data( grch37_ashm <- time_it("grch37_ashm get_ssm_by_regions", { get_ssm_by_regions( - these_samples_metadata = sample_data$meta, + these_samples_metadata = ashm_pull_meta, regions_bed = regions_bed_grch37, streamlined = FALSE, basic_columns = FALSE @@ -970,7 +989,7 @@ regions_bed_hg38 <- create_bed_data( hg38_ashm <- time_it("hg38_ashm get_ssm_by_regions", { get_ssm_by_regions( - these_samples_metadata = sample_data$meta, + these_samples_metadata = ashm_pull_meta, regions_bed = regions_bed_hg38, projection = "hg38", streamlined = FALSE, From 45afd435289c0c7396a8a583232be7438ac6a908 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 08:36:48 -0700 Subject: [PATCH 27/58] Record convergence: sample_data$meta fix confirmed, refactor stable --- data-raw/PR_verification_notes.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md index 8db3260..875c738 100644 --- a/data-raw/PR_verification_notes.md +++ b/data-raw/PR_verification_notes.md @@ -177,9 +177,30 @@ call filtered to that same `sample_id` set. General principle going forward, not just for these two spots: never pass `sample_data$meta` (or anything derived from it) directly to a GAMBLR.results function. -Still need to: rebuild and re-check the Dreval `"SP"` patient example, and -re-run `compare_bundle_changes.R` broadly in case this affected other -samples/cohorts beyond just the one example that happened to catch it. +**Update**: rebuilt and re-ran `compare_bundle_changes.R` broadly (not just +the one example). Confirmed fixed -- samples that previously showed +catastrophic 0-retained losses now show large, healthy retained counts +(`SP116715`: 2,901 retained; `16-11636T`: 1,624; `14-20552_tumorB`: 670; +`04-24937T`: 990), with only small gains/losses on top (single digits to +low teens). Total SNV loss across the whole bundle dropped from 3.1M rows +to 299, spread across 113 samples (~2.6 rows/sample average) -- a +different order of magnitude, consistent with normal residual noise rather +than a bug. + +## Current state (post all fixes above) + +- **SNV**: 348 samples w/ gains, 113 samples w/ losses, 299 total lost rows + -- converged to normal residual noise. The gained-only, 0-retained + samples at the top of `snv_persample_counts.tsv` are the known "left out + of the last release" bonus fix (unrelated, already understood). +- **CNV (seg)**: unchanged from prior runs (5 cell lines w/ gains, 0 w/ + losses) -- expected, nothing in this session touched seg assembly. +- **SV (bedpe)**: unchanged from prior runs (433 w/ gains, 7 w/ losses) -- + expected, consistent with the earlier Arthur/Hilton SV coverage bonus + fixes already being stable. + +Considering this refactor converged and ready for PR, pending final +sign-off. ## Fix: cell lines need a separate, genome-wide SNV pull From 9b71e2d3504972a1da976e7669fb1462d7ae438e Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 09:20:21 -0700 Subject: [PATCH 28/58] fix: proteinpainter-compat block filtered stale sample_data on cohort, not study GAMBLR.data::sample_data$meta (the installed, stale snapshot this block intentionally reads) has FL_Dreval under the study column, not cohort -- filtering this_study_samples on cohort silently collapsed it to just the 5 cell-line sample_ids, so coding_maf (meant to enrich Dreval's Publication rows with RefSeq/Protein_position) was built from cell-line mutations instead and never matched any Dreval row. Same cohort/study mismatch fixed for the BL_Thomas/DLBCL_Thomas/DLBCL_cell_lines lookups above it. Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 70675ba..396f7bb 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -805,11 +805,11 @@ selected_columns <- c( ) these_samples <- GAMBLR.data::sample_data$meta %>% - filter(cohort %in% c("BL_Thomas")) %>% + filter(study %in% c("BL_Thomas")) %>% pull(sample_id) these_samples_dlbcl <- GAMBLR.data::sample_data$meta %>% - filter(cohort %in% c("DLBCL_Thomas", "DLBCL_cell_lines")) %>% + filter(study %in% c("DLBCL_Thomas", "DLBCL_cell_lines")) %>% pull(sample_id) coding_maf <- read_tsv("/projects/adult_blgsp/results_manuscript/BL.hg38.CDS.maf") %>% # get from flat maf file to show SSM in hg38 coordinates similar to the original manuscript @@ -848,7 +848,7 @@ hg38_publication_rows <- bind_rows( left_join(coding_maf) this_study_samples <- GAMBLR.data::sample_data$meta %>% - filter(cohort %in% c("FL_Dreval", "DLBCL_cell_lines")) %>% + filter(study %in% c("FL_Dreval", "DLBCL_cell_lines")) %>% pull(sample_id) # FLs in grch37 From 18f42a97e02b9b2e2b432c0ee061c52a627f7a63 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 09:38:55 -0700 Subject: [PATCH 29/58] add real-time diagnostics to assemble_bundled_data.R checkpoints Prints per-study row/sample counts (and, at the two live-metadata refetch points, exactly which sample_ids get silently dropped) at every major transition: per-cohort metadata assembly, the cohort->study rename/rejoin block, sample_study assembly, the Phase 3 proteinpainter-enrichment joins, the Phase 4 all_slms3_meta refetch and pull outputs, the Phase 5 aSHM pull, and the final per-study coding-classified maf counts. Goal: see exactly where a cohort's count changes during a build instead of only being able to diagnose it after the fact via a separate GSC session against the finished DB. Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 142 +++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 396f7bb..977361b 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -139,6 +139,54 @@ pull_data <- function( } +# --- Diagnostics ------------------------------------------------------------- +# Prints per-study row/sample counts at a labeled checkpoint, so a build's +# log shows exactly where a cohort's count changes (or unexpectedly drops) +# in real time, instead of only being discoverable afterward via a separate +# GSC session against the finished DB. sample_col/study_col differ across +# checkpoints (metadata frames use sample_id + study or cohort; maf-shaped +# frames use Tumor_Sample_Barcode and carry no inline study column at all -- +# use diag_summary_maf for those). +diag_summary <- function(df, label, sample_col = "sample_id", study_col = "study") { + message(sprintf("[DIAG] %s: %d rows, %d distinct %s", label, nrow(df), n_distinct(df[[sample_col]]), sample_col)) + if (!is.null(study_col) && study_col %in% names(df)) { + summary_tbl <- df %>% + group_by(.data[[study_col]]) %>% + summarise(n_rows = n(), n_samples = n_distinct(.data[[sample_col]]), .groups = "drop") %>% + arrange(desc(n_rows)) + print(as.data.frame(summary_tbl)) + } + invisible(df) +} + +# Same idea for maf/ashm-shaped frames: no inline study column, so study is +# looked up via sample_study. A sample in >1 study is counted once per study +# it belongs to (matching how sample_study itself works), so totals across +# studies can exceed nrow(df) -- that's expected, not a bug. +diag_summary_maf <- function(df, label, study_lookup = sample_data$sample_study) { + message(sprintf("[DIAG] %s: %d rows, %d distinct Tumor_Sample_Barcode", label, nrow(df), n_distinct(df$Tumor_Sample_Barcode))) + summary_tbl <- df %>% + left_join(study_lookup %>% select(sample_id, study), by = c("Tumor_Sample_Barcode" = "sample_id")) %>% + group_by(study) %>% + summarise(n_rows = n(), n_samples = n_distinct(Tumor_Sample_Barcode), .groups = "drop") %>% + arrange(desc(n_rows)) + print(as.data.frame(summary_tbl)) + invisible(df) +} + +# Prints which of before_ids are missing from after_ids -- for catching +# silent-filter row loss (e.g. a get_gambl_metadata() re-fetch that quietly +# drops sample_ids not present in the live source) at the exact step it +# happens, instead of guessing after the fact. +diag_missing <- function(before_ids, after_ids, label) { + missing <- setdiff(before_ids, after_ids) + message(sprintf("[DIAG] %s: %d of %d input ids missing after this step%s", + label, length(missing), length(before_ids), + if (length(missing) > 0) paste0(" (e.g. ", paste(head(missing, 5), collapse = ", "), ")") else "")) + invisible(missing) +} + + # ============================================================================ # Phase 1: metadata (and, where genuinely distinct per-paper data exists, # Publication-pipeline SSM calls) for every cohort. No SLMS-3 pulling here -- @@ -192,6 +240,8 @@ bl_data$meta_to_bundle <- read_xlsx( mutate(genetic_subgroup = Subgroup) %>% select(- Subgroup) +diag_summary(bl_data$meta_to_bundle, "bl_data$meta_to_bundle", study_col = "cohort") + bl_data$ssm_to_bundle <- read_xlsx( "inst/extdata/studies/BL_Thomas.xlsx", sheet = 6 @@ -256,6 +306,7 @@ fl_data$meta_to_bundle <- fl_data$meta_to_bundle %>% filter(! sample_id == "NA") %>% arrange(sample_id) +diag_summary(fl_data$meta_to_bundle, "fl_data$meta_to_bundle", study_col = "cohort") fl_data$ssm_to_bundle <- read_xlsx( "inst/extdata/studies/FL_Dreval.xlsx", @@ -274,6 +325,8 @@ fl_data$ssm_to_bundle <- fl_data$ssm_to_bundle %>% select(names(GAMBLR.helpers:::maf_header[1:45])) +diag_summary_maf(fl_data$ssm_to_bundle, "fl_data$ssm_to_bundle (raw, as read from xlsx sheet 2)", study_lookup = data.frame(sample_id = fl_data$meta_to_bundle$sample_id, study = "FL_Dreval")) + fl_data$cnv_to_bundle <- read_xlsx( "inst/extdata/studies/FL_Dreval.xlsx", sheet = 3 @@ -318,6 +371,8 @@ dlbcl_data$meta_to_bundle <- dlbcl_data$meta_to_bundle %>% filter(! sample_id == "NA") %>% arrange(sample_id) +diag_summary(dlbcl_data$meta_to_bundle, "dlbcl_data$meta_to_bundle", study_col = "cohort") + dlbcl_data$ssm_to_bundle <- read_xlsx( "inst/extdata/studies/BL_Thomas.xlsx", sheet = 6 @@ -421,6 +476,9 @@ reddy_data$meta_to_bundle <- left_join( reddy_meta_gambl ) +diag_missing(reddy_meta$sample_id, reddy_data$meta_to_bundle$sample_id[!is.na(reddy_data$meta_to_bundle$cohort)], "Reddy meta -> reddy_meta_gambl join (missing cohort after join)") +diag_summary(reddy_data$meta_to_bundle, "reddy_data$meta_to_bundle", study_col = "cohort") + schmitz_data$meta <- get_gambl_metadata() %>% dplyr::filter(cohort == "dlbcl_schmitz") %>% mutate(reference_PMID = pmids$Schmitz_DLBCL) %>% @@ -455,6 +513,8 @@ all_capture_meta <- bind_rows( reddy_meta_gambl ) +diag_summary(all_capture_meta, "all_capture_meta", study_col = "cohort") + # No paper-specific ID has been parsed anywhere in this script for these # three cohorts (unlike Reddy/Thomas/Dreval/Arthur, they're pulled straight # from GAMBL's own metadata, never cross-referenced against each paper's own @@ -496,6 +556,9 @@ cell_lines_data$meta <- get_gambl_metadata(seq_type_filter = "genome") %>% )) %>% arrange(sample_id) +message(sprintf("[DIAG] cell_lines_data$meta: %d rows (expect 5): %s", + nrow(cell_lines_data$meta), paste(cell_lines_data$meta$sample_id, collapse = ", "))) + cell_lines_data$meta_to_bundle <- data.frame( cell_lines_data$meta$patient_id, cell_lines_data$meta$sample_id, @@ -571,6 +634,9 @@ arthur_meta <- get_gambl_metadata() %>% reference_PMID = pmids$Arthur_DLBCL ) +diag_missing(arthur_case_ids$patient_id, arthur_meta$patient_id, "Arthur Case ID -> patient_id join (WGS-flagged patients with no matching genome sample in get_gambl_metadata())") +diag_summary(arthur_meta, "arthur_meta", study_col = "cohort") + arthur_study <- data.frame( sample_id = arthur_meta$sample_id, study = "Arthur", @@ -598,6 +664,9 @@ trios_meta <- get_gambl_metadata() %>% reference_PMID = pmids$Hilton_DLBCL ) +diag_missing(trios_samples$DNAseq_sample_id, trios_meta$sample_id, "Hilton trios DNAseq_sample_id -> get_gambl_metadata() (samples in the trios sheet not found live)") +diag_summary(trios_meta, "trios_meta", study_col = "cohort") + # study_id uses sample_id, not patient_id: Hilton is a trios study, so a # single patient can have multiple samples (e.g. LY_RELY_116_tumorA and # LY_RELY_116_tumorB) -- patient_id would collapse them to the same @@ -648,22 +717,35 @@ sample_data$meta <- bind_rows( trios_meta ) +diag_summary(sample_data$meta, "sample_data$meta (all cohorts bound, pre metadata-fixing)", study_col = "cohort") + ### begin metadata fixing # This preserves the original cohort column and ensures there are no duplicates # in the metadata fix <- sample_data$meta fix <- fix %>% rename(study = cohort) +diag_summary(fix, "fix (post rename cohort->study)", study_col = "study") + +pre_join_ids <- fix$sample_id fix <- left_join( fix, get_gambl_metadata() %>% select(sample_id, seq_type, cohort) ) +message(sprintf("[DIAG] fix (post live get_gambl_metadata() join): %d rows (was %d rows pre-join -- any increase means the join fanned out)", nrow(fix), length(pre_join_ids))) +diag_summary(fix, "fix (post live get_gambl_metadata() join)", study_col = "study") + fix <- fix %>% filter(!is.na(study)) +diag_missing(pre_join_ids, fix$sample_id, "fix: post-join !is.na(study) filter (sample_ids dropped here)") +diag_summary(fix, "fix (post filter !is.na(study))", study_col = "study") + fix <- distinct(fix) +diag_summary(fix, "fix (post distinct -- this becomes sample_data$meta)", study_col = "study") + sample_data$meta <- fix ### end metadata fixing @@ -683,6 +765,8 @@ sample_data$sample_study <- bind_rows( hilton_study ) %>% distinct() +diag_summary(sample_data$sample_study, "sample_data$sample_study", study_col = "study") + #add SVs # Adding the manta SVs for published studies. Scoped to sample_data$meta (the @@ -812,6 +896,9 @@ these_samples_dlbcl <- GAMBLR.data::sample_data$meta %>% filter(study %in% c("DLBCL_Thomas", "DLBCL_cell_lines")) %>% pull(sample_id) +message(sprintf("[DIAG] these_samples (BL_Thomas, from installed GAMBLR.data::sample_data): %d ids", length(these_samples))) +message(sprintf("[DIAG] these_samples_dlbcl (DLBCL_Thomas + DLBCL_cell_lines, from installed GAMBLR.data::sample_data): %d ids", length(these_samples_dlbcl))) + coding_maf <- read_tsv("/projects/adult_blgsp/results_manuscript/BL.hg38.CDS.maf") %>% # get from flat maf file to show SSM in hg38 coordinates similar to the original manuscript filter(Tumor_Sample_Barcode %in% these_samples & # drop BL58 cell line ! str_detect(Tumor_Sample_Barcode, "^SP|^06")) %>% # drop ICGC and 1 LLMPP case @@ -835,6 +922,8 @@ coding_maf <- bind_rows( coding_maf_dlbcl ) +message(sprintf("[DIAG] coding_maf (BL+DLBCL Thomas, hg38 proteinpainter enrichment source): %d rows, %d distinct Tumor_Sample_Barcode", nrow(coding_maf), n_distinct(coding_maf$Tumor_Sample_Barcode))) + # hg38 Publication rows (Thomas BL + Thomas DLBCL), enriched via the # proteinpainter-compatibility join above. This used to be applied directly # to sample_data$hg38$maf once it existed early in the script; now it's @@ -847,10 +936,14 @@ hg38_publication_rows <- bind_rows( relabel_to_sample_id("Thomas") %>% left_join(coding_maf) +diag_summary_maf(hg38_publication_rows, "hg38_publication_rows (Thomas BL+DLBCL, final)") + this_study_samples <- GAMBLR.data::sample_data$meta %>% filter(study %in% c("FL_Dreval", "DLBCL_cell_lines")) %>% pull(sample_id) +message(sprintf("[DIAG] this_study_samples (FL_Dreval + DLBCL_cell_lines, from installed GAMBLR.data::sample_data): %d ids", length(this_study_samples))) + # FLs in grch37 coding_maf <- time_it("coding_maf get_ssm_by_samples", { get_ssm_by_samples( @@ -862,6 +955,10 @@ coding_maf <- time_it("coding_maf get_ssm_by_samples", { ) }) +message(sprintf("[DIAG] coding_maf (FL_Dreval + DLBCL_cell_lines, grch37 proteinpainter enrichment source): %d rows, %d distinct Tumor_Sample_Barcode", nrow(coding_maf), n_distinct(coding_maf$Tumor_Sample_Barcode))) + +diag_summary_maf(fl_data$ssm_to_bundle, "fl_data$ssm_to_bundle (pre relabel/enrichment)", study_lookup = data.frame(sample_id = fl_data$meta_to_bundle$sample_id, study = "FL_Dreval")) + fl_data$ssm_to_bundle <- fl_data$ssm_to_bundle %>% relabel_to_sample_id("Dreval") %>% dplyr::left_join( @@ -869,12 +966,16 @@ fl_data$ssm_to_bundle <- fl_data$ssm_to_bundle %>% ) %>% distinct() +diag_summary_maf(fl_data$ssm_to_bundle, "fl_data$ssm_to_bundle (post relabel/enrichment/distinct -- final)", study_lookup = data.frame(sample_id = fl_data$meta_to_bundle$sample_id, study = "FL_Dreval")) + # grch37 Publication rows (Dreval FL + Reddy's original variants). grch37_publication_rows <- bind_rows( fl_data$ssm_to_bundle %>% mutate(Pipeline = "Publication"), reddy_original_maf %>% relabel_to_sample_id("Reddy") %>% mutate(Pipeline = "Publication") ) +diag_summary_maf(grch37_publication_rows, "grch37_publication_rows (Dreval FL + Reddy original, final)") + setwd(PKG_ROOT) @@ -904,20 +1005,38 @@ setwd(PKG_ROOT) all_slms3_meta <- sample_data$meta %>% filter(seq_type %in% c("genome", "capture"), ! sample_id %in% cell_lines_data$meta$sample_id) + +diag_summary(all_slms3_meta, "all_slms3_meta (pre live-metadata refetch)", study_col = "study") +pre_refetch_ids <- all_slms3_meta$sample_id + all_slms3_meta <- get_gambl_metadata() %>% filter(sample_id %in% all_slms3_meta$sample_id) +# The single most important check in this script: if get_gambl_metadata()'s +# live source is missing sample_ids that sample_data$meta has (e.g. an +# externally-integrated cohort not fully registered there), this filter +# silently drops them with no error -- every study's SLMS-3 coverage for +# those samples goes to zero downstream. diag_missing() catches this by name +# instead of leaving it to be inferred later from a low mutation count. +diag_missing(pre_refetch_ids, all_slms3_meta$sample_id, "all_slms3_meta live-metadata refetch (sample_ids dropped because live get_gambl_metadata() doesn't have them)") +# study isn't in all_slms3_meta itself (it comes straight from +# get_gambl_metadata(), which has no such column) -- join sample_study just +# for this diagnostic so the breakdown is by study, not left un-grouped. +diag_summary_maf(all_slms3_meta %>% rename(Tumor_Sample_Barcode = sample_id), "all_slms3_meta (post live-metadata refetch -- what Phase 4 actually pulls for)") + slms3_grch37 <- bind_rows( time_it("SLMS-3 genome grch37", pull_data(all_slms3_meta %>% filter(seq_type == "genome"))), time_it("SLMS-3 capture grch37", pull_data(all_slms3_meta %>% filter(seq_type == "capture"))) ) %>% mutate(Pipeline = "SLMS-3") print("Done collecting grch37 SLMS-3") +diag_summary_maf(slms3_grch37, "slms3_grch37 (panel-restricted pull, pre cell-line union)") slms3_hg38 <- bind_rows( time_it("SLMS-3 genome hg38", pull_data(all_slms3_meta %>% filter(seq_type == "genome"), "hg38")), time_it("SLMS-3 capture hg38", pull_data(all_slms3_meta %>% filter(seq_type == "capture"), "hg38")) ) %>% mutate(Pipeline = "SLMS-3") print("Done collecting hg38 SLMS-3") +diag_summary_maf(slms3_hg38, "slms3_hg38 (panel-restricted pull, pre cell-line union)") # Cell lines get their own, separate, genome-WIDE SNV pull -- not restricted # to the lymphoma gene panel like every other cohort above. This matches @@ -941,9 +1060,14 @@ cell_lines_ssm_hg38 <- time_it("cell lines get_ssm_by_samples hg38", { ) %>% select(all_of(all_cols)) %>% mutate(Pipeline = "SLMS-3") }) +message(sprintf("[DIAG] cell_lines_ssm_grch37: %d rows, %d distinct Tumor_Sample_Barcode (expect 5)", nrow(cell_lines_ssm_grch37), n_distinct(cell_lines_ssm_grch37$Tumor_Sample_Barcode))) +message(sprintf("[DIAG] cell_lines_ssm_hg38: %d rows, %d distinct Tumor_Sample_Barcode (expect 5)", nrow(cell_lines_ssm_hg38), n_distinct(cell_lines_ssm_hg38$Tumor_Sample_Barcode))) + slms3_grch37 <- bind_rows(slms3_grch37, cell_lines_ssm_grch37) slms3_hg38 <- bind_rows(slms3_hg38, cell_lines_ssm_hg38) print("Done collecting cell line SLMS-3 (genome-wide)") +diag_summary_maf(slms3_grch37, "slms3_grch37 (final, incl. cell lines)") +diag_summary_maf(slms3_hg38, "slms3_hg38 (final, incl. cell lines)") # ============================================================================ @@ -961,6 +1085,9 @@ print("Done collecting cell line SLMS-3 (genome-wide)") ashm_pull_meta <- get_gambl_metadata() %>% filter(sample_id %in% sample_data$meta$sample_id) +diag_missing(sample_data$meta$sample_id, ashm_pull_meta$sample_id, "ashm_pull_meta live-metadata refetch (sample_ids dropped because live get_gambl_metadata() doesn't have them)") +diag_summary_maf(ashm_pull_meta %>% rename(Tumor_Sample_Barcode = sample_id), "ashm_pull_meta (what Phase 5 actually pulls for)") + regions_bed_grch37 <- create_bed_data( grch37_ashm_regions, fix_names = "concat", @@ -1004,6 +1131,9 @@ hg38_ashm <- hg38_ashm %>% sample_data$grch37$ashm <- grch37_ashm %>% distinct() sample_data$hg38$ashm <- hg38_ashm %>% distinct() +diag_summary_maf(sample_data$grch37$ashm, "sample_data$grch37$ashm (final)") +diag_summary_maf(sample_data$hg38$ashm, "sample_data$hg38$ashm (final)") + print("done extracting aSHM mutations from GAMBLR.results") @@ -1022,6 +1152,18 @@ sample_data$hg38$maf <- bind_rows( slms3_hg38 ) +# The other money diagnostic: per-study coding-classified counts, mirroring +# what get_all_coding_ssm() showed missing on the GSC -- printed at build +# time now instead of only being discoverable in a separate later session. +coding_classes <- c("Missense_Mutation", "Nonsense_Mutation", "Frame_Shift_Del", + "Frame_Shift_Ins", "In_Frame_Del", "In_Frame_Ins", + "Splice_Site", "Splice_Region", "Nonstop_Mutation", + "Translation_Start_Site") +diag_summary_maf(sample_data$grch37$maf, "sample_data$grch37$maf (final, all Variant_Classifications)") +diag_summary_maf(sample_data$grch37$maf %>% filter(Variant_Classification %in% coding_classes), "sample_data$grch37$maf (final, coding-classified only)") +diag_summary_maf(sample_data$hg38$maf, "sample_data$hg38$maf (final, all Variant_Classifications)") +diag_summary_maf(sample_data$hg38$maf %>% filter(Variant_Classification %in% coding_classes), "sample_data$hg38$maf (final, coding-classified only)") + print("done extracting all mutations in lymphoma genes with GAMBLR.results") From 4f849cd064405cef45db827672075e4f024fabfb Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 09:47:43 -0700 Subject: [PATCH 30/58] fix: diag_summary_maf rename collided with existing Tumor_Sample_Barcode column get_gambl_metadata() already returns a Tumor_Sample_Barcode column alongside sample_id, so renaming sample_id -> Tumor_Sample_Barcode for the all_slms3_meta and ashm_pull_meta diagnostics collided and crashed the build (rename(): "Names must be unique"). Drop the existing column first. Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 977361b..339f029 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -1019,10 +1019,12 @@ all_slms3_meta <- get_gambl_metadata() %>% # those samples goes to zero downstream. diag_missing() catches this by name # instead of leaving it to be inferred later from a low mutation count. diag_missing(pre_refetch_ids, all_slms3_meta$sample_id, "all_slms3_meta live-metadata refetch (sample_ids dropped because live get_gambl_metadata() doesn't have them)") -# study isn't in all_slms3_meta itself (it comes straight from -# get_gambl_metadata(), which has no such column) -- join sample_study just -# for this diagnostic so the breakdown is by study, not left un-grouped. -diag_summary_maf(all_slms3_meta %>% rename(Tumor_Sample_Barcode = sample_id), "all_slms3_meta (post live-metadata refetch -- what Phase 4 actually pulls for)") +# study isn't in all_slms3_meta itself -- join sample_study just for this +# diagnostic so the breakdown is by study, not left un-grouped. Drop the +# existing (unrelated) Tumor_Sample_Barcode column first: get_gambl_metadata() +# already returns one alongside sample_id, so renaming sample_id would +# otherwise collide with it. +diag_summary_maf(all_slms3_meta %>% select(-any_of("Tumor_Sample_Barcode")) %>% rename(Tumor_Sample_Barcode = sample_id), "all_slms3_meta (post live-metadata refetch -- what Phase 4 actually pulls for)") slms3_grch37 <- bind_rows( time_it("SLMS-3 genome grch37", pull_data(all_slms3_meta %>% filter(seq_type == "genome"))), @@ -1086,7 +1088,7 @@ ashm_pull_meta <- get_gambl_metadata() %>% filter(sample_id %in% sample_data$meta$sample_id) diag_missing(sample_data$meta$sample_id, ashm_pull_meta$sample_id, "ashm_pull_meta live-metadata refetch (sample_ids dropped because live get_gambl_metadata() doesn't have them)") -diag_summary_maf(ashm_pull_meta %>% rename(Tumor_Sample_Barcode = sample_id), "ashm_pull_meta (what Phase 5 actually pulls for)") +diag_summary_maf(ashm_pull_meta %>% select(-any_of("Tumor_Sample_Barcode")) %>% rename(Tumor_Sample_Barcode = sample_id), "ashm_pull_meta (what Phase 5 actually pulls for)") regions_bed_grch37 <- create_bed_data( grch37_ashm_regions, From 841716d46767fb53ca6bdc566c794ab910b5da32 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 11:57:47 -0700 Subject: [PATCH 31/58] add variant_pipeline join table; fix silent SLMS-3->Publication relabeling Root cause of the FL_Dreval "near-zero coding variants" symptom: a real mutation independently called by both a cohort's own published/curated maf (Pipeline="Publication") and our separate SLMS-3 recall can share an identical (Tumor_Sample_Barcode, Chromosome, Start_Position, End_Position, Tumor_Seq_Allele2) key. write_mutations_db()'s write-time dedup keeps only one row per key (whichever pipeline's data was assembled first -- Publication, for Dreval), silently making the variant invisible to any tool_name="slms-3"-filtered query even though it's still fully present in the table. Confirmed empirically: 6,791 of 6,807 (99.8%) of Dreval's real, undeduplicated SLMS-3 coding calls have an exact key match among the DB's Publication-tagged rows. Same architectural fix as sample_study: a single-valued Pipeline column can't represent a variant called by more than one pipeline. Adds variant_pipeline (one row per variant x Pipeline pair), captured from the full pre-dedup data in write_mutations_db(). get_ssm_from_db()'s tool_name filter now resolves via a semi-join against variant_pipeline instead of maf/ashm's own Pipeline column, falling back to the old direct-column filter for DBs built before this table existed. Verified with synthetic data (dedup collapses a dual-pipeline variant to one maf row but produces two variant_pipeline rows; tool_name="slms-3" and "publication" both find it; dropping variant_pipeline falls back to the old, narrower behavior rather than erroring). Also: - assemble_bundled_data.R: extensive real-time build diagnostics (diag_summary/diag_summary_maf/diag_missing) at every major checkpoint, plus per-call raw-vs-post-Hugo_Symbol-filter logging inside pull_data(), added while investigating the above. - Fixed a real (separate, narrower) bug found along the way: the proteinpainter-compatibility block's these_samples/these_samples_dlbcl/ this_study_samples filtered the installed GAMBLR.data::sample_data$meta on a column called "cohort" instead of "study", silently reducing FL_Dreval+DLBCL_cell_lines to just 5 ids and breaking their RefSeq/ Protein_position enrichment join. - data-vars.R: added "study", "Pipeline", "elem" to globalVariables() for the new NSE column references (also covers a pre-existing gap from the earlier this_study feature). - PR_verification_notes.md: corrected the premature "converged, ready for PR" framing now that the real root cause is understood and fixed. Co-Authored-By: Claude Sonnet 5 --- R/data-vars.R | 11 ++- R/gambl_mutations_db.R | 23 ++++- R/get_ssm_from_db.R | 32 ++++++- data-raw/PR_verification_notes.md | 147 +++++++++++++++++++++++++++++- data-raw/assemble_bundled_data.R | 45 ++++++++- data-raw/test_gambl_db.R | 13 ++- data-raw/write_mutations_db.R | 35 ++++++- 7 files changed, 283 insertions(+), 23 deletions(-) diff --git a/R/data-vars.R b/R/data-vars.R index 11cf5ab..098acdc 100644 --- a/R/data-vars.R +++ b/R/data-vars.R @@ -21,10 +21,11 @@ if (getRversion() >= "2.15.1") { "bin", "category", "chrom", "cohort", "colour", "curated", "end", "ensembl_gene_id", "gambl_metadata", "gene", "genome_build", "grch37_ashm_regions", "group", "head", "hg38_ashm_regions", - "hot_spot", "hotspot_regions_grch37", "hotspot_regions_hg38", - "is_alias", "log.ratio", "mutated", "mutation_count", "n_mut", - "name", "pair_status", "pathology", "patient_id", "region", - "row_id", "sample_id", "seq_type", "start", "t_alt_count", - "tumour_sample_id", "window_end", "window_start" + "hot_spot", "hotspot_regions_grch37", "hotspot_regions_hg38", + "is_alias", "log.ratio", "mutated", "mutation_count", "n_mut", + "name", "pair_status", "pathology", "patient_id", "region", + "row_id", "sample_id", "seq_type", "start", "t_alt_count", + "tumour_sample_id", "window_end", "window_start", + "study", "Pipeline", "elem" )) } diff --git a/R/gambl_mutations_db.R b/R/gambl_mutations_db.R index 9f9d80b..b5b2f05 100644 --- a/R/gambl_mutations_db.R +++ b/R/gambl_mutations_db.R @@ -32,7 +32,8 @@ #' | `bedpe` | ~900 | one Manta structural-variant breakpoint pair | `CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build` | #' | `sample_meta` | ~3.3k | one sample | `patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...` (its own `genome_build` column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) | #' | `sample_study` | varies | one (sample, study) membership fact -- many-to-many, a sample belonging to N studies is N rows | `sample_id, study, study_id, reference_PMID`. `study_id` is that study's own identifier for the sample where it differs from GAMBL's `sample_id` (e.g. a paper's own case/patient ID); NA where no study-specific ID has been sourced. | -#' | `build_info` | 9 | key/value | provenance (`source`, `built_at`, `builder`) and expected row counts (`n_maf`, `n_ashm`, `n_seg`, `n_bedpe`, `n_samples`, `n_sample_study`) used by the `data-raw/test_gambl_db.R` regression checks | +#' | `variant_pipeline` | varies | one (variant, Pipeline) fact -- many-to-many, a variant independently called by N pipelines is N rows | `Tumor_Sample_Barcode, Chromosome, Start_Position, End_Position, Tumor_Seq_Allele2, genome_build, elem, Pipeline`. `elem` is `"maf"` or `"ashm"`, identifying which table the variant belongs to. | +#' | `build_info` | 10 | key/value | provenance (`source`, `built_at`, `builder`) and expected row counts (`n_maf`, `n_ashm`, `n_seg`, `n_bedpe`, `n_samples`, `n_sample_study`, `n_variant_pipeline`) used by the `data-raw/test_gambl_db.R` regression checks | #' #' `maf`/`ashm` do NOT carry a `Study` column. Cohort/study membership is #' tracked once per sample in `sample_study`, not once per mutation row -- @@ -42,11 +43,26 @@ #' that claimed it (see `GAMBLR.data::get_ssm_from_db()`'s `this_study` #' parameter for how to filter by study post-refactor). #' +#' Similarly, `maf`/`ashm` only ever carry ONE `Pipeline` value per variant, +#' even though the same real mutation can be independently produced by more +#' than one pipeline (e.g. a cohort's own published/curated maf and our +#' separate SLMS-3 recall both calling the same position) -- when that +#' happens, `write_mutations_db()`'s write-time dedup keeps one row +#' arbitrarily (whichever pipeline's data was assembled first), which would +#' otherwise make the variant invisible to a query for the *other* pipeline +#' even though it's still fully present in the table. `variant_pipeline` +#' records every pipeline that actually produced each variant, independent +#' of which one "won" as `maf`/`ashm`'s single representative row; see +#' `GAMBLR.data::get_ssm_from_db()`'s `tool_name` parameter, which resolves +#' against this table rather than `maf`/`ashm`'s own `Pipeline` column. +#' #' ## Join keys #' `maf`, `ashm`, and `bedpe` (via `tumour_sample_id`) key to #' `sample_meta$Tumor_Sample_Barcode` / `sample_meta$sample_id`; `seg$ID` keys #' to `sample_meta$sample_id`; `sample_study$sample_id` keys to -#' `sample_meta$sample_id`. +#' `sample_meta$sample_id`; `variant_pipeline` keys to `maf`/`ashm` (per +#' `elem`) on `Tumor_Sample_Barcode, Chromosome, Start_Position, End_Position, +#' Tumor_Seq_Allele2, genome_build`. #' #' ## Indexes #' `maf`/`ashm`: `(genome_build, Chromosome, Start_Position)`, @@ -57,7 +73,8 @@ #' `GAMBLR.data::get_ssm_from_db()`. `seg`: `(genome_build, ID)`, #' `(genome_build, chrom, start)`. `bedpe`: `tumour_sample_id`. #' `sample_meta`: `sample_id`, `Tumor_Sample_Barcode`. `sample_study`: -#' `sample_id`, `study`. +#' `sample_id`, `study`. `variant_pipeline`: `Tumor_Sample_Barcode`, +#' `(elem, genome_build, Pipeline)`. #' #' @param db_path Optional explicit path to the .db file. #' diff --git a/R/get_ssm_from_db.R b/R/get_ssm_from_db.R index 97d6aed..16c3630 100644 --- a/R/get_ssm_from_db.R +++ b/R/get_ssm_from_db.R @@ -8,7 +8,13 @@ #' @param projection Genome build / `genome_build` column value. Default "grch37". #' @param sample_ids Optional character vector of `Tumor_Sample_Barcode` to keep. #' @param tool_name Pipeline to keep (matched case-insensitively). Default -#' "slms-3"; set NULL to skip the Pipeline filter. +#' "slms-3"; set NULL to skip the Pipeline filter. Resolved via the +#' `variant_pipeline` join table (see [gambl_mutations_db()]'s schema +#' docs) rather than `maf`/`ashm`'s own Pipeline column, so a variant +#' independently called by more than one pipeline is still found by a +#' query for either one, even though its single maf/ashm row can only +#' carry one Pipeline value. Falls back to filtering the column directly +#' against DBs built before variant_pipeline existed. #' @param include_ashm When TRUE, also query the `ashm` table and row-bind it. #' @param coding_only When TRUE, keep only coding `Variant_Classification`s. #' @param include_silent When FALSE (and `coding_only`), drop Silent mutations. @@ -58,11 +64,33 @@ get_ssm_from_db <- function(projection = "grch37", else intersect(sample_ids, study_sample_ids) } + # A variant that's independently produced by more than one pipeline (e.g. a + # cohort's own published maf and our SLMS-3 recall both calling the same + # position) only has room for one Pipeline value on its single maf/ashm + # row -- write_mutations_db() picks one arbitrarily when it deduplicates. + # variant_pipeline (sample_id/Chromosome/Start_Position/End_Position/ + # Tumor_Seq_Allele2/genome_build, Pipeline) records every pipeline that + # actually produced each variant, so tool_name is resolved against it via a + # semi-join instead of the maf/ashm row's own (possibly-not-representative) + # Pipeline value. Falls back to the old direct-column filter against DBs + # built before this table existed. + has_variant_pipeline <- !is.null(tn) && "variant_pipeline" %in% DBI::dbListTables(con) + variant_key_cols <- c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2") + # apply the filters that are common to a single (table, region) query base_query <- function(table_name, region = NULL) { q <- dplyr::tbl(con, table_name) %>% dplyr::filter(genome_build == projection) - if (!is.null(tn)) q <- dplyr::filter(q, tolower(Pipeline) == tn) + if (!is.null(tn)) { + if (has_variant_pipeline) { + matching_keys <- dplyr::tbl(con, "variant_pipeline") %>% + dplyr::filter(elem == table_name, genome_build == projection, tolower(Pipeline) == tn) %>% + dplyr::distinct(dplyr::across(dplyr::all_of(variant_key_cols))) + q <- dplyr::semi_join(q, matching_keys, by = variant_key_cols) + } else { + q <- dplyr::filter(q, tolower(Pipeline) == tn) + } + } if (coding_only) q <- dplyr::filter(q, Variant_Classification %in% cc) if (min_read_support > 0) q <- dplyr::filter(q, t_alt_count >= min_read_support) if (!is.null(sample_ids)) q <- dplyr::filter(q, Tumor_Sample_Barcode %in% sample_ids) diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md index 875c738..11630fb 100644 --- a/data-raw/PR_verification_notes.md +++ b/data-raw/PR_verification_notes.md @@ -187,7 +187,7 @@ to 299, spread across 113 samples (~2.6 rows/sample average) -- a different order of magnitude, consistent with normal residual noise rather than a bug. -## Current state (post all fixes above) +## Current state as of this point in the session (superseded -- see below) - **SNV**: 348 samples w/ gains, 113 samples w/ losses, 299 total lost rows -- converged to normal residual noise. The gained-only, 0-retained @@ -199,8 +199,15 @@ than a bug. expected, consistent with the earlier Arthur/Hilton SV coverage bonus fixes already being stable. -Considering this refactor converged and ready for PR, pending final -sign-off. +`compare_bundle_changes.R`'s row-count-based diffing was genuinely clean at +this point -- but it can't detect a variant surviving with the *wrong* +Pipeline tag (the row is still there, at the same position, just labeled +differently), which is exactly the bug found afterward (see "Fix: +write-time dedup silently relabeled real SLMS-3 calls as 'Publication'" +below). Do not treat a clean `compare_bundle_changes.R` run alone as +sufficient sign-off for Pipeline-level correctness -- cross-check with a +`tool_name`-filtered query (e.g. `get_all_coding_ssm()` or +`get_ssm_from_db(tool_name=...)`) per cohort as well. ## Fix: cell lines need a separate, genome-wide SNV pull @@ -249,3 +256,137 @@ Reddy were left unchanged -- Arthur's block already excludes `tumor`-suffixed sample_ids per patient (structurally safe from the same ambiguity), and Reddy's `study_id` comes from the paper's own distinct "Sample ID" column, already 1:1 with `sample_id`. + +## Fix: `this_study_samples`/`these_samples`/`these_samples_dlbcl` filtered on `cohort` instead of `study` + +Found via a fresh GSC test (`get_all_coding_ssm()` against the built DB, +joined to metadata): `FL_Dreval` showed only 7 of 441 samples with any +coding-classified variant at all, each with 1-2 rows; `DLBCL_cell_lines` +looked similarly too low. Traced to the "proteinpainter compatibility" +block (Phase 3), which intentionally reads the *installed, stale* +`GAMBLR.data::sample_data$meta` rather than the locally-built one (a +pre-existing, documented, out-of-scope inconsistency) -- but filtered that +stale snapshot on a column called `cohort`, when this script's own +convention (and, empirically, the stale snapshot itself) uses `study`. +`GAMBLR.data::sample_data$meta %>% filter(cohort %in% c("FL_Dreval", +"DLBCL_cell_lines"))` returned only the 5 cell-line ids; switching to +`filter(study %in% ...)` returned the correct 400+ ids. Fixed at all three +call sites (`these_samples`, `these_samples_dlbcl`, `this_study_samples`). + +Mechanistically this bug only affected the RefSeq/Protein_position +enrichment `left_join()` for Dreval/cell-line Publication rows (a +non-match there just leaves those two columns `NA`, it can't drop rows) -- +so on its own it was not expected to explain the *row-count* collapse +`get_all_coding_ssm()` showed. Confirmed by rebuilding: see below. + +## Real-time build diagnostics added to `assemble_bundled_data.R` + +To stop diagnosing via a slow guess-rebuild-test loop against the GSC, added +`diag_summary()` / `diag_summary_maf()` / `diag_missing()` helper calls at +every major checkpoint in the script (every Phase 1 cohort, the +cohort-\>study rename/rejoin block, `sample_study` assembly, the Phase 3 +enrichment joins, the Phase 4 `all_slms3_meta` live-metadata refetch and +pull outputs, the Phase 5 aSHM pull, and the final per-study +coding-classified `maf` counts). Prints per-study row/sample counts and, +at the two live-metadata refetch points, exactly which sample_ids get +silently dropped (if any) -- the log now shows where a cohort's count +changes during the build itself, not just after the fact. + +### First full run with diagnostics -- 2026-07-14 + +The `cohort`/`study` fix produced healthy-looking **aggregate** per-study +coding counts in the in-memory `sample_data` object, before the SQLite +write: + +| Study | grch37 maf, coding-classified only | | hg38 maf, coding-classified only | | +| --- | --- | --- | --- | --- | +| | rows | samples | rows | samples | +| Dreval | 39,765 | 441 | 4,982 | 437 | +| Arthur | 17,855 | 159 | 2,084 | 157 | +| Reddy | 17,805 | 981 | 10,816 | 947 | +| Schmitz | 8,352 | 468 | 8,320 | 468 | +| Thomas | 3,577 | 276 | 46,207 | 277 | +| Chapuy | 3,226 | 230 | 3,189 | 230 | +| Hilton | 3,078 | 158 | 2,607 | 158 | +| NCI_Golub | 2,030 | 114 | 2,017 | 114 | +| cell lines (no `sample_study` row, shown as `NA`) | 1,211 | 5 | 1,214 | 5 | + +Also visible in this run, two small pre-existing gaps (not blocking, not +part of this refactor's scope): +- `Reddy meta -> reddy_meta_gambl join`: 2 of 1001 Reddy sample_ids + (`Reddy_832T`, `Reddy_3813T`) have no matching live cohort. +- `Hilton trios`: 1 of 160 (`14-27873_tumorA`) not found in live metadata. + +**This table turned out to mask a real, separate problem** -- see below. +It aggregates Publication + SLMS-3 pipeline rows together, and Dreval's +Publication-pipeline pull (the paper's own curated maf) dominates the +total, so the healthy-looking aggregate hid the fact that the *SLMS-3* +pipeline's own contribution was nearly empty. + +## Fix: write-time dedup silently relabeled real SLMS-3 calls as "Publication" + +A follow-up GSC test using `get_all_coding_ssm()` (which specifically +restricts to `Pipeline == "slms-3"`, unlike the aggregate table above) +still showed `FL_Dreval` at 7/441 samples with any coding variant, each +with 1-2 rows -- i.e. the `cohort`/`study` fix above did not actually +resolve the originally-reported symptom; it only fixed a separate, +real-but-narrower bug (broken RefSeq/Protein_position enrichment). + +Extensive isolation via direct SQL against the built DB and side-by-side +`GAMBLR.results::get_ssm_by_regions()` calls ruled out, in order: a stale +DB path/cache (confirmed same file, fresh mtime), `tool_name` +case-sensitivity (already handled via `tolower()` on both sides), +`coding_class` casing/definition mismatches (byte-identical, confirmed via +`LENGTH()`/`HEX()` in SQL), an oversized `sample_ids` `IN`-clause hitting a +SQLite variable limit (reproduced with just Dreval's own 441 ids), a +scale/multi-cohort-merge bug in `get_ssm_by_regions()` (incrementally +rebuilt the exact Phase 4 sample scope -- Dreval alone, +Hilton, +Thomas, ++Arthur -- and every combination pulled healthy coding calls interactively), +and the Hugo_Symbol post-filter dropping bystander genes (a single sample's +raw coding calls were all confirmed to be on canonical panel genes and all +survived the filter). + +Root cause, confirmed empirically: `sample_data$grch37$maf <- +bind_rows(grch37_publication_rows, slms3_grch37)` puts Publication rows +first, and `write_mutations_db()`'s dedup +(`distinct(..., .keep_all = TRUE)`, keyed on `Tumor_Sample_Barcode, +Chromosome, Start_Position, End_Position, Tumor_Seq_Allele2`) keeps +whichever row comes first for a given key. Dreval's own published maf +(`fl_data$ssm_to_bundle`) was itself originally SLMS-3-called, so a real +mutation legitimately appears in *both* `grch37_publication_rows` and +`slms3_grch37` with an identical key -- the dedup silently kept the +Publication-tagged copy and dropped the SLMS-3-tagged one every time. The +variant was never lost from the bundle, but it became invisible to +`tool_name = "slms-3"`-style queries. Confirmed directly: of 6,807 real, +undeduplicated SLMS-3 coding calls for Dreval (from a raw +`get_ssm_by_regions()` pull), 6,791 (99.8%) have an exact key match among +the DB's `Pipeline = "Publication"` rows. + +### Fix: `variant_pipeline` join table + +Same architectural pattern as `sample_study`: a single-valued `Pipeline` +column on `maf`/`ashm` can't represent a variant independently called by +more than one pipeline, any more than a single-valued `study` column could +represent multi-cohort sample membership. Added `variant_pipeline` +(`Tumor_Sample_Barcode, Chromosome, Start_Position, End_Position, +Tumor_Seq_Allele2, genome_build, elem, Pipeline` -- one row per +(variant, Pipeline) pair, `elem` distinguishing `maf`/`ashm`), captured in +`write_mutations_db()` from the full pre-dedup data before the existing +dedup collapses each variant to one representative row. `get_ssm_from_db()`'s +`tool_name` filter now resolves via a semi-join against `variant_pipeline` +instead of `maf`/`ashm`'s own `Pipeline` column, with a fallback to the old +direct-column filter for DBs built before this table existed. + +Verified via synthetic data: a variant called by both "Publication" and +"SLMS-3" pipelines (identical key) is correctly deduplicated to one +`maf` row but produces two `variant_pipeline` rows; `get_ssm_from_db(tool_name += "slms-3")` correctly finds it even though its surviving `maf` row is +tagged "Publication"; `tool_name = "publication"` also finds it; and with +`variant_pipeline` dropped (simulating an old DB), the query correctly +falls back to the old (narrower, expected) behavior rather than erroring. + +**Still to do**: rebuild on the GSC with this fix and re-run +`get_all_coding_ssm()`/direct SQL against the fresh DB to confirm Dreval's +SLMS-3-tagged coding counts are healthy through the new join, and re-run +`compare_bundle_changes.R` for the full per-sample gained/lost view against +this latest build. diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 339f029..2093d2f 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -125,16 +125,55 @@ pull_data <- function( pull_projection = "grch37" ){ lymphoma_genes_bed <- if(pull_projection == "grch37") lymphoma_genes_bed_grch37 else lymphoma_genes_bed_hg38 - slms3 <- get_ssm_by_regions( + + # label identifies this specific call (projection x seq_type) in the log, + # since pull_data() is called 4 times per build (genome/capture x + # grch37/hg38) -- without this, diagnostics from different calls would + # be indistinguishable in the log. + call_label <- sprintf("pull_data(%s, seq_type=%s, n=%d samples)", + pull_projection, + paste(unique(pull_meta$seq_type), collapse = "/"), + n_distinct(pull_meta$sample_id)) + message(sprintf("[DIAG] %s: starting", call_label)) + diag_summary(pull_meta, paste0(call_label, ": input metadata"), study_col = NULL) + + raw <- get_ssm_by_regions( regions_bed = lymphoma_genes_bed, these_samples_metadata = pull_meta, basic_columns = FALSE, projection = pull_projection - ) %>% + ) + + # Catches exactly the kind of silent, no-error data loss this + # investigation is chasing: if get_ssm_by_regions() only partially read + # an underlying file, readr/vroom's parsing-problem log (normally only + # surfaced as a generic console warning) is captured here per-call + # instead, so a future run can show precisely which rows/columns had + # problems rather than just that "one or more parsing issues" occurred + # somewhere in the whole build. + probs <- tryCatch(readr::problems(raw), error = function(e) NULL) + n_probs <- if (!is.null(probs)) nrow(probs) else NA_integer_ + message(sprintf("[DIAG] %s: raw get_ssm_by_regions() = %d rows, %d distinct Tumor_Sample_Barcode, %s parsing problem(s)", + call_label, nrow(raw), n_distinct(raw$Tumor_Sample_Barcode), + if (is.na(n_probs)) "unknown (problems() not applicable to this object)" else n_probs)) + if (!is.null(probs) && nrow(probs) > 0) { + message(sprintf("[DIAG] %s: parsing problem detail (up to 20 rows):", call_label)) + print(utils::head(probs, 20)) + } + diag_summary_maf(raw, paste0(call_label, ": raw, pre Hugo_Symbol filter, all Variant_Classifications")) + diag_summary_maf(raw %>% filter(Variant_Classification %in% GAMBLR.data:::coding_class), + paste0(call_label, ": raw, pre Hugo_Symbol filter, coding-classified only")) + + slms3 <- raw %>% filter(Hugo_Symbol %in% all_lymphoma_genes) %>% select( all_of(all_cols) ) + + diag_summary_maf(slms3, paste0(call_label, ": final, post Hugo_Symbol filter, all Variant_Classifications")) + diag_summary_maf(slms3 %>% filter(Variant_Classification %in% GAMBLR.data:::coding_class), + paste0(call_label, ": final, post Hugo_Symbol filter, coding-classified only")) + return(slms3) } @@ -166,7 +205,7 @@ diag_summary <- function(df, label, sample_col = "sample_id", study_col = "study diag_summary_maf <- function(df, label, study_lookup = sample_data$sample_study) { message(sprintf("[DIAG] %s: %d rows, %d distinct Tumor_Sample_Barcode", label, nrow(df), n_distinct(df$Tumor_Sample_Barcode))) summary_tbl <- df %>% - left_join(study_lookup %>% select(sample_id, study), by = c("Tumor_Sample_Barcode" = "sample_id")) %>% + left_join(study_lookup %>% select(sample_id, study), by = c("Tumor_Sample_Barcode" = "sample_id"), relationship = "many-to-many") %>% group_by(study) %>% summarise(n_rows = n(), n_samples = n_distinct(Tumor_Sample_Barcode), .groups = "drop") %>% arrange(desc(n_rows)) diff --git a/data-raw/test_gambl_db.R b/data-raw/test_gambl_db.R index d28d146..50f9131 100644 --- a/data-raw/test_gambl_db.R +++ b/data-raw/test_gambl_db.R @@ -32,11 +32,11 @@ cols <- function(t) dbListFields(con, t) builds_in <- function(t) sort(dbGetQuery(con, sprintf("SELECT DISTINCT genome_build g FROM %s", t))$g) cat("== tables present ==\n") -for (t in c("maf","ashm","seg","bedpe","sample_meta","sample_study","build_info")) +for (t in c("maf","ashm","seg","bedpe","sample_meta","sample_study","variant_pipeline","build_info")) check(t %in% tbls, sprintf("table %s exists", t)) cat("\n== row counts (all > 0) ==\n") -for (t in c("maf","ashm","seg","bedpe","sample_meta","sample_study")) { +for (t in c("maf","ashm","seg","bedpe","sample_meta","sample_study","variant_pipeline")) { cnt <- if (t %in% tbls) n(t) else 0 check(cnt > 0, sprintf("%-11s has %d rows", t, cnt)) } @@ -54,7 +54,9 @@ req <- list( seg = c("ID","chrom","start","end","CN","genome_build"), bedpe = c("tumour_sample_id","CHROM_A","START_A","CHROM_B","START_B","VAF_tumour","SCORE","FILTER","genome_build"), sample_meta = c("sample_id","Tumor_Sample_Barcode","seq_type","study"), - sample_study = c("sample_id","study","study_id","reference_PMID") + sample_study = c("sample_id","study","study_id","reference_PMID"), + variant_pipeline = c("Tumor_Sample_Barcode","Chromosome","Start_Position","End_Position", + "Tumor_Seq_Allele2","genome_build","elem","Pipeline") ) for (t in names(req)) if (t %in% tbls) { missing <- setdiff(req[[t]], cols(t)) @@ -71,7 +73,8 @@ cat("\n== indexes present ==\n") # assert indexes that actually exist. idx <- dbGetQuery(con, "SELECT name FROM sqlite_master WHERE type='index'")$name for (i in c("idx_maf_pos","idx_maf_sample","idx_maf_pipe","idx_seg_sample", - "idx_bedpe_sample","idx_study_sample","idx_study_study")) + "idx_bedpe_sample","idx_study_sample","idx_study_study", + "idx_vp_sample","idx_vp_pipe")) check(i %in% idx, sprintf("index %s", i)) cat("\n== pipelines present ==\n") @@ -92,6 +95,8 @@ if ("build_info" %in% tbls) { for (t in c("maf","ashm","seg","bedpe")) check(isTRUE(getbi(paste0("n_", t)) == n(t)), sprintf("build_info n_%s == COUNT(%s)", t, t)) + check(isTRUE(getbi("n_variant_pipeline") == n("variant_pipeline")), + sprintf("build_info n_variant_pipeline == COUNT(variant_pipeline)")) } cat("\n== sample_metadata.rda matches sample_meta table ==\n") diff --git a/data-raw/write_mutations_db.R b/data-raw/write_mutations_db.R index 13eaa10..4cf1624 100644 --- a/data-raw/write_mutations_db.R +++ b/data-raw/write_mutations_db.R @@ -51,6 +51,20 @@ write_mutations_db <- function(sample_data, ashm = c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2") ) + # variant_pipeline: many-to-many bridge table (variant key, Pipeline), + # same pattern as sample_study (sample_id, study). A single Pipeline column + # on maf/ashm can only hold one value per variant, but the same real + # mutation can legitimately be produced by more than one pipeline (e.g. a + # cohort's own published/curated maf and our independent SLMS-3 recall + # both calling the same position) -- when that happens the dedup below + # collapses them to one row keyed on whichever pipeline came first in + # sample_data[[b]][[elem]], silently making the call invisible to a + # tool_name-filtered query for the other pipeline even though the variant + # itself is still fully present in the table. Captured here, before dedup, + # so no pipeline's claim to a variant is ever lost regardless of which row + # "wins" as maf/ashm's single representative copy. + variant_pipeline_rows <- list() + # genome-build-stamped frames; append per build to keep peak memory low write_element <- function(elem) { first <- TRUE @@ -61,6 +75,12 @@ write_mutations_db <- function(sample_data, if (is.null(x) || !nrow(x)) next x <- as.data.frame(x) x$genome_build <- b + if (!is.null(dk) && "Pipeline" %in% names(x)) { + variant_pipeline_rows[[length(variant_pipeline_rows) + 1]] <<- x %>% + dplyr::select(dplyr::all_of(dk), genome_build, Pipeline) %>% + dplyr::distinct() %>% + dplyr::mutate(elem = elem) + } if (!is.null(dk)) { before <- nrow(x) x <- dplyr::distinct(x, dplyr::across(dplyr::all_of(dk)), .keep_all = TRUE) @@ -77,6 +97,13 @@ write_mutations_db <- function(sample_data, } counts <- vapply(c("maf", "ashm", "seg", "bedpe"), write_element, integer(1)) + n_variant_pipeline <- 0L + if (length(variant_pipeline_rows)) { + variant_pipeline <- dplyr::bind_rows(variant_pipeline_rows) + DBI::dbWriteTable(con, "variant_pipeline", as.data.frame(variant_pipeline), overwrite = TRUE) + n_variant_pipeline <- nrow(variant_pipeline) + } + # indexes mirroring how the GAMBLR.open accessors query the data. # wrapped in try() so a build missing an optional table/column is non-fatal. idx <- c( @@ -91,19 +118,21 @@ write_mutations_db <- function(sample_data, "CREATE INDEX idx_meta_sample ON sample_meta(sample_id)", "CREATE INDEX idx_meta_barcode ON sample_meta(Tumor_Sample_Barcode)", "CREATE INDEX idx_study_sample ON sample_study(sample_id)", - "CREATE INDEX idx_study_study ON sample_study(study)" + "CREATE INDEX idx_study_study ON sample_study(study)", + "CREATE INDEX idx_vp_sample ON variant_pipeline(Tumor_Sample_Barcode)", + "CREATE INDEX idx_vp_pipe ON variant_pipeline(elem, genome_build, Pipeline)" ) for (stmt in idx) try(DBI::dbExecute(con, stmt), silent = TRUE) # self-describing provenance / expected-counts table (used by the tests) DBI::dbWriteTable(con, "build_info", data.frame( key = c("source", "built_at", "builder", - "n_maf", "n_ashm", "n_seg", "n_bedpe", "n_samples", "n_sample_study"), + "n_maf", "n_ashm", "n_seg", "n_bedpe", "n_samples", "n_sample_study", "n_variant_pipeline"), value = c(source_desc, format(Sys.time(), tz = "UTC", usetz = TRUE), "write_mutations_db", counts[["maf"]], counts[["ashm"]], counts[["seg"]], - counts[["bedpe"]], nrow(sample_data$meta), n_sample_study), + counts[["bedpe"]], nrow(sample_data$meta), n_sample_study, n_variant_pipeline), stringsAsFactors = FALSE ), overwrite = TRUE) From 1b3988a45f7fd8d8ecc0f1ca9368f700c49cc227 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 12:14:25 -0700 Subject: [PATCH 32/58] capture GAMBLR.data:::coding_class once at script start, not per pull_data() call Reading this lazy-loaded internal object for the first time deep into Phase 4 (rather than right after library(GAMBLR) loads the package) meant a devtools::install() of GAMBLR.data running concurrently with an already-started assemble_bundled_data.R process could delete/replace the installed package's .rdb file out from under it, crashing well into the build instead of failing fast (or not at all) at startup. Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 2093d2f..985aeb1 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -75,6 +75,14 @@ all_cols <- c( # other name. all_lymphoma_genes <- GAMBLR.utils::expand_gene_aliases(lymphoma_genes_comprehensive$Gene) +# Captured once, up front -- referenced by pull_data()'s diagnostics below. +# GAMBLR.data:::coding_class is a lazy-loaded internal object; reading it on +# every pull_data() call (deep into Phase 4) instead of once here means any +# mid-run change to the installed GAMBLR.data package (e.g. a concurrent +# devtools::install()) can hit a missing/replaced .rdb file well after this +# script started, rather than failing fast at startup if at all. +diag_coding_class <- GAMBLR.data:::coding_class + # Built once, reused by every get_ssm_by_regions() call below that wants only # lymphoma-gene mutations. Restricting via tabix -R (region) instead of # pulling every mutation for a sample set is the performance win; every call @@ -161,7 +169,7 @@ pull_data <- function( print(utils::head(probs, 20)) } diag_summary_maf(raw, paste0(call_label, ": raw, pre Hugo_Symbol filter, all Variant_Classifications")) - diag_summary_maf(raw %>% filter(Variant_Classification %in% GAMBLR.data:::coding_class), + diag_summary_maf(raw %>% filter(Variant_Classification %in% diag_coding_class), paste0(call_label, ": raw, pre Hugo_Symbol filter, coding-classified only")) slms3 <- raw %>% @@ -171,7 +179,7 @@ pull_data <- function( ) diag_summary_maf(slms3, paste0(call_label, ": final, post Hugo_Symbol filter, all Variant_Classifications")) - diag_summary_maf(slms3 %>% filter(Variant_Classification %in% GAMBLR.data:::coding_class), + diag_summary_maf(slms3 %>% filter(Variant_Classification %in% diag_coding_class), paste0(call_label, ": final, post Hugo_Symbol filter, coding-classified only")) return(slms3) From 17a167b202488c6aa98bc7f10e593054a4e92fc2 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 12:42:06 -0700 Subject: [PATCH 33/58] normalize Pipeline to lowercase at write time; add variant-key indexes Two DB performance issues, likely contributing to the lag observed while rerunning GAMBLR.open's tests: 1. idx_maf_pipe/idx_vp_pipe were plain b-tree indexes on the raw Pipeline column, but every actual query filters on tolower(Pipeline)/lower(Pipeline) for case-insensitive matching -- SQLite can't use a plain index to satisfy a function-wrapped predicate, so these indexes were never actually usable and every tool_name-filtered query fell back to a full table scan. Fixed at the source instead of with expression indexes: Pipeline is now normalized to lowercase once, at write time, in write_mutations_db() (both maf/ashm and variant_pipeline), so reads can use a plain equality against a plain index. get_ssm_from_db() still accepts any-case tool_name from callers (lowercased before comparing), so this is invisible to existing callers. 2. The new variant_pipeline semi-join (tool_name resolution) had no index covering its join key on either side, meaning that join -- now on the path of every default-tool_name query -- had to scan maf/ashm in full. Added idx_maf_variant_key, idx_ashm_variant_key, idx_vp_variant_key on (Tumor_Sample_Barcode, Chromosome, Start_Position, End_Position, Tumor_Seq_Allele2). Verified with a synthetic build: Pipeline values come out lowercase in both tables, the LOWER()-expression indexes are gone (plain indexes only), and tool_name resolution (both exact-case and caller-supplied mixed-case) still works correctly through the variant_pipeline join. Co-Authored-By: Claude Sonnet 5 --- R/gambl_mutations_db.R | 7 ++++--- R/get_ssm_from_db.R | 9 ++++++--- data-raw/test_gambl_db.R | 12 ++++++++---- data-raw/write_mutations_db.R | 23 ++++++++++++++++++++++- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/R/gambl_mutations_db.R b/R/gambl_mutations_db.R index b5b2f05..493b819 100644 --- a/R/gambl_mutations_db.R +++ b/R/gambl_mutations_db.R @@ -26,7 +26,7 @@ #' #' | Table | Rows (typical) | Grain | Key columns | #' | --- | --- | --- | --- | -#' | `maf` | ~3.6M | one somatic mutation call | `Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline, genome_build` (+ ~35 more MAF-standard columns) | +#' | `maf` | ~3.6M | one somatic mutation call | `Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. `"slms-3"`/`"publication"` -- normalized at write time so it can be matched with a plain, indexed equality), genome_build` (+ ~35 more MAF-standard columns) | #' | `ashm` | ~135k | one mutation call in an aSHM region | same schema as `maf` | #' | `seg` | ~126k | one copy-number segment | `ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build` | #' | `bedpe` | ~900 | one Manta structural-variant breakpoint pair | `CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build` | @@ -85,10 +85,11 @@ #' con <- gambl_mutations_db() #' DBI::dbListTables(con) #' -#' # coding mutations in TP53, grch37, one pipeline +#' # coding mutations in TP53, grch37, one pipeline (Pipeline is stored +#' # lowercase -- see the Pipeline column note above) #' dplyr::tbl(con, "maf") |> #' dplyr::filter(genome_build == "grch37", Hugo_Symbol == "TP53", -#' Pipeline == "SLMS-3") |> +#' Pipeline == "slms-3") |> #' dplyr::collect() #' } #' @export diff --git a/R/get_ssm_from_db.R b/R/get_ssm_from_db.R index 16c3630..3f53bcb 100644 --- a/R/get_ssm_from_db.R +++ b/R/get_ssm_from_db.R @@ -7,7 +7,10 @@ #' #' @param projection Genome build / `genome_build` column value. Default "grch37". #' @param sample_ids Optional character vector of `Tumor_Sample_Barcode` to keep. -#' @param tool_name Pipeline to keep (matched case-insensitively). Default +#' @param tool_name Pipeline to keep, matched case-insensitively (the value +#' you pass is lowercased before comparing; the stored Pipeline column is +#' itself normalized to lowercase at write time, so this is a plain, +#' indexed equality check rather than a function-wrapped one). Default #' "slms-3"; set NULL to skip the Pipeline filter. Resolved via the #' `variant_pipeline` join table (see [gambl_mutations_db()]'s schema #' docs) rather than `maf`/`ashm`'s own Pipeline column, so a variant @@ -84,11 +87,11 @@ get_ssm_from_db <- function(projection = "grch37", if (!is.null(tn)) { if (has_variant_pipeline) { matching_keys <- dplyr::tbl(con, "variant_pipeline") %>% - dplyr::filter(elem == table_name, genome_build == projection, tolower(Pipeline) == tn) %>% + dplyr::filter(elem == table_name, genome_build == projection, Pipeline == tn) %>% dplyr::distinct(dplyr::across(dplyr::all_of(variant_key_cols))) q <- dplyr::semi_join(q, matching_keys, by = variant_key_cols) } else { - q <- dplyr::filter(q, tolower(Pipeline) == tn) + q <- dplyr::filter(q, Pipeline == tn) } } if (coding_only) q <- dplyr::filter(q, Variant_Classification %in% cc) diff --git a/data-raw/test_gambl_db.R b/data-raw/test_gambl_db.R index 50f9131..78db82c 100644 --- a/data-raw/test_gambl_db.R +++ b/data-raw/test_gambl_db.R @@ -72,20 +72,24 @@ cat("\n== indexes present ==\n") # this check always failed regardless of any other change. Fixed here to # assert indexes that actually exist. idx <- dbGetQuery(con, "SELECT name FROM sqlite_master WHERE type='index'")$name -for (i in c("idx_maf_pos","idx_maf_sample","idx_maf_pipe","idx_seg_sample", +for (i in c("idx_maf_pos","idx_maf_sample","idx_maf_pipe","idx_maf_variant_key", + "idx_ashm_variant_key","idx_seg_sample", "idx_bedpe_sample","idx_study_sample","idx_study_study", - "idx_vp_sample","idx_vp_pipe")) + "idx_vp_sample","idx_vp_pipe","idx_vp_variant_key")) check(i %in% idx, sprintf("index %s", i)) cat("\n== pipelines present ==\n") +# Pipeline is normalized to lowercase at write time (see write_mutations_db.R) +# so it can be matched with a plain, indexed equality instead of every query +# needing to wrap the column in LOWER()/tolower(). pipes <- dbGetQuery(con, "SELECT DISTINCT Pipeline p FROM maf")$p -for (p in c("SLMS-3","Publication")) +for (p in c("slms-3","publication")) check(p %in% pipes, sprintf("Pipeline '%s' present (all: %s)", p, paste(pipes, collapse=", "))) cat("\n== spot query: MYC locus, grch37, slms-3 returns variants ==\n") myc <- dbGetQuery(con, "SELECT COUNT(*) n FROM maf WHERE genome_build='grch37' AND Chromosome='8' AND Start_Position>128723128 AND Start_Position<128774067 - AND lower(Pipeline)='slms-3'")$n + AND Pipeline='slms-3'")$n check(myc > 0, sprintf("MYC slms-3 variants found: %d", myc)) cat("\n== build_info counts match actual table counts ==\n") diff --git a/data-raw/write_mutations_db.R b/data-raw/write_mutations_db.R index 4cf1624..ed53a3b 100644 --- a/data-raw/write_mutations_db.R +++ b/data-raw/write_mutations_db.R @@ -75,6 +75,13 @@ write_mutations_db <- function(sample_data, if (is.null(x) || !nrow(x)) next x <- as.data.frame(x) x$genome_build <- b + # Normalized to lowercase here, once, at the write boundary -- so + # every reader (get_ssm_from_db(), raw SQL, this build's own + # variant_pipeline capture below) can match Pipeline with a plain + # equality against a plain index, instead of every query needing to + # wrap the column in LOWER()/tolower() (which a normal b-tree index + # can't be used to satisfy). + if ("Pipeline" %in% names(x)) x$Pipeline <- tolower(x$Pipeline) if (!is.null(dk) && "Pipeline" %in% names(x)) { variant_pipeline_rows[[length(variant_pipeline_rows) + 1]] <<- x %>% dplyr::select(dplyr::all_of(dk), genome_build, Pipeline) %>% @@ -106,12 +113,25 @@ write_mutations_db <- function(sample_data, # indexes mirroring how the GAMBLR.open accessors query the data. # wrapped in try() so a build missing an optional table/column is non-fatal. + # + # idx_maf_pipe/idx_vp_pipe are plain indexes on Pipeline -- safe because + # Pipeline is normalized to lowercase above, at write time, so readers + # (get_ssm_from_db(), raw SQL) can match it with a plain equality instead + # of wrapping the column in LOWER()/tolower(), which a normal b-tree index + # can't be used to satisfy. idx_*_variant_key give the variant_pipeline + # semi-join (in get_ssm_from_db()'s tool_name resolution) an index to + # actually use on both sides of the join -- without one, that join has to + # scan maf/ashm in full for every query with a non-NULL tool_name (the + # default), which is most of them. + variant_key_ddl <- "Tumor_Sample_Barcode, Chromosome, Start_Position, End_Position, Tumor_Seq_Allele2" idx <- c( "CREATE INDEX idx_maf_pos ON maf(genome_build, Chromosome, Start_Position)", "CREATE INDEX idx_maf_sample ON maf(Tumor_Sample_Barcode)", "CREATE INDEX idx_maf_pipe ON maf(Pipeline)", + sprintf("CREATE INDEX idx_maf_variant_key ON maf(%s)", variant_key_ddl), "CREATE INDEX idx_ashm_pos ON ashm(genome_build, Chromosome, Start_Position)", "CREATE INDEX idx_ashm_sample ON ashm(Tumor_Sample_Barcode)", + sprintf("CREATE INDEX idx_ashm_variant_key ON ashm(%s)", variant_key_ddl), "CREATE INDEX idx_seg_sample ON seg(genome_build, ID)", "CREATE INDEX idx_seg_pos ON seg(genome_build, chrom, start)", "CREATE INDEX idx_bedpe_sample ON bedpe(tumour_sample_id)", @@ -120,7 +140,8 @@ write_mutations_db <- function(sample_data, "CREATE INDEX idx_study_sample ON sample_study(sample_id)", "CREATE INDEX idx_study_study ON sample_study(study)", "CREATE INDEX idx_vp_sample ON variant_pipeline(Tumor_Sample_Barcode)", - "CREATE INDEX idx_vp_pipe ON variant_pipeline(elem, genome_build, Pipeline)" + "CREATE INDEX idx_vp_pipe ON variant_pipeline(elem, genome_build, Pipeline)", + sprintf("CREATE INDEX idx_vp_variant_key ON variant_pipeline(%s)", variant_key_ddl) ) for (stmt in idx) try(DBI::dbExecute(con, stmt), silent = TRUE) From fea2fdfa3bb8ffa8403e46ab04d7e01952d3411e Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 13:16:42 -0700 Subject: [PATCH 34/58] consolidate get_ssm_from_db() region queries into a single query get_ssm_by_regions() commonly passes one region per gene in a panel (150-250+ for the full lymphoma gene list). The previous implementation looped and issued one full query per region, each one re-deriving the tool_name/coding_only/sample_ids filters and re-running the variant_pipeline semi-join from scratch -- 150-250x more query overhead than necessary for a single call, and on the hot path of every panel-restricted SLMS-3 pull. Regions are now combined into a single OR'd filter condition and applied in one query per table (maf/ashm), matching the same logical semantics (each region an independent, OR-ed match) but with one round trip instead of N. This also fixes a latent duplicate-row bug: a variant landing inside two overlapping regions (e.g. neighbouring genes' padded windows) was previously returned once per matching region and bind_rows()'d into duplicate rows; a single query's OR condition doesn't double-count a row just because more than one disjunct matches it. Verified with synthetic data: exact expected rows returned across overlapping regions with no duplicates, a non-matching region correctly returns zero rows, and omitting regions entirely is unchanged. Adds rlang to Imports (used directly now via rlang::expr(); previously only a transitive dependency via dplyr). Co-Authored-By: Claude Sonnet 5 --- DESCRIPTION | 1 + R/get_ssm_from_db.R | 49 ++++++++++++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 17d0a8c..d2eafb8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -34,6 +34,7 @@ Imports: dbplyr, dplyr, ggplot2, + rlang, RSQLite, tidyr LazyDataCompression: xz diff --git a/R/get_ssm_from_db.R b/R/get_ssm_from_db.R index 3f53bcb..6b7d2e8 100644 --- a/R/get_ssm_from_db.R +++ b/R/get_ssm_from_db.R @@ -30,8 +30,10 @@ #' `this_study="Reddy"` alone returns Reddy's SLMS-3 recall by default, #' while `this_study="Reddy", tool_name="publication"` returns Reddy's #' as-published rows only). -#' @param regions Optional data frame with columns `chrom`, `start`, `end`; rows -#' are OR-ed (each region is an indexed range scan). +#' @param regions Optional data frame with columns `chrom`, `start`, `end`; +#' rows are OR-ed and applied as a single combined query (not one query per +#' region), so passing hundreds of regions (e.g. one per gene in a panel) +#' doesn't cost hundreds of separate round trips. #' @param con Optional DBI connection (defaults to [gambl_mutations_db()]). #' #' @return A data frame of MAF rows (the `genome_build` helper column is dropped). @@ -80,8 +82,9 @@ get_ssm_from_db <- function(projection = "grch37", has_variant_pipeline <- !is.null(tn) && "variant_pipeline" %in% DBI::dbListTables(con) variant_key_cols <- c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2") - # apply the filters that are common to a single (table, region) query - base_query <- function(table_name, region = NULL) { + # apply the filters that are common to every query against this table, + # regardless of how many regions (if any) are requested + base_query <- function(table_name) { q <- dplyr::tbl(con, table_name) %>% dplyr::filter(genome_build == projection) if (!is.null(tn)) { @@ -97,23 +100,33 @@ get_ssm_from_db <- function(projection = "grch37", if (coding_only) q <- dplyr::filter(q, Variant_Classification %in% cc) if (min_read_support > 0) q <- dplyr::filter(q, t_alt_count >= min_read_support) if (!is.null(sample_ids)) q <- dplyr::filter(q, Tumor_Sample_Barcode %in% sample_ids) - if (!is.null(region)) { - rc <- region$chrom; rs <- region$start; re <- region$end - q <- dplyr::filter(q, Chromosome == rc & - Start_Position > rs & Start_Position < re) - } - dplyr::collect(q) + q } - # one query per region (each uses the (genome_build, Chromosome, Start_Position) - # index), otherwise a single unrestricted query + # All regions are combined into a single OR'd condition and applied in one + # query, not one query per region. get_ssm_by_regions() commonly passes a + # region per gene in a panel (150-250+ for the full lymphoma gene list) -- + # looping here used to mean that many separate round trips, each one + # redoing the tool_name/coding_only/sample_ids filters and re-running the + # variant_pipeline semi-join from scratch for every single region. A single + # combined query also fixes a latent duplicate-row bug the loop had: a + # variant landing inside two overlapping regions (e.g. neighbouring genes' + # padded windows) was previously returned once per matching region and + # bind_rows()'d into two identical rows; SQL's OR doesn't double-count a + # row just because more than one disjunct matches it. gather <- function(table_name) { - if (is.null(regions) || nrow(regions) == 0) return(base_query(table_name)) - dplyr::bind_rows(lapply(seq_len(nrow(regions)), function(i) - base_query(table_name, region = list( - chrom = as.character(regions$chrom[i]), - start = as.numeric(regions$start[i]), - end = as.numeric(regions$end[i]))))) + q <- base_query(table_name) + if (!is.null(regions) && nrow(regions) > 0) { + region_exprs <- lapply(seq_len(nrow(regions)), function(i) { + rc <- as.character(regions$chrom[i]) + rs <- as.numeric(regions$start[i]) + re <- as.numeric(regions$end[i]) + rlang::expr(Chromosome == !!rc & Start_Position > !!rs & Start_Position < !!re) + }) + combined <- Reduce(function(a, b) rlang::expr(!!a | !!b), region_exprs) + q <- dplyr::filter(q, !!combined) + } + dplyr::collect(q) } res <- gather("maf") From ede521be7873c406ba7342a11a1986b256e6ec84 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 13:56:30 -0700 Subject: [PATCH 35/58] add missing bedpe indexes for genome_build and breakpoint positions bedpe previously had only one index (tumour_sample_id alone). Every get_sv_from_db()/get_manta_sv() call also filters genome_build, and its optional region filter checks both breakpoint ends (CHROM_A/START_A OR CHROM_B/START_B) -- none of which had anything to use, meaning a full table scan on every call regardless of how selective sample_ids/region actually were. Extended idx_bedpe_sample to (tumour_sample_id, genome_build) -- keeping the more selective sample filter leading -- and added genome_build-led indexes on each breakpoint end so SQLite's OR-optimization can use a different index per side of a region search. Verified with synthetic data: all 3 indexes present, sample_ids and region filters both still return correct results. Co-Authored-By: Claude Sonnet 5 --- R/gambl_mutations_db.R | 5 ++++- data-raw/test_gambl_db.R | 3 ++- data-raw/write_mutations_db.R | 14 +++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/R/gambl_mutations_db.R b/R/gambl_mutations_db.R index 493b819..987ac07 100644 --- a/R/gambl_mutations_db.R +++ b/R/gambl_mutations_db.R @@ -71,7 +71,10 @@ #' (the same logic used to populate these tables) and filter on #' `(genome_build, Chromosome, Start_Position)` instead -- see #' `GAMBLR.data::get_ssm_from_db()`. `seg`: `(genome_build, ID)`, -#' `(genome_build, chrom, start)`. `bedpe`: `tumour_sample_id`. +#' `(genome_build, chrom, start)`. `bedpe`: `(tumour_sample_id, +#' genome_build)`, `(genome_build, CHROM_A, START_A)`, `(genome_build, +#' CHROM_B, START_B)` (one per breakpoint end, so a region search that OR's +#' both ends can use a different index per side). #' `sample_meta`: `sample_id`, `Tumor_Sample_Barcode`. `sample_study`: #' `sample_id`, `study`. `variant_pipeline`: `Tumor_Sample_Barcode`, #' `(elem, genome_build, Pipeline)`. diff --git a/data-raw/test_gambl_db.R b/data-raw/test_gambl_db.R index 78db82c..8edb7bc 100644 --- a/data-raw/test_gambl_db.R +++ b/data-raw/test_gambl_db.R @@ -74,7 +74,8 @@ cat("\n== indexes present ==\n") idx <- dbGetQuery(con, "SELECT name FROM sqlite_master WHERE type='index'")$name for (i in c("idx_maf_pos","idx_maf_sample","idx_maf_pipe","idx_maf_variant_key", "idx_ashm_variant_key","idx_seg_sample", - "idx_bedpe_sample","idx_study_sample","idx_study_study", + "idx_bedpe_sample","idx_bedpe_pos_a","idx_bedpe_pos_b", + "idx_study_sample","idx_study_study", "idx_vp_sample","idx_vp_pipe","idx_vp_variant_key")) check(i %in% idx, sprintf("index %s", i)) diff --git a/data-raw/write_mutations_db.R b/data-raw/write_mutations_db.R index ed53a3b..daa2b82 100644 --- a/data-raw/write_mutations_db.R +++ b/data-raw/write_mutations_db.R @@ -134,7 +134,19 @@ write_mutations_db <- function(sample_data, sprintf("CREATE INDEX idx_ashm_variant_key ON ashm(%s)", variant_key_ddl), "CREATE INDEX idx_seg_sample ON seg(genome_build, ID)", "CREATE INDEX idx_seg_pos ON seg(genome_build, chrom, start)", - "CREATE INDEX idx_bedpe_sample ON bedpe(tumour_sample_id)", + # bedpe previously only had an index on tumour_sample_id alone -- every + # get_sv_from_db()/get_manta_sv() call also filters genome_build first, + # and its optional region filter checks both breakpoint ends (CHROM_A/ + # START_A OR CHROM_B/START_B), none of which had anything to use, meaning + # a full table scan regardless of how selective the actual filters were. + # tumour_sample_id leads its own index (more selective than genome_build + # alone) with genome_build appended so sample-restricted queries resolve + # in one lookup; separate genome_build-led indexes on each breakpoint end + # let SQLite's OR-optimization use a different index per side of a + # region search. + "CREATE INDEX idx_bedpe_sample ON bedpe(tumour_sample_id, genome_build)", + "CREATE INDEX idx_bedpe_pos_a ON bedpe(genome_build, CHROM_A, START_A)", + "CREATE INDEX idx_bedpe_pos_b ON bedpe(genome_build, CHROM_B, START_B)", "CREATE INDEX idx_meta_sample ON sample_meta(sample_id)", "CREATE INDEX idx_meta_barcode ON sample_meta(Tumor_Sample_Barcode)", "CREATE INDEX idx_study_sample ON sample_study(sample_id)", From fde31b6042de39c5c1c760cdae28029e3c4f1065 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 14:32:28 -0700 Subject: [PATCH 36/58] replace variant_pipeline's 5-column natural key with a surrogate mutation_id Per review feedback: instead of variant_pipeline repeating the full 5-column natural key (Tumor_Sample_Barcode, Chromosome, Start_Position, End_Position, Tumor_Seq_Allele2, genome_build) on every row, maf/ashm now get a surrogate integer mutation_id assigned at write time, right after deduplication (unique within an elem, across both genome builds). variant_pipeline shrinks to just (mutation_id, elem, Pipeline) -- a plain foreign key instead of 8 columns. The natural-key match doesn't disappear -- write_mutations_db() still has to correlate the pre-dedup, Pipeline-tagged rows against the now-ID-assigned deduped rows, via the same 5-column key, to figure out which mutation_id each pipeline claim belongs to. But that now happens once, at write time (a one-time cost during the already-slow batch build), instead of inside get_ssm_from_db()'s semi-join on every single read query. The read-time join is now a single indexed integer column instead of a 5-column composite (two of them text), which SQLite can resolve far more cheaply, and sidesteps SQLite's lack of native tuple-IN matching entirely rather than working around it. Replaced idx_maf_variant_key/idx_ashm_variant_key/idx_vp_variant_key/ idx_vp_sample with idx_maf_mutation_id/idx_ashm_mutation_id/ idx_vp_mutation_id; idx_vp_pipe drops genome_build (redundant now -- mutation_id already implies build via the maf/ashm row it references). Verified with synthetic data: mutation_id stays unique and correctly isolated across genome builds even when two builds share identical coordinate values; variant_pipeline has exactly the 3 expected columns; a dual-pipeline variant is still found via tool_name resolution despite its surviving maf row being tagged the other pipeline; mutation_id never leaks into caller-facing output; backward-compat fallback (DB built before variant_pipeline existed) still behaves correctly. Co-Authored-By: Claude Sonnet 5 --- R/gambl_mutations_db.R | 18 ++++---- R/get_ssm_from_db.R | 26 ++++++------ data-raw/test_gambl_db.R | 11 +++-- data-raw/write_mutations_db.R | 77 +++++++++++++++++++++-------------- 4 files changed, 74 insertions(+), 58 deletions(-) diff --git a/R/gambl_mutations_db.R b/R/gambl_mutations_db.R index 987ac07..a4deb0e 100644 --- a/R/gambl_mutations_db.R +++ b/R/gambl_mutations_db.R @@ -26,13 +26,13 @@ #' #' | Table | Rows (typical) | Grain | Key columns | #' | --- | --- | --- | --- | -#' | `maf` | ~3.6M | one somatic mutation call | `Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. `"slms-3"`/`"publication"` -- normalized at write time so it can be matched with a plain, indexed equality), genome_build` (+ ~35 more MAF-standard columns) | -#' | `ashm` | ~135k | one mutation call in an aSHM region | same schema as `maf` | +#' | `maf` | ~3.6M | one somatic mutation call | `mutation_id` (surrogate integer key, unique within `maf` across both genome builds -- assigned at write time, after deduplication; `variant_pipeline` references it as a foreign key), `Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. `"slms-3"`/`"publication"` -- normalized at write time so it can be matched with a plain, indexed equality), genome_build` (+ ~35 more MAF-standard columns) | +#' | `ashm` | ~135k | one mutation call in an aSHM region | same schema as `maf` (its own independent `mutation_id` sequence) | #' | `seg` | ~126k | one copy-number segment | `ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build` | #' | `bedpe` | ~900 | one Manta structural-variant breakpoint pair | `CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build` | #' | `sample_meta` | ~3.3k | one sample | `patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...` (its own `genome_build` column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) | #' | `sample_study` | varies | one (sample, study) membership fact -- many-to-many, a sample belonging to N studies is N rows | `sample_id, study, study_id, reference_PMID`. `study_id` is that study's own identifier for the sample where it differs from GAMBL's `sample_id` (e.g. a paper's own case/patient ID); NA where no study-specific ID has been sourced. | -#' | `variant_pipeline` | varies | one (variant, Pipeline) fact -- many-to-many, a variant independently called by N pipelines is N rows | `Tumor_Sample_Barcode, Chromosome, Start_Position, End_Position, Tumor_Seq_Allele2, genome_build, elem, Pipeline`. `elem` is `"maf"` or `"ashm"`, identifying which table the variant belongs to. | +#' | `variant_pipeline` | varies | one (mutation, Pipeline) fact -- many-to-many, a variant independently called by N pipelines is N rows | `mutation_id, elem, Pipeline`. `mutation_id` is a foreign key to `maf.mutation_id` or `ashm.mutation_id` depending on `elem` (`"maf"` or `"ashm"`) -- not a natural key, so this table doesn't repeat the variant's own position/sample columns. | #' | `build_info` | 10 | key/value | provenance (`source`, `built_at`, `builder`) and expected row counts (`n_maf`, `n_ashm`, `n_seg`, `n_bedpe`, `n_samples`, `n_sample_study`, `n_variant_pipeline`) used by the `data-raw/test_gambl_db.R` regression checks | #' #' `maf`/`ashm` do NOT carry a `Study` column. Cohort/study membership is @@ -60,13 +60,13 @@ #' `maf`, `ashm`, and `bedpe` (via `tumour_sample_id`) key to #' `sample_meta$Tumor_Sample_Barcode` / `sample_meta$sample_id`; `seg$ID` keys #' to `sample_meta$sample_id`; `sample_study$sample_id` keys to -#' `sample_meta$sample_id`; `variant_pipeline` keys to `maf`/`ashm` (per -#' `elem`) on `Tumor_Sample_Barcode, Chromosome, Start_Position, End_Position, -#' Tumor_Seq_Allele2, genome_build`. +#' `sample_meta$sample_id`; `variant_pipeline$mutation_id` keys to +#' `maf.mutation_id` or `ashm.mutation_id`, selecting which one via +#' `variant_pipeline$elem`. #' #' ## Indexes #' `maf`/`ashm`: `(genome_build, Chromosome, Start_Position)`, -#' `Tumor_Sample_Barcode`, `Pipeline` (maf only). No index on +#' `Tumor_Sample_Barcode`, `Pipeline`, `mutation_id`. No index on #' `Hugo_Symbol`: gene-restricted queries resolve the gene to a region first #' (the same logic used to populate these tables) and filter on #' `(genome_build, Chromosome, Start_Position)` instead -- see @@ -76,8 +76,8 @@ #' CHROM_B, START_B)` (one per breakpoint end, so a region search that OR's #' both ends can use a different index per side). #' `sample_meta`: `sample_id`, `Tumor_Sample_Barcode`. `sample_study`: -#' `sample_id`, `study`. `variant_pipeline`: `Tumor_Sample_Barcode`, -#' `(elem, genome_build, Pipeline)`. +#' `sample_id`, `study`. `variant_pipeline`: `mutation_id`, +#' `(elem, Pipeline)`. #' #' @param db_path Optional explicit path to the .db file. #' diff --git a/R/get_ssm_from_db.R b/R/get_ssm_from_db.R index 6b7d2e8..159a6bf 100644 --- a/R/get_ssm_from_db.R +++ b/R/get_ssm_from_db.R @@ -36,7 +36,8 @@ #' doesn't cost hundreds of separate round trips. #' @param con Optional DBI connection (defaults to [gambl_mutations_db()]). #' -#' @return A data frame of MAF rows (the `genome_build` helper column is dropped). +#' @return A data frame of MAF rows (the `genome_build`/`mutation_id` helper +#' columns are dropped). #' #' @export get_ssm_from_db <- function(projection = "grch37", @@ -73,14 +74,14 @@ get_ssm_from_db <- function(projection = "grch37", # cohort's own published maf and our SLMS-3 recall both calling the same # position) only has room for one Pipeline value on its single maf/ashm # row -- write_mutations_db() picks one arbitrarily when it deduplicates. - # variant_pipeline (sample_id/Chromosome/Start_Position/End_Position/ - # Tumor_Seq_Allele2/genome_build, Pipeline) records every pipeline that - # actually produced each variant, so tool_name is resolved against it via a - # semi-join instead of the maf/ashm row's own (possibly-not-representative) - # Pipeline value. Falls back to the old direct-column filter against DBs - # built before this table existed. + # variant_pipeline (mutation_id, elem, Pipeline) records every pipeline + # that actually produced each variant, keyed on the surrogate mutation_id + # surviving rows are assigned at write time (not the 5-column natural key + # -- see write_mutations_db.R for why), so tool_name is resolved against + # it via a single-column semi-join instead of the maf/ashm row's own + # (possibly-not-representative) Pipeline value. Falls back to the old + # direct-column filter against DBs built before this table existed. has_variant_pipeline <- !is.null(tn) && "variant_pipeline" %in% DBI::dbListTables(con) - variant_key_cols <- c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2") # apply the filters that are common to every query against this table, # regardless of how many regions (if any) are requested @@ -89,10 +90,10 @@ get_ssm_from_db <- function(projection = "grch37", dplyr::filter(genome_build == projection) if (!is.null(tn)) { if (has_variant_pipeline) { - matching_keys <- dplyr::tbl(con, "variant_pipeline") %>% - dplyr::filter(elem == table_name, genome_build == projection, Pipeline == tn) %>% - dplyr::distinct(dplyr::across(dplyr::all_of(variant_key_cols))) - q <- dplyr::semi_join(q, matching_keys, by = variant_key_cols) + matching_ids <- dplyr::tbl(con, "variant_pipeline") %>% + dplyr::filter(elem == table_name, Pipeline == tn) %>% + dplyr::distinct(mutation_id) + q <- dplyr::semi_join(q, matching_ids, by = "mutation_id") } else { q <- dplyr::filter(q, Pipeline == tn) } @@ -132,5 +133,6 @@ get_ssm_from_db <- function(projection = "grch37", res <- gather("maf") if (include_ashm) res <- dplyr::bind_rows(res, gather("ashm")) res$genome_build <- NULL + res$mutation_id <- NULL res } diff --git a/data-raw/test_gambl_db.R b/data-raw/test_gambl_db.R index 8edb7bc..c91fa80 100644 --- a/data-raw/test_gambl_db.R +++ b/data-raw/test_gambl_db.R @@ -49,14 +49,13 @@ for (t in c("maf","ashm","seg","bedpe")) cat("\n== required columns present (what the accessors filter on) ==\n") req <- list( - maf = c("Chromosome","Start_Position","End_Position","Tumor_Sample_Barcode", + maf = c("mutation_id","Chromosome","Start_Position","End_Position","Tumor_Sample_Barcode", "Pipeline","Hugo_Symbol","Variant_Classification","t_alt_count","genome_build"), seg = c("ID","chrom","start","end","CN","genome_build"), bedpe = c("tumour_sample_id","CHROM_A","START_A","CHROM_B","START_B","VAF_tumour","SCORE","FILTER","genome_build"), sample_meta = c("sample_id","Tumor_Sample_Barcode","seq_type","study"), sample_study = c("sample_id","study","study_id","reference_PMID"), - variant_pipeline = c("Tumor_Sample_Barcode","Chromosome","Start_Position","End_Position", - "Tumor_Seq_Allele2","genome_build","elem","Pipeline") + variant_pipeline = c("mutation_id","elem","Pipeline") ) for (t in names(req)) if (t %in% tbls) { missing <- setdiff(req[[t]], cols(t)) @@ -72,11 +71,11 @@ cat("\n== indexes present ==\n") # this check always failed regardless of any other change. Fixed here to # assert indexes that actually exist. idx <- dbGetQuery(con, "SELECT name FROM sqlite_master WHERE type='index'")$name -for (i in c("idx_maf_pos","idx_maf_sample","idx_maf_pipe","idx_maf_variant_key", - "idx_ashm_variant_key","idx_seg_sample", +for (i in c("idx_maf_pos","idx_maf_sample","idx_maf_pipe","idx_maf_mutation_id", + "idx_ashm_mutation_id","idx_seg_sample", "idx_bedpe_sample","idx_bedpe_pos_a","idx_bedpe_pos_b", "idx_study_sample","idx_study_study", - "idx_vp_sample","idx_vp_pipe","idx_vp_variant_key")) + "idx_vp_pipe","idx_vp_mutation_id")) check(i %in% idx, sprintf("index %s", i)) cat("\n== pipelines present ==\n") diff --git a/data-raw/write_mutations_db.R b/data-raw/write_mutations_db.R index daa2b82..f4055d3 100644 --- a/data-raw/write_mutations_db.R +++ b/data-raw/write_mutations_db.R @@ -51,18 +51,27 @@ write_mutations_db <- function(sample_data, ashm = c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2") ) - # variant_pipeline: many-to-many bridge table (variant key, Pipeline), - # same pattern as sample_study (sample_id, study). A single Pipeline column - # on maf/ashm can only hold one value per variant, but the same real - # mutation can legitimately be produced by more than one pipeline (e.g. a - # cohort's own published/curated maf and our independent SLMS-3 recall - # both calling the same position) -- when that happens the dedup below - # collapses them to one row keyed on whichever pipeline came first in - # sample_data[[b]][[elem]], silently making the call invisible to a - # tool_name-filtered query for the other pipeline even though the variant - # itself is still fully present in the table. Captured here, before dedup, - # so no pipeline's claim to a variant is ever lost regardless of which row + # variant_pipeline: many-to-many bridge table (mutation_id, Pipeline), same + # pattern as sample_study (sample_id, study). A single Pipeline column on + # maf/ashm can only hold one value per variant, but the same real mutation + # can legitimately be produced by more than one pipeline (e.g. a cohort's + # own published/curated maf and our independent SLMS-3 recall both calling + # the same position) -- when that happens the dedup below collapses them + # to one row, silently making the call invisible to a tool_name-filtered + # query for the other pipeline even though the variant itself is still + # fully present in the table. Captured here, before dedup, so no + # pipeline's claim to a variant is ever lost regardless of which row # "wins" as maf/ashm's single representative copy. + # + # mutation_id is a surrogate integer key assigned to each deduped maf/ashm + # row (unique within an elem, across both genome builds), and + # variant_pipeline references it as a plain foreign key instead of + # repeating the 5-column natural key on every row. Matching pre-dedup + # pipeline claims back to their surviving row still requires the natural + # key -- that cost doesn't disappear, it just moves here, to a one-time + # write-time join, instead of every read-time query in get_ssm_from_db() + # having to join on a 5-column composite (two of them text) instead of a + # single indexed integer column. variant_pipeline_rows <- list() # genome-build-stamped frames; append per build to keep peak memory low @@ -70,31 +79,39 @@ write_mutations_db <- function(sample_data, first <- TRUE total <- 0L dk <- dedup_keys[[elem]] + next_id <- 1L for (b in builds) { x <- sample_data[[b]][[elem]] if (is.null(x) || !nrow(x)) next x <- as.data.frame(x) x$genome_build <- b # Normalized to lowercase here, once, at the write boundary -- so - # every reader (get_ssm_from_db(), raw SQL, this build's own - # variant_pipeline capture below) can match Pipeline with a plain - # equality against a plain index, instead of every query needing to - # wrap the column in LOWER()/tolower() (which a normal b-tree index + # every reader (get_ssm_from_db(), raw SQL) can match Pipeline with a + # plain equality against a plain index, instead of every query needing + # to wrap the column in LOWER()/tolower() (which a normal b-tree index # can't be used to satisfy). if ("Pipeline" %in% names(x)) x$Pipeline <- tolower(x$Pipeline) - if (!is.null(dk) && "Pipeline" %in% names(x)) { - variant_pipeline_rows[[length(variant_pipeline_rows) + 1]] <<- x %>% - dplyr::select(dplyr::all_of(dk), genome_build, Pipeline) %>% - dplyr::distinct() %>% - dplyr::mutate(elem = elem) - } + if (!is.null(dk)) { before <- nrow(x) - x <- dplyr::distinct(x, dplyr::across(dplyr::all_of(dk)), .keep_all = TRUE) - if (nrow(x) < before) { + deduped <- dplyr::distinct(x, dplyr::across(dplyr::all_of(dk)), .keep_all = TRUE) + if (nrow(deduped) < before) { message(sprintf("[write_mutations_db] %s/%s: dropped %d duplicate row(s)", - elem, b, before - nrow(x))) + elem, b, before - nrow(deduped))) + } + deduped$mutation_id <- seq.int(next_id, length.out = nrow(deduped)) + next_id <- next_id + nrow(deduped) + + if ("Pipeline" %in% names(x)) { + id_lookup <- deduped %>% dplyr::select(dplyr::all_of(dk), mutation_id) + variant_pipeline_rows[[length(variant_pipeline_rows) + 1]] <<- x %>% + dplyr::select(dplyr::all_of(dk), Pipeline) %>% + dplyr::distinct() %>% + dplyr::inner_join(id_lookup, by = dk) %>% + dplyr::select(mutation_id, Pipeline) %>% + dplyr::mutate(elem = elem) } + x <- deduped } DBI::dbWriteTable(con, elem, x, append = !first, overwrite = first) total <- total + nrow(x) @@ -118,20 +135,19 @@ write_mutations_db <- function(sample_data, # Pipeline is normalized to lowercase above, at write time, so readers # (get_ssm_from_db(), raw SQL) can match it with a plain equality instead # of wrapping the column in LOWER()/tolower(), which a normal b-tree index - # can't be used to satisfy. idx_*_variant_key give the variant_pipeline + # can't be used to satisfy. idx_*_mutation_id give the variant_pipeline # semi-join (in get_ssm_from_db()'s tool_name resolution) an index to # actually use on both sides of the join -- without one, that join has to # scan maf/ashm in full for every query with a non-NULL tool_name (the # default), which is most of them. - variant_key_ddl <- "Tumor_Sample_Barcode, Chromosome, Start_Position, End_Position, Tumor_Seq_Allele2" idx <- c( "CREATE INDEX idx_maf_pos ON maf(genome_build, Chromosome, Start_Position)", "CREATE INDEX idx_maf_sample ON maf(Tumor_Sample_Barcode)", "CREATE INDEX idx_maf_pipe ON maf(Pipeline)", - sprintf("CREATE INDEX idx_maf_variant_key ON maf(%s)", variant_key_ddl), + "CREATE INDEX idx_maf_mutation_id ON maf(mutation_id)", "CREATE INDEX idx_ashm_pos ON ashm(genome_build, Chromosome, Start_Position)", "CREATE INDEX idx_ashm_sample ON ashm(Tumor_Sample_Barcode)", - sprintf("CREATE INDEX idx_ashm_variant_key ON ashm(%s)", variant_key_ddl), + "CREATE INDEX idx_ashm_mutation_id ON ashm(mutation_id)", "CREATE INDEX idx_seg_sample ON seg(genome_build, ID)", "CREATE INDEX idx_seg_pos ON seg(genome_build, chrom, start)", # bedpe previously only had an index on tumour_sample_id alone -- every @@ -151,9 +167,8 @@ write_mutations_db <- function(sample_data, "CREATE INDEX idx_meta_barcode ON sample_meta(Tumor_Sample_Barcode)", "CREATE INDEX idx_study_sample ON sample_study(sample_id)", "CREATE INDEX idx_study_study ON sample_study(study)", - "CREATE INDEX idx_vp_sample ON variant_pipeline(Tumor_Sample_Barcode)", - "CREATE INDEX idx_vp_pipe ON variant_pipeline(elem, genome_build, Pipeline)", - sprintf("CREATE INDEX idx_vp_variant_key ON variant_pipeline(%s)", variant_key_ddl) + "CREATE INDEX idx_vp_pipe ON variant_pipeline(elem, Pipeline)", + "CREATE INDEX idx_vp_mutation_id ON variant_pipeline(mutation_id)" ) for (stmt in idx) try(DBI::dbExecute(con, stmt), silent = TRUE) From cb3d57a5277653fc2f9b1da6e1c62a585c3a093d Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 17:48:10 -0700 Subject: [PATCH 37/58] document performance investigation and fixes in PR notes Records the query-layer indexing work (Pipeline case mismatch, missing variant_pipeline/bedpe indexes, region-query consolidation) and the GAMBLR.open-side calc_mutation_frequency_bin_regions fix, with before/after timing from tools/fast_example_timer.R (~242s -> ~130s total, 142s -> 30.2s for the single largest item). Also updates the earlier variant_pipeline "still to do" note now that it's confirmed fixed on the GSC. Co-Authored-By: Claude Sonnet 5 --- data-raw/PR_verification_notes.md | 100 ++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 5 deletions(-) diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md index 11630fb..5fcc529 100644 --- a/data-raw/PR_verification_notes.md +++ b/data-raw/PR_verification_notes.md @@ -385,8 +385,98 @@ tagged "Publication"; `tool_name = "publication"` also finds it; and with `variant_pipeline` dropped (simulating an old DB), the query correctly falls back to the old (narrower, expected) behavior rather than erroring. -**Still to do**: rebuild on the GSC with this fix and re-run -`get_all_coding_ssm()`/direct SQL against the fresh DB to confirm Dreval's -SLMS-3-tagged coding counts are healthy through the new join, and re-run -`compare_bundle_changes.R` for the full per-sample gained/lost view against -this latest build. +**Update**: rebuilt and confirmed on the GSC. `get_all_coding_ssm()` and +direct SQL against the fresh DB both show healthy Dreval SLMS-3-tagged +coding counts through the `variant_pipeline` join, resolving the original +symptom. + +## Performance: query-layer indexing and GAMBLR.open query patterns + +While retesting the fix above, `GAMBLR.open`'s `tools/logExampleOutputs.R` +example suite runtime came back noticeably regressed against its +pre-refactor baseline: 51.2s (`b061311`, 2026-06-12, pre-`gambl_mutations.db` +sample_study/variant_pipeline work) vs. 4m37s-11m35s across several +mid-refactor reruns. Investigated and fixed several distinct, compounding +issues: + +- **`Pipeline` case mismatch broke every index on it.** `idx_maf_pipe`/ + `idx_vp_pipe` were plain indexes on the raw `Pipeline` column, but every + actual query filters on `tolower(Pipeline)`/`lower(Pipeline)` for + case-insensitive matching -- SQLite can't use a plain b-tree index to + satisfy a function-wrapped predicate, so these indexes were never + actually usable; every `tool_name`-filtered query (the default) fell + back to a full table scan. Fixed at the source instead of with + expression indexes: `Pipeline` is now normalized to lowercase once, at + write time, in both `maf`/`ashm` and `variant_pipeline`, so reads use a + plain equality against a plain index. +- **No index at all on the new `variant_pipeline` semi-join's key.** Added + composite indexes on the variant key to both sides of the join. +- **`variant_pipeline`'s 5-column natural key replaced with a surrogate + `mutation_id`** (see "Fix: `variant_pipeline` join table" above) -- a + single indexed integer join instead of a 5-column composite (two of them + text). +- **`bedpe` had only one index** (`tumour_sample_id` alone) despite every + `get_sv_from_db()`/`get_manta_sv()` call also filtering `genome_build` + and its region search checking both breakpoint ends. Added + `(tumour_sample_id, genome_build)` and a `genome_build`-led index per + breakpoint end. +- **`get_ssm_from_db()`'s `regions` parameter looped, one query per + region.** `get_ssm_by_regions()` commonly passes 100+ regions (one per + gene in the aSHM/lymphoma panel) in a single call -- consolidated into + one query with a combined OR'd condition instead of N round trips. Also + fixed a latent duplicate-row bug this had: a variant inside two + overlapping regions was previously returned once per matching region and + `bind_rows()`'d into duplicate rows. +- **`GAMBLR.open::calc_mutation_frequency_bin_regions()` looped over every + region calling a function that queries the database per region** (not a + `gambl_mutations.db`-side issue, but the single largest cost found: 142s + of a ~240s total example-suite run). Its own per-region helper already + supported a pre-fetched `maf_data` short-circuit that skipped the + database entirely; the plural function just never used it. Fixed to + bulk-fetch once via `get_ssm_by_regions()` before the per-region loop. + Also replaced `mclapply()` with `lapply()` here, at the user's request -- + once each iteration is fast in-memory subsetting rather than a database + call, there's little left to parallelize, and `mclapply()`'s + fork-per-worker model both multiplies peak memory (each fork copies the + parent's memory) and forking a live DB connection across processes is a + correctness/contention risk this avoids entirely. + +All fixes verified with synthetic-data/stubbed-out control-flow tests +(see individual commits) rather than against the live GSC, since none of +this repo's automated tests have GSC access. + +### Results + +Measured with `tools/fast_example_timer.R` (GAMBLR.open), a leaner +per-example-timed replacement for `logExampleOutputs.R` written during +this investigation -- `devtools::run_examples()` has `document = TRUE` by +default, meaning every run also pays for a full roxygen regeneration of +every `.Rd` file + `NAMESPACE` plus two full package reloads, unrelated to +anything under test here. + +| Stage | Total suite time | `calc_mutation_frequency_bin_regions` | +| --- | --- | --- | +| Mid-refactor, before indexing fixes | ~242s (4m22s incl. `run_examples()` overhead) | 142s | +| After `calc_mutation_frequency_bin_regions` bulk-fetch + `lapply` fix (indexing fixes already included) | ~130s | 30.2s | + +Remaining largest items (`assign_cn_to_ssm` 23.7s, `get_ashm_count_matrix` +19.2s) were investigated and are **not** the same class of bug -- both +already make single, consolidated database calls. Their cost is genuine +R-side computation (`assign_cn_to_ssm`'s `cool_overlaps()` call is a +chromosome-level many-to-many join between mutations and CN segments, +scaling with mutations x segments per chromosome per sample) rather than +a missing index or a redundant query loop, and would need an algorithmic +change (e.g. a genuine interval-join data structure) rather than a query +fix -- out of scope for this pass. `calc_mutation_frequency_bin_regions`'s +own remaining 30.2s is not yet decomposed between its single bulk query +and its (now-sequential) per-region windowing computation. + +Not a like-for-like comparison to the original 51.2s baseline: that +predates `gambl_mutations.db` entirely (in-memory, lazy-loaded +`sample_data.rda`, not a SQL database), and cohort sizes have grown +materially since then regardless (e.g. `DLBCL_Gascoyne` 21->42, +`DLBCL_GenomeCanada` 59->110). A SQL-backed system pays real per-query +overhead (connection, parse, plan, result marshaling) on every one of the +dozens of separate calls across the example suite that an in-memory R +object never had to pay; indexing closes the "full scan vs. indexed +lookup" gap, not that architectural difference. From 86d787b0ec70c8be52babb97e6d634bd51909a2d Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 17:50:21 -0700 Subject: [PATCH 38/58] correct assign_cn_to_ssm performance characterization in PR notes The laptop-vs-GSC comparison (same DB, same code, local disk instead of GSC storage) showed assign_cn_to_ssm dropping from 23.7s to under ~1.1s -- too large a swing for a purely CPU-bound cool_overlaps() join, meaning the GSC-side cost was actually dominated by disk/network I/O for its two underlying queries, not the join itself as originally assessed. calc_mutation_frequency_bin_regions shows the same pattern to a lesser degree. Updates the practical conclusion accordingly: further code-level query optimization has diminishing returns here, the larger remaining lever is GSC storage infrastructure, not this refactor. Co-Authored-By: Claude Sonnet 5 --- data-raw/PR_verification_notes.md | 43 +++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/data-raw/PR_verification_notes.md b/data-raw/PR_verification_notes.md index 5fcc529..bde22dd 100644 --- a/data-raw/PR_verification_notes.md +++ b/data-raw/PR_verification_notes.md @@ -457,19 +457,36 @@ anything under test here. | Stage | Total suite time | `calc_mutation_frequency_bin_regions` | | --- | --- | --- | | Mid-refactor, before indexing fixes | ~242s (4m22s incl. `run_examples()` overhead) | 142s | -| After `calc_mutation_frequency_bin_regions` bulk-fetch + `lapply` fix (indexing fixes already included) | ~130s | 30.2s | - -Remaining largest items (`assign_cn_to_ssm` 23.7s, `get_ashm_count_matrix` -19.2s) were investigated and are **not** the same class of bug -- both -already make single, consolidated database calls. Their cost is genuine -R-side computation (`assign_cn_to_ssm`'s `cool_overlaps()` call is a -chromosome-level many-to-many join between mutations and CN segments, -scaling with mutations x segments per chromosome per sample) rather than -a missing index or a redundant query loop, and would need an algorithmic -change (e.g. a genuine interval-join data structure) rather than a query -fix -- out of scope for this pass. `calc_mutation_frequency_bin_regions`'s -own remaining 30.2s is not yet decomposed between its single bulk query -and its (now-sequential) per-region windowing computation. +| After `calc_mutation_frequency_bin_regions` bulk-fetch + `lapply` fix (indexing fixes already included), on the GSC | ~130s | 30.2s | +| Same DB synced to a local laptop, same code, local disk instead of GSC storage | 33.8s (incl. 5.8s package load) | 12.2s | + +On the GSC, the next-largest remaining items after the fix above were +`assign_cn_to_ssm` (23.7s) and `get_ashm_count_matrix` (19.2s). Investigated +both -- neither has the same "N queries instead of 1" bug already fixed +above; both already make single, consolidated database calls +(`get_cn_segments()` + `get_ssm_by_samples()`, and `get_ssm_by_regions()` +respectively). Initially assessed `assign_cn_to_ssm`'s cost as inherent +R-side computation (its `cool_overlaps()` call is a chromosome-level +many-to-many join between mutations and CN segments, scaling with +mutations x segments per chromosome per sample) rather than something an +index could fix. + +**That assessment was wrong, or at least incomplete** -- the laptop +comparison above shows `assign_cn_to_ssm` dropping out of the slowest-10 +list entirely (under ~1.1s) on the same DB, same code, local disk. A +`cool_overlaps()` join being purely CPU-bound R computation wouldn't +plausibly speed up ~20x just from moving the DB file to local storage; the +dominant cost on the GSC was disk/network I/O for its two underlying +queries, not the join itself. `calc_mutation_frequency_bin_regions` shows +the same pattern to a lesser degree (30.2s -> 12.2s on the identical single +bulk query + windowing code) -- some of its remaining cost is I/O-bound +too, not purely the sliding-window computation. + +Practical implication: further **code**-level query optimization on the +GSC has sharply diminishing returns at this point. The larger remaining +lever is infrastructure -- where `gambl_mutations.db` physically lives on +GSC storage (e.g. local scratch/SSD vs. a networked home-directory mount) +-- not something fixable from this refactor. Not a like-for-like comparison to the original 51.2s baseline: that predates `gambl_mutations.db` entirely (in-memory, lazy-loaded From bd89fea2d83db841202aa7e9cef1ddcfcecf7b29 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 14 Jul 2026 18:16:26 -0700 Subject: [PATCH 39/58] proteinpainter-compat block: use local sample_data\$meta, not installed Removes the last remaining dependency on GAMBLR.data::sample_data (the installed, bundled object) from assemble_bundled_data.R. By Phase 3 (where this block runs), the local sample_data\$meta already has everything needed -- every cohort, and the study column (renamed from cohort) -- so there's no longer a reason to reach into whatever happens to be installed, which could easily be out of sync with the current run (the exact class of staleness bug that caused the cohort/study mismatch fixed earlier this session). This also removes the last blocker to dropping sample_data.rda from the package entirely: nothing in this script depends on GAMBLR.data::sample_data existing anymore. Since this is a data-raw/ script (sourced directly, not loaded via library()), the fix takes effect on the very next run with no reinstall required, and is independent of whether/when sample_data.rda itself actually gets removed from the package. Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 985aeb1..d468a1d 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -924,10 +924,14 @@ relabel_to_sample_id <- function(df, study_name) { select(-.gambl_sample_id) } -# This is needed for the proteinpainter compatibility. Reads from the -# *installed* GAMBLR.data::sample_data (not the locally-built sample_data -# above) -- a pre-existing, out-of-scope inconsistency, left as-is. -GAMBLR.data::sample_data$meta$cohort %>% table +# This is needed for the proteinpainter compatibility. Previously read +# from the *installed* GAMBLR.data::sample_data rather than the +# locally-built sample_data above -- a stale-data risk (the installed copy +# could be out of sync with this run, and is the reason sample_data.rda +# was still shipped in the package at all) fixed by switching to the local +# object, which by this point (Phase 2 already ran) has every cohort this +# block needs. +sample_data$meta$study %>% table selected_columns <- c( "Tumor_Sample_Barcode", "Hugo_Symbol", @@ -935,16 +939,16 @@ selected_columns <- c( "Tumor_Seq_Allele2", maf_columns_to_keep ) -these_samples <- GAMBLR.data::sample_data$meta %>% +these_samples <- sample_data$meta %>% filter(study %in% c("BL_Thomas")) %>% pull(sample_id) -these_samples_dlbcl <- GAMBLR.data::sample_data$meta %>% +these_samples_dlbcl <- sample_data$meta %>% filter(study %in% c("DLBCL_Thomas", "DLBCL_cell_lines")) %>% pull(sample_id) -message(sprintf("[DIAG] these_samples (BL_Thomas, from installed GAMBLR.data::sample_data): %d ids", length(these_samples))) -message(sprintf("[DIAG] these_samples_dlbcl (DLBCL_Thomas + DLBCL_cell_lines, from installed GAMBLR.data::sample_data): %d ids", length(these_samples_dlbcl))) +message(sprintf("[DIAG] these_samples (BL_Thomas): %d ids", length(these_samples))) +message(sprintf("[DIAG] these_samples_dlbcl (DLBCL_Thomas + DLBCL_cell_lines): %d ids", length(these_samples_dlbcl))) coding_maf <- read_tsv("/projects/adult_blgsp/results_manuscript/BL.hg38.CDS.maf") %>% # get from flat maf file to show SSM in hg38 coordinates similar to the original manuscript filter(Tumor_Sample_Barcode %in% these_samples & # drop BL58 cell line @@ -985,11 +989,11 @@ hg38_publication_rows <- bind_rows( diag_summary_maf(hg38_publication_rows, "hg38_publication_rows (Thomas BL+DLBCL, final)") -this_study_samples <- GAMBLR.data::sample_data$meta %>% +this_study_samples <- sample_data$meta %>% filter(study %in% c("FL_Dreval", "DLBCL_cell_lines")) %>% pull(sample_id) -message(sprintf("[DIAG] this_study_samples (FL_Dreval + DLBCL_cell_lines, from installed GAMBLR.data::sample_data): %d ids", length(this_study_samples))) +message(sprintf("[DIAG] this_study_samples (FL_Dreval + DLBCL_cell_lines): %d ids", length(this_study_samples))) # FLs in grch37 coding_maf <- time_it("coding_maf get_ssm_by_samples", { From 2dc5f11ee8e1afc2b53b666bf31208c52f5d2a4e Mon Sep 17 00:00:00 2001 From: rdmorin Date: Wed, 15 Jul 2026 09:38:49 -0700 Subject: [PATCH 40/58] add automatic GitHub-release-based download for gambl_mutations.db gambl_mutations_db() previously just errored if no local copy was found in any of its resolved paths, requiring the file to be manually placed there (scp/rsync -- how it's been distributed all session). Now falls back to downloading it automatically via the new download_gambl_mutations_db(), using the ropensci piggyback package to fetch it from this repo's GitHub Release assets. Downloads are tagged per package version by default ("data-v"), so a given install always fetches data built for its own schema rather than whatever the newest release happens to be -- directly addresses the "is my DB in sync with my installed code" confusion that caused real friction earlier this session (e.g. the package-reinstall-vs-DB-rebuild timing mismatches while chasing the variant_pipeline fix). Auto-download can be disabled per-call (auto_download = FALSE) or globally (options(GAMBLR.data.auto_download = FALSE)) for offline/CI environments, falling back to the original explicit error. piggyback is a Suggests, not a hard dependency -- most calls to gambl_mutations_db() never need to download anything, and the function gives a clear, actionable error if it's missing when actually needed. Added data-raw/release_mutations_db.R, the corresponding admin-side upload script (not run as part of this change -- requires a GitHub PAT with write access to this repo; creates the release/tag if needed and uploads via piggyback::pb_upload()). Bump DESCRIPTION's Version and run this manually after each fresh gambl_mutations.db build to keep code and data in lockstep. Verified with synthetic tests: skip-if-exists short-circuit, clear error when piggyback is absent, auto_download=FALSE preserves the exact original error with no piggyback attempt, the global option override works, and an explicit db_path still works unaffected. Also regenerates man/NAMESPACE for several roxygen edits made earlier this session (Pipeline lowercase normalization, region-query consolidation, variant_pipeline/mutation_id schema docs) that were pending a devtools::document() run. Co-Authored-By: Claude Sonnet 5 --- DESCRIPTION | 1 + NAMESPACE | 1 + R/download_gambl_mutations_db.R | 69 ++++++++++++++++++++++++++++++ R/gambl_mutations_db.R | 33 +++++++++++--- data-raw/release_mutations_db.R | 51 ++++++++++++++++++++++ man/download_gambl_mutations_db.Rd | 46 ++++++++++++++++++++ man/gambl_mutations_db.Rd | 52 +++++++++++++++++----- man/get_ssm_from_db.Rd | 22 +++++++--- 8 files changed, 252 insertions(+), 23 deletions(-) create mode 100644 R/download_gambl_mutations_db.R create mode 100644 data-raw/release_mutations_db.R create mode 100644 man/download_gambl_mutations_db.Rd diff --git a/DESCRIPTION b/DESCRIPTION index d2eafb8..c72af39 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -39,5 +39,6 @@ Imports: tidyr LazyDataCompression: xz Suggests: + piggyback, testthat (>= 3.0.0) Config/testthat/edition: 3 diff --git a/NAMESPACE b/NAMESPACE index a791c72..03d6819 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(download_gambl_mutations_db) export(gambl_mutations_db) export(gambl_reference_db) export(get_cn_segments_from_db) diff --git a/R/download_gambl_mutations_db.R b/R/download_gambl_mutations_db.R new file mode 100644 index 0000000..c202237 --- /dev/null +++ b/R/download_gambl_mutations_db.R @@ -0,0 +1,69 @@ +#' @title Download the GAMBL mutations database. +#' +#' @description Downloads `gambl_mutations.db` from this package's GitHub +#' Release assets, if not already present. Normally called automatically by +#' [gambl_mutations_db()] the first time it's needed; call it directly to +#' pre-fetch the file (e.g. before a batch job on a machine with no other +#' network access) or to force a re-download. +#' +#' @details Requires the `piggyback` package (Suggests, not a hard +#' dependency of GAMBLR.data -- most calls to [gambl_mutations_db()] never +#' need to download anything). Downloads are tagged per package version by +#' default (`"data-v"`), so a given install always fetches data +#' built for its own schema, not whatever happens to be the newest release +#' -- see `data-raw/release_mutations_db.R` for the corresponding upload +#' step (admin-only, run manually after a fresh build; bump +#' `DESCRIPTION`'s `Version` and cut a matching release whenever the schema +#' changes, so the two stay in lockstep). +#' +#' @param dest_path Where to save the downloaded file. Defaults to the +#' standard cache location, `tools::R_user_dir("GAMBLR.data", "cache")`. +#' @param tag GitHub release tag to download from. Defaults to +#' `paste0("data-v", utils::packageVersion("GAMBLR.data"))`. +#' @param repo GitHub repo in `"owner/repo"` form. +#' @param overwrite Re-download even if `dest_path` already exists. Default +#' `FALSE`. +#' +#' @return `dest_path`, invisibly. +#' +#' @export +download_gambl_mutations_db <- function( + dest_path = file.path(tools::R_user_dir("GAMBLR.data", "cache"), "gambl_mutations.db"), + tag = paste0("data-v", utils::packageVersion("GAMBLR.data")), + repo = "morinlab/GAMBLR.data", + overwrite = FALSE) { + + if (file.exists(dest_path) && !overwrite) { + message("gambl_mutations.db already exists at ", dest_path, + "; set overwrite = TRUE to re-download.") + return(invisible(dest_path)) + } + + if (!requireNamespace("piggyback", quietly = TRUE)) { + stop("Downloading gambl_mutations.db requires the 'piggyback' package. ", + "Install it with install.packages(\"piggyback\"), or point ", + "gambl_mutations_db() at an existing copy via the db_path argument, ", + "options(GAMBLR.data.mutations_db = ...), or the GAMBLR_MUTATIONS_DB ", + "environment variable.", call. = FALSE) + } + + dir.create(dirname(dest_path), recursive = TRUE, showWarnings = FALSE) + message(sprintf( + "Downloading gambl_mutations.db from %s @ %s into %s -- this is a large file (several hundred MB) and may take a while...", + repo, tag, dirname(dest_path) + )) + piggyback::pb_download( + "gambl_mutations.db", + repo = repo, + tag = tag, + dest = dirname(dest_path), + overwrite = TRUE + ) + if (!file.exists(dest_path)) { + stop("Download appeared to succeed but ", dest_path, " still doesn't exist -- ", + "check that the release tag \"", tag, "\" actually has a gambl_mutations.db asset.", + call. = FALSE) + } + message("Done: ", dest_path) + invisible(dest_path) +} diff --git a/R/gambl_mutations_db.R b/R/gambl_mutations_db.R index a4deb0e..ad58ac8 100644 --- a/R/gambl_mutations_db.R +++ b/R/gambl_mutations_db.R @@ -8,7 +8,8 @@ #' #' @details Unlike the small bundled [gambl_reference_db()], this file is large #' (several hundred MB) and is NOT shipped inside the package. It is -#' distributed as a cached release asset. The path is resolved in this order: +#' distributed as a GitHub Release asset (see [download_gambl_mutations_db()]) +#' and cached locally. The path is resolved in this order: #' \enumerate{ #' \item the `db_path` argument; #' \item `getOption("GAMBLR.data.mutations_db")`; @@ -16,6 +17,11 @@ #' \item the user cache dir, `tools::R_user_dir("GAMBLR.data", "cache")`; #' \item `gambl_mutations.db` in the working directory (development). #' } +#' If none of these exist and `auto_download` is `TRUE` (the default), the +#' file is downloaded automatically into the user cache dir via +#' [download_gambl_mutations_db()]. Set `auto_download = FALSE`, or +#' `options(GAMBLR.data.auto_download = FALSE)` to disable this globally +#' (e.g. offline/CI environments) and get an explicit error instead. #' #' # Table reference #' @@ -80,6 +86,10 @@ #' `(elem, Pipeline)`. #' #' @param db_path Optional explicit path to the .db file. +#' @param auto_download When no existing copy is found, download it +#' automatically via [download_gambl_mutations_db()]. Default `TRUE`; +#' can also be disabled globally with +#' `options(GAMBLR.data.auto_download = FALSE)`. #' #' @return A read-only DBIConnection. #' @@ -96,24 +106,33 @@ #' dplyr::collect() #' } #' @export -gambl_mutations_db <- function(db_path = NULL) { +gambl_mutations_db <- function(db_path = NULL, auto_download = TRUE) { if (is.null(db_path)) { opt <- getOption("GAMBLR.data.mutations_db", default = NULL) env <- Sys.getenv("GAMBLR_MUTATIONS_DB", unset = "") + cache_path <- file.path(tools::R_user_dir("GAMBLR.data", "cache"), "gambl_mutations.db") candidates <- c( opt, if (nzchar(env)) env, - file.path(tools::R_user_dir("GAMBLR.data", "cache"), "gambl_mutations.db"), + cache_path, "gambl_mutations.db" ) candidates <- candidates[!is.null(candidates)] hit <- candidates[file.exists(candidates)] if (length(hit) == 0) { - stop("gambl_mutations.db not found. Set options(GAMBLR.data.mutations_db = \"/path\"), ", - "the GAMBLR_MUTATIONS_DB env var, or place it in ", - tools::R_user_dir("GAMBLR.data", "cache"), call. = FALSE) + can_auto_download <- isTRUE(auto_download) && isTRUE(getOption("GAMBLR.data.auto_download", TRUE)) + if (can_auto_download) { + db_path <- download_gambl_mutations_db(dest_path = cache_path) + } else { + stop("gambl_mutations.db not found. Set options(GAMBLR.data.mutations_db = \"/path\"), ", + "the GAMBLR_MUTATIONS_DB env var, place it in ", + tools::R_user_dir("GAMBLR.data", "cache"), + ", or call gambl_mutations_db() with auto_download = TRUE (the default) ", + "to fetch it automatically.", call. = FALSE) + } + } else { + db_path <- hit[1] } - db_path <- hit[1] } stopifnot(file.exists(db_path)) diff --git a/data-raw/release_mutations_db.R b/data-raw/release_mutations_db.R new file mode 100644 index 0000000..b641a34 --- /dev/null +++ b/data-raw/release_mutations_db.R @@ -0,0 +1,51 @@ +# release_mutations_db.R +# +# Admin-only: uploads a freshly-built gambl_mutations.db as a GitHub Release +# asset, so GAMBLR.data::gambl_mutations_db() can auto-download it for new +# installs (see R/download_gambl_mutations_db.R). Run this AFTER +# assemble_bundled_data.R / write_mutations_db() has produced a fresh +# gambl_mutations.db, from the GAMBLR.data repo root. This script only +# uploads -- it does not build the database itself. +# +# Requires: +# - the piggyback package: install.packages("piggyback") +# - a GitHub PAT with write access to this repo, either already configured +# (gitcreds::gitcreds_set()) or set via the GITHUB_PAT/GITHUB_TOKEN +# environment variable +# +# The release tag defaults to "data-v", matching what +# download_gambl_mutations_db() looks for by default. Keep these in lockstep: +# bump DESCRIPTION's Version whenever the DB schema changes (as it did +# several times in the sample_study/variant_pipeline refactor), and cut a +# matching release here, so a given package version always downloads data +# built for its own schema rather than whatever the newest release happens +# to be. +# +# Usage: Rscript data-raw/release_mutations_db.R [path to .db] [tag] + +args <- commandArgs(trailingOnly = TRUE) +db_path <- if (length(args) >= 1) args[[1]] else "gambl_mutations.db" +pkg_version <- read.dcf("DESCRIPTION")[, "Version"] +tag <- if (length(args) >= 2) args[[2]] else paste0("data-v", pkg_version) +repo <- "morinlab/GAMBLR.data" + +stopifnot(file.exists(db_path)) +if (!requireNamespace("piggyback", quietly = TRUE)) { + stop("Install piggyback first: install.packages(\"piggyback\")") +} + +message(sprintf("Releasing %s to %s @ %s (%.0f MB)", + db_path, repo, tag, file.info(db_path)$size / 1024^2)) + +# create the release/tag if it doesn't already exist +existing_releases <- piggyback::pb_releases(repo = repo) +if (!tag %in% existing_releases$tag_name) { + message("Creating new release tag: ", tag) + piggyback::pb_new_release(repo = repo, tag = tag) +} + +piggyback::pb_upload(db_path, repo = repo, tag = tag) +message(sprintf( + "Done. Users on GAMBLR.data v%s will now auto-download gambl_mutations.db from this release.", + pkg_version +)) diff --git a/man/download_gambl_mutations_db.Rd b/man/download_gambl_mutations_db.Rd new file mode 100644 index 0000000..4eb8796 --- /dev/null +++ b/man/download_gambl_mutations_db.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/download_gambl_mutations_db.R +\name{download_gambl_mutations_db} +\alias{download_gambl_mutations_db} +\title{Download the GAMBL mutations database.} +\usage{ +download_gambl_mutations_db( + dest_path = file.path(tools::R_user_dir("GAMBLR.data", "cache"), "gambl_mutations.db"), + tag = paste0("data-v", utils::packageVersion("GAMBLR.data")), + repo = "morinlab/GAMBLR.data", + overwrite = FALSE +) +} +\arguments{ +\item{dest_path}{Where to save the downloaded file. Defaults to the +standard cache location, \code{tools::R_user_dir("GAMBLR.data", "cache")}.} + +\item{tag}{GitHub release tag to download from. Defaults to +\code{paste0("data-v", utils::packageVersion("GAMBLR.data"))}.} + +\item{repo}{GitHub repo in \code{"owner/repo"} form.} + +\item{overwrite}{Re-download even if \code{dest_path} already exists. Default +\code{FALSE}.} +} +\value{ +\code{dest_path}, invisibly. +} +\description{ +Downloads \code{gambl_mutations.db} from this package's GitHub +Release assets, if not already present. Normally called automatically by +\code{\link[=gambl_mutations_db]{gambl_mutations_db()}} the first time it's needed; call it directly to +pre-fetch the file (e.g. before a batch job on a machine with no other +network access) or to force a re-download. +} +\details{ +Requires the \code{piggyback} package (Suggests, not a hard +dependency of GAMBLR.data -- most calls to \code{\link[=gambl_mutations_db]{gambl_mutations_db()}} never +need to download anything). Downloads are tagged per package version by +default (\code{"data-v"}), so a given install always fetches data +built for its own schema, not whatever happens to be the newest release +-- see \code{data-raw/release_mutations_db.R} for the corresponding upload +step (admin-only, run manually after a fresh build; bump +\code{DESCRIPTION}'s \code{Version} and cut a matching release whenever the schema +changes, so the two stay in lockstep). +} diff --git a/man/gambl_mutations_db.Rd b/man/gambl_mutations_db.Rd index 8617064..2488395 100644 --- a/man/gambl_mutations_db.Rd +++ b/man/gambl_mutations_db.Rd @@ -4,10 +4,15 @@ \alias{gambl_mutations_db} \title{Connect to the GAMBL mutations database.} \usage{ -gambl_mutations_db(db_path = NULL) +gambl_mutations_db(db_path = NULL, auto_download = TRUE) } \arguments{ \item{db_path}{Optional explicit path to the .db file.} + +\item{auto_download}{When no existing copy is found, download it +automatically via \code{\link[=download_gambl_mutations_db]{download_gambl_mutations_db()}}. Default \code{TRUE}; +can also be disabled globally with +\code{options(GAMBLR.data.auto_download = FALSE)}.} } \value{ A read-only DBIConnection. @@ -22,7 +27,8 @@ helper (\code{data-raw/write_mutations_db.R}). \details{ Unlike the small bundled \code{\link[=gambl_reference_db]{gambl_reference_db()}}, this file is large (several hundred MB) and is NOT shipped inside the package. It is -distributed as a cached release asset. The path is resolved in this order: +distributed as a GitHub Release asset (see \code{\link[=download_gambl_mutations_db]{download_gambl_mutations_db()}}) +and cached locally. The path is resolved in this order: \enumerate{ \item the \code{db_path} argument; \item \code{getOption("GAMBLR.data.mutations_db")}; @@ -30,6 +36,11 @@ distributed as a cached release asset. The path is resolved in this order: \item the user cache dir, \code{tools::R_user_dir("GAMBLR.data", "cache")}; \item \code{gambl_mutations.db} in the working directory (development). } +If none of these exist and \code{auto_download} is \code{TRUE} (the default), the +file is downloaded automatically into the user cache dir via +\code{\link[=download_gambl_mutations_db]{download_gambl_mutations_db()}}. Set \code{auto_download = FALSE}, or +\code{options(GAMBLR.data.auto_download = FALSE)} to disable this globally +(e.g. offline/CI environments) and get an explicit error instead. } \section{Table reference}{ Both genome builds are stacked into the same table (not split across @@ -37,13 +48,14 @@ separate tables); filter on \code{genome_build} (\code{"grch37"} / \code{"hg38"} one. \code{maf} and \code{ashm} share the same (curated, 49-column) schema: a reduced column set, not the full upstream flatfile MAF.\tabular{llll}{ Table \tab Rows (typical) \tab Grain \tab Key columns \cr - \code{maf} \tab ~3.6M \tab one somatic mutation call \tab \verb{Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline, genome_build} (+ ~35 more MAF-standard columns) \cr - \code{ashm} \tab ~135k \tab one mutation call in an aSHM region \tab same schema as \code{maf} \cr + \code{maf} \tab ~3.6M \tab one somatic mutation call \tab \code{mutation_id} (surrogate integer key, unique within \code{maf} across both genome builds -- assigned at write time, after deduplication; \code{variant_pipeline} references it as a foreign key), \verb{Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. }"slms-3"\code{/}"publication"\verb{ -- normalized at write time so it can be matched with a plain, indexed equality), genome_build} (+ ~35 more MAF-standard columns) \cr + \code{ashm} \tab ~135k \tab one mutation call in an aSHM region \tab same schema as \code{maf} (its own independent \code{mutation_id} sequence) \cr \code{seg} \tab ~126k \tab one copy-number segment \tab \verb{ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build} \cr \code{bedpe} \tab ~900 \tab one Manta structural-variant breakpoint pair \tab \verb{CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build} \cr \code{sample_meta} \tab ~3.3k \tab one sample \tab \verb{patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...} (its own \code{genome_build} column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) \cr \code{sample_study} \tab varies \tab one (sample, study) membership fact -- many-to-many, a sample belonging to N studies is N rows \tab \verb{sample_id, study, study_id, reference_PMID}. \code{study_id} is that study's own identifier for the sample where it differs from GAMBL's \code{sample_id} (e.g. a paper's own case/patient ID); NA where no study-specific ID has been sourced. \cr - \code{build_info} \tab 9 \tab key/value \tab provenance (\code{source}, \code{built_at}, \code{builder}) and expected row counts (\code{n_maf}, \code{n_ashm}, \code{n_seg}, \code{n_bedpe}, \code{n_samples}, \code{n_sample_study}) used by the \code{data-raw/test_gambl_db.R} regression checks \cr + \code{variant_pipeline} \tab varies \tab one (mutation, Pipeline) fact -- many-to-many, a variant independently called by N pipelines is N rows \tab \verb{mutation_id, elem, Pipeline}. \code{mutation_id} is a foreign key to \code{maf.mutation_id} or \code{ashm.mutation_id} depending on \code{elem} (\code{"maf"} or \code{"ashm"}) -- not a natural key, so this table doesn't repeat the variant's own position/sample columns. \cr + \code{build_info} \tab 10 \tab key/value \tab provenance (\code{source}, \code{built_at}, \code{builder}) and expected row counts (\code{n_maf}, \code{n_ashm}, \code{n_seg}, \code{n_bedpe}, \code{n_samples}, \code{n_sample_study}, \code{n_variant_pipeline}) used by the \code{data-raw/test_gambl_db.R} regression checks \cr } @@ -54,25 +66,42 @@ than one study anyway, and a per-row tag caused the same sample's mutations to be pulled and duplicated once per cohort-specific code path that claimed it (see \code{GAMBLR.data::get_ssm_from_db()}'s \code{this_study} parameter for how to filter by study post-refactor). + +Similarly, \code{maf}/\code{ashm} only ever carry ONE \code{Pipeline} value per variant, +even though the same real mutation can be independently produced by more +than one pipeline (e.g. a cohort's own published/curated maf and our +separate SLMS-3 recall both calling the same position) -- when that +happens, \code{write_mutations_db()}'s write-time dedup keeps one row +arbitrarily (whichever pipeline's data was assembled first), which would +otherwise make the variant invisible to a query for the \emph{other} pipeline +even though it's still fully present in the table. \code{variant_pipeline} +records every pipeline that actually produced each variant, independent +of which one "won" as \code{maf}/\code{ashm}'s single representative row; see +\code{GAMBLR.data::get_ssm_from_db()}'s \code{tool_name} parameter, which resolves +against this table rather than \code{maf}/\code{ashm}'s own \code{Pipeline} column. \subsection{Join keys}{ \code{maf}, \code{ashm}, and \code{bedpe} (via \code{tumour_sample_id}) key to \code{sample_meta$Tumor_Sample_Barcode} / \code{sample_meta$sample_id}; \code{seg$ID} keys to \code{sample_meta$sample_id}; \code{sample_study$sample_id} keys to -\code{sample_meta$sample_id}. +\code{sample_meta$sample_id}; \code{variant_pipeline$mutation_id} keys to +\code{maf.mutation_id} or \code{ashm.mutation_id}, selecting which one via +\code{variant_pipeline$elem}. } \subsection{Indexes}{ \code{maf}/\code{ashm}: \verb{(genome_build, Chromosome, Start_Position)}, -\code{Tumor_Sample_Barcode}, \code{Pipeline} (maf only). No index on +\code{Tumor_Sample_Barcode}, \code{Pipeline}, \code{mutation_id}. No index on \code{Hugo_Symbol}: gene-restricted queries resolve the gene to a region first (the same logic used to populate these tables) and filter on \verb{(genome_build, Chromosome, Start_Position)} instead -- see \code{GAMBLR.data::get_ssm_from_db()}. \code{seg}: \verb{(genome_build, ID)}, -\verb{(genome_build, chrom, start)}. \code{bedpe}: \code{tumour_sample_id}. +\verb{(genome_build, chrom, start)}. \code{bedpe}: \verb{(tumour_sample_id, genome_build)}, \verb{(genome_build, CHROM_A, START_A)}, \verb{(genome_build, CHROM_B, START_B)} (one per breakpoint end, so a region search that OR's +both ends can use a different index per side). \code{sample_meta}: \code{sample_id}, \code{Tumor_Sample_Barcode}. \code{sample_study}: -\code{sample_id}, \code{study}. +\code{sample_id}, \code{study}. \code{variant_pipeline}: \code{mutation_id}, +\verb{(elem, Pipeline)}. } } @@ -81,10 +110,11 @@ to \code{sample_meta$sample_id}; \code{sample_study$sample_id} keys to con <- gambl_mutations_db() DBI::dbListTables(con) -# coding mutations in TP53, grch37, one pipeline +# coding mutations in TP53, grch37, one pipeline (Pipeline is stored +# lowercase -- see the Pipeline column note above) dplyr::tbl(con, "maf") |> dplyr::filter(genome_build == "grch37", Hugo_Symbol == "TP53", - Pipeline == "SLMS-3") |> + Pipeline == "slms-3") |> dplyr::collect() } } diff --git a/man/get_ssm_from_db.Rd b/man/get_ssm_from_db.Rd index 5185858..e460c57 100644 --- a/man/get_ssm_from_db.Rd +++ b/man/get_ssm_from_db.Rd @@ -22,8 +22,17 @@ get_ssm_from_db( \item{sample_ids}{Optional character vector of \code{Tumor_Sample_Barcode} to keep.} -\item{tool_name}{Pipeline to keep (matched case-insensitively). Default -"slms-3"; set NULL to skip the Pipeline filter.} +\item{tool_name}{Pipeline to keep, matched case-insensitively (the value +you pass is lowercased before comparing; the stored Pipeline column is +itself normalized to lowercase at write time, so this is a plain, +indexed equality check rather than a function-wrapped one). Default +"slms-3"; set NULL to skip the Pipeline filter. Resolved via the +\code{variant_pipeline} join table (see \code{\link[=gambl_mutations_db]{gambl_mutations_db()}}'s schema +docs) rather than \code{maf}/\code{ashm}'s own Pipeline column, so a variant +independently called by more than one pipeline is still found by a +query for either one, even though its single maf/ashm row can only +carry one Pipeline value. Falls back to filtering the column directly +against DBs built before variant_pipeline existed.} \item{include_ashm}{When TRUE, also query the \code{ashm} table and row-bind it.} @@ -42,13 +51,16 @@ supplied; composes with \code{tool_name}/Pipeline exactly as before (e.g. while \verb{this_study="Reddy", tool_name="publication"} returns Reddy's as-published rows only).} -\item{regions}{Optional data frame with columns \code{chrom}, \code{start}, \code{end}; rows -are OR-ed (each region is an indexed range scan).} +\item{regions}{Optional data frame with columns \code{chrom}, \code{start}, \code{end}; +rows are OR-ed and applied as a single combined query (not one query per +region), so passing hundreds of regions (e.g. one per gene in a panel) +doesn't cost hundreds of separate round trips.} \item{con}{Optional DBI connection (defaults to \code{\link[=gambl_mutations_db]{gambl_mutations_db()}}).} } \value{ -A data frame of MAF rows (the \code{genome_build} helper column is dropped). +A data frame of MAF rows (the \code{genome_build}/\code{mutation_id} helper +columns are dropped). } \description{ Shared data-access helper that retrieves simple somatic mutations From 075812ea29777dc7cf946fe5402031e4b9152d5f Mon Sep 17 00:00:00 2001 From: rdmorin Date: Wed, 15 Jul 2026 09:43:44 -0700 Subject: [PATCH 41/58] release_mutations_db.R: refuse to run with an uncommitted version bump The release tag is derived from DESCRIPTION's Version on disk, but every OTHER install resolves the same tag from utils::packageVersion("GAMBLR.data") -- i.e. whatever's actually committed and installed elsewhere. Running this with a locally-bumped but uncommitted/unpushed DESCRIPTION would create a release that no install (including a future reinstall of this exact checkout) could ever find, since they'd all keep resolving the old version. Now refuses to proceed if DESCRIPTION has any uncommitted (staged or unstaged) changes, and prints the commit it's releasing from as a reminder to make sure it's been pushed. Co-Authored-By: Claude Sonnet 5 --- data-raw/release_mutations_db.R | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/data-raw/release_mutations_db.R b/data-raw/release_mutations_db.R index b641a34..98978b1 100644 --- a/data-raw/release_mutations_db.R +++ b/data-raw/release_mutations_db.R @@ -34,6 +34,25 @@ if (!requireNamespace("piggyback", quietly = TRUE)) { stop("Install piggyback first: install.packages(\"piggyback\")") } +# The tag is derived from DESCRIPTION's Version on disk, but +# download_gambl_mutations_db() on every *other* install resolves the same +# tag from utils::packageVersion("GAMBLR.data") -- i.e. whatever's actually +# committed and installed elsewhere. If the version bump used to build this +# tag isn't committed and pushed, every install (including a future +# reinstall of this exact checkout) will keep resolving the OLD version and +# never find this release at all. +git_diff_status <- suppressWarnings(system2("git", c("diff", "--quiet", "--", "DESCRIPTION"), stdout = FALSE, stderr = FALSE)) +git_staged_status <- suppressWarnings(system2("git", c("diff", "--quiet", "--cached", "--", "DESCRIPTION"), stdout = FALSE, stderr = FALSE)) +if (git_diff_status != 0 || git_staged_status != 0) { + stop("DESCRIPTION has uncommitted changes. Commit and push the version bump ", + "to \"", pkg_version, "\" BEFORE running this script, or every install ", + "(including your own) will keep resolving a different version and ", + "never find the \"", tag, "\" release you're about to create.", + call. = FALSE) +} +head_tag_check <- system2("git", c("log", "-1", "--format=%H"), stdout = TRUE, stderr = FALSE) +message("Releasing from commit ", head_tag_check, " -- make sure this has been pushed to origin.") + message(sprintf("Releasing %s to %s @ %s (%.0f MB)", db_path, repo, tag, file.info(db_path)$size / 1024^2)) From 5871b8e6883b2c260b73ffe5faba2956f2583436 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Wed, 15 Jul 2026 09:46:07 -0700 Subject: [PATCH 42/58] add data-raw/RELEASING.md: maintainer-only gambl_mutations.db release steps Documents the version-bump -> commit/push -> release_mutations_db.R sequence discussed in chat, plus prerequisites (piggyback, GitHub PAT) and a troubleshooting section. Deliberately not in README.md -- this is a maintainer-only workflow, not something package users need, but it's co-located with the build/release scripts in data-raw/ so it's easy to find for anyone actually working on the build process. Co-Authored-By: Claude Sonnet 5 --- data-raw/RELEASING.md | 81 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 data-raw/RELEASING.md diff --git a/data-raw/RELEASING.md b/data-raw/RELEASING.md new file mode 100644 index 0000000..d2f586f --- /dev/null +++ b/data-raw/RELEASING.md @@ -0,0 +1,81 @@ +# Releasing a new `gambl_mutations.db` + +This is the maintainer-only process for publishing a freshly-built +`gambl_mutations.db` so that `GAMBLR.data::gambl_mutations_db()` can +auto-download it for new installs. It's not part of the main `README.md` +because it's not something package users ever need to do -- only whoever +is building and shipping a new version of the bundled data. + +## Background + +`gambl_mutations.db` is too large (several hundred MB) to ship inside the +package itself. Instead it's distributed as a GitHub Release asset and +downloaded on demand: + +- `data-raw/assemble_bundled_data.R` (+ `data-raw/write_mutations_db.R`) + builds the file from GSC data. +- `data-raw/release_mutations_db.R` (this doc) uploads it to a GitHub + Release. +- `R/download_gambl_mutations_db.R` downloads it back down for any install + that doesn't already have a copy -- called automatically by + `R/gambl_mutations_db.R` the first time it's needed. + +Releases are tagged `data-v`. This is deliberate: +`download_gambl_mutations_db()` resolves the tag to fetch from +`utils::packageVersion("GAMBLR.data")` on the *installing* machine, so a +given install always gets data built for its own schema -- not whatever +the newest release happens to be. This means **a schema change requires +both a version bump and a new release**, not just a new upload. + +## Steps + +1. **Build a fresh `gambl_mutations.db`** via `assemble_bundled_data.R` (on + the GSC). Run `data-raw/test_gambl_db.R` and `data-raw/compare_bundle_changes.R` + against it first -- don't release something that hasn't passed those. + +2. **Bump `DESCRIPTION`'s `Version`.** Required any time the DB schema + changed since the last release (new/changed tables, columns, or + indexes) -- if in doubt, bump it. The release tag is derived from this. + +3. **Commit and push the version bump.** `release_mutations_db.R` will + refuse to run otherwise. The reason: the tag it creates is only + findable by other installs if the version that generated it is what's + actually committed and installed elsewhere -- an uncommitted or + unpushed bump would create a release nothing can ever resolve to, + including your own next reinstall of this checkout. + +4. **Install `piggyback`** if you haven't already: `install.packages("piggyback")`. + +5. **Make sure you have a GitHub PAT with write access to this repo.** + Either already configured (`gitcreds::gitcreds_set()`) or set via the + `GITHUB_PAT`/`GITHUB_TOKEN` environment variable. This is the step most + likely to trip you up if it's not already set -- `piggyback::pb_upload()` + fails with an auth error without it, not a helpful one. + +6. **Run the release script from the repo root:** + ```bash + Rscript data-raw/release_mutations_db.R /path/to/gambl_mutations.db + ``` + This creates the `data-v` release/tag if it doesn't already + exist and uploads the file to it. There's no dry-run mode -- double + check the path argument before running, since this creates a real, + public release with a several-hundred-MB asset attached. + +7. **Verify the download side works** by clearing your local cache + (`unlink(file.path(tools::R_user_dir("GAMBLR.data", "cache")), recursive = TRUE)`) + and calling `GAMBLR.data::gambl_mutations_db()` fresh -- it should + report downloading, then connect successfully. + +## Troubleshooting + +- **"DESCRIPTION has uncommitted changes"** -- commit and push the version + bump first (step 3). +- **Auth error from `piggyback::pb_upload()`/`pb_new_release()`** -- your + GitHub PAT isn't configured, or doesn't have write access to this repo + (step 5). +- **A user reports a download that 404s or an outdated schema** -- check + that their installed `packageVersion("GAMBLR.data")` actually matches a + tag that exists (`piggyback::pb_releases(repo = "morinlab/GAMBLR.data")`); + usually means either the release for their version was never cut, or + they installed from a commit whose `DESCRIPTION` version doesn't have a + matching release yet. From e5df74cd24c4945287d27e465654179458370aea Mon Sep 17 00:00:00 2001 From: Ryan Morin Date: Thu, 16 Jul 2026 06:42:44 -0700 Subject: [PATCH 43/58] missing docs --- data/sample_metadata.rda | Bin 13444 -> 48420 bytes man/gambl_mutations_db.Rd | 37 ++++++++++++++++++++++++++++--------- man/gencode_to_symbol.Rd | 4 ++-- man/get_ssm_from_db.Rd | 22 +++++++++++++++++----- 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/data/sample_metadata.rda b/data/sample_metadata.rda index 968b9a9b6b9ceee336901c33441555cf2cd670b2..b6a22a85a59f13179d52427f4b4e7b69090c35cc 100644 GIT binary patch literal 48420 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 K;Lu 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 literal 13444 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 diff --git a/man/gambl_mutations_db.Rd b/man/gambl_mutations_db.Rd index 8617064..364e5ea 100644 --- a/man/gambl_mutations_db.Rd +++ b/man/gambl_mutations_db.Rd @@ -37,13 +37,14 @@ separate tables); filter on \code{genome_build} (\code{"grch37"} / \code{"hg38"} one. \code{maf} and \code{ashm} share the same (curated, 49-column) schema: a reduced column set, not the full upstream flatfile MAF.\tabular{llll}{ Table \tab Rows (typical) \tab Grain \tab Key columns \cr - \code{maf} \tab ~3.6M \tab one somatic mutation call \tab \verb{Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline, genome_build} (+ ~35 more MAF-standard columns) \cr - \code{ashm} \tab ~135k \tab one mutation call in an aSHM region \tab same schema as \code{maf} \cr + \code{maf} \tab ~3.6M \tab one somatic mutation call \tab \code{mutation_id} (surrogate integer key, unique within \code{maf} across both genome builds -- assigned at write time, after deduplication; \code{variant_pipeline} references it as a foreign key), \verb{Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. }"slms-3"\code{/}"publication"\verb{ -- normalized at write time so it can be matched with a plain, indexed equality), genome_build} (+ ~35 more MAF-standard columns) \cr + \code{ashm} \tab ~135k \tab one mutation call in an aSHM region \tab same schema as \code{maf} (its own independent \code{mutation_id} sequence) \cr \code{seg} \tab ~126k \tab one copy-number segment \tab \verb{ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build} \cr \code{bedpe} \tab ~900 \tab one Manta structural-variant breakpoint pair \tab \verb{CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build} \cr \code{sample_meta} \tab ~3.3k \tab one sample \tab \verb{patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...} (its own \code{genome_build} column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) \cr \code{sample_study} \tab varies \tab one (sample, study) membership fact -- many-to-many, a sample belonging to N studies is N rows \tab \verb{sample_id, study, study_id, reference_PMID}. \code{study_id} is that study's own identifier for the sample where it differs from GAMBL's \code{sample_id} (e.g. a paper's own case/patient ID); NA where no study-specific ID has been sourced. \cr - \code{build_info} \tab 9 \tab key/value \tab provenance (\code{source}, \code{built_at}, \code{builder}) and expected row counts (\code{n_maf}, \code{n_ashm}, \code{n_seg}, \code{n_bedpe}, \code{n_samples}, \code{n_sample_study}) used by the \code{data-raw/test_gambl_db.R} regression checks \cr + \code{variant_pipeline} \tab varies \tab one (mutation, Pipeline) fact -- many-to-many, a variant independently called by N pipelines is N rows \tab \verb{mutation_id, elem, Pipeline}. \code{mutation_id} is a foreign key to \code{maf.mutation_id} or \code{ashm.mutation_id} depending on \code{elem} (\code{"maf"} or \code{"ashm"}) -- not a natural key, so this table doesn't repeat the variant's own position/sample columns. \cr + \code{build_info} \tab 10 \tab key/value \tab provenance (\code{source}, \code{built_at}, \code{builder}) and expected row counts (\code{n_maf}, \code{n_ashm}, \code{n_seg}, \code{n_bedpe}, \code{n_samples}, \code{n_sample_study}, \code{n_variant_pipeline}) used by the \code{data-raw/test_gambl_db.R} regression checks \cr } @@ -54,25 +55,42 @@ than one study anyway, and a per-row tag caused the same sample's mutations to be pulled and duplicated once per cohort-specific code path that claimed it (see \code{GAMBLR.data::get_ssm_from_db()}'s \code{this_study} parameter for how to filter by study post-refactor). + +Similarly, \code{maf}/\code{ashm} only ever carry ONE \code{Pipeline} value per variant, +even though the same real mutation can be independently produced by more +than one pipeline (e.g. a cohort's own published/curated maf and our +separate SLMS-3 recall both calling the same position) -- when that +happens, \code{write_mutations_db()}'s write-time dedup keeps one row +arbitrarily (whichever pipeline's data was assembled first), which would +otherwise make the variant invisible to a query for the \emph{other} pipeline +even though it's still fully present in the table. \code{variant_pipeline} +records every pipeline that actually produced each variant, independent +of which one "won" as \code{maf}/\code{ashm}'s single representative row; see +\code{GAMBLR.data::get_ssm_from_db()}'s \code{tool_name} parameter, which resolves +against this table rather than \code{maf}/\code{ashm}'s own \code{Pipeline} column. \subsection{Join keys}{ \code{maf}, \code{ashm}, and \code{bedpe} (via \code{tumour_sample_id}) key to \code{sample_meta$Tumor_Sample_Barcode} / \code{sample_meta$sample_id}; \code{seg$ID} keys to \code{sample_meta$sample_id}; \code{sample_study$sample_id} keys to -\code{sample_meta$sample_id}. +\code{sample_meta$sample_id}; \code{variant_pipeline$mutation_id} keys to +\code{maf.mutation_id} or \code{ashm.mutation_id}, selecting which one via +\code{variant_pipeline$elem}. } \subsection{Indexes}{ \code{maf}/\code{ashm}: \verb{(genome_build, Chromosome, Start_Position)}, -\code{Tumor_Sample_Barcode}, \code{Pipeline} (maf only). No index on +\code{Tumor_Sample_Barcode}, \code{Pipeline}, \code{mutation_id}. No index on \code{Hugo_Symbol}: gene-restricted queries resolve the gene to a region first (the same logic used to populate these tables) and filter on \verb{(genome_build, Chromosome, Start_Position)} instead -- see \code{GAMBLR.data::get_ssm_from_db()}. \code{seg}: \verb{(genome_build, ID)}, -\verb{(genome_build, chrom, start)}. \code{bedpe}: \code{tumour_sample_id}. +\verb{(genome_build, chrom, start)}. \code{bedpe}: \verb{(tumour_sample_id, genome_build)}, \verb{(genome_build, CHROM_A, START_A)}, \verb{(genome_build, CHROM_B, START_B)} (one per breakpoint end, so a region search that OR's +both ends can use a different index per side). \code{sample_meta}: \code{sample_id}, \code{Tumor_Sample_Barcode}. \code{sample_study}: -\code{sample_id}, \code{study}. +\code{sample_id}, \code{study}. \code{variant_pipeline}: \code{mutation_id}, +\verb{(elem, Pipeline)}. } } @@ -81,10 +99,11 @@ to \code{sample_meta$sample_id}; \code{sample_study$sample_id} keys to con <- gambl_mutations_db() DBI::dbListTables(con) -# coding mutations in TP53, grch37, one pipeline +# coding mutations in TP53, grch37, one pipeline (Pipeline is stored +# lowercase -- see the Pipeline column note above) dplyr::tbl(con, "maf") |> dplyr::filter(genome_build == "grch37", Hugo_Symbol == "TP53", - Pipeline == "SLMS-3") |> + Pipeline == "slms-3") |> dplyr::collect() } } diff --git a/man/gencode_to_symbol.Rd b/man/gencode_to_symbol.Rd index 05dfc0b..9556c80 100644 --- a/man/gencode_to_symbol.Rd +++ b/man/gencode_to_symbol.Rd @@ -9,8 +9,8 @@ A data frame with 60038 rows and 3 columns \describe{ -\item{ensembl_gene_id}{ENSEMBL gene id in a format without .\if{html}{\out{}}.} -\item{gene_id}{ENSEMBL gene id in a format containing .\if{html}{\out{}}.} +\item{ensembl_gene_id}{ENSEMBL gene id in a format without ..} +\item{gene_id}{ENSEMBL gene id in a format containing ..} \item{hgnc_symbol}{Human-readable gene symbol.} } } diff --git a/man/get_ssm_from_db.Rd b/man/get_ssm_from_db.Rd index 5185858..e460c57 100644 --- a/man/get_ssm_from_db.Rd +++ b/man/get_ssm_from_db.Rd @@ -22,8 +22,17 @@ get_ssm_from_db( \item{sample_ids}{Optional character vector of \code{Tumor_Sample_Barcode} to keep.} -\item{tool_name}{Pipeline to keep (matched case-insensitively). Default -"slms-3"; set NULL to skip the Pipeline filter.} +\item{tool_name}{Pipeline to keep, matched case-insensitively (the value +you pass is lowercased before comparing; the stored Pipeline column is +itself normalized to lowercase at write time, so this is a plain, +indexed equality check rather than a function-wrapped one). Default +"slms-3"; set NULL to skip the Pipeline filter. Resolved via the +\code{variant_pipeline} join table (see \code{\link[=gambl_mutations_db]{gambl_mutations_db()}}'s schema +docs) rather than \code{maf}/\code{ashm}'s own Pipeline column, so a variant +independently called by more than one pipeline is still found by a +query for either one, even though its single maf/ashm row can only +carry one Pipeline value. Falls back to filtering the column directly +against DBs built before variant_pipeline existed.} \item{include_ashm}{When TRUE, also query the \code{ashm} table and row-bind it.} @@ -42,13 +51,16 @@ supplied; composes with \code{tool_name}/Pipeline exactly as before (e.g. while \verb{this_study="Reddy", tool_name="publication"} returns Reddy's as-published rows only).} -\item{regions}{Optional data frame with columns \code{chrom}, \code{start}, \code{end}; rows -are OR-ed (each region is an indexed range scan).} +\item{regions}{Optional data frame with columns \code{chrom}, \code{start}, \code{end}; +rows are OR-ed and applied as a single combined query (not one query per +region), so passing hundreds of regions (e.g. one per gene in a panel) +doesn't cost hundreds of separate round trips.} \item{con}{Optional DBI connection (defaults to \code{\link[=gambl_mutations_db]{gambl_mutations_db()}}).} } \value{ -A data frame of MAF rows (the \code{genome_build} helper column is dropped). +A data frame of MAF rows (the \code{genome_build}/\code{mutation_id} helper +columns are dropped). } \description{ Shared data-access helper that retrieves simple somatic mutations From 0b4ecddbc207981e140e28bc488531575ab71bf3 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Thu, 16 Jul 2026 07:00:04 -0700 Subject: [PATCH 44/58] Bump version to 1.4 for variant_pipeline/mutation_id schema and auto-download Gates the data-v1.4 GitHub Release tag used by gambl_mutations_db()'s auto-download path. --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index c72af39..0f95aa8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: GAMBLR.data Title: Collection of Curated Data for Genomic Analysis of Mature B-cell Lymphomas in R -Version: 1.3 +Version: 1.4 Authors@R: c( person(given = "Ryan", From e69f340c0a7623e38d8cf3adb39acd1a84920b85 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Thu, 16 Jul 2026 07:08:12 -0700 Subject: [PATCH 45/58] Retry pb_upload() after pb_new_release() to handle GitHub API lag pb_upload() re-checks the release list to confirm the tag exists, which can momentarily miss a just-created release. piggyback's own retry fallback for this only kicks in when interactive(), which Rscript never is, so add a backoff retry here instead. Confirmed against a real "Release not found" failure while cutting data-v1.4. --- data-raw/release_mutations_db.R | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/data-raw/release_mutations_db.R b/data-raw/release_mutations_db.R index 98978b1..0072152 100644 --- a/data-raw/release_mutations_db.R +++ b/data-raw/release_mutations_db.R @@ -63,7 +63,31 @@ if (!tag %in% existing_releases$tag_name) { piggyback::pb_new_release(repo = repo, tag = tag) } -piggyback::pb_upload(db_path, repo = repo, tag = tag) +# GitHub's list-releases endpoint can lag a moment after pb_new_release() +# creates a tag -- pb_upload() re-fetches that list to confirm the tag +# exists, and can spuriously fail with "Release not found" if it queries +# before GitHub's side catches up. piggyback's own pb_upload() has a +# Sys.sleep()-and-retry fallback for this, but only when interactive(), +# which Rscript never is -- so retry it here instead. +upload_with_retry <- function(max_tries = 5) { + for (i in seq_len(max_tries)) { + ok <- tryCatch({ + piggyback::pb_upload(db_path, repo = repo, tag = tag) + TRUE + }, error = function(e) { + if (grepl("not found", conditionMessage(e), ignore.case = TRUE) && i < max_tries) { + message("Release not yet visible via GitHub's API (attempt ", i, "/", max_tries, + "), retrying in ", 2 * i, "s...") + Sys.sleep(2 * i) + FALSE + } else { + stop(e) + } + }) + if (isTRUE(ok)) return(invisible()) + } +} +upload_with_retry() message(sprintf( "Done. Users on GAMBLR.data v%s will now auto-download gambl_mutations.db from this release.", pkg_version From 22ba4fd13d179c0bdc1943a11311cb76dd553799 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Thu, 16 Jul 2026 07:34:22 -0700 Subject: [PATCH 46/58] CI: pin r-rsqlite via conda to avoid broken from-source compile devtools::install() was compiling RSQLite from CRAN source using this env's toolchain (gcc/g++ 11.2.0 vs libstdc++-ng runtime 13.2.0), which fails with "'timespec_get' has not been declared in '::'" in RSQLite's C++ SQLite wrapper. Not caused by this branch -- the same workflow passed on an unrelated branch two weeks ago with this same env file. Pinning a conda-forge prebuilt binary (r43-compatible, matching r-base=4.3.3) sidesteps the broken compile path entirely. --- envs/r.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/envs/r.yaml b/envs/r.yaml index c6af660..1bc87ea 100644 --- a/envs/r.yaml +++ b/envs/r.yaml @@ -149,6 +149,7 @@ dependencies: - r-rmarkdown=2.25 - r-roxygen2=7.2.3 - r-rprojroot=2.0.3 + - r-rsqlite=2.4.3 - r-rstudioapi=0.15.0 - r-rversions=2.1.2 - r-sass=0.4.7 From 3479aadce51fc63232ad20da51207ba786e406ee Mon Sep 17 00:00:00 2001 From: rdmorin Date: Thu, 16 Jul 2026 07:53:06 -0700 Subject: [PATCH 47/58] CI: pin r-rsqlite to 2.4.1, not 2.4.3 (requires libgcc>=14) Conda-forge's linux-64 builds of r-rsqlite 2.4.2/2.4.3 all require libgcc/libstdcxx >=14, which conflicts with this env's pinned libgcc-ng=13.2.0 (confirmed via LibMambaUnsatisfiableError on the previous push). 2.4.1 only requires >=13, satisfied by the existing pin. --- envs/r.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/r.yaml b/envs/r.yaml index 1bc87ea..a58f361 100644 --- a/envs/r.yaml +++ b/envs/r.yaml @@ -149,7 +149,7 @@ dependencies: - r-rmarkdown=2.25 - r-roxygen2=7.2.3 - r-rprojroot=2.0.3 - - r-rsqlite=2.4.3 + - r-rsqlite=2.4.1 - r-rstudioapi=0.15.0 - r-rversions=2.1.2 - r-sass=0.4.7 From bc21b2235462c0d17d04fdb530617fdcaca0271f Mon Sep 17 00:00:00 2001 From: rdmorin Date: Thu, 16 Jul 2026 07:57:32 -0700 Subject: [PATCH 48/58] CI: pin r-rsqlite to exact build 2.3.7=r43h0d4f4ea_0 2.4.x builds all depend on the newer libgcc/libstdcxx metapackages (>=13 or >=14), which this env's libgcc-ng=13.2.0 pin doesn't satisfy (confirmed via two failed solves). 2.3.7's r43h0d4f4ea_0 build predates that migration and depends on libgcc-ng/libstdcxx-ng >=12 directly -- the same package names already pinned here, satisfied by 13.2.0. Pinning the full build string since 2.3.7 also has a newer-style build the solver could otherwise pick instead. --- envs/r.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/r.yaml b/envs/r.yaml index a58f361..d256efa 100644 --- a/envs/r.yaml +++ b/envs/r.yaml @@ -149,7 +149,7 @@ dependencies: - r-rmarkdown=2.25 - r-roxygen2=7.2.3 - r-rprojroot=2.0.3 - - r-rsqlite=2.4.1 + - r-rsqlite=2.3.7=r43h0d4f4ea_0 - r-rstudioapi=0.15.0 - r-rversions=2.1.2 - r-sass=0.4.7 From 767fe8f215ac49717f7bc5d97ac3a38cf715185d Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 27 Jul 2026 15:30:26 -0700 Subject: [PATCH 49/58] Widen assemble_bundled_data.R's live metadata rejoin; retire sample_metadata/gambl_metadata assemble_bundled_data.R's Phase 2 rejoin against live GAMBLR.results::get_gambl_metadata() only pulled sample_id/seq_type/cohort, so sample_meta (and, downstream, the now-retired sample_metadata bundled object) was missing ~40 columns the live function actually provides (COO_consensus, DHITsig_consensus, EBV_status_inf, ffpe_or_frozen, time_point, lymphgen_*, normal_sample_id, pairing_status, Tumor_Sample_Barcode, sex, bam_available, genome_build, etc.). Now keeps only genuinely paper-supplement-specific columns from each cohort block (sample_id, study, genetic_subgroup, reference_PMID) and left_joins the full, unrestricted live metadata by (sample_id, seq_type) instead of a narrow select(). Retires two bundled objects entirely, now that GAMBLR.open::get_gambl_metadata() (a separate commit) queries sample_meta from gambl_mutations.db directly: - sample_metadata: had exactly one real consumer anywhere in the GAMBLR.data/.open/.results/.utils/.viz ecosystem (verified precisely, not by substring grep). Drops its build step, data/sample_metadata.rda, R/sample_metadata.R, data-raw/make_sample_metadata.R (now fully dead), and its regression check in data-raw/test_gambl_db.R. - gambl_metadata: a ~3-year-stale (2023-08-28) object with zero real consumers and zero build step already -- fully orphaned dead weight. Drops data/gambl_metadata.rda and its doc entries in R/data.R / R/data-vars.R. Co-Authored-By: Claude Sonnet 5 --- R/data-vars.R | 2 +- R/data.R | 39 ------------------------- R/sample_metadata.R | 10 ------- data-raw/assemble_bundled_data.R | 35 +++++++++++++--------- data-raw/make_sample_metadata.R | 16 ----------- data-raw/test_gambl_db.R | 12 ++------ data/gambl_metadata.rda | Bin 53069 -> 0 bytes data/sample_metadata.rda | Bin 48420 -> 0 bytes man/gambl_metadata.Rd | 48 ------------------------------- man/gencode_to_symbol.Rd | 4 +-- man/sample_metadata.Rd | 20 ------------- 11 files changed, 27 insertions(+), 159 deletions(-) delete mode 100644 R/sample_metadata.R delete mode 100644 data-raw/make_sample_metadata.R delete mode 100644 data/gambl_metadata.rda delete mode 100644 data/sample_metadata.rda delete mode 100644 man/gambl_metadata.Rd delete mode 100644 man/sample_metadata.Rd diff --git a/R/data-vars.R b/R/data-vars.R index 098acdc..cbe9cbd 100644 --- a/R/data-vars.R +++ b/R/data-vars.R @@ -19,7 +19,7 @@ if (getRversion() >= "2.15.1") { "SCORE", "START_A", "START_B", "Start_Position", "Start_Position.x", "Tumor_Sample_Barcode", "VAF_tumour", "Variant_Classification", "bin", "category", "chrom", "cohort", "colour", "curated", "end", - "ensembl_gene_id", "gambl_metadata", "gene", "genome_build", + "ensembl_gene_id", "gene", "genome_build", "grch37_ashm_regions", "group", "head", "hg38_ashm_regions", "hot_spot", "hotspot_regions_grch37", "hotspot_regions_hg38", "is_alias", "log.ratio", "mutated", "mutation_count", "n_mut", diff --git a/R/data.R b/R/data.R index 02179a8..dad8c53 100644 --- a/R/data.R +++ b/R/data.R @@ -933,45 +933,6 @@ "somatic_hypermutation_locations_GRCh38_v_latest" -#' GAMBL Metadata -#' -#' A data frame with metadata for a collection of GAMBL samples. -#' -#' @format ## `gambl_metadata` -#' A data frame with 4785 rows and 27 columns -#' \describe{ -#' \item{sample_id}{Sample identifier.} -#' \item{patient_id}{Patient identifier.} -#' \item{pathology}{Pathology.} -#' \item{seq_type}{Sample sequencing type.} -#' \item{genome_build}{Genome build the sample coordinates are in reference to.} -#' \item{pairing_status}{Matched or unmatched.} -#' \item{Tumor_Sample_Barcode}{Sample ID in another column, needed for certain functions.} -#' \item{age_group}{Sample age group.} -#' \item{compression}{The compression available for a particular sample.} -#' \item{bam_available}{Boolean.} -#' \item{pathology_rank}{Pathology rank.} -#' \item{cohort}{Sample cohort} -#' \item{COO_consensus}{COO consensus.} -#' \item{DHITsig_consensus}{DHIT signature consensus.} -#' \item{EBV_status_inf}{EBV status.} -#' \item{ffpe_or_frozen}{FFPE or frozen.} -#' \item{fl_grade}{FL grade.} -#' \item{hiv_status}{Sample HIV status.} -#' \item{lymphgen}{Lymphgen.} -#' \item{lymphgen_cnv_noA53}{Lymphgen with CNV no A53.} -#' \item{lymphgen_no_cnv}{Lymphgen no CNV.} -#' \item{lymphgen_with_cnv}{Lymphgen with CNV} -#' \item{lymphgen_wright}{Lymphgen Wright.} -#' \item{molecular_BL}{Molecualr BL.} -#' \item{normal_sample_id}{Normal sample ID} -#' \item{sex}{Female or Male} -#' \item{time_point}{Smaple timepoint.} -#' } -#' @keywords internal -"gambl_metadata" - - #' Mirage Metrics #' #' A data frame with Quality Control metrics for a collection of GAMBL samples. diff --git a/R/sample_metadata.R b/R/sample_metadata.R deleted file mode 100644 index f328d61..0000000 --- a/R/sample_metadata.R +++ /dev/null @@ -1,10 +0,0 @@ -#' Lightweight sample metadata for the bundled GAMBL samples. -#' -#' The per-sample metadata table (formerly `sample_data$meta`), split into its -#' own object so that loading metadata does not pull the large MAF/seg/bedpe -#' frames of `sample_data` into memory. The heavy mutation frames now live in -#' the separate `gambl_mutations.db` SQLite database (see -#' `data-raw/build_mutations_db.R`). Rebuilt by `data-raw/make_sample_metadata.R`. -#' -#' @format A data frame with 3344 rows and 31 columns, keyed by `sample_id`. -"sample_metadata" diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index d468a1d..9750a5f 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -774,11 +774,23 @@ fix <- fix %>% rename(study = cohort) diag_summary(fix, "fix (post rename cohort->study)", study_col = "study") +# Keep only what's genuinely paper-supplement-specific -- everything else +# (COO_consensus, lymphgen, EBV_status_inf, pathology, sex, +# Tumor_Sample_Barcode, patient_id, and the ~40 other columns live +# get_gambl_metadata() provides) comes fresh from the join below instead of +# whatever narrow/partial copy a given cohort block happened to carry. This +# is what makes sample_meta (and, downstream, GAMBLR.open::get_gambl_metadata(), +# which now reads sample_meta directly) actually complete -- mirrors the +# same select-then-rejoin pattern already used a few lines above for +# COO_consensus/lymphgen/EBV_status_inf. +fix <- fix %>% + select(sample_id, seq_type, study, genetic_subgroup, reference_PMID) + pre_join_ids <- fix$sample_id fix <- left_join( fix, - get_gambl_metadata() %>% - select(sample_id, seq_type, cohort) + get_gambl_metadata(), + by = c("sample_id", "seq_type") ) message(sprintf("[DIAG] fix (post live get_gambl_metadata() join): %d rows (was %d rows pre-join -- any increase means the join fanned out)", nrow(fix), length(pre_join_ids))) @@ -1221,19 +1233,16 @@ print("done extracting all mutations in lymphoma genes with GAMBLR.results") # --- Persist the assembled data ------------------------------------------- -# Previously this bundled the multi-GB `sample_data.rda`. Instead we now write: -# 1. a lightweight, bundled `sample_metadata` object (metadata only), and -# 2. the large per-sample frames to gambl_mutations.db (a release asset, -# NOT shipped in the package tarball). -# This is built straight from the in-memory `sample_data` above, so there is no -# sample_data.rda round-trip. - -# 1. lightweight bundled metadata (replaces sample_data$meta lookups) -sample_metadata <- sample_data$meta -usethis::use_data(sample_metadata, overwrite = TRUE, compress = "xz") +# Previously this bundled the multi-GB `sample_data.rda`, and later a +# lightweight `sample_metadata` object shipped inside the package itself. +# That bundled-metadata object is retired: GAMBLR.open::get_gambl_metadata() +# (its only real consumer) now queries the sample_meta table in +# gambl_mutations.db directly instead, so metadata and mutation data always +# come from the same build with no separate reinstall-triggered refresh +# cadence to drift out of sync. This is built straight from the in-memory +# `sample_data` above, so there is no sample_data.rda round-trip. print("Starting sqlite build") -# 2. large sample-level frames -> SQLite source("data-raw/write_mutations_db.R") write_mutations_db( sample_data, diff --git a/data-raw/make_sample_metadata.R b/data-raw/make_sample_metadata.R deleted file mode 100644 index bfe14ce..0000000 --- a/data-raw/make_sample_metadata.R +++ /dev/null @@ -1,16 +0,0 @@ -# make_sample_metadata.R -# -# Extract the lightweight sample metadata (sample_data$meta) into its own bundled -# object, `sample_metadata`, so that loading metadata does NOT drag the multi-GB -# MAF frames of `sample_data` into memory. The heavy mutation frames now live in -# gambl_mutations.db (see data-raw/build_mutations_db.R). - -e <- new.env() -load(file.path("data", "sample_data.rda"), envir = e) -sample_metadata <- get("sample_data", envir = e)$meta - -message(sprintf("sample_metadata: %d rows x %d cols (%.0f KB)", - nrow(sample_metadata), ncol(sample_metadata), - as.numeric(object.size(sample_metadata)) / 1024)) - -save(sample_metadata, file = file.path("data", "sample_metadata.rda"), compress = "xz") diff --git a/data-raw/test_gambl_db.R b/data-raw/test_gambl_db.R index c91fa80..4e2b105 100644 --- a/data-raw/test_gambl_db.R +++ b/data-raw/test_gambl_db.R @@ -5,7 +5,7 @@ # the GAMBLR.open accessors. # # Usage: -# Rscript data-raw/test_gambl_db.R [OUT_DB] [SAMPLE_METADATA_RDA] [SAMPLE_DATA_RDA] +# Rscript data-raw/test_gambl_db.R [OUT_DB] [SAMPLE_DATA_RDA] # # Exits non-zero if any critical check fails. The optional SAMPLE_DATA_RDA # enables a regression check (DB row counts vs the in-memory frames). @@ -14,8 +14,7 @@ suppressMessages({ library(DBI); library(RSQLite) }) args <- commandArgs(trailingOnly = TRUE) db <- if (length(args) >= 1) args[[1]] else "gambl_mutations.db" -meta <- if (length(args) >= 2) args[[2]] else "data/sample_metadata.rda" -rda <- if (length(args) >= 3) args[[3]] else "data/sample_data.rda" +rda <- if (length(args) >= 2) args[[2]] else "data/sample_data.rda" fails <- 0L check <- function(cond, msg) { @@ -103,13 +102,6 @@ if ("build_info" %in% tbls) { sprintf("build_info n_variant_pipeline == COUNT(variant_pipeline)")) } -cat("\n== sample_metadata.rda matches sample_meta table ==\n") -if (file.exists(meta)) { - e <- new.env(); load(meta, envir = e); sm <- get("sample_metadata", envir = e) - check(nrow(sm) == n("sample_meta"), - sprintf("sample_metadata rows (%d) == sample_meta table (%d)", nrow(sm), n("sample_meta"))) -} else cat(" [skip] no", meta, "\n") - cat("\n== regression vs sample_data.rda (optional) ==\n") if (file.exists(rda)) { e <- new.env(); load(rda, envir = e); sd <- get("sample_data", envir = e) diff --git a/data/gambl_metadata.rda b/data/gambl_metadata.rda deleted file mode 100644 index d830dacabc48ce156196cb6ec6d7e5e3e08438f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53069 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 diff --git a/data/sample_metadata.rda b/data/sample_metadata.rda deleted file mode 100644 index b6a22a85a59f13179d52427f4b4e7b69090c35cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48420 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 K;Lu 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 diff --git a/man/gambl_metadata.Rd b/man/gambl_metadata.Rd deleted file mode 100644 index 03d44c1..0000000 --- a/man/gambl_metadata.Rd +++ /dev/null @@ -1,48 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/data.R -\docType{data} -\name{gambl_metadata} -\alias{gambl_metadata} -\title{GAMBL Metadata} -\format{ -\subsection{\code{gambl_metadata}}{ - -A data frame with 4785 rows and 27 columns -\describe{ -\item{sample_id}{Sample identifier.} -\item{patient_id}{Patient identifier.} -\item{pathology}{Pathology.} -\item{seq_type}{Sample sequencing type.} -\item{genome_build}{Genome build the sample coordinates are in reference to.} -\item{pairing_status}{Matched or unmatched.} -\item{Tumor_Sample_Barcode}{Sample ID in another column, needed for certain functions.} -\item{age_group}{Sample age group.} -\item{compression}{The compression available for a particular sample.} -\item{bam_available}{Boolean.} -\item{pathology_rank}{Pathology rank.} -\item{cohort}{Sample cohort} -\item{COO_consensus}{COO consensus.} -\item{DHITsig_consensus}{DHIT signature consensus.} -\item{EBV_status_inf}{EBV status.} -\item{ffpe_or_frozen}{FFPE or frozen.} -\item{fl_grade}{FL grade.} -\item{hiv_status}{Sample HIV status.} -\item{lymphgen}{Lymphgen.} -\item{lymphgen_cnv_noA53}{Lymphgen with CNV no A53.} -\item{lymphgen_no_cnv}{Lymphgen no CNV.} -\item{lymphgen_with_cnv}{Lymphgen with CNV} -\item{lymphgen_wright}{Lymphgen Wright.} -\item{molecular_BL}{Molecualr BL.} -\item{normal_sample_id}{Normal sample ID} -\item{sex}{Female or Male} -\item{time_point}{Smaple timepoint.} -} -} -} -\usage{ -gambl_metadata -} -\description{ -A data frame with metadata for a collection of GAMBL samples. -} -\keyword{internal} diff --git a/man/gencode_to_symbol.Rd b/man/gencode_to_symbol.Rd index 9556c80..05dfc0b 100644 --- a/man/gencode_to_symbol.Rd +++ b/man/gencode_to_symbol.Rd @@ -9,8 +9,8 @@ A data frame with 60038 rows and 3 columns \describe{ -\item{ensembl_gene_id}{ENSEMBL gene id in a format without ..} -\item{gene_id}{ENSEMBL gene id in a format containing ..} +\item{ensembl_gene_id}{ENSEMBL gene id in a format without .\if{html}{\out{}}.} +\item{gene_id}{ENSEMBL gene id in a format containing .\if{html}{\out{}}.} \item{hgnc_symbol}{Human-readable gene symbol.} } } diff --git a/man/sample_metadata.Rd b/man/sample_metadata.Rd deleted file mode 100644 index d61ca49..0000000 --- a/man/sample_metadata.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/sample_metadata.R -\docType{data} -\name{sample_metadata} -\alias{sample_metadata} -\title{Lightweight sample metadata for the bundled GAMBL samples.} -\format{ -A data frame with 3344 rows and 31 columns, keyed by \code{sample_id}. -} -\usage{ -sample_metadata -} -\description{ -The per-sample metadata table (formerly \code{sample_data$meta}), split into its -own object so that loading metadata does not pull the large MAF/seg/bedpe -frames of \code{sample_data} into memory. The heavy mutation frames now live in -the separate \code{gambl_mutations.db} SQLite database (see -\code{data-raw/build_mutations_db.R}). Rebuilt by \code{data-raw/make_sample_metadata.R}. -} -\keyword{datasets} From 47dee08f8fb2c94addea9587c110b2195399b77c Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 27 Jul 2026 16:13:47 -0700 Subject: [PATCH 50/58] Restrict public sample_meta to an explicit column whitelist The previous commit widened sample_meta's rejoin against live GAMBLR.results::get_gambl_metadata() to fix missing columns, but captured everything unfiltered -- including internal-only columns like biopsy_id that must never end up in a publicly-distributed database. This matters beyond GAMBLR.open's R-level filtering: anyone can download gambl_mutations.db and query sample_meta directly with SQL, so the restriction has to happen at the point this table is built, not just in one consumer function. Adds data-raw/public_sample_meta_columns.txt: a plain-text whitelist (one column per line, # comments) that a developer can extend for future builds without touching code. assemble_bundled_data.R applies it right after the live metadata join, via select(..., all_of(cols)) rather than any_of() -- a typo'd/renamed column name fails the build loudly instead of silently vanishing or silently including something unintended. sample_id/seq_type/study/reference_PMID stay hardcoded (required for the pipeline's own internal joins/filters), plus genetic_subgroup (paper- supplement-specific, not something the live-metadata join would restore if dropped before it). Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 38 +++++++++++---- data-raw/public_sample_meta_columns.txt | 61 +++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 data-raw/public_sample_meta_columns.txt diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 9750a5f..eeb2f8d 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -774,17 +774,15 @@ fix <- fix %>% rename(study = cohort) diag_summary(fix, "fix (post rename cohort->study)", study_col = "study") -# Keep only what's genuinely paper-supplement-specific -- everything else -# (COO_consensus, lymphgen, EBV_status_inf, pathology, sex, -# Tumor_Sample_Barcode, patient_id, and the ~40 other columns live -# get_gambl_metadata() provides) comes fresh from the join below instead of -# whatever narrow/partial copy a given cohort block happened to carry. This -# is what makes sample_meta (and, downstream, GAMBLR.open::get_gambl_metadata(), -# which now reads sample_meta directly) actually complete -- mirrors the -# same select-then-rejoin pattern already used a few lines above for -# COO_consensus/lymphgen/EBV_status_inf. +# Keep only sample_id/seq_type/study/reference_PMID/genetic_subgroup from +# the per-block frames -- everything else comes fresh from the join below +# instead of whatever narrow/partial copy a given cohort block happened to +# carry. genetic_subgroup is kept here specifically because it's paper- +# supplement-specific (from the Thomas/Dreval xlsx sheets, not central +# tracking) and would NOT be reintroduced by that join -- live +# get_gambl_metadata() has no such column. fix <- fix %>% - select(sample_id, seq_type, study, genetic_subgroup, reference_PMID) + select(sample_id, seq_type, study, reference_PMID, genetic_subgroup) pre_join_ids <- fix$sample_id fix <- left_join( @@ -796,6 +794,26 @@ fix <- left_join( message(sprintf("[DIAG] fix (post live get_gambl_metadata() join): %d rows (was %d rows pre-join -- any increase means the join fanned out)", nrow(fix), length(pre_join_ids))) diag_summary(fix, "fix (post live get_gambl_metadata() join)", study_col = "study") +# sample_meta ends up in the PUBLICLY-DISTRIBUTED gambl_mutations.db (a +# GitHub Release asset anyone can download and query directly with SQL) -- +# restrict it to an explicit whitelist rather than everything live +# get_gambl_metadata() happens to return, which includes internal-only +# columns (biopsy_id, data_path, fastq_data_path/fastq_link_name, +# link_name, library_id, res_id, unix_group, ...) that must never end up +# in a public artifact. The whitelist lives in a plain text file, not +# here, so a developer can add a column to future builds by editing that +# file -- no code change needed. all_of() (not any_of()) deliberately +# errors on a typo'd/renamed column name rather than silently dropping it. +public_metadata_cols <- readLines("data-raw/public_sample_meta_columns.txt") +public_metadata_cols <- trimws(public_metadata_cols) +public_metadata_cols <- public_metadata_cols[ + nzchar(public_metadata_cols) & !startsWith(public_metadata_cols, "#") +] +fix <- fix %>% + select(sample_id, seq_type, study, reference_PMID, all_of(public_metadata_cols)) + +diag_summary(fix, "fix (post public-column whitelist)", study_col = "study") + fix <- fix %>% filter(!is.na(study)) diag_missing(pre_join_ids, fix$sample_id, "fix: post-join !is.na(study) filter (sample_ids dropped here)") diff --git a/data-raw/public_sample_meta_columns.txt b/data-raw/public_sample_meta_columns.txt new file mode 100644 index 0000000..3501eb6 --- /dev/null +++ b/data-raw/public_sample_meta_columns.txt @@ -0,0 +1,61 @@ +# Columns from live GAMBLR.results::get_gambl_metadata() to carry into the +# public sample_meta table -- and therefore into the publicly-distributed +# gambl_mutations.db (a GitHub Release asset anyone can download and query +# directly with SQL), and into GAMBLR.open::get_gambl_metadata()'s output. +# +# One column name per line. Lines starting with # are comments; blank +# lines are ignored. Add a line to include a new column in future builds -- +# no code changes needed, just edit this file and re-run +# assemble_bundled_data.R. +# +# sample_id, seq_type, study, and reference_PMID are always included +# separately (required for the pipeline's own internal joins/filters) and +# don't need to be listed here. +# +# IMPORTANT: this is a whitelist for a PUBLIC, downloadable database file, +# not just a filter on one R function -- anyone can query gambl_mutations.db +# directly, bypassing GAMBLR.open entirely. Only add a column here if it's +# genuinely safe to distribute publicly (e.g. NOT biopsy_id, data_path, +# fastq_data_path, fastq_link_name, link_name, library_id, res_id, +# unix_group, or anything else that's an internal tracking/file-path +# identifier rather than sample-level scientific metadata). When in doubt, +# ask before adding. +# +# Confirmed list, 2026-07-27. age_group and hiv_status were deliberately +# left out: neither is actually populated by the current live +# get_gambl_metadata() (age_group is only computed by the old, unused +# og_get_gambl_metadata(), gated behind an optional with_outcomes +# parameter; hiv_status was never implemented anywhere -- its own roxygen +# entry says {TODO}). Add them here once/if that derivation exists in the +# current function. +patient_id +pathology +biopsy_res +genome_build +pairing_status +Tumor_Sample_Barcode +cohort +COO_consensus +DHITsig_consensus +EBV_status_inf +ffpe_or_frozen +fl_grade +bcl2_ba +bcl2_cn +bcl6_ba +bcl6_cn +myc_ba +myc_cn +lymphgen +lymphgen_cnv_noA53 +lymphgen_no_cnv +lymphgen_with_cnv +lymphgen_wright +normal_sample_id +sex +time_point +TotalDuplicatedreads +TotalReads +TotalUniquelyMapped +TotalUnmappedreads +transformation From 4e353617496c141bb741036794cbbaf010a43dd6 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 27 Jul 2026 16:45:34 -0700 Subject: [PATCH 51/58] Add temporary diagnostic checkpoints to trace sample_meta column count Debugging: a full rebuild is producing an unfiltered sample_meta (90 columns) despite the whitelist restriction in the previous commit -- verified correct in isolation (both the file read and the select() logic, tested live against real data on the GSC), and the source on disk matches what's committed byte-for-byte. Something between the Phase 2 fix and the actual dbWriteTable() call is not behaving as the code reads. Adds three [DIAG-COLS] checkpoints printing ncol()/colnames(): right after sample_data$meta is assigned in Phase 2, right before write_mutations_db() is called, and right before the sample_meta dbWriteTable() call inside it. Whichever pair first disagrees pinpoints exactly where the columns come back. Temporary -- remove once the root cause is found. Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 4 ++++ data-raw/write_mutations_db.R | 2 ++ 2 files changed, 6 insertions(+) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index eeb2f8d..3c53381 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -824,6 +824,8 @@ fix <- distinct(fix) diag_summary(fix, "fix (post distinct -- this becomes sample_data$meta)", study_col = "study") sample_data$meta <- fix +message(sprintf("[DIAG-COLS] sample_data$meta right after Phase 2 (should be ~35 cols): %d cols: %s", + ncol(sample_data$meta), paste(colnames(sample_data$meta), collapse = ", "))) ### end metadata fixing # sample_study: a many-to-many bridge table (one row per (sample_id, study) @@ -1261,6 +1263,8 @@ print("done extracting all mutations in lymphoma genes with GAMBLR.results") # `sample_data` above, so there is no sample_data.rda round-trip. print("Starting sqlite build") +message(sprintf("[DIAG-COLS] sample_data$meta right before write_mutations_db() call (should still be ~35 cols): %d cols: %s", + ncol(sample_data$meta), paste(colnames(sample_data$meta), collapse = ", "))) source("data-raw/write_mutations_db.R") write_mutations_db( sample_data, diff --git a/data-raw/write_mutations_db.R b/data-raw/write_mutations_db.R index f4055d3..0dce3dc 100644 --- a/data-raw/write_mutations_db.R +++ b/data-raw/write_mutations_db.R @@ -28,6 +28,8 @@ write_mutations_db <- function(sample_data, # its own PMID); sample_study is the source of truth for that now. sample_meta_out <- as.data.frame(sample_data$meta) sample_meta_out$reference_PMID <- NULL + message(sprintf("[DIAG-COLS] sample_meta_out right before dbWriteTable() (should still be ~34 cols): %d cols: %s", + ncol(sample_meta_out), paste(colnames(sample_meta_out), collapse = ", "))) DBI::dbWriteTable(con, "sample_meta", sample_meta_out, overwrite = TRUE) # sample_study: many-to-many bridge table (sample_id, study) replacing the From 1885052dd899ae6090a365bd69848e04e32d4ef1 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Mon, 27 Jul 2026 17:02:11 -0700 Subject: [PATCH 52/58] Remove temporary [DIAG-COLS] diagnostic checkpoints They did their job: confirmed sample_meta genuinely has the correct, whitelist-restricted 34-column set at every stage through to the actual dbWriteTable() call. The "90 columns" symptom that prompted them was a stale/wrong file at the destination path during copy-to-laptop, not a bug in this script -- verified end to end with a fresh filename and a raw SQLite read bypassing GAMBLR.open entirely. Co-Authored-By: Claude Sonnet 5 --- data-raw/assemble_bundled_data.R | 4 ---- data-raw/write_mutations_db.R | 2 -- 2 files changed, 6 deletions(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 3c53381..eeb2f8d 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -824,8 +824,6 @@ fix <- distinct(fix) diag_summary(fix, "fix (post distinct -- this becomes sample_data$meta)", study_col = "study") sample_data$meta <- fix -message(sprintf("[DIAG-COLS] sample_data$meta right after Phase 2 (should be ~35 cols): %d cols: %s", - ncol(sample_data$meta), paste(colnames(sample_data$meta), collapse = ", "))) ### end metadata fixing # sample_study: a many-to-many bridge table (one row per (sample_id, study) @@ -1263,8 +1261,6 @@ print("done extracting all mutations in lymphoma genes with GAMBLR.results") # `sample_data` above, so there is no sample_data.rda round-trip. print("Starting sqlite build") -message(sprintf("[DIAG-COLS] sample_data$meta right before write_mutations_db() call (should still be ~35 cols): %d cols: %s", - ncol(sample_data$meta), paste(colnames(sample_data$meta), collapse = ", "))) source("data-raw/write_mutations_db.R") write_mutations_db( sample_data, diff --git a/data-raw/write_mutations_db.R b/data-raw/write_mutations_db.R index 0dce3dc..f4055d3 100644 --- a/data-raw/write_mutations_db.R +++ b/data-raw/write_mutations_db.R @@ -28,8 +28,6 @@ write_mutations_db <- function(sample_data, # its own PMID); sample_study is the source of truth for that now. sample_meta_out <- as.data.frame(sample_data$meta) sample_meta_out$reference_PMID <- NULL - message(sprintf("[DIAG-COLS] sample_meta_out right before dbWriteTable() (should still be ~34 cols): %d cols: %s", - ncol(sample_meta_out), paste(colnames(sample_meta_out), collapse = ", "))) DBI::dbWriteTable(con, "sample_meta", sample_meta_out, overwrite = TRUE) # sample_study: many-to-many bridge table (sample_id, study) replacing the From bef89c02899f0c5e4fd64dbada23fc018a5f4606 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 28 Jul 2026 13:44:03 -0700 Subject: [PATCH 53/58] Carry maf_seq_type through the build pipeline into gambl_mutations.db get_ssm_by_regions() now stamps maf_seq_type (GAMBLR.results), but the build script was dropping it via select(all_of(all_cols)), and write_mutations_db()'s dedup key and get_ssm_from_db() had no way to use it -- so a sample_id with both genome and capture data was still indistinguishable once it reached the shipped DB, even though the underlying data now carries the distinction. - assemble_bundled_data.R: keep maf_seq_type through all_cols; stamp it explicitly for the cell-line pulls (get_ssm_by_samples() doesn't set it, but cell_lines_data$meta is confirmed genome-only). Publication-pipeline rows are left NA -- no per-row seq_type provenance exists for them, and guessing would be worse than NA. - write_mutations_db.R: add maf_seq_type to the maf/ashm dedup key (so a genome/capture collision at the same position+allele survives as two rows instead of one) and index it. - get_ssm_from_db(): new seq_type filter param, no-ops safely against a gambl_mutations.db built before this column existed. Verified against synthetic sample_data (dual-seq_type collision, NA publication row, pre-existing-schema DB) -- see conversation for the check script. Not yet verified against a real GAMBLR.results rebuild. --- R/data-vars.R | 2 +- R/gambl_mutations_db.R | 4 ++-- R/get_ssm_from_db.R | 12 ++++++++++++ data-raw/assemble_bundled_data.R | 20 +++++++++++++++++--- data-raw/write_mutations_db.R | 13 +++++++++++-- man/gambl_mutations_db.Rd | 4 ++-- man/get_ssm_from_db.Rd | 10 ++++++++++ 7 files changed, 55 insertions(+), 10 deletions(-) diff --git a/R/data-vars.R b/R/data-vars.R index cbe9cbd..f2c79e0 100644 --- a/R/data-vars.R +++ b/R/data-vars.R @@ -22,7 +22,7 @@ if (getRversion() >= "2.15.1") { "ensembl_gene_id", "gene", "genome_build", "grch37_ashm_regions", "group", "head", "hg38_ashm_regions", "hot_spot", "hotspot_regions_grch37", "hotspot_regions_hg38", - "is_alias", "log.ratio", "mutated", "mutation_count", "n_mut", + "is_alias", "log.ratio", "maf_seq_type", "mutated", "mutation_count", "n_mut", "name", "pair_status", "pathology", "patient_id", "region", "row_id", "sample_id", "seq_type", "start", "t_alt_count", "tumour_sample_id", "window_end", "window_start", diff --git a/R/gambl_mutations_db.R b/R/gambl_mutations_db.R index ad58ac8..dc57e97 100644 --- a/R/gambl_mutations_db.R +++ b/R/gambl_mutations_db.R @@ -32,7 +32,7 @@ #' #' | Table | Rows (typical) | Grain | Key columns | #' | --- | --- | --- | --- | -#' | `maf` | ~3.6M | one somatic mutation call | `mutation_id` (surrogate integer key, unique within `maf` across both genome builds -- assigned at write time, after deduplication; `variant_pipeline` references it as a foreign key), `Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. `"slms-3"`/`"publication"` -- normalized at write time so it can be matched with a plain, indexed equality), genome_build` (+ ~35 more MAF-standard columns) | +#' | `maf` | ~3.6M | one somatic mutation call | `mutation_id` (surrogate integer key, unique within `maf` across both genome builds -- assigned at write time, after deduplication; `variant_pipeline` references it as a foreign key), `Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. `"slms-3"`/`"publication"` -- normalized at write time so it can be matched with a plain, indexed equality), genome_build, maf_seq_type` (`"genome"`/`"capture"`, so a Tumor_Sample_Barcode with both isn't indistinguishable once merged; `NA` for publication-pipeline rows, which don't carry per-row seq_type provenance -- see [get_ssm_from_db()]'s `seq_type` parameter) (+ ~35 more MAF-standard columns) | #' | `ashm` | ~135k | one mutation call in an aSHM region | same schema as `maf` (its own independent `mutation_id` sequence) | #' | `seg` | ~126k | one copy-number segment | `ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build` | #' | `bedpe` | ~900 | one Manta structural-variant breakpoint pair | `CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build` | @@ -72,7 +72,7 @@ #' #' ## Indexes #' `maf`/`ashm`: `(genome_build, Chromosome, Start_Position)`, -#' `Tumor_Sample_Barcode`, `Pipeline`, `mutation_id`. No index on +#' `Tumor_Sample_Barcode`, `Pipeline`, `maf_seq_type`, `mutation_id`. No index on #' `Hugo_Symbol`: gene-restricted queries resolve the gene to a region first #' (the same logic used to populate these tables) and filter on #' `(genome_build, Chromosome, Start_Position)` instead -- see diff --git a/R/get_ssm_from_db.R b/R/get_ssm_from_db.R index 159a6bf..a851eb2 100644 --- a/R/get_ssm_from_db.R +++ b/R/get_ssm_from_db.R @@ -30,6 +30,14 @@ #' `this_study="Reddy"` alone returns Reddy's SLMS-3 recall by default, #' while `this_study="Reddy", tool_name="publication"` returns Reddy's #' as-published rows only). +#' @param seq_type Optional character vector to restrict rows to one or more +#' `maf_seq_type` values (`"genome"`/`"capture"`), so a sample_id with both +#' can be queried separately instead of getting both back merged together. +#' Default `NULL` (no filter, matching prior behaviour). Rows predating +#' `maf_seq_type` (e.g. publication-pipeline rows, which don't carry +#' per-row seq_type provenance) have `NA` here and are excluded whenever +#' this filter is set, same as any other `NA` under a `%in%` filter. Silently +#' ignored against a `gambl_mutations.db` built before this column existed. #' @param regions Optional data frame with columns `chrom`, `start`, `end`; #' rows are OR-ed and applied as a single combined query (not one query per #' region), so passing hundreds of regions (e.g. one per gene in a panel) @@ -49,6 +57,7 @@ get_ssm_from_db <- function(projection = "grch37", min_read_support = 0, this_study = NULL, regions = NULL, + seq_type = NULL, con = NULL) { if (is.null(con)) con <- gambl_mutations_db() cc <- if (include_silent) coding_class else coding_class[coding_class != "Silent"] @@ -101,6 +110,9 @@ get_ssm_from_db <- function(projection = "grch37", if (coding_only) q <- dplyr::filter(q, Variant_Classification %in% cc) if (min_read_support > 0) q <- dplyr::filter(q, t_alt_count >= min_read_support) if (!is.null(sample_ids)) q <- dplyr::filter(q, Tumor_Sample_Barcode %in% sample_ids) + if (!is.null(seq_type) && "maf_seq_type" %in% DBI::dbListFields(con, table_name)) { + q <- dplyr::filter(q, maf_seq_type %in% seq_type) + } q } diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index eeb2f8d..51c063e 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -61,7 +61,12 @@ pmids <- list( maf_columns_to_keep <- c( "RefSeq", - "Protein_position" + "Protein_position", + # Carries genome/capture provenance through every select(all_of(all_cols))/ + # select(any_of(all_cols)) call below, so a sample_id with both seq_types + # doesn't become indistinguishable once its rows are merged (see + # GAMBLR.results::get_ssm_by_regions(), which now stamps this per-row). + "maf_seq_type" ) all_cols <- c( @@ -1132,7 +1137,14 @@ cell_lines_ssm_grch37 <- time_it("cell lines get_ssm_by_samples grch37", { get_ssm_by_samples( these_samples_metadata = cell_lines_data$meta, basic_columns = FALSE - ) %>% select(all_of(all_cols)) %>% mutate(Pipeline = "SLMS-3") + ) %>% + # get_ssm_by_samples() doesn't stamp maf_seq_type the way + # get_ssm_by_regions() does (see all_slms3_meta pulls above) -- + # cell_lines_data$meta is unconditionally get_gambl_metadata(seq_type_filter + # = "genome") (see its construction above), so "genome" is correct here, + # not a guess. + mutate(maf_seq_type = "genome") %>% + select(all_of(all_cols)) %>% mutate(Pipeline = "SLMS-3") }) cell_lines_ssm_hg38 <- time_it("cell lines get_ssm_by_samples hg38", { @@ -1140,7 +1152,9 @@ cell_lines_ssm_hg38 <- time_it("cell lines get_ssm_by_samples hg38", { these_samples_metadata = cell_lines_data$meta, projection = "hg38", basic_columns = FALSE - ) %>% select(all_of(all_cols)) %>% mutate(Pipeline = "SLMS-3") + ) %>% + mutate(maf_seq_type = "genome") %>% + select(all_of(all_cols)) %>% mutate(Pipeline = "SLMS-3") }) message(sprintf("[DIAG] cell_lines_ssm_grch37: %d rows, %d distinct Tumor_Sample_Barcode (expect 5)", nrow(cell_lines_ssm_grch37), n_distinct(cell_lines_ssm_grch37$Tumor_Sample_Barcode))) diff --git a/data-raw/write_mutations_db.R b/data-raw/write_mutations_db.R index f4055d3..e5c51cf 100644 --- a/data-raw/write_mutations_db.R +++ b/data-raw/write_mutations_db.R @@ -46,9 +46,16 @@ write_mutations_db <- function(sample_data, # safety net: no cohort-specific pull block should ever again be able to # introduce duplicate mutation rows for a sample that's pulled more than # once, regardless of the reason. + # maf_seq_type is part of the key so a genome-called and capture-called + # mutation for the same sample that happen to land on the same + # position/allele stay two distinct rows instead of collapsing into one -- + # dplyr::distinct() treats NA (publication-pipeline rows, which don't carry + # per-row seq_type) as matching other NAs, so this doesn't change dedup + # behaviour for rows that predate maf_seq_type, only adds separation where + # it's actually known. dedup_keys <- list( - maf = c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2"), - ashm = c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2") + maf = c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2", "maf_seq_type"), + ashm = c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2", "maf_seq_type") ) # variant_pipeline: many-to-many bridge table (mutation_id, Pipeline), same @@ -144,9 +151,11 @@ write_mutations_db <- function(sample_data, "CREATE INDEX idx_maf_pos ON maf(genome_build, Chromosome, Start_Position)", "CREATE INDEX idx_maf_sample ON maf(Tumor_Sample_Barcode)", "CREATE INDEX idx_maf_pipe ON maf(Pipeline)", + "CREATE INDEX idx_maf_seqtype ON maf(maf_seq_type)", "CREATE INDEX idx_maf_mutation_id ON maf(mutation_id)", "CREATE INDEX idx_ashm_pos ON ashm(genome_build, Chromosome, Start_Position)", "CREATE INDEX idx_ashm_sample ON ashm(Tumor_Sample_Barcode)", + "CREATE INDEX idx_ashm_seqtype ON ashm(maf_seq_type)", "CREATE INDEX idx_ashm_mutation_id ON ashm(mutation_id)", "CREATE INDEX idx_seg_sample ON seg(genome_build, ID)", "CREATE INDEX idx_seg_pos ON seg(genome_build, chrom, start)", diff --git a/man/gambl_mutations_db.Rd b/man/gambl_mutations_db.Rd index 2488395..f60063d 100644 --- a/man/gambl_mutations_db.Rd +++ b/man/gambl_mutations_db.Rd @@ -48,7 +48,7 @@ separate tables); filter on \code{genome_build} (\code{"grch37"} / \code{"hg38"} one. \code{maf} and \code{ashm} share the same (curated, 49-column) schema: a reduced column set, not the full upstream flatfile MAF.\tabular{llll}{ Table \tab Rows (typical) \tab Grain \tab Key columns \cr - \code{maf} \tab ~3.6M \tab one somatic mutation call \tab \code{mutation_id} (surrogate integer key, unique within \code{maf} across both genome builds -- assigned at write time, after deduplication; \code{variant_pipeline} references it as a foreign key), \verb{Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. }"slms-3"\code{/}"publication"\verb{ -- normalized at write time so it can be matched with a plain, indexed equality), genome_build} (+ ~35 more MAF-standard columns) \cr + \code{maf} \tab ~3.6M \tab one somatic mutation call \tab \code{mutation_id} (surrogate integer key, unique within \code{maf} across both genome builds -- assigned at write time, after deduplication; \code{variant_pipeline} references it as a foreign key), \verb{Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. }"slms-3"\code{/}"publication"\verb{ -- normalized at write time so it can be matched with a plain, indexed equality), genome_build, maf_seq_type} (\code{"genome"}/\code{"capture"}, so a Tumor_Sample_Barcode with both isn't indistinguishable once merged; \code{NA} for publication-pipeline rows, which don't carry per-row seq_type provenance -- see \code{\link[=get_ssm_from_db]{get_ssm_from_db()}}'s \code{seq_type} parameter) (+ ~35 more MAF-standard columns) \cr \code{ashm} \tab ~135k \tab one mutation call in an aSHM region \tab same schema as \code{maf} (its own independent \code{mutation_id} sequence) \cr \code{seg} \tab ~126k \tab one copy-number segment \tab \verb{ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build} \cr \code{bedpe} \tab ~900 \tab one Manta structural-variant breakpoint pair \tab \verb{CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build} \cr @@ -92,7 +92,7 @@ to \code{sample_meta$sample_id}; \code{sample_study$sample_id} keys to \subsection{Indexes}{ \code{maf}/\code{ashm}: \verb{(genome_build, Chromosome, Start_Position)}, -\code{Tumor_Sample_Barcode}, \code{Pipeline}, \code{mutation_id}. No index on +\code{Tumor_Sample_Barcode}, \code{Pipeline}, \code{maf_seq_type}, \code{mutation_id}. No index on \code{Hugo_Symbol}: gene-restricted queries resolve the gene to a region first (the same logic used to populate these tables) and filter on \verb{(genome_build, Chromosome, Start_Position)} instead -- see diff --git a/man/get_ssm_from_db.Rd b/man/get_ssm_from_db.Rd index e460c57..aac9fc9 100644 --- a/man/get_ssm_from_db.Rd +++ b/man/get_ssm_from_db.Rd @@ -14,6 +14,7 @@ get_ssm_from_db( min_read_support = 0, this_study = NULL, regions = NULL, + seq_type = NULL, con = NULL ) } @@ -56,6 +57,15 @@ rows are OR-ed and applied as a single combined query (not one query per region), so passing hundreds of regions (e.g. one per gene in a panel) doesn't cost hundreds of separate round trips.} +\item{seq_type}{Optional character vector to restrict rows to one or more +\code{maf_seq_type} values (\code{"genome"}/\code{"capture"}), so a sample_id with both +can be queried separately instead of getting both back merged together. +Default \code{NULL} (no filter, matching prior behaviour). Rows predating +\code{maf_seq_type} (e.g. publication-pipeline rows, which don't carry +per-row seq_type provenance) have \code{NA} here and are excluded whenever +this filter is set, same as any other \code{NA} under a \code{\%in\%} filter. Silently +ignored against a \code{gambl_mutations.db} built before this column existed.} + \item{con}{Optional DBI connection (defaults to \code{\link[=gambl_mutations_db]{gambl_mutations_db()}}).} } \value{ From 7736205daf3412cec2de675422f6374a56a5370f Mon Sep 17 00:00:00 2001 From: rdmorin Date: Tue, 28 Jul 2026 14:26:56 -0700 Subject: [PATCH 54/58] Fix: selected_columns shouldn't inherit maf_seq_type from maf_columns_to_keep selected_columns (used only for the proteinpainter-compatibility enrichment join sources around line 971, not the final maf table) derived its extra columns from maf_columns_to_keep, so it silently picked up maf_seq_type too once that was added there. Two of its three consumers never carry that column (a raw external flat-file read, and a get_ssm_by_samples() pull -- that function doesn't stamp maf_seq_type), so select(all_of(selected_columns)) hard-errored on the very first one. Give selected_columns its own explicit column list instead of inheriting from a list that now serves a different purpose. --- data-raw/assemble_bundled_data.R | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index 51c063e..f719d06 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -971,7 +971,14 @@ sample_data$meta$study %>% table selected_columns <- c( "Tumor_Sample_Barcode", "Hugo_Symbol", "NCBI_Build", "Chromosome", "Start_Position", "End_Position", - "Tumor_Seq_Allele2", maf_columns_to_keep + "Tumor_Seq_Allele2", "RefSeq", "Protein_position" + # Deliberately NOT maf_columns_to_keep (which now also carries + # maf_seq_type) -- these are join-source columns for the + # proteinpainter-compatibility enrichment below, not the final maf + # table. Two of the three sources selected against this (the raw + # BL_Thomas manuscript flat file, and the get_ssm_by_samples() FL + # pull below) never carry maf_seq_type at all, so all_of() on the + # full list would hard-error here every time. ) these_samples <- sample_data$meta %>% From ee39c9a78a9536f0fee970aa9cbba88715c6249f Mon Sep 17 00:00:00 2001 From: rdmorin Date: Wed, 29 Jul 2026 08:56:16 -0700 Subject: [PATCH 55/58] get_ssm_from_db(): dedup maf+ashm when include_ashm=TRUE maf and ashm dedup independently at write time, so nothing prevented the same real mutation (same sample/position/allele) landing in both tables when a gene is both aSHM-target and coding-panel (BCL2, BCL6, MYC, PIM1, etc.). Three GAMBLR.open accessors (get_ssm_by_samples(), get_ssm_by_regions(), get_ssm_by_region()) hardcode include_ashm=TRUE with no way for a caller to disable it, so this was reaching users on every call, not just as an opt-in edge case. Verified against the real (non-synthetic) rebuilt gambl_mutations.db: 59,457 of 85,884 ashm rows (69%) were exact duplicates of a maf row before this fix; after it, the combined maf+ashm result adds exactly the 26,427 genuinely aSHM-only mutations with zero remaining duplicate natural keys. --- R/get_ssm_from_db.R | 13 +++++++++++-- man/get_ssm_from_db.Rd | 7 ++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/R/get_ssm_from_db.R b/R/get_ssm_from_db.R index a851eb2..7bca0c8 100644 --- a/R/get_ssm_from_db.R +++ b/R/get_ssm_from_db.R @@ -18,7 +18,12 @@ #' query for either one, even though its single maf/ashm row can only #' carry one Pipeline value. Falls back to filtering the column directly #' against DBs built before variant_pipeline existed. -#' @param include_ashm When TRUE, also query the `ashm` table and row-bind it. +#' @param include_ashm When TRUE, also query the `ashm` table and row-bind it, +#' deduplicated against `maf` on the same natural key `write_mutations_db()` +#' uses at write time -- `maf` and `ashm` dedup independently there, so +#' nothing else prevents the same real mutation (same sample/position/ +#' allele) landing in both when it falls in a gene that's both aSHM-target +#' and coding-panel (e.g. BCL2, BCL6, MYC, PIM1). `maf`'s copy wins ties. #' @param coding_only When TRUE, keep only coding `Variant_Classification`s. #' @param include_silent When FALSE (and `coding_only`), drop Silent mutations. #' @param min_read_support Keep only variants with `t_alt_count` >= this value. @@ -143,7 +148,11 @@ get_ssm_from_db <- function(projection = "grch37", } res <- gather("maf") - if (include_ashm) res <- dplyr::bind_rows(res, gather("ashm")) + if (include_ashm) { + res <- dplyr::bind_rows(res, gather("ashm")) %>% + dplyr::distinct(Tumor_Sample_Barcode, Chromosome, Start_Position, + End_Position, Tumor_Seq_Allele2, .keep_all = TRUE) + } res$genome_build <- NULL res$mutation_id <- NULL res diff --git a/man/get_ssm_from_db.Rd b/man/get_ssm_from_db.Rd index aac9fc9..4350f80 100644 --- a/man/get_ssm_from_db.Rd +++ b/man/get_ssm_from_db.Rd @@ -35,7 +35,12 @@ query for either one, even though its single maf/ashm row can only carry one Pipeline value. Falls back to filtering the column directly against DBs built before variant_pipeline existed.} -\item{include_ashm}{When TRUE, also query the \code{ashm} table and row-bind it.} +\item{include_ashm}{When TRUE, also query the \code{ashm} table and row-bind it, +deduplicated against \code{maf} on the same natural key \code{write_mutations_db()} +uses at write time -- \code{maf} and \code{ashm} dedup independently there, so +nothing else prevents the same real mutation (same sample/position/ +allele) landing in both when it falls in a gene that's both aSHM-target +and coding-panel (e.g. BCL2, BCL6, MYC, PIM1). \code{maf}'s copy wins ties.} \item{coding_only}{When TRUE, keep only coding \code{Variant_Classification}s.} From be621ebb192fd13386612a2369cdcf2cedbf28f4 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Wed, 29 Jul 2026 10:48:01 -0700 Subject: [PATCH 56/58] Merge maf/ashm into one table; remove get_ssm_from_db()'s include_ashm maf and ashm had identical schemas, independent mutation_id sequences, and independent write-time dedup passes, despite nothing anywhere querying ashm in isolation -- the only reference was get_ssm_from_db()'s include_ashm toggle, which always bind_rows()'d it onto maf. Confirmed against the real rebuilt DB: 59,457 of 85,884 ashm rows (69%) exactly duplicated a maf row (genes that are both aSHM targets and coding-panel genes: BCL2, BCL6, MYC, PIM1...). A same- session query-time dplyr::distinct() patch fixed the symptom; this removes the cause. Considered and rejected keeping a filterable ashm-provenance flag: the three GAMBLR.open accessors that ever set include_ashm=TRUE do so unconditionally (their own regions/sample filters do the real restricting), and the only place the FALSE default was ever exercised is get_coding_ssm()/get_all_coding_ssm(), which hardcode coding_only=TRUE -- checked the Variant_Classification breakdown of the 26,427 aSHM-only rows: only 991 are coding-classified, the rest already excluded by coding_only regardless of any aSHM flag. So plain unconditional pooling isn't just simpler, it's more correct -- get_coding_ssm() gains those 991 previously-invisible real coding mutations for free. - assemble_bundled_data.R: aSHM pull rows fold into the same bind_rows() that builds sample_data$grch37/hg38$maf; no more separate $ashm slot. - write_mutations_db.R: one dedup pass, one elem ("maf") instead of two. variant_pipeline drops its now-always-constant elem column. - get_ssm_from_db(): include_ashm parameter removed entirely, along with the gather("ashm")/bind_rows()/cross-table distinct() added earlier this session (structurally unnecessary now). Backward-compat guard added for variant_pipeline's elem column in case of an old-schema DB. - gambl_mutations_db(): schema docs updated to match. Bumped Version to 1.5 (schema-breaking, feeds the data-v release-tag convention). Verified with a synthetic write_mutations_db() run (overlapping coding-panel + aSHM row collapses to one; genuinely aSHM-only row survives; no ashm table, no elem column, no ashm indexes, get_ssm_from_db() has no include_ashm formal). Not yet verified against a real GAMBLR.results rebuild. --- DESCRIPTION | 2 +- R/gambl_mutations_db.R | 36 +++++++++++----------- R/get_ssm_from_db.R | 40 +++++++++++-------------- data-raw/assemble_bundled_data.R | 22 +++++++++----- data-raw/write_mutations_db.R | 51 +++++++++++++++----------------- man/gambl_mutations_db.Rd | 36 +++++++++++----------- man/get_ssm_from_db.Rd | 14 ++------- 7 files changed, 97 insertions(+), 104 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 0f95aa8..d583655 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: GAMBLR.data Title: Collection of Curated Data for Genomic Analysis of Mature B-cell Lymphomas in R -Version: 1.4 +Version: 1.5 Authors@R: c( person(given = "Ryan", diff --git a/R/gambl_mutations_db.R b/R/gambl_mutations_db.R index dc57e97..2722808 100644 --- a/R/gambl_mutations_db.R +++ b/R/gambl_mutations_db.R @@ -1,7 +1,7 @@ #' @title Connect to the GAMBL mutations database. #' #' @description Returns a (cached) read-only DBI connection to `gambl_mutations.db`, -#' the SQLite database holding the large sample-level frames (MAF, aSHM, seg, +#' the SQLite database holding the large sample-level frames (MAF, seg, #' bedpe) built from the in-memory `sample_data` object assembled by #' `data-raw/assemble_bundled_data.R`, via the shared `write_mutations_db()` #' helper (`data-raw/write_mutations_db.R`). @@ -27,21 +27,23 @@ #' #' Both genome builds are stacked into the same table (not split across #' separate tables); filter on `genome_build` (`"grch37"` / `"hg38"`) to select -#' one. `maf` and `ashm` share the same (curated, 49-column) schema: a reduced -#' column set, not the full upstream flatfile MAF. +#' one. `maf` holds both coding-panel and aSHM-region mutations, pooled +#' unconditionally at write time (a mutation found by both pulls -- common +#' for genes that are both aSHM targets and coding-panel genes, e.g. BCL2, +#' BCL6, MYC, PIM1 -- collapses to one row, not two; see +#' [get_ssm_from_db()]). #' #' | Table | Rows (typical) | Grain | Key columns | #' | --- | --- | --- | --- | -#' | `maf` | ~3.6M | one somatic mutation call | `mutation_id` (surrogate integer key, unique within `maf` across both genome builds -- assigned at write time, after deduplication; `variant_pipeline` references it as a foreign key), `Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. `"slms-3"`/`"publication"` -- normalized at write time so it can be matched with a plain, indexed equality), genome_build, maf_seq_type` (`"genome"`/`"capture"`, so a Tumor_Sample_Barcode with both isn't indistinguishable once merged; `NA` for publication-pipeline rows, which don't carry per-row seq_type provenance -- see [get_ssm_from_db()]'s `seq_type` parameter) (+ ~35 more MAF-standard columns) | -#' | `ashm` | ~135k | one mutation call in an aSHM region | same schema as `maf` (its own independent `mutation_id` sequence) | +#' | `maf` | ~3.7M | one somatic mutation call | `mutation_id` (surrogate integer key, unique within `maf` across both genome builds -- assigned at write time, after deduplication; `variant_pipeline` references it as a foreign key), `Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. `"slms-3"`/`"publication"` -- normalized at write time so it can be matched with a plain, indexed equality), genome_build, maf_seq_type` (`"genome"`/`"capture"`, so a Tumor_Sample_Barcode with both isn't indistinguishable once merged; `NA` for publication-pipeline rows, which don't carry per-row seq_type provenance -- see [get_ssm_from_db()]'s `seq_type` parameter) (+ ~35 more MAF-standard columns) | #' | `seg` | ~126k | one copy-number segment | `ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build` | #' | `bedpe` | ~900 | one Manta structural-variant breakpoint pair | `CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build` | #' | `sample_meta` | ~3.3k | one sample | `patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...` (its own `genome_build` column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) | #' | `sample_study` | varies | one (sample, study) membership fact -- many-to-many, a sample belonging to N studies is N rows | `sample_id, study, study_id, reference_PMID`. `study_id` is that study's own identifier for the sample where it differs from GAMBL's `sample_id` (e.g. a paper's own case/patient ID); NA where no study-specific ID has been sourced. | -#' | `variant_pipeline` | varies | one (mutation, Pipeline) fact -- many-to-many, a variant independently called by N pipelines is N rows | `mutation_id, elem, Pipeline`. `mutation_id` is a foreign key to `maf.mutation_id` or `ashm.mutation_id` depending on `elem` (`"maf"` or `"ashm"`) -- not a natural key, so this table doesn't repeat the variant's own position/sample columns. | -#' | `build_info` | 10 | key/value | provenance (`source`, `built_at`, `builder`) and expected row counts (`n_maf`, `n_ashm`, `n_seg`, `n_bedpe`, `n_samples`, `n_sample_study`, `n_variant_pipeline`) used by the `data-raw/test_gambl_db.R` regression checks | +#' | `variant_pipeline` | varies | one (mutation, Pipeline) fact -- many-to-many, a variant independently called by N pipelines is N rows | `mutation_id, Pipeline`. `mutation_id` is a foreign key to `maf.mutation_id` -- not a natural key, so this table doesn't repeat the variant's own position/sample columns. | +#' | `build_info` | 9 | key/value | provenance (`source`, `built_at`, `builder`) and expected row counts (`n_maf`, `n_seg`, `n_bedpe`, `n_samples`, `n_sample_study`, `n_variant_pipeline`) used by the `data-raw/test_gambl_db.R` regression checks | #' -#' `maf`/`ashm` do NOT carry a `Study` column. Cohort/study membership is +#' `maf` does NOT carry a `Study` column. Cohort/study membership is #' tracked once per sample in `sample_study`, not once per mutation row -- #' a mutation row's `Study` couldn't represent a sample belonging to more #' than one study anyway, and a per-row tag caused the same sample's @@ -49,7 +51,7 @@ #' that claimed it (see `GAMBLR.data::get_ssm_from_db()`'s `this_study` #' parameter for how to filter by study post-refactor). #' -#' Similarly, `maf`/`ashm` only ever carry ONE `Pipeline` value per variant, +#' Similarly, `maf` only ever carries ONE `Pipeline` value per variant, #' even though the same real mutation can be independently produced by more #' than one pipeline (e.g. a cohort's own published/curated maf and our #' separate SLMS-3 recall both calling the same position) -- when that @@ -58,23 +60,22 @@ #' otherwise make the variant invisible to a query for the *other* pipeline #' even though it's still fully present in the table. `variant_pipeline` #' records every pipeline that actually produced each variant, independent -#' of which one "won" as `maf`/`ashm`'s single representative row; see +#' of which one "won" as `maf`'s single representative row; see #' `GAMBLR.data::get_ssm_from_db()`'s `tool_name` parameter, which resolves -#' against this table rather than `maf`/`ashm`'s own `Pipeline` column. +#' against this table rather than `maf`'s own `Pipeline` column. #' #' ## Join keys -#' `maf`, `ashm`, and `bedpe` (via `tumour_sample_id`) key to +#' `maf` and `bedpe` (via `tumour_sample_id`) key to #' `sample_meta$Tumor_Sample_Barcode` / `sample_meta$sample_id`; `seg$ID` keys #' to `sample_meta$sample_id`; `sample_study$sample_id` keys to #' `sample_meta$sample_id`; `variant_pipeline$mutation_id` keys to -#' `maf.mutation_id` or `ashm.mutation_id`, selecting which one via -#' `variant_pipeline$elem`. +#' `maf.mutation_id`. #' #' ## Indexes -#' `maf`/`ashm`: `(genome_build, Chromosome, Start_Position)`, +#' `maf`: `(genome_build, Chromosome, Start_Position)`, #' `Tumor_Sample_Barcode`, `Pipeline`, `maf_seq_type`, `mutation_id`. No index on #' `Hugo_Symbol`: gene-restricted queries resolve the gene to a region first -#' (the same logic used to populate these tables) and filter on +#' (the same logic used to populate this table) and filter on #' `(genome_build, Chromosome, Start_Position)` instead -- see #' `GAMBLR.data::get_ssm_from_db()`. `seg`: `(genome_build, ID)`, #' `(genome_build, chrom, start)`. `bedpe`: `(tumour_sample_id, @@ -82,8 +83,7 @@ #' CHROM_B, START_B)` (one per breakpoint end, so a region search that OR's #' both ends can use a different index per side). #' `sample_meta`: `sample_id`, `Tumor_Sample_Barcode`. `sample_study`: -#' `sample_id`, `study`. `variant_pipeline`: `mutation_id`, -#' `(elem, Pipeline)`. +#' `sample_id`, `study`. `variant_pipeline`: `mutation_id`, `Pipeline`. #' #' @param db_path Optional explicit path to the .db file. #' @param auto_download When no existing copy is found, download it diff --git a/R/get_ssm_from_db.R b/R/get_ssm_from_db.R index 7bca0c8..0749282 100644 --- a/R/get_ssm_from_db.R +++ b/R/get_ssm_from_db.R @@ -13,22 +13,16 @@ #' indexed equality check rather than a function-wrapped one). Default #' "slms-3"; set NULL to skip the Pipeline filter. Resolved via the #' `variant_pipeline` join table (see [gambl_mutations_db()]'s schema -#' docs) rather than `maf`/`ashm`'s own Pipeline column, so a variant +#' docs) rather than `maf`'s own Pipeline column, so a variant #' independently called by more than one pipeline is still found by a -#' query for either one, even though its single maf/ashm row can only +#' query for either one, even though its single maf row can only #' carry one Pipeline value. Falls back to filtering the column directly #' against DBs built before variant_pipeline existed. -#' @param include_ashm When TRUE, also query the `ashm` table and row-bind it, -#' deduplicated against `maf` on the same natural key `write_mutations_db()` -#' uses at write time -- `maf` and `ashm` dedup independently there, so -#' nothing else prevents the same real mutation (same sample/position/ -#' allele) landing in both when it falls in a gene that's both aSHM-target -#' and coding-panel (e.g. BCL2, BCL6, MYC, PIM1). `maf`'s copy wins ties. #' @param coding_only When TRUE, keep only coding `Variant_Classification`s. #' @param include_silent When FALSE (and `coding_only`), drop Silent mutations. #' @param min_read_support Keep only variants with `t_alt_count` >= this value. #' @param this_study Optional single study to restrict to, matched against -#' `sample_study$study` (not a `maf`/`ashm` column -- see +#' `sample_study$study` (not a `maf` column -- see #' [gambl_mutations_db()]'s schema docs). Resolves to the set of sample_ids #' belonging to that study, intersected with `sample_ids` if both are #' supplied; composes with `tool_name`/Pipeline exactly as before (e.g. @@ -56,7 +50,6 @@ get_ssm_from_db <- function(projection = "grch37", sample_ids = NULL, tool_name = "slms-3", - include_ashm = FALSE, coding_only = FALSE, include_silent = TRUE, min_read_support = 0, @@ -71,7 +64,7 @@ get_ssm_from_db <- function(projection = "grch37", # this_study resolves to sample_ids via a join against sample_study, done # once here rather than as a per-query filter inside base_query() -- reuses # the existing sample_ids/Tumor_Sample_Barcode machinery below instead of - # requiring maf/ashm to carry their own Study column (they don't; see + # requiring maf to carry its own Study column (it doesn't; see # gambl_mutations_db()'s schema docs), and keeps this_study (which # samples) and tool_name/Pipeline (which rows for those samples) composable # exactly as they were. @@ -86,16 +79,20 @@ get_ssm_from_db <- function(projection = "grch37", # A variant that's independently produced by more than one pipeline (e.g. a # cohort's own published maf and our SLMS-3 recall both calling the same - # position) only has room for one Pipeline value on its single maf/ashm + # position) only has room for one Pipeline value on its single maf # row -- write_mutations_db() picks one arbitrarily when it deduplicates. - # variant_pipeline (mutation_id, elem, Pipeline) records every pipeline + # variant_pipeline (mutation_id, Pipeline) records every pipeline # that actually produced each variant, keyed on the surrogate mutation_id # surviving rows are assigned at write time (not the 5-column natural key # -- see write_mutations_db.R for why), so tool_name is resolved against - # it via a single-column semi-join instead of the maf/ashm row's own + # it via a single-column semi-join instead of the maf row's own # (possibly-not-representative) Pipeline value. Falls back to the old # direct-column filter against DBs built before this table existed. has_variant_pipeline <- !is.null(tn) && "variant_pipeline" %in% DBI::dbListTables(con) + # elem distinguished maf's and ashm's independent mutation_id spaces before + # the two tables were merged into one -- only present on a DB built before + # that merge, kept here purely for backward compatibility with one. + has_vp_elem <- has_variant_pipeline && "elem" %in% DBI::dbListFields(con, "variant_pipeline") # apply the filters that are common to every query against this table, # regardless of how many regions (if any) are requested @@ -104,9 +101,13 @@ get_ssm_from_db <- function(projection = "grch37", dplyr::filter(genome_build == projection) if (!is.null(tn)) { if (has_variant_pipeline) { - matching_ids <- dplyr::tbl(con, "variant_pipeline") %>% - dplyr::filter(elem == table_name, Pipeline == tn) %>% - dplyr::distinct(mutation_id) + matching_ids <- dplyr::tbl(con, "variant_pipeline") + matching_ids <- if (has_vp_elem) { + dplyr::filter(matching_ids, elem == table_name, Pipeline == tn) + } else { + dplyr::filter(matching_ids, Pipeline == tn) + } + matching_ids <- dplyr::distinct(matching_ids, mutation_id) q <- dplyr::semi_join(q, matching_ids, by = "mutation_id") } else { q <- dplyr::filter(q, Pipeline == tn) @@ -148,11 +149,6 @@ get_ssm_from_db <- function(projection = "grch37", } res <- gather("maf") - if (include_ashm) { - res <- dplyr::bind_rows(res, gather("ashm")) %>% - dplyr::distinct(Tumor_Sample_Barcode, Chromosome, Start_Position, - End_Position, Tumor_Seq_Allele2, .keep_all = TRUE) - } res$genome_build <- NULL res$mutation_id <- NULL res diff --git a/data-raw/assemble_bundled_data.R b/data-raw/assemble_bundled_data.R index f719d06..a1fba17 100644 --- a/data-raw/assemble_bundled_data.R +++ b/data-raw/assemble_bundled_data.R @@ -1232,28 +1232,36 @@ hg38_ashm <- hg38_ashm %>% filter(Tumor_Sample_Barcode %in% sample_data$meta$Tumor_Sample_Barcode) %>% mutate(Pipeline = "SLMS-3") -sample_data$grch37$ashm <- grch37_ashm %>% distinct() -sample_data$hg38$ashm <- hg38_ashm %>% distinct() +grch37_ashm <- grch37_ashm %>% distinct() +hg38_ashm <- hg38_ashm %>% distinct() -diag_summary_maf(sample_data$grch37$ashm, "sample_data$grch37$ashm (final)") -diag_summary_maf(sample_data$hg38$ashm, "sample_data$hg38$ashm (final)") +diag_summary_maf(grch37_ashm, "grch37_ashm (final, pre-merge into maf)") +diag_summary_maf(hg38_ashm, "hg38_ashm (final, pre-merge into maf)") print("done extracting aSHM mutations from GAMBLR.results") # ============================================================================ # Phase 6: final maf assembly -- Publication rows + the consolidated SLMS-3 -# pull, per genome build. No `Study =` in any mutate() anywhere in this +# pull + the aSHM-region pull, per genome build. No separate ashm table/slot +# -- maf and ashm had identical schemas, independent mutation_id sequences, +# and independent write-time dedup, with no code anywhere querying ashm in +# isolation (confirmed: GAMBLR.open's only reference is get_ssm_from_db()'s +# now-removed include_ashm toggle, which always merged it onto maf anyway). +# Pooled here unconditionally instead -- one table, one dedup pass in +# write_mutations_db.R. No `Study =` in any mutate() anywhere in this # script; cohort membership lives entirely in sample_data$sample_study. # ============================================================================ sample_data$grch37$maf <- bind_rows( grch37_publication_rows, - slms3_grch37 + slms3_grch37, + grch37_ashm ) sample_data$hg38$maf <- bind_rows( hg38_publication_rows, - slms3_hg38 + slms3_hg38, + hg38_ashm ) # The other money diagnostic: per-study coding-classified counts, mirroring diff --git a/data-raw/write_mutations_db.R b/data-raw/write_mutations_db.R index e5c51cf..3020c9c 100644 --- a/data-raw/write_mutations_db.R +++ b/data-raw/write_mutations_db.R @@ -31,7 +31,7 @@ write_mutations_db <- function(sample_data, DBI::dbWriteTable(con, "sample_meta", sample_meta_out, overwrite = TRUE) # sample_study: many-to-many bridge table (sample_id, study) replacing the - # old inline Study column on maf/ashm rows -- guarded so older sample_data + # old inline Study column on maf rows -- guarded so older sample_data # objects that predate this table (e.g. via the legacy .rda adapter) don't # hard-crash a build that simply won't have cohort-membership data. n_sample_study <- 0L @@ -54,13 +54,12 @@ write_mutations_db <- function(sample_data, # behaviour for rows that predate maf_seq_type, only adds separation where # it's actually known. dedup_keys <- list( - maf = c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2", "maf_seq_type"), - ashm = c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2", "maf_seq_type") + maf = c("Tumor_Sample_Barcode", "Chromosome", "Start_Position", "End_Position", "Tumor_Seq_Allele2", "maf_seq_type") ) # variant_pipeline: many-to-many bridge table (mutation_id, Pipeline), same # pattern as sample_study (sample_id, study). A single Pipeline column on - # maf/ashm can only hold one value per variant, but the same real mutation + # maf can only hold one value per variant, but the same real mutation # can legitimately be produced by more than one pipeline (e.g. a cohort's # own published/curated maf and our independent SLMS-3 recall both calling # the same position) -- when that happens the dedup below collapses them @@ -68,17 +67,20 @@ write_mutations_db <- function(sample_data, # query for the other pipeline even though the variant itself is still # fully present in the table. Captured here, before dedup, so no # pipeline's claim to a variant is ever lost regardless of which row - # "wins" as maf/ashm's single representative copy. + # "wins" as maf's single representative copy. # - # mutation_id is a surrogate integer key assigned to each deduped maf/ashm - # row (unique within an elem, across both genome builds), and - # variant_pipeline references it as a plain foreign key instead of - # repeating the 5-column natural key on every row. Matching pre-dedup - # pipeline claims back to their surviving row still requires the natural - # key -- that cost doesn't disappear, it just moves here, to a one-time - # write-time join, instead of every read-time query in get_ssm_from_db() - # having to join on a 5-column composite (two of them text) instead of a - # single indexed integer column. + # mutation_id is a surrogate integer key assigned to each deduped maf + # row (unique across both genome builds), and variant_pipeline references + # it as a plain foreign key instead of repeating the 5-column natural key + # on every row. Matching pre-dedup pipeline claims back to their surviving + # row still requires the natural key -- that cost doesn't disappear, it + # just moves here, to a one-time write-time join, instead of every + # read-time query in get_ssm_from_db() having to join on a 5-column + # composite (two of them text) instead of a single indexed integer column. + # (No elem column here -- that only ever existed to disambiguate maf's and + # ashm's independent mutation_id spaces; maf and ashm were merged into one + # table with one mutation_id sequence, so there's nothing left to + # disambiguate.) variant_pipeline_rows <- list() # genome-build-stamped frames; append per build to keep peak memory low @@ -115,8 +117,7 @@ write_mutations_db <- function(sample_data, dplyr::select(dplyr::all_of(dk), Pipeline) %>% dplyr::distinct() %>% dplyr::inner_join(id_lookup, by = dk) %>% - dplyr::select(mutation_id, Pipeline) %>% - dplyr::mutate(elem = elem) + dplyr::select(mutation_id, Pipeline) } x <- deduped } @@ -126,7 +127,7 @@ write_mutations_db <- function(sample_data, } total } - counts <- vapply(c("maf", "ashm", "seg", "bedpe"), write_element, integer(1)) + counts <- vapply(c("maf", "seg", "bedpe"), write_element, integer(1)) n_variant_pipeline <- 0L if (length(variant_pipeline_rows)) { @@ -145,7 +146,7 @@ write_mutations_db <- function(sample_data, # can't be used to satisfy. idx_*_mutation_id give the variant_pipeline # semi-join (in get_ssm_from_db()'s tool_name resolution) an index to # actually use on both sides of the join -- without one, that join has to - # scan maf/ashm in full for every query with a non-NULL tool_name (the + # scan maf in full for every query with a non-NULL tool_name (the # default), which is most of them. idx <- c( "CREATE INDEX idx_maf_pos ON maf(genome_build, Chromosome, Start_Position)", @@ -153,10 +154,6 @@ write_mutations_db <- function(sample_data, "CREATE INDEX idx_maf_pipe ON maf(Pipeline)", "CREATE INDEX idx_maf_seqtype ON maf(maf_seq_type)", "CREATE INDEX idx_maf_mutation_id ON maf(mutation_id)", - "CREATE INDEX idx_ashm_pos ON ashm(genome_build, Chromosome, Start_Position)", - "CREATE INDEX idx_ashm_sample ON ashm(Tumor_Sample_Barcode)", - "CREATE INDEX idx_ashm_seqtype ON ashm(maf_seq_type)", - "CREATE INDEX idx_ashm_mutation_id ON ashm(mutation_id)", "CREATE INDEX idx_seg_sample ON seg(genome_build, ID)", "CREATE INDEX idx_seg_pos ON seg(genome_build, chrom, start)", # bedpe previously only had an index on tumour_sample_id alone -- every @@ -176,7 +173,7 @@ write_mutations_db <- function(sample_data, "CREATE INDEX idx_meta_barcode ON sample_meta(Tumor_Sample_Barcode)", "CREATE INDEX idx_study_sample ON sample_study(sample_id)", "CREATE INDEX idx_study_study ON sample_study(study)", - "CREATE INDEX idx_vp_pipe ON variant_pipeline(elem, Pipeline)", + "CREATE INDEX idx_vp_pipe ON variant_pipeline(Pipeline)", "CREATE INDEX idx_vp_mutation_id ON variant_pipeline(mutation_id)" ) for (stmt in idx) try(DBI::dbExecute(con, stmt), silent = TRUE) @@ -184,11 +181,11 @@ write_mutations_db <- function(sample_data, # self-describing provenance / expected-counts table (used by the tests) DBI::dbWriteTable(con, "build_info", data.frame( key = c("source", "built_at", "builder", - "n_maf", "n_ashm", "n_seg", "n_bedpe", "n_samples", "n_sample_study", "n_variant_pipeline"), + "n_maf", "n_seg", "n_bedpe", "n_samples", "n_sample_study", "n_variant_pipeline"), value = c(source_desc, format(Sys.time(), tz = "UTC", usetz = TRUE), "write_mutations_db", - counts[["maf"]], counts[["ashm"]], counts[["seg"]], + counts[["maf"]], counts[["seg"]], counts[["bedpe"]], nrow(sample_data$meta), n_sample_study, n_variant_pipeline), stringsAsFactors = FALSE ), overwrite = TRUE) @@ -196,9 +193,9 @@ write_mutations_db <- function(sample_data, DBI::dbExecute(con, "VACUUM") DBI::dbExecute(con, "ANALYZE") - message(sprintf("Wrote %s (%.0f MB) maf=%d ashm=%d seg=%d bedpe=%d samples=%d", + message(sprintf("Wrote %s (%.0f MB) maf=%d seg=%d bedpe=%d samples=%d", out_db, file.info(out_db)$size / 1024^2, - counts[["maf"]], counts[["ashm"]], counts[["seg"]], + counts[["maf"]], counts[["seg"]], counts[["bedpe"]], nrow(sample_data$meta))) invisible(out_db) } diff --git a/man/gambl_mutations_db.Rd b/man/gambl_mutations_db.Rd index f60063d..4bf26e0 100644 --- a/man/gambl_mutations_db.Rd +++ b/man/gambl_mutations_db.Rd @@ -19,7 +19,7 @@ A read-only DBIConnection. } \description{ Returns a (cached) read-only DBI connection to \code{gambl_mutations.db}, -the SQLite database holding the large sample-level frames (MAF, aSHM, seg, +the SQLite database holding the large sample-level frames (MAF, seg, bedpe) built from the in-memory \code{sample_data} object assembled by \code{data-raw/assemble_bundled_data.R}, via the shared \code{write_mutations_db()} helper (\code{data-raw/write_mutations_db.R}). @@ -45,21 +45,23 @@ file is downloaded automatically into the user cache dir via \section{Table reference}{ Both genome builds are stacked into the same table (not split across separate tables); filter on \code{genome_build} (\code{"grch37"} / \code{"hg38"}) to select -one. \code{maf} and \code{ashm} share the same (curated, 49-column) schema: a reduced -column set, not the full upstream flatfile MAF.\tabular{llll}{ +one. \code{maf} holds both coding-panel and aSHM-region mutations, pooled +unconditionally at write time (a mutation found by both pulls -- common +for genes that are both aSHM targets and coding-panel genes, e.g. BCL2, +BCL6, MYC, PIM1 -- collapses to one row, not two; see +\code{\link[=get_ssm_from_db]{get_ssm_from_db()}}).\tabular{llll}{ Table \tab Rows (typical) \tab Grain \tab Key columns \cr - \code{maf} \tab ~3.6M \tab one somatic mutation call \tab \code{mutation_id} (surrogate integer key, unique within \code{maf} across both genome builds -- assigned at write time, after deduplication; \code{variant_pipeline} references it as a foreign key), \verb{Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. }"slms-3"\code{/}"publication"\verb{ -- normalized at write time so it can be matched with a plain, indexed equality), genome_build, maf_seq_type} (\code{"genome"}/\code{"capture"}, so a Tumor_Sample_Barcode with both isn't indistinguishable once merged; \code{NA} for publication-pipeline rows, which don't carry per-row seq_type provenance -- see \code{\link[=get_ssm_from_db]{get_ssm_from_db()}}'s \code{seq_type} parameter) (+ ~35 more MAF-standard columns) \cr - \code{ashm} \tab ~135k \tab one mutation call in an aSHM region \tab same schema as \code{maf} (its own independent \code{mutation_id} sequence) \cr + \code{maf} \tab ~3.7M \tab one somatic mutation call \tab \code{mutation_id} (surrogate integer key, unique within \code{maf} across both genome builds -- assigned at write time, after deduplication; \code{variant_pipeline} references it as a foreign key), \verb{Hugo_Symbol, Chromosome, Start_Position, End_Position, Tumor_Sample_Barcode, Variant_Classification, HGVSp_Short, t_alt_count, n_alt_count, Pipeline (lowercase, e.g. }"slms-3"\code{/}"publication"\verb{ -- normalized at write time so it can be matched with a plain, indexed equality), genome_build, maf_seq_type} (\code{"genome"}/\code{"capture"}, so a Tumor_Sample_Barcode with both isn't indistinguishable once merged; \code{NA} for publication-pipeline rows, which don't carry per-row seq_type provenance -- see \code{\link[=get_ssm_from_db]{get_ssm_from_db()}}'s \code{seq_type} parameter) (+ ~35 more MAF-standard columns) \cr \code{seg} \tab ~126k \tab one copy-number segment \tab \verb{ID, chrom, start, end, LOH_flag, log.ratio, CN, genome_build} \cr \code{bedpe} \tab ~900 \tab one Manta structural-variant breakpoint pair \tab \verb{CHROM_A, START_A, END_A, CHROM_B, START_B, END_B, manta_name, SCORE, STRAND_A, STRAND_B, tumour_sample_id, normal_sample_id, VAF_tumour, DP, pair_status, FILTER, genome_build} \cr \code{sample_meta} \tab ~3.3k \tab one sample \tab \verb{patient_id, sample_id, Tumor_Sample_Barcode, seq_type, pathology, cohort, study, ...} (its own \code{genome_build} column records the sample's native alignment build, unrelated to the per-row build stamp used in the other tables) \cr \code{sample_study} \tab varies \tab one (sample, study) membership fact -- many-to-many, a sample belonging to N studies is N rows \tab \verb{sample_id, study, study_id, reference_PMID}. \code{study_id} is that study's own identifier for the sample where it differs from GAMBL's \code{sample_id} (e.g. a paper's own case/patient ID); NA where no study-specific ID has been sourced. \cr - \code{variant_pipeline} \tab varies \tab one (mutation, Pipeline) fact -- many-to-many, a variant independently called by N pipelines is N rows \tab \verb{mutation_id, elem, Pipeline}. \code{mutation_id} is a foreign key to \code{maf.mutation_id} or \code{ashm.mutation_id} depending on \code{elem} (\code{"maf"} or \code{"ashm"}) -- not a natural key, so this table doesn't repeat the variant's own position/sample columns. \cr - \code{build_info} \tab 10 \tab key/value \tab provenance (\code{source}, \code{built_at}, \code{builder}) and expected row counts (\code{n_maf}, \code{n_ashm}, \code{n_seg}, \code{n_bedpe}, \code{n_samples}, \code{n_sample_study}, \code{n_variant_pipeline}) used by the \code{data-raw/test_gambl_db.R} regression checks \cr + \code{variant_pipeline} \tab varies \tab one (mutation, Pipeline) fact -- many-to-many, a variant independently called by N pipelines is N rows \tab \verb{mutation_id, Pipeline}. \code{mutation_id} is a foreign key to \code{maf.mutation_id} -- not a natural key, so this table doesn't repeat the variant's own position/sample columns. \cr + \code{build_info} \tab 9 \tab key/value \tab provenance (\code{source}, \code{built_at}, \code{builder}) and expected row counts (\code{n_maf}, \code{n_seg}, \code{n_bedpe}, \code{n_samples}, \code{n_sample_study}, \code{n_variant_pipeline}) used by the \code{data-raw/test_gambl_db.R} regression checks \cr } -\code{maf}/\code{ashm} do NOT carry a \code{Study} column. Cohort/study membership is +\code{maf} does NOT carry a \code{Study} column. Cohort/study membership is tracked once per sample in \code{sample_study}, not once per mutation row -- a mutation row's \code{Study} couldn't represent a sample belonging to more than one study anyway, and a per-row tag caused the same sample's @@ -67,7 +69,7 @@ mutations to be pulled and duplicated once per cohort-specific code path that claimed it (see \code{GAMBLR.data::get_ssm_from_db()}'s \code{this_study} parameter for how to filter by study post-refactor). -Similarly, \code{maf}/\code{ashm} only ever carry ONE \code{Pipeline} value per variant, +Similarly, \code{maf} only ever carries ONE \code{Pipeline} value per variant, even though the same real mutation can be independently produced by more than one pipeline (e.g. a cohort's own published/curated maf and our separate SLMS-3 recall both calling the same position) -- when that @@ -76,32 +78,30 @@ arbitrarily (whichever pipeline's data was assembled first), which would otherwise make the variant invisible to a query for the \emph{other} pipeline even though it's still fully present in the table. \code{variant_pipeline} records every pipeline that actually produced each variant, independent -of which one "won" as \code{maf}/\code{ashm}'s single representative row; see +of which one "won" as \code{maf}'s single representative row; see \code{GAMBLR.data::get_ssm_from_db()}'s \code{tool_name} parameter, which resolves -against this table rather than \code{maf}/\code{ashm}'s own \code{Pipeline} column. +against this table rather than \code{maf}'s own \code{Pipeline} column. \subsection{Join keys}{ -\code{maf}, \code{ashm}, and \code{bedpe} (via \code{tumour_sample_id}) key to +\code{maf} and \code{bedpe} (via \code{tumour_sample_id}) key to \code{sample_meta$Tumor_Sample_Barcode} / \code{sample_meta$sample_id}; \code{seg$ID} keys to \code{sample_meta$sample_id}; \code{sample_study$sample_id} keys to \code{sample_meta$sample_id}; \code{variant_pipeline$mutation_id} keys to -\code{maf.mutation_id} or \code{ashm.mutation_id}, selecting which one via -\code{variant_pipeline$elem}. +\code{maf.mutation_id}. } \subsection{Indexes}{ -\code{maf}/\code{ashm}: \verb{(genome_build, Chromosome, Start_Position)}, +\code{maf}: \verb{(genome_build, Chromosome, Start_Position)}, \code{Tumor_Sample_Barcode}, \code{Pipeline}, \code{maf_seq_type}, \code{mutation_id}. No index on \code{Hugo_Symbol}: gene-restricted queries resolve the gene to a region first -(the same logic used to populate these tables) and filter on +(the same logic used to populate this table) and filter on \verb{(genome_build, Chromosome, Start_Position)} instead -- see \code{GAMBLR.data::get_ssm_from_db()}. \code{seg}: \verb{(genome_build, ID)}, \verb{(genome_build, chrom, start)}. \code{bedpe}: \verb{(tumour_sample_id, genome_build)}, \verb{(genome_build, CHROM_A, START_A)}, \verb{(genome_build, CHROM_B, START_B)} (one per breakpoint end, so a region search that OR's both ends can use a different index per side). \code{sample_meta}: \code{sample_id}, \code{Tumor_Sample_Barcode}. \code{sample_study}: -\code{sample_id}, \code{study}. \code{variant_pipeline}: \code{mutation_id}, -\verb{(elem, Pipeline)}. +\code{sample_id}, \code{study}. \code{variant_pipeline}: \code{mutation_id}, \code{Pipeline}. } } diff --git a/man/get_ssm_from_db.Rd b/man/get_ssm_from_db.Rd index 4350f80..04db7c4 100644 --- a/man/get_ssm_from_db.Rd +++ b/man/get_ssm_from_db.Rd @@ -8,7 +8,6 @@ get_ssm_from_db( projection = "grch37", sample_ids = NULL, tool_name = "slms-3", - include_ashm = FALSE, coding_only = FALSE, include_silent = TRUE, min_read_support = 0, @@ -29,19 +28,12 @@ itself normalized to lowercase at write time, so this is a plain, indexed equality check rather than a function-wrapped one). Default "slms-3"; set NULL to skip the Pipeline filter. Resolved via the \code{variant_pipeline} join table (see \code{\link[=gambl_mutations_db]{gambl_mutations_db()}}'s schema -docs) rather than \code{maf}/\code{ashm}'s own Pipeline column, so a variant +docs) rather than \code{maf}'s own Pipeline column, so a variant independently called by more than one pipeline is still found by a -query for either one, even though its single maf/ashm row can only +query for either one, even though its single maf row can only carry one Pipeline value. Falls back to filtering the column directly against DBs built before variant_pipeline existed.} -\item{include_ashm}{When TRUE, also query the \code{ashm} table and row-bind it, -deduplicated against \code{maf} on the same natural key \code{write_mutations_db()} -uses at write time -- \code{maf} and \code{ashm} dedup independently there, so -nothing else prevents the same real mutation (same sample/position/ -allele) landing in both when it falls in a gene that's both aSHM-target -and coding-panel (e.g. BCL2, BCL6, MYC, PIM1). \code{maf}'s copy wins ties.} - \item{coding_only}{When TRUE, keep only coding \code{Variant_Classification}s.} \item{include_silent}{When FALSE (and \code{coding_only}), drop Silent mutations.} @@ -49,7 +41,7 @@ and coding-panel (e.g. BCL2, BCL6, MYC, PIM1). \code{maf}'s copy wins ties.} \item{min_read_support}{Keep only variants with \code{t_alt_count} >= this value.} \item{this_study}{Optional single study to restrict to, matched against -\code{sample_study$study} (not a \code{maf}/\code{ashm} column -- see +\code{sample_study$study} (not a \code{maf} column -- see \code{\link[=gambl_mutations_db]{gambl_mutations_db()}}'s schema docs). Resolves to the set of sample_ids belonging to that study, intersected with \code{sample_ids} if both are supplied; composes with \code{tool_name}/Pipeline exactly as before (e.g. From 7c4372d788005e9f6b9172bd51b59222fa77fa75 Mon Sep 17 00:00:00 2001 From: rdmorin Date: Wed, 29 Jul 2026 12:17:11 -0700 Subject: [PATCH 57/58] Update pre-release check scripts for the maf/ashm merge Both scripts hardcoded assumptions from when maf and ashm were separate tables. Left unfixed, test_gambl_db.R would report false failures (ashm table/index checks) and compare_bundle_changes.R would hard-error outright (SELECT * FROM ashm, now nonexistent). - test_gambl_db.R: drop ashm from table/row-count/index/build_info checks; assert it does NOT exist instead. variant_pipeline's column check drops elem. The legacy sample_data.rda regression check can no longer expect exact equality for maf (the DB's maf now also absorbs what used to be ashm, deduplicated against it) -- replaced with a range check: old maf <= new maf <= old maf + old ashm. - compare_bundle_changes.R: SNV comparison reads maf alone on the new side instead of maf UNION ashm; .source is uniformly "maf" there since the new schema has no retained per-row provenance to trace gained/lost examples back to (the old side, from the .rda, is unaffected and still distinguishes them). Ran both against the real rebuilt gambl_mutations.db: test_gambl_db.R passes every check except the optional legacy-.rda comparisons, whose baseline (data/sample_data.rda, last updated 2025-06-30) is confirmed stale independent of this change -- seg and bedpe, untouched by any of today's work, mismatch it by similarly large margins. compare_bundle_changes.R runs clean; per-event diff against that same stale baseline shows 299 SNV / 0 CNV / 14 SV losses against thousands of gains out of ~889K total maf rows, consistent with ~13 months of accumulated cohort/sample growth rather than a regression. --- data-raw/compare_bundle_changes.R | 26 ++++++++++++-------------- data-raw/test_gambl_db.R | 31 ++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/data-raw/compare_bundle_changes.R b/data-raw/compare_bundle_changes.R index d81cc93..4ee4150 100644 --- a/data-raw/compare_bundle_changes.R +++ b/data-raw/compare_bundle_changes.R @@ -2,7 +2,7 @@ # # Compares the OLD bundled sample_data.rda against a NEW gambl_mutations.db # build at the level of individual events (not just row totals), for each of -# three data types: SNV (maf + ashm), CNV (seg), SV (bedpe). For each type: +# three data types: SNV (maf), CNV (seg), SV (bedpe). For each type: # - counts, per sample_id, how many events are NEW (gained: present in the # new build but not the old) and how many are LOST (missing: present in # the old build but not the new) @@ -106,18 +106,16 @@ write_counts <- function(per_sample, path) { message("wrote per-sample counts to ", path) } -# --- SNV: maf + ashm combined ------------------------------------------------ -# maf and ashm are built by different code paths in assemble_bundled_data.R -# (gene-region-restricted pull vs aSHM-region-restricted pull) -- .source -# records which table each row came from so gained/lost examples can be -# traced back to the right code path instead of being lumped together as -# an undifferentiated "SNV". Not part of the comparison key: a row that -# moved from one table to the other between builds still counts as -# retained if its sample/position match. -new_snv <- bind_rows( - dbGetQuery(con, "SELECT * FROM maf") %>% mutate(.source = "maf"), - dbGetQuery(con, "SELECT * FROM ashm") %>% mutate(.source = "ashm") -) +# --- SNV: maf (which now also absorbs what used to be a separate ashm pull, +# deduplicated against it at write time -- see write_mutations_db.R) -------- +# .source records which table/pull each row came from on the OLD side so +# gained/lost examples can still be traced back to the right code path. +# The new side no longer has that distinction to make (maf and ashm were +# merged into one table with no retained provenance column -- deliberately; +# nothing consumed it), so .source is uniformly "maf" there. Not part of the +# comparison key either way: a row that moved from one source to the other +# between builds still counts as retained if its sample/position match. +new_snv <- dbGetQuery(con, "SELECT * FROM maf") %>% mutate(.source = "maf") old_snv <- bind_rows( old_sd$grch37$maf %>% mutate(genome_build = "grch37", .source = "maf"), old_sd$hg38$maf %>% mutate(genome_build = "hg38", .source = "maf"), @@ -136,7 +134,7 @@ old_snv <- bind_rows( old_snv <- old_snv %>% filter(is.na(Pipeline) | Pipeline != "strelka") snv_key <- c("Tumor_Sample_Barcode", "genome_build", "Chromosome", "Start_Position", "End_Position") -snv_cmp <- compare_events(old_snv, new_snv, "Tumor_Sample_Barcode", snv_key, "SNV (maf+ashm)") +snv_cmp <- compare_events(old_snv, new_snv, "Tumor_Sample_Barcode", snv_key, "SNV (maf, incl. aSHM-region pull)") write_examples(snv_cmp$gained, "Tumor_Sample_Barcode", file.path(outdir, "snv_gained_examples.log")) write_examples(snv_cmp$lost, "Tumor_Sample_Barcode", file.path(outdir, "snv_lost_examples.log")) write_counts(snv_cmp$per_sample, file.path(outdir, "snv_persample_counts.tsv")) diff --git a/data-raw/test_gambl_db.R b/data-raw/test_gambl_db.R index 4e2b105..079c885 100644 --- a/data-raw/test_gambl_db.R +++ b/data-raw/test_gambl_db.R @@ -31,17 +31,18 @@ cols <- function(t) dbListFields(con, t) builds_in <- function(t) sort(dbGetQuery(con, sprintf("SELECT DISTINCT genome_build g FROM %s", t))$g) cat("== tables present ==\n") -for (t in c("maf","ashm","seg","bedpe","sample_meta","sample_study","variant_pipeline","build_info")) +for (t in c("maf","seg","bedpe","sample_meta","sample_study","variant_pipeline","build_info")) check(t %in% tbls, sprintf("table %s exists", t)) +check(!("ashm" %in% tbls), "table ashm does NOT exist (merged into maf)") cat("\n== row counts (all > 0) ==\n") -for (t in c("maf","ashm","seg","bedpe","sample_meta","sample_study","variant_pipeline")) { +for (t in c("maf","seg","bedpe","sample_meta","sample_study","variant_pipeline")) { cnt <- if (t %in% tbls) n(t) else 0 check(cnt > 0, sprintf("%-11s has %d rows", t, cnt)) } cat("\n== both genome builds present ==\n") -for (t in c("maf","ashm","seg","bedpe")) +for (t in c("maf","seg","bedpe")) if (t %in% tbls) check(all(c("grch37","hg38") %in% builds_in(t)), sprintf("%-6s has builds: %s", t, paste(builds_in(t), collapse=", "))) @@ -54,7 +55,7 @@ req <- list( bedpe = c("tumour_sample_id","CHROM_A","START_A","CHROM_B","START_B","VAF_tumour","SCORE","FILTER","genome_build"), sample_meta = c("sample_id","Tumor_Sample_Barcode","seq_type","study"), sample_study = c("sample_id","study","study_id","reference_PMID"), - variant_pipeline = c("mutation_id","elem","Pipeline") + variant_pipeline = c("mutation_id","Pipeline") ) for (t in names(req)) if (t %in% tbls) { missing <- setdiff(req[[t]], cols(t)) @@ -71,7 +72,7 @@ cat("\n== indexes present ==\n") # assert indexes that actually exist. idx <- dbGetQuery(con, "SELECT name FROM sqlite_master WHERE type='index'")$name for (i in c("idx_maf_pos","idx_maf_sample","idx_maf_pipe","idx_maf_mutation_id", - "idx_ashm_mutation_id","idx_seg_sample", + "idx_seg_sample", "idx_bedpe_sample","idx_bedpe_pos_a","idx_bedpe_pos_b", "idx_study_sample","idx_study_study", "idx_vp_pipe","idx_vp_mutation_id")) @@ -95,7 +96,7 @@ cat("\n== build_info counts match actual table counts ==\n") if ("build_info" %in% tbls) { bi <- dbGetQuery(con, "SELECT key, value FROM build_info") getbi <- function(k) as.numeric(bi$value[bi$key == k]) - for (t in c("maf","ashm","seg","bedpe")) + for (t in c("maf","seg","bedpe")) check(isTRUE(getbi(paste0("n_", t)) == n(t)), sprintf("build_info n_%s == COUNT(%s)", t, t)) check(isTRUE(getbi("n_variant_pipeline") == n("variant_pipeline")), @@ -105,11 +106,23 @@ if ("build_info" %in% tbls) { cat("\n== regression vs sample_data.rda (optional) ==\n") if (file.exists(rda)) { e <- new.env(); load(rda, envir = e); sd <- get("sample_data", envir = e) - for (t in c("maf","ashm","seg","bedpe")) { - expect <- sum(vapply(c("grch37","hg38"), - function(b) { x <- sd[[b]][[t]]; if (is.null(x)) 0L else nrow(x) }, integer(1))) + rda_rows <- function(t) sum(vapply(c("grch37","hg38"), + function(b) { x <- sd[[b]][[t]]; if (is.null(x)) 0L else nrow(x) }, integer(1))) + for (t in c("seg","bedpe")) { + expect <- rda_rows(t) check(expect == n(t), sprintf("%-6s DB rows (%d) == sample_data rows (%d)", t, n(t), expect)) } + # maf no longer has an exact-equality invariant against the legacy .rda: + # the DB's maf now also absorbs what used to be a separate ashm pull, + # deduplicated against it at write time (see write_mutations_db.R), so the + # new count is expected to be somewhere between the old maf alone (nothing + # was ever removed from maf's own content) and old maf + old ashm combined + # (the ceiling, only reached if the two pulls never overlapped at all). + old_maf <- rda_rows("maf") + old_ashm <- rda_rows("ashm") + check(n("maf") >= old_maf && n("maf") <= old_maf + old_ashm, + sprintf("maf DB rows (%d) within [old maf (%d), old maf+ashm (%d)]", + n("maf"), old_maf, old_maf + old_ashm)) } else cat(" [skip] no", rda, "(fresh GSC build has no legacy .rda — expected)\n") cat(sprintf("\n==== %s: %d check(s) failed ====\n", if (fails==0) "ALL PASSED" else "FAILURES", fails)) From 772c70f20aa9d495ad61daa2a99530aebc81bb7e Mon Sep 17 00:00:00 2001 From: rdmorin Date: Wed, 29 Jul 2026 12:22:56 -0700 Subject: [PATCH 58/58] Revert Version bump to 1.5 -- reuse 1.4 for this release data-v1.4 was only published a few days ago and this PR hasn't landed on master yet, so there's no meaningful population of installs depending on that exact release asset staying as-is. Reusing 1.4 avoids the awkwardness of a 1.5 bump for what's still all one unmerged, in-flight PR. --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index d583655..0f95aa8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: GAMBLR.data Title: Collection of Curated Data for Genomic Analysis of Mature B-cell Lymphomas in R -Version: 1.5 +Version: 1.4 Authors@R: c( person(given = "Ryan",