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 02824cc4 fix(r): correct two int64 corruption bugs affecting
list<int64> conversion (#933)
02824cc4 is described below
commit 02824cc4149ce1f7e80e25406da81ef76efbe99a
Author: Bruno Tremblay <[email protected]>
AuthorDate: Fri Sep 4 13:36:58 2026 -0400
fix(r): correct two int64 corruption bugs affecting list<int64> conversion
(#933)
Fixes #932.
While investigating why converting a `list<int64>` array to a
`vctrs::list_of(ptype = bit64::integer64())` target silently produced
wrong values (not just a lossy-precision warning), I found two separate,
independent bugs, both in `r/`:
**1. `as_nanoarrow_array.list()` corrupted `integer64` list elements
while building the child array**
It built the flattened child vector with:
```r
child <- unlist(x, recursive = FALSE, use.names = FALSE)
```
`unlist()` strips the `integer64` S3 class from each element before
concatenating, so the elements are concatenated as plain `numeric`,
reinterpreting the raw 64-bit integer bit pattern as a double bit
pattern. The resulting child array is corrupted before conversion even
runs. Fixed by using `do.call(c, x)`, which dispatches to `c.integer64`
(and any other registered `c()` S3 method) and preserves the
class/underlying representation.
**2. `nanoarrow_materialize_int64()` used the wrong buffer view for
pointer arithmetic**
```c
case NANOARROW_TYPE_INT64:
memcpy(result + dst->offset,
src->array_view->buffer_views[1].data.as_int32 + raw_src_offset,
dst->length * sizeof(int64_t));
```
`raw_src_offset` is added to `data.as_int32`, so the offset is scaled by
4 bytes instead of 8. Any int64 array/slice with a nonzero starting
offset — such as the per-row child slices nanoarrow builds internally
when materializing a `list<int64>` column — reads from the wrong memory
location. Offset-0 conversions (the case covered by existing tests) are
unaffected, which is presumably why this slipped through since it was
introduced in #293.
Both bugs needed fixing to correctly round-trip a `list<int64>` through
`bit64::integer64`; either one alone still corrupts values.
**Testing**
Added regression tests to `test-as-array.R` and `test-convert-array.R`.
I confirmed both new tests fail against the pre-fix code and pass after
the fix (see repro below), and that the full existing `r/tests/testthat`
suite still passes (1408 passed, 0 failed).
Minimal repro of bug 1 (pre-fix):
```r
library(nanoarrow)
schema <- na_list(na_int64())
arr <- as_nanoarrow_array(
list(bit64::as.integer64(c("9223372036854775295", "2")),
bit64::as.integer64("3")),
schema = schema
)
to <- vctrs::new_list_of(list(), ptype = bit64::integer64())
convert_array(arr, to = to)
#> [[1]]
#> integer64
#> [1] <NA> 0
#>
#> [[2]]
#> integer64
#> [1] 0
```
After this PR, this returns `list(c(9223372036854775295, 2), 3)` as
expected.
Originally reported against a downstream package:
https://github.com/meztez/bigrquerystorage/issues/86.
---
r/R/as-array.R | 2 +-
r/src/materialize_int64.h | 2 +-
r/tests/testthat/test-as-array.R | 23 +++++++++++++++++++++++
r/tests/testthat/test-convert-array.R | 18 ++++++++++++++++++
4 files changed, 43 insertions(+), 2 deletions(-)
diff --git a/r/R/as-array.R b/r/R/as-array.R
index d3aa5ee9..c45fdf24 100644
--- a/r/R/as-array.R
+++ b/r/R/as-array.R
@@ -257,7 +257,7 @@ as_nanoarrow_array.list <- function(x, ..., schema = NULL) {
array <- nanoarrow_array_init(schema)
- child <- unlist(x, recursive = FALSE, use.names = FALSE)
+ child <- do.call(c, x)
if (is.null(child)) {
child_array <- as_nanoarrow_array.vctrs_unspecified(logical(), schema =
na_na())
} else {
diff --git a/r/src/materialize_int64.h b/r/src/materialize_int64.h
index ad83671e..086a7505 100644
--- a/r/src/materialize_int64.h
+++ b/r/src/materialize_int64.h
@@ -49,7 +49,7 @@ static inline int nanoarrow_materialize_int64(struct
ArrayViewSlice* src,
break;
case NANOARROW_TYPE_INT64:
memcpy(result + dst->offset,
- src->array_view->buffer_views[1].data.as_int32 + raw_src_offset,
+ src->array_view->buffer_views[1].data.as_int64 + raw_src_offset,
dst->length * sizeof(int64_t));
// Set any nulls to NA_INTEGER64
diff --git a/r/tests/testthat/test-as-array.R b/r/tests/testthat/test-as-array.R
index 6157bf55..7c301e0b 100644
--- a/r/tests/testthat/test-as-array.R
+++ b/r/tests/testthat/test-as-array.R
@@ -824,6 +824,29 @@ test_that("as_nanoarrow_array() works for list(integer())
-> na_list(na_int32())
expect_identical(array$children[[1]]$length, 10L)
})
+test_that("as_nanoarrow_array() works for list(integer64()) ->
na_list(na_int64())", {
+ # GH932: unlist() silently strips the integer64 class from list elements,
+ # reinterpreting the underlying int64 bit pattern as a double and corrupting
+ # values before the child array is even built.
+ skip_if_not_installed("bit64")
+
+ big <- bit64::as.integer64(c("9223372036854775295", "2"))
+ x <- list(big, bit64::as.integer64("3"))
+ array <- as_nanoarrow_array(x, schema = na_list(na_int64()))
+
+ expect_identical(infer_nanoarrow_schema(array)$format, "+l")
+ expect_identical(array$length, 2L)
+ expect_identical(array$null_count, 0L)
+ expect_identical(infer_nanoarrow_schema(array$children[[1]])$format, "l")
+ expect_identical(array$children[[1]]$length, 3L)
+
+ to <- vctrs::new_list_of(list(), ptype = bit64::integer64())
+ expect_identical(
+ convert_array(array, to),
+ vctrs::new_list_of(list(big, bit64::as.integer64("3")), ptype =
bit64::integer64())
+ )
+})
+
test_that("as_nanoarrow_array() works for unspecified() -> na_na()", {
skip_if_not_installed("vctrs")
diff --git a/r/tests/testthat/test-convert-array.R
b/r/tests/testthat/test-convert-array.R
index 8ceca779..deedb7e3 100644
--- a/r/tests/testthat/test-convert-array.R
+++ b/r/tests/testthat/test-convert-array.R
@@ -744,6 +744,24 @@ test_that("convert to vector works for valid integer64()",
{
)
})
+test_that("convert to vector works for int64 with a nonzero offset", {
+ # GH932: the int64 materializer read from buffer_views[1].data.as_int32
+ # (i.e., with int32-sized pointer arithmetic) instead of as_int64, so any
+ # slice with a nonzero starting offset (e.g., a list child sliced per-row)
+ # read from the wrong memory location.
+ skip_if_not_installed("bit64")
+
+ vals <- bit64::as.integer64(c("9223372036854775295", "2", "3"))
+ array <- as_nanoarrow_array(vals, schema = na_int64())
+
+ sliced <- nanoarrow_array_modify(array, list(offset = 1L, length = 2L))
+
+ expect_identical(
+ convert_array(sliced, bit64::integer64()),
+ vals[2:3]
+ )
+})
+
test_that("convert to vector works for null -> integer64()", {
skip_if_not_installed("bit64")