Skip to content

Commit 14353e9

Browse files
committed
Type __index__ int64 for resources above the 32-bit row limit
writeParquet()'s flat / data-frame path typed the __index__ column from each part's own value range, so part 0 of a large streamed resource narrowed to int32 and later append parts overflowed on cast once the running index passed ~2.1e9. .writeDataFrameParquet() gains an optional index_max argument: when the declared total index range exceeds the 32-bit limit, __index__ is typed as a 64-bit integer up front (part 0 included) so every streamed part shares one type, mirroring the coord-array max_dim typing. Pass index_max = Inf when the total row count is unknown before streaming (e.g. graph edges). Small resources are unaffected -- the index still narrows to the smallest integer type. Index generation is numeric-safe, so offsets past 2^31 no longer overflow to NA. Adds test-writeParquet-large-index.R (streams a > 2^31 index across two parts, asserts a consistent on-disk int64 column with exact values, and guards the small-resource narrowing path). Companion to the DuckDBDataFrame validateAppendOffset() / buildTableSelectSQL() fix. Bump version to 0.99.4; update NEWS.
1 parent e6ab9c0 commit 14353e9

5 files changed

Lines changed: 78 additions & 3 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.3
2+
Version: 0.99.4
33
Date: 2026-07-22
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
@@ -123,6 +123,7 @@ importFrom(DuckDBArray,writeCoordArray)
123123
importFrom(DuckDBDataFrame,DuckDBDataFrame)
124124
importFrom(DuckDBDataFrame,DuckDBSelfHits)
125125
importFrom(DuckDBDataFrame,acquireDuckDBConn)
126+
importFrom(DuckDBDataFrame,arrowIntType)
126127
importFrom(DuckDBDataFrame,arrowType)
127128
importFrom(DuckDBDataFrame,clusterSort)
128129
importFrom(DuckDBDataFrame,configureCloud)

NEWS.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
# BiocDuckDB 0.99.4
2+
3+
## Bug fixes
4+
5+
- `writeParquet()` (flat / data-frame path) can now write a resource with more
6+
than ~2.1e9 rows without the `__index__` column overflowing 32-bit integers. A
7+
new optional `index_max` argument declares the resource's total index range;
8+
when it exceeds the 32-bit limit the `__index__` column is typed as a 64-bit
9+
integer up front (part 0 included) so every streamed append part shares one
10+
type, instead of narrowing part 0 to `int32` and overflowing later parts on
11+
cast (mirrors the coord-array `max_dim` typing). Pass `index_max = Inf` when
12+
the total row count is unknown before streaming (e.g. graph edges). Small
13+
resources are unaffected --- the index still narrows to the smallest integer
14+
type. Requires the companion `DuckDBDataFrame` `validateAppendOffset()` fix.
15+
116
# BiocDuckDB 0.99.3
217

318
## New features

R/writeParquet.R

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -732,13 +732,13 @@ function(x, path, indexcol, keycol, dimtbl, name, dimension, layout,
732732
}
733733

734734
#' @importFrom arrow Array as_arrow_table write_dataset write_parquet
735-
#' @importFrom DuckDBDataFrame arrowType reconcileParquetSchema setupFlatParquetWrite clusterSort
735+
#' @importFrom DuckDBDataFrame arrowIntType arrowType reconcileParquetSchema setupFlatParquetWrite clusterSort
736736
#' @importFrom S4Vectors I
737737
#' @importFrom stats setNames
738738
.writeDataFrameParquet <-
739739
function(x, path, indexcol, keycol, dimtbl, name, dimension, layout,
740740
refs = NULL, append = FALSE, offset = 0L, part = NULL,
741-
part_digits = 0L, cluster_by = NULL, ...)
741+
part_digits = 0L, cluster_by = NULL, index_max = NULL, ...)
742742
{
743743
prep <- setupFlatParquetWrite(
744744
path, append = append, offset = offset, part = part,
@@ -813,6 +813,15 @@ function(x, path, indexcol, keycol, dimtbl, name, dimension, layout,
813813
}
814814
}
815815

816+
# Type the __index__ column by the producer-declared total range
817+
# (`index_max`), not this block's, so every part of a > 2^31-row resource
818+
# shares one wide type instead of narrowing part 0 to int32 and
819+
# overflowing later parts (mirrors the coord-array `max_dim` typing).
820+
if (!is.null(indexcol) && !is.null(index_max) &&
821+
indexcol %in% colnames(x)) {
822+
narrow_types[[indexcol]] <- arrowIntType(c(0, index_max))
823+
}
824+
816825
# Narrow only the write payload (an Arrow table); `x` stays an R
817826
# data.frame so the schema builder reports the base "integer" type with
818827
# the narrowed arrowType recorded per column below.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Regression: append offsets and __index__ values above the 32-bit ceiling (a
2+
# resource with more than ~2.1e9 rows). `index_max` declares the index range so
3+
# the __index__ column is typed wide enough (int64) up front -- part 0 included
4+
# -- and every streamed part shares one type, instead of narrowing part 0 to
5+
# int32 and overflowing later parts on cast. See NEWS 0.99.3.
6+
# Run: library(BiocDuckDB); library(testthat); source("test-writeParquet-large-index.R")
7+
8+
library(arrow)
9+
10+
.indexArrowType <- function(f) {
11+
ParquetFileReader$create(f)$GetSchema()$GetFieldByName("__index__")$type$ToString()
12+
}
13+
14+
test_that("index_max streams a > 2^31 index as a consistent int64 column", {
15+
dir <- tempfile()
16+
# Part 0 at offset 0, then a part whose offset crosses the 32-bit ceiling.
17+
writeParquet(data.frame(v = 1:5), dir, indexcol = "__index__", keycol = NULL,
18+
dimension = "sample", layout = "data_frame",
19+
offset = 0, part = 0L, part_digits = 2L, append = FALSE,
20+
index_max = Inf)
21+
writeParquet(data.frame(v = 11:15), dir, indexcol = "__index__", keycol = NULL,
22+
dimension = "sample", layout = "data_frame",
23+
offset = 3e9, part = 1L, part_digits = 2L, append = TRUE,
24+
index_max = Inf)
25+
26+
files <- sort(list.files(dir, pattern = "parquet$", recursive = TRUE,
27+
full.names = TRUE))
28+
expect_length(files, 2L)
29+
# Both parts, and the unified dataset, are int64 (schema-consistent).
30+
expect_true(all(vapply(files, .indexArrowType, character(1L)) == "int64"))
31+
ds <- open_dataset(dir)
32+
expect_identical(ds$schema$GetFieldByName("__index__")$type$ToString(), "int64")
33+
34+
# The index values above 2^31 are stored exactly, with no overflow to NA.
35+
idx <- sort(as.data.frame(ds)[["__index__"]])
36+
expect_false(anyNA(idx))
37+
expect_equal(idx[6:10],
38+
c(3000000001, 3000000002, 3000000003, 3000000004, 3000000005))
39+
unlink(dir, recursive = TRUE)
40+
})
41+
42+
test_that("without index_max a small index still narrows (no regression)", {
43+
dir <- tempfile()
44+
writeParquet(data.frame(v = 1:5), dir, indexcol = "__index__", keycol = NULL,
45+
dimension = "sample", layout = "data_frame")
46+
f <- list.files(dir, pattern = "parquet$", recursive = TRUE,
47+
full.names = TRUE)[1L]
48+
expect_identical(.indexArrowType(f), "uint8")
49+
unlink(dir, recursive = TRUE)
50+
})

0 commit comments

Comments
 (0)