Skip to content

Commit 9470b24

Browse files
committed
Assemble MAE/MASE reads with new2(check = FALSE); guard with validObject tests
readParquet() now builds multi_assay_experiment and multi_assay_spatial_experiment objects directly via S4Vectors::new2(check = FALSE) instead of the MultiAssayExperiment() / MultiAssaySpatialExperiment() constructors. Those constructors re-derive sample-name set equality at construction time with a locale-collation sort over the full, cell-grain sampleMap, which dominates read time on large products (tens of millions of sampleMap rows -- a cell-resolved spatial atlas took ~22 min to read, over 60% of it in that redundant sort). The datapackage already carries writeParquet()'s write-time integrity guarantee (a DuckDB anti-join proving every sampleMap 'primary' is a subjects rowname), so the constructor-time re-check is redundant; the assembled object still passes validObject(). The direct assembly also skips harmonization, so unreferenced colData rows are retained rather than dropped on read (write with subset_subjects_to_referenced upstream to avoid them). Because the read path no longer runs constructor-time validity, the MAE/MASE round-trip tests now assert validObject() on every readParquet() result -- in the two MAE round-trips, the MASE datapackage-schema round-trip, and the two spatial fixture helpers (makeLazySpatialMASE, .lazyPointsMASE) covering every spatial-query test. This closes the gap that would otherwise let a structurally-invalid readParquet()-assembled object through.
1 parent c08db3b commit 9470b24

8 files changed

Lines changed: 82 additions & 19 deletions

File tree

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Package: BiocDuckDB
2-
Version: 0.99.10
2+
Version: 0.99.11
33
Date: 2026-07-28
44
Title: Bioconductor DuckDB Integration and High-Level I/O
55
Description:

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ importClassesFrom(MultiAssayExperiment,ExperimentList)
9595
importClassesFrom(MultiAssayExperiment,MultiAssayExperiment)
9696
importClassesFrom(MultiAssaySpatialExperiment,MultiAssaySpatialExperiment)
9797
importClassesFrom(MultiAssaySpatialExperiment,PointsLayerList)
98+
importClassesFrom(MultiAssaySpatialExperiment,RasterLayerList)
9899
importClassesFrom(MultiAssaySpatialExperiment,ShapesLayerList)
99100
importClassesFrom(S4Vectors,DFrame)
100101
importClassesFrom(S4Vectors,DataFrame)

NEWS.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
# BiocDuckDB 0.99.11
2+
3+
## Enhancements
4+
5+
- `readParquet()` now assembles `multi_assay_experiment` and
6+
`multi_assay_spatial_experiment` objects directly with
7+
`S4Vectors::new2(check = FALSE)` instead of calling the
8+
`MultiAssayExperiment()` / `MultiAssaySpatialExperiment()` constructors. Those
9+
constructors re-derive sample-name set equality at construction time via a
10+
locale-collation sort over the full, cell-grain `sampleMap`, which dominates
11+
read time on large products (tens of millions of `sampleMap` rows -- e.g. a
12+
cell-resolved spatial atlas took ~22 min to read, over 60% of it in that
13+
redundant sort). The datapackage already carries `writeParquet()`'s write-time
14+
integrity guarantee (a DuckDB anti-join proving every `sampleMap` `primary` is
15+
a subjects rowname), so the constructor-time re-check is redundant; the
16+
assembled object still passes `validObject()`. The direct assembly also skips
17+
harmonization, so unreferenced `colData` rows are retained rather than dropped
18+
on read (write with `subset_subjects_to_referenced` upstream to avoid them).
19+
20+
## Testing
21+
22+
- The `multi_assay_experiment` and `multi_assay_spatial_experiment` round-trip
23+
tests now assert `validObject()` on every object returned by `readParquet()`.
24+
Because the read path assembles with `new2(check = FALSE)` (no constructor-time
25+
validity), these explicit checks guard against a `readParquet()`-assembled
26+
object being structurally invalid.
27+
128
# BiocDuckDB 0.99.10
229

330
## Enhancements

R/readParquet.R

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,41 @@ function(path,
699699
### MultiAssayExperiment objects
700700
###
701701

702+
#' @importClassesFrom MultiAssayExperiment MultiAssayExperiment
703+
#' @importClassesFrom S4Vectors DataFrame
704+
#' @importFrom MultiAssayExperiment ExperimentList
705+
#' @importFrom S4Vectors new2
706+
.newMAE <- function(experiments, colData, sampleMap, metadata = list()) {
707+
if (is.null(metadata)) {
708+
metadata <- list()
709+
}
710+
new2("MultiAssayExperiment",
711+
ExperimentList = ExperimentList(experiments),
712+
colData = colData,
713+
sampleMap = as(sampleMap, "DataFrame"),
714+
metadata = metadata,
715+
check = FALSE)
716+
}
717+
718+
#' @importClassesFrom MultiAssaySpatialExperiment MultiAssaySpatialExperiment
719+
#' @importClassesFrom MultiAssaySpatialExperiment RasterLayerList
720+
#' @importClassesFrom MultiAssaySpatialExperiment PointsLayerList
721+
#' @importClassesFrom MultiAssaySpatialExperiment ShapesLayerList
722+
#' @importFrom S4Vectors new2
723+
.newMASE <-
724+
function(mae, images, labels, points, shapes, imgData, spatialMap)
725+
{
726+
new2("MultiAssaySpatialExperiment",
727+
mae,
728+
images = as(images, "RasterLayerList"),
729+
labels = as(labels, "RasterLayerList"),
730+
points = as(points, "PointsLayerList"),
731+
shapes = as(shapes, "ShapesLayerList"),
732+
imgData = imgData,
733+
spatialMap = spatialMap,
734+
check = FALSE)
735+
}
736+
702737
#' @importFrom MultiAssayExperiment MultiAssayExperiment
703738
#' @importFrom stats setNames
704739
.readParquetMAE <- function(path, package, ...) {
@@ -735,10 +770,8 @@ function(path,
735770
metadata <- .deserializeMetadata(package[["annotations"]], resources, path, ...)
736771

737772
# MultiAssayExperiment
738-
MultiAssayExperiment(experiments,
739-
colData = subjects,
740-
sampleMap = sample_map,
741-
metadata = metadata)
773+
.newMAE(experiments, colData = subjects, sampleMap = sample_map,
774+
metadata = metadata)
742775
}
743776

744777
### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -896,16 +929,7 @@ function(path,
896929
spatial_map <- DataFrame(spatial_map, check.names = FALSE)
897930
}
898931

899-
MultiAssaySpatialExperiment::MultiAssaySpatialExperiment(
900-
experiments = experiments(mae),
901-
colData = colData(mae),
902-
sampleMap = sampleMap(mae),
903-
images = images,
904-
labels = labels,
905-
points = points,
906-
shapes = shapes,
907-
imgData = img_data,
908-
spatialMap = spatial_map,
909-
metadata = metadata(mae)
910-
)
932+
# MultiAssaySpatialExperiment
933+
.newMASE(mae, images = images, labels = labels, points = points,
934+
shapes = shapes, imgData = img_data, spatialMap = spatial_map)
911935
}

tests/testthat/setup.R

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,5 +299,9 @@ makeLazySpatialMASE <- function(path = NULL) {
299299
if (is.null(path))
300300
path <- file.path(tempdir(), paste0("mase_lazy_", sample.int(1e6, 1L)))
301301
BiocDuckDB::writeParquet(mase, path)
302-
BiocDuckDB::readParquet(path)
302+
out <- BiocDuckDB::readParquet(path)
303+
# readParquet assembles the MASE with new2(check = FALSE); assert the
304+
# write-trusted fast path still yields a valid object (guards every caller).
305+
expect_true(validObject(out))
306+
out
303307
}

tests/testthat/test-datapackage-schema.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ test_that("MASE conforms spatialMap via an spatial_element_registry + monomorphi
222222

223223
# Round-trip: the internal spine does not leak back into the object model.
224224
m2 <- readParquet(tmpdir)
225+
expect_true(validObject(m2))
225226
sm2 <- MultiAssaySpatialExperiment::spatialMap(m2)
226227
expect_false(any(c("__element__", "__index__") %in% colnames(sm2)))
227228
expect_true(all(c("assay", "colname", "element_type", "region",

tests/testthat/test-parquet-roundtrip.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ test_that("MultiAssayExperiment with nested experiments works", {
351351

352352
# Check structure
353353
expect_s4_class(mae2, "MultiAssayExperiment")
354+
expect_true(validObject(mae2))
354355
expect_identical(names(experiments(mae2)), c("RNA", "Protein"))
355356
expect_identical(length(experiments(mae2)), 2L)
356357

@@ -393,6 +394,7 @@ test_that("MultiAssayExperiment with flat array-like objects works", {
393394

394395
# Check structure
395396
expect_s4_class(mae2, "MultiAssayExperiment")
397+
expect_true(validObject(mae2))
396398
expect_identical(names(experiments(mae2)), c("Dataset1", "Dataset2"))
397399
expect_identical(length(experiments(mae2)), 2L)
398400

tests/testthat/test-spatial-query.R

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@
3232
scaled = list(type = "scale", scale = c(2, 2))))))
3333
path <- file.path(tempdir(), paste0("mase_q_", sample.int(1e6, 1L)))
3434
BiocDuckDB::writeParquet(mase, path)
35-
BiocDuckDB::readParquet(path)
35+
out <- BiocDuckDB::readParquet(path)
36+
# readParquet assembles the MASE with new2(check = FALSE); assert the
37+
# write-trusted fast path still yields a valid object (guards every caller).
38+
expect_true(validObject(out))
39+
out
3640
}
3741

3842
test_that("per-element transforms round-trip and expose coordinate systems", {

0 commit comments

Comments
 (0)