You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by pa...@apache.org on 2026/10/21 17:14:24 UTC
(arrow-nanoarrow) branch main updated: fix(r): reuse single-chunk nanoarrow vectors in as_nanoarrow_array (#951)
This is an automated email from the ASF dual-hosted git repository.
paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-nanoarrow.git
The following commit(s) were added to refs/heads/main by this push:
new efe42748 fix(r): reuse single-chunk nanoarrow vectors in as_nanoarrow_array (#951)
efe42748 is described below
commit efe42748545c5c886391e1794786828aff09711d
Author: Dewey Dunnington <de...@dunnington.ca>
AuthorDate: Wed Oct 21 12:14:17 2026 -0500
fix(r): reuse single-chunk nanoarrow vectors in as_nanoarrow_array (#951)
Adds an `as_nanoarrow_array.nanoarrow_vctr()` fast path for full-slice,
single-chunk vectors. Existing buffers are reused; a shallow array
wrapper is created only when a data-frame field name must be attached.
This avoids extension round-trips such as rebuilding GeoArrow geometry
through WK.
On a 3.23M-row GeoArrow reproduction, data-frame-to-stream conversion
fell from 3.44 s to 0.32 s. The available R test suite and `R CMD check
--no-manual` pass.
```r
library(nanoarrow)
x <- as_nanoarrow_vctr(as_nanoarrow_array(c("one", "two")))
as.data.frame(as_nanoarrow_array(data.frame(x = x)))
#> x
#> 1 one
#> 2 two
```
---
r/NAMESPACE | 1 +
r/R/schema.R | 20 +++++++----
r/R/vctr.R | 68 ++++++++++++++++++++++++++++++++++--
r/tests/testthat/test-schema.R | 17 +++++++++
r/tests/testthat/test-vctr.R | 79 ++++++++++++++++++++++++++++++++++++++++--
5 files changed, 175 insertions(+), 10 deletions(-)
diff --git a/r/NAMESPACE b/r/NAMESPACE
index 1ce6c21e..05b280fc 100644
--- a/r/NAMESPACE
+++ b/r/NAMESPACE
@@ -40,6 +40,7 @@ S3method(as_nanoarrow_array,list)
S3method(as_nanoarrow_array,matrix)
S3method(as_nanoarrow_array,nanoarrow_array)
S3method(as_nanoarrow_array,nanoarrow_buffer)
+S3method(as_nanoarrow_array,nanoarrow_vctr)
S3method(as_nanoarrow_array,python.builtin.object)
S3method(as_nanoarrow_array,vctrs_unspecified)
S3method(as_nanoarrow_array_extension,default)
diff --git a/r/R/schema.R b/r/R/schema.R
index 31211f6b..df6f41be 100644
--- a/r/R/schema.R
+++ b/r/R/schema.R
@@ -287,12 +287,20 @@ nanoarrow_schema_modify <- function(x, new_values, validate = TRUE) {
schema_deep_copy
}
-nanoarrow_schema_identical <- function(x, y) {
- identical(x, y) ||
- identical(
- nanoarrow_schema_proxy(x, recursive = TRUE),
- nanoarrow_schema_proxy(y, recursive = TRUE)
- )
+nanoarrow_schema_identical <- function(x, y, ignore_name = FALSE) {
+ if (identical(x, y)) {
+ return(TRUE)
+ }
+
+ x_proxy <- nanoarrow_schema_proxy(x, recursive = TRUE)
+ y_proxy <- nanoarrow_schema_proxy(y, recursive = TRUE)
+
+ if (ignore_name) {
+ x_proxy$name <- NULL
+ y_proxy$name <- NULL
+ }
+
+ identical(x_proxy, y_proxy)
}
#' @importFrom utils str
diff --git a/r/R/vctr.R b/r/R/vctr.R
index 4d1c443b..1e8f1103 100644
--- a/r/R/vctr.R
+++ b/r/R/vctr.R
@@ -154,6 +154,62 @@ as_nanoarrow_schema.nanoarrow_vctr <- function(x, ...) {
attr(x, "schema", exact = TRUE)
}
+#' @export
+as_nanoarrow_array.nanoarrow_vctr <- function(x, ..., schema = NULL) {
+ x_schema <- attr(x, "schema", exact = TRUE)
+ update_schema <- FALSE
+ if (!is.null(schema)) {
+ # A field name is not part of an array's physical type. In particular,
+ # data.frame conversion supplies the column name on its child schema even
+ # though the schema stored by the vctr usually has an empty name.
+ schemas_identical <- nanoarrow_schema_identical(schema, x_schema)
+ if (!schemas_identical) {
+ if (!nanoarrow_schema_identical(schema, x_schema, ignore_name = TRUE)) {
+ return(NextMethod())
+ }
+
+ update_schema <- TRUE
+ }
+ }
+
+ # A full-slice, single-chunk vctr is already represented by exactly the
+ # ArrowArray requested by the caller. Reuse it instead of dispatching to a
+ # potentially expensive extension conversion.
+ slice <- vctr_as_slice(x)
+ chunks <- attr(x, "chunks", exact = TRUE)
+ offsets <- attr(x, "offsets", exact = TRUE)
+ if (length(chunks) == 1 && !is.null(slice)) {
+ is_full_slice <- isTRUE(slice[1] == 1L) && slice[2] == max(offsets)
+ if (is_full_slice) {
+ if (!update_schema) {
+ return(chunks[[1]])
+ }
+
+ # Export a shallow copy so that attaching the requested field name does
+ # not modify the schema associated with the vctr's source chunk.
+ out <- nanoarrow_allocate_array()
+ nanoarrow_pointer_export(chunks[[1]], out)
+ nanoarrow_array_set_schema(out, schema, validate = FALSE)
+ return(out)
+ }
+
+ # An empty slice has no meaningful start index; any zero-length slice of
+ # the source array represents it. Otherwise, convert the 1-based vector
+ # index to an offset relative to the source array.
+ slice_offset <- if (slice[2] == 0L) 0L else slice[1] - 1L
+ if (!is.na(slice_offset)) {
+ out <- vctr_array_slice(chunks[[1]], slice_offset, slice[2])
+ if (update_schema) {
+ nanoarrow_array_set_schema(out, schema, validate = FALSE)
+ }
+
+ return(out)
+ }
+ }
+
+ NextMethod()
+}
+
#' @export
as_nanoarrow_array_stream.nanoarrow_vctr <- function(x, ..., schema = NULL) {
as_nanoarrow_array_stream.nanoarrow_vctr(x, ..., schema = schema)
@@ -266,7 +322,10 @@ c.nanoarrow_vctr <- function(...) {
#' @export
as.data.frame.nanoarrow_vctr <- function(x, ..., optional = FALSE) {
if (!optional) {
- stop(sprintf("cannot coerce object of tyoe '%s' to data.frame", class(x)[1]))
+ stop(sprintf(
+ "cannot coerce object of tyoe '%s' to data.frame",
+ class(x)[1]
+ ))
} else {
new_data_frame(list(x), nrow = length(x))
}
@@ -335,9 +394,14 @@ vctr_as_slice <- function(x) {
vctr_array_slice <- function(x, offset, length) {
new_offset <- x$offset + offset
new_length <- length
+ new_null_count <- if (length == 0 || x$null_count == 0) 0 else -1
nanoarrow_array_modify(
x,
- list(offset = new_offset, length = new_length),
+ list(
+ offset = new_offset,
+ length = new_length,
+ null_count = new_null_count
+ ),
validate = FALSE
)
}
diff --git a/r/tests/testthat/test-schema.R b/r/tests/testthat/test-schema.R
index 708b1bed..321ed807 100644
--- a/r/tests/testthat/test-schema.R
+++ b/r/tests/testthat/test-schema.R
@@ -34,6 +34,23 @@ test_that("as_nanoarrow_schema() works for nanoarrow_schema", {
expect_identical(as_nanoarrow_schema(schema), schema)
})
+test_that("nanoarrow_schema_identical() can ignore the root name", {
+ schema <- na_int32()
+ named_schema <- na_int32()
+ named_schema$name <- "x"
+
+ expect_false(nanoarrow_schema_identical(schema, named_schema))
+ expect_true(
+ nanoarrow_schema_identical(schema, named_schema, ignore_name = TRUE)
+ )
+
+ schema_x <- na_struct(list(x = na_int32()))
+ schema_y <- na_struct(list(y = na_int32()))
+ expect_false(
+ nanoarrow_schema_identical(schema_x, schema_y, ignore_name = TRUE)
+ )
+})
+
test_that("infer_nanoarrow_schema() errors for unsupported types", {
expect_error(
infer_nanoarrow_schema(environment()),
diff --git a/r/tests/testthat/test-vctr.R b/r/tests/testthat/test-vctr.R
index 95bbd14f..82451690 100644
--- a/r/tests/testthat/test-vctr.R
+++ b/r/tests/testthat/test-vctr.R
@@ -28,7 +28,7 @@ test_that("as_nanoarrow_vctr() works for basic input", {
test_that("print() and str() work on empty nanoarrow_vctr", {
vctr <- nanoarrow_vctr()
expect_identical(
- expect_output(print(vctr),"<nanoarrow_vctr <any>>"),
+ expect_output(print(vctr), "<nanoarrow_vctr <any>>"),
vctr
)
@@ -80,6 +80,78 @@ test_that("nanoarrow_vctr works in a data.frame()", {
expect_error(as.data.frame(vctr), "cannot coerce object")
})
+test_that("single-chunk nanoarrow_vctr converts to an array without copying", {
+ vctr <- as_nanoarrow_vctr(as_nanoarrow_array(c("one", "two")))
+ chunk <- attr(vctr, "chunks", exact = TRUE)[[1]]
+
+ expect_identical(as_nanoarrow_array(vctr), chunk)
+ expect_identical(as_nanoarrow_array(vctr, schema = na_string()), chunk)
+
+ named_schema <- na_string()
+ named_schema$name <- "x"
+ named_array <- as_nanoarrow_array(vctr, schema = named_schema)
+ expect_false(identical(named_array, chunk))
+ expect_identical(infer_nanoarrow_schema(named_array)$name, "x")
+
+ array <- as_nanoarrow_array(data.frame(x = vctr))
+ expect_identical(as.data.frame(array), data.frame(x = c("one", "two")))
+ expect_identical(infer_nanoarrow_schema(chunk)$name, "")
+})
+
+test_that("single-chunk nanoarrow_vctr slices convert to shallow arrays", {
+ array <- as_nanoarrow_array(c("one", NA, "three"))
+ vctr <- as_nanoarrow_vctr(array)
+
+ sliced <- as_nanoarrow_array(vctr[2:3])
+ expect_identical(sliced$offset, 1L)
+ expect_identical(sliced$length, 2L)
+ expect_identical(sliced$null_count, -1L)
+ expect_identical(as.vector(sliced), c(NA_character_, "three"))
+
+ named_schema <- na_string()
+ named_schema$name <- "x"
+ named_slice <- as_nanoarrow_array(vctr[2:3], schema = named_schema)
+ expect_identical(infer_nanoarrow_schema(named_slice)$name, "x")
+ expect_identical(as.vector(named_slice), c(NA_character_, "three"))
+
+ empty <- as_nanoarrow_array(vctr[integer()])
+ expect_identical(empty$length, 0L)
+ expect_identical(empty$null_count, 0L)
+ expect_identical(as.vector(empty), character())
+
+ expect_identical(array$offset, 0L)
+ expect_identical(array$length, 3L)
+ expect_identical(array$null_count, 1L)
+})
+
+test_that("nanoarrow_vctr array fast path falls through for other cases", {
+ array <- as_nanoarrow_array(c("one", "two"))
+ vctr <- as_nanoarrow_vctr(array)
+ multi_chunk_vctr <- new_nanoarrow_vctr(list(array, array), na_string())
+ fallback <- structure(list(), class = "fallback_array")
+ local_mocked_bindings(
+ as_nanoarrow_array.default = function(...) fallback,
+ .package = "nanoarrow"
+ )
+
+ # Schema differences other than the root name are not identity casts.
+ expect_identical(
+ as_nanoarrow_array(vctr, schema = na_large_string()),
+ fallback
+ )
+
+ metadata_schema <- na_string()
+ metadata_schema$metadata <- list(key = "value")
+ expect_identical(as_nanoarrow_array(vctr, schema = metadata_schema), fallback)
+
+ flags_schema <- na_string()
+ flags_schema$flags <- 0L
+ expect_identical(as_nanoarrow_array(vctr, schema = flags_schema), fallback)
+
+ # Multiple chunks cannot reuse a single complete array.
+ expect_identical(as_nanoarrow_array(multi_chunk_vctr), fallback)
+})
+
test_that("format() works for nanoarrow_vctr", {
empty_vctr <- nanoarrow_vctr(na_string())
expect_identical(format(empty_vctr), character())
@@ -122,7 +194,10 @@ test_that("nanoarrow_vctr to stream generates identical stream for identity slic
test_that("nanoarrow_vctr to stream works for arbitrary slices", {
array1 <- as_nanoarrow_array(c("one", "two", "three"))
array2 <- as_nanoarrow_array(c("four", "five", "six", "seven"))
- vctr <- new_nanoarrow_vctr(list(array1, array2), infer_nanoarrow_schema(array1))
+ vctr <- new_nanoarrow_vctr(
+ list(array1, array2),
+ infer_nanoarrow_schema(array1)
+ )
chunks16 <- collect_array_stream(
as_nanoarrow_array_stream(vctr[1:6])