From f3f6d19c7c81faed425a9c1eb049643e2b6e5920 Mon Sep 17 00:00:00 2001 From: Nic Crane Date: Sun, 5 Jul 2026 14:43:05 +0100 Subject: [PATCH] Always convert uint64 to double --- r/NEWS.md | 5 +++- r/R/type.R | 14 +++++------ r/man/acero.Rd | 2 +- r/man/data-type.Rd | 14 +++++------ r/src/array_to_vector.cpp | 9 ++----- r/src/arrowExports.cpp | 44 +++++++++++++++++------------------ r/tests/testthat/test-Array.R | 29 +++++++++++++++++------ r/vignettes/data_types.Rmd | 11 +++++---- 8 files changed, 71 insertions(+), 57 deletions(-) diff --git a/r/NEWS.md b/r/NEWS.md index b80639fd0e3..040fa783fd7 100644 --- a/r/NEWS.md +++ b/r/NEWS.md @@ -19,7 +19,10 @@ # arrow 24.0.0.9000 -# arrow 24.0.0 +- Arrow `uint64` types are now always converted to R `double` (numeric) vectors, + regardless of the values. Previously, small `uint64` values were converted to + R `integer`, which could cause inconsistent types within list columns when + different list elements had different value ranges (#50339). # arrow 24.0.0 diff --git a/r/R/type.R b/r/R/type.R index 27cb0afe3db..120cff810b1 100644 --- a/r/R/type.R +++ b/r/R/type.R @@ -356,13 +356,13 @@ NestedType <- R6Class("NestedType", inherit = DataType) #' `date32()` creates a datetime type with a "day" unit, like the R `Date` #' class. `date64()` has a "ms" unit. #' -#' `uint32` (32 bit unsigned integer), `uint64` (64 bit unsigned integer), and -#' `int64` (64-bit signed integer) types may contain values that exceed the -#' range of R's `integer` type (32-bit signed integer). When these arrow objects -#' are translated to R objects, `uint32` and `uint64` are converted to `double` -#' ("numeric") and `int64` is converted to `bit64::integer64`. For `int64` -#' types, this conversion can be disabled (so that `int64` always yields a -#' `bit64::integer64` object) by setting `options(arrow.int64_downcast = +#' `uint64` (64 bit unsigned integer) is always converted to `double` +#' ("numeric") in R. `uint32` (32 bit unsigned integer) and `int64` (64-bit +#' signed integer) types may contain values that exceed the range of R's +#' `integer` type (32-bit signed integer). When they do, `uint32` is converted +#' to `double` ("numeric") and `int64` is converted to `bit64::integer64`. For +#' `int64` types, this conversion can be disabled (so that `int64` always yields +#' a `bit64::integer64` object) by setting `options(arrow.int64_downcast = #' FALSE)`. #' #' `decimal128()` creates a `Decimal128Type`. Arrow decimals are fixed-point diff --git a/r/man/acero.Rd b/r/man/acero.Rd index 3821eaef3a9..8f480553548 100644 --- a/r/man/acero.Rd +++ b/r/man/acero.Rd @@ -72,7 +72,7 @@ can assume that the function works in Acero just as it does in R. Functions can be called either as \code{pkg::fun()} or just \code{fun()}, i.e. both \code{str_sub()} and \code{stringr::str_sub()} work. -In addition to these functions, you can call any of Arrow's 281 compute +In addition to these functions, you can call any of Arrow's 253 compute functions directly. Arrow has many functions that don't map to an existing R function. In other cases where there is an R function mapping, you can still call the Arrow function directly if you don't want the adaptations that the R diff --git a/r/man/data-type.Rd b/r/man/data-type.Rd index aa11c222bc5..2cc3cc45449 100644 --- a/r/man/data-type.Rd +++ b/r/man/data-type.Rd @@ -171,13 +171,13 @@ A few functions have aliases: \code{date32()} creates a datetime type with a "day" unit, like the R \code{Date} class. \code{date64()} has a "ms" unit. -\code{uint32} (32 bit unsigned integer), \code{uint64} (64 bit unsigned integer), and -\code{int64} (64-bit signed integer) types may contain values that exceed the -range of R's \code{integer} type (32-bit signed integer). When these arrow objects -are translated to R objects, \code{uint32} and \code{uint64} are converted to \code{double} -("numeric") and \code{int64} is converted to \code{bit64::integer64}. For \code{int64} -types, this conversion can be disabled (so that \code{int64} always yields a -\code{bit64::integer64} object) by setting \code{options(arrow.int64_downcast = FALSE)}. +\code{uint64} (64 bit unsigned integer) is always converted to \code{double} +("numeric") in R. \code{uint32} (32 bit unsigned integer) and \code{int64} (64-bit +signed integer) types may contain values that exceed the range of R's +\code{integer} type (32-bit signed integer). When they do, \code{uint32} is converted +to \code{double} ("numeric") and \code{int64} is converted to \code{bit64::integer64}. For +\code{int64} types, this conversion can be disabled (so that \code{int64} always yields +a \code{bit64::integer64} object) by setting \code{options(arrow.int64_downcast = FALSE)}. \code{decimal128()} creates a \code{Decimal128Type}. Arrow decimals are fixed-point decimal numbers encoded as a scalar integer. The \code{precision} is the number of diff --git a/r/src/array_to_vector.cpp b/r/src/array_to_vector.cpp index 0992181acfe..dc02c711d1e 100644 --- a/r/src/array_to_vector.cpp +++ b/r/src/array_to_vector.cpp @@ -1296,13 +1296,8 @@ std::shared_ptr Converter::Make( } case Type::UINT64: - if (ArraysCanFitInteger(chunked_array->chunks())) { - return std::make_shared>( - chunked_array); - } else { - return std::make_shared>( - chunked_array); - } + return std::make_shared>( + chunked_array); case Type::HALF_FLOAT: return std::make_shared>( diff --git a/r/src/arrowExports.cpp b/r/src/arrowExports.cpp index 5482c8679f6..8a67d0acd89 100644 --- a/r/src/arrowExports.cpp +++ b/r/src/arrowExports.cpp @@ -3238,22 +3238,6 @@ BEGIN_CPP11 END_CPP11 } // field.cpp -bool Field__nullable(const std::shared_ptr& field); -extern "C" SEXP _arrow_Field__nullable(SEXP field_sexp){ -BEGIN_CPP11 - arrow::r::Input&>::type field(field_sexp); - return cpp11::as_sexp(Field__nullable(field)); -END_CPP11 -} -// field.cpp -std::shared_ptr Field__type(const std::shared_ptr& field); -extern "C" SEXP _arrow_Field__type(SEXP field_sexp){ -BEGIN_CPP11 - arrow::r::Input&>::type field(field_sexp); - return cpp11::as_sexp(Field__type(field)); -END_CPP11 -} -// field.cpp bool Field__HasMetadata(const std::shared_ptr& field); extern "C" SEXP _arrow_Field__HasMetadata(SEXP field_sexp){ BEGIN_CPP11 @@ -3286,6 +3270,22 @@ BEGIN_CPP11 return cpp11::as_sexp(Field__RemoveMetadata(field)); END_CPP11 } +// field.cpp +bool Field__nullable(const std::shared_ptr& field); +extern "C" SEXP _arrow_Field__nullable(SEXP field_sexp){ +BEGIN_CPP11 + arrow::r::Input&>::type field(field_sexp); + return cpp11::as_sexp(Field__nullable(field)); +END_CPP11 +} +// field.cpp +std::shared_ptr Field__type(const std::shared_ptr& field); +extern "C" SEXP _arrow_Field__type(SEXP field_sexp){ +BEGIN_CPP11 + arrow::r::Input&>::type field(field_sexp); + return cpp11::as_sexp(Field__type(field)); +END_CPP11 +} // filesystem.cpp fs::FileType fs___FileInfo__type(const std::shared_ptr& x); extern "C" SEXP _arrow_fs___FileInfo__type(SEXP x_sexp){ @@ -5878,10 +5878,10 @@ static const R_CallMethodDef CallEntries[] = { { "_arrow_compute__GetFunctionNames", (DL_FUNC) &_arrow_compute__GetFunctionNames, 0}, { "_arrow_compute__Initialize", (DL_FUNC) &_arrow_compute__Initialize, 0}, { "_arrow_RegisterScalarUDF", (DL_FUNC) &_arrow_RegisterScalarUDF, 2}, - { "_arrow_build_info", (DL_FUNC) &_arrow_build_info, 0}, - { "_arrow_runtime_info", (DL_FUNC) &_arrow_runtime_info, 0}, - { "_arrow_set_timezone_database", (DL_FUNC) &_arrow_set_timezone_database, 1}, - { "_arrow_csv___WriteOptions__initialize", (DL_FUNC) &_arrow_csv___WriteOptions__initialize, 1}, + { "_arrow_build_info", (DL_FUNC) &_arrow_build_info, 0}, + { "_arrow_runtime_info", (DL_FUNC) &_arrow_runtime_info, 0}, + { "_arrow_set_timezone_database", (DL_FUNC) &_arrow_set_timezone_database, 1}, + { "_arrow_csv___WriteOptions__initialize", (DL_FUNC) &_arrow_csv___WriteOptions__initialize, 1}, { "_arrow_csv___ReadOptions__initialize", (DL_FUNC) &_arrow_csv___ReadOptions__initialize, 1}, { "_arrow_csv___ParseOptions__initialize", (DL_FUNC) &_arrow_csv___ParseOptions__initialize, 1}, { "_arrow_csv___ReadOptions__column_names", (DL_FUNC) &_arrow_csv___ReadOptions__column_names, 1}, @@ -6054,12 +6054,12 @@ static const R_CallMethodDef CallEntries[] = { { "_arrow_Field__ToString", (DL_FUNC) &_arrow_Field__ToString, 1}, { "_arrow_Field__name", (DL_FUNC) &_arrow_Field__name, 1}, { "_arrow_Field__Equals", (DL_FUNC) &_arrow_Field__Equals, 3}, - { "_arrow_Field__nullable", (DL_FUNC) &_arrow_Field__nullable, 1}, - { "_arrow_Field__type", (DL_FUNC) &_arrow_Field__type, 1}, { "_arrow_Field__HasMetadata", (DL_FUNC) &_arrow_Field__HasMetadata, 1}, { "_arrow_Field__metadata", (DL_FUNC) &_arrow_Field__metadata, 1}, { "_arrow_Field__WithMetadata", (DL_FUNC) &_arrow_Field__WithMetadata, 2}, { "_arrow_Field__RemoveMetadata", (DL_FUNC) &_arrow_Field__RemoveMetadata, 1}, + { "_arrow_Field__nullable", (DL_FUNC) &_arrow_Field__nullable, 1}, + { "_arrow_Field__type", (DL_FUNC) &_arrow_Field__type, 1}, { "_arrow_fs___FileInfo__type", (DL_FUNC) &_arrow_fs___FileInfo__type, 1}, { "_arrow_fs___FileInfo__set_type", (DL_FUNC) &_arrow_fs___FileInfo__set_type, 2}, { "_arrow_fs___FileInfo__path", (DL_FUNC) &_arrow_fs___FileInfo__path, 1}, diff --git a/r/tests/testthat/test-Array.R b/r/tests/testthat/test-Array.R index 9fcc3e6b868..80062a3e785 100644 --- a/r/tests/testthat/test-Array.R +++ b/r/tests/testthat/test-Array.R @@ -521,23 +521,25 @@ test_that("Array$as_vector() converts to integer (ARROW-3794)", { expect_as_vector(a, u8) }) -test_that("Arrays of {,u}int{32,64} convert to integer if they can fit", { +test_that("Arrays of {,u}int{32} convert to integer if they can fit", { u32 <- arrow_array(1L)$cast(uint32()) expect_identical(as.vector(u32), 1L) - u64 <- arrow_array(1L)$cast(uint64()) - expect_identical(as.vector(u64), 1L) - i64 <- arrow_array(bit64::as.integer64(1:10)) expect_identical(as.vector(i64), 1:10) }) -test_that("Arrays of uint{32,64} convert to numeric if they can't fit integer", { +test_that("Arrays of uint32 convert to numeric if they can't fit integer", { u32 <- arrow_array(bit64::as.integer64(1) + MAX_INT)$cast(uint32()) expect_identical(as.vector(u32), 1 + MAX_INT) +}) - u64 <- arrow_array(bit64::as.integer64(1) + MAX_INT)$cast(uint64()) - expect_identical(as.vector(u64), 1 + MAX_INT) +test_that("Arrays of uint64 always convert to numeric (double)", { + u64_small <- arrow_array(1L)$cast(uint64()) + expect_identical(as.vector(u64_small), 1) + + u64_large <- arrow_array(bit64::as.integer64(1) + MAX_INT)$cast(uint64()) + expect_identical(as.vector(u64_large), 1 + MAX_INT) }) test_that("arrow_array() recognise arrow::Array (ARROW-3815)", { @@ -1453,3 +1455,16 @@ test_that("Array handles negative fractional dates correctly (GH-46873)", { arr <- arrow_array(d) expect_equal(as.vector(arr), as.Date("1969-12-31", origin = "1970-01-01")) }) + +test_that("uint64 inside list columns always converts to double (GH-50339)", { + # When uint64 is nested inside a list, all elements should get the same type + # (double) regardless of whether individual values fit in int32 + list_arr <- arrow_array( + list(1, 9999999999), + type = list_of(uint64()) + ) + + result <- as.vector(list_arr) + expect_type(result[[1]], "double") + expect_type(result[[2]], "double") +}) diff --git a/r/vignettes/data_types.Rmd b/r/vignettes/data_types.Rmd index d5c70a8f02d..27c1b8e9750 100644 --- a/r/vignettes/data_types.Rmd +++ b/r/vignettes/data_types.Rmd @@ -99,7 +99,8 @@ chunked_array(c(10L, 3L, 200L), type = int8()) When translating from Arrow to R, integer types alway translate to R integers unless one of the following exceptions applies: -- If the value of an Arrow uint32 or uint64 falls outside the range allowed for R integers, the result will be a numeric vector in R +- Arrow uint64 types are always converted to numeric (double) vectors in R +- If the value of an Arrow uint32 falls outside the range allowed for R integers, the result will be a numeric vector in R - If the value of an Arrow int64 variable falls outside the range allowed for R integers, the result will be a `bit64::integer64` vector in R - If the user sets `options(arrow.int64_downcast = FALSE)`, the Arrow int64 type always yields a `bit64::integer64` vector in R regardless of the value @@ -351,7 +352,7 @@ to Arrow list type (which is a "list of" some type). | uint8 | integer | | uint16 | integer | | uint32 | integer ^1^ | -| uint64 | integer ^1^ | +| uint64 | double | | float16 | - ^2^ | | float32 | double | | float64 | double | @@ -376,9 +377,9 @@ to Arrow list type (which is a "list of" some type). | map | arrow_list ^5^ | | union | - ^2^ | -^1^: These integer types may contain values that exceed the range of R's -`integer` type (32 bit signed integer). When they do, `uint32` and `uint64` are -converted to `double` ("numeric") and `int64` is converted to +^1^: These integer types may contain values that exceed the range of R's +`integer` type (32 bit signed integer). When they do, `uint32` is +converted to `double` ("numeric") and `int64` is converted to `bit64::integer64`. This conversion can be disabled (so that `int64` always yields a `bit64::integer64` vector) by setting `options(arrow.int64_downcast = FALSE)`.