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 2b2e5c17 chore(r): Add tests for nested dictionary conversion (#918)
2b2e5c17 is described below
commit 2b2e5c17133749e95139dc941ec0673cfbd3295c
Author: Dewey Dunnington <[email protected]>
AuthorDate: Thu Jul 30 21:58:55 2026 -0400
chore(r): Add tests for nested dictionary conversion (#918)
Adds some tests. They all pass, but I thought i should check my release
post claim of "supportes nested dictionaries" before publishing. I am
also not sure this case is tested in the integration testing files.
---
r/tests/testthat/test-convert-array-stream.R | 96 ++++++++++++++++++++++++++++
r/tests/testthat/test-convert-array.R | 75 ++++++++++++++++++++++
r/tests/testthat/test-ipc.R | 89 ++++++++++++++++++++++++++
3 files changed, 260 insertions(+)
diff --git a/r/tests/testthat/test-convert-array-stream.R
b/r/tests/testthat/test-convert-array-stream.R
index 068e3958..da81c08e 100644
--- a/r/tests/testthat/test-convert-array-stream.R
+++ b/r/tests/testthat/test-convert-array-stream.R
@@ -345,3 +345,99 @@ test_that("fixed-size stream conversion errors when the
output has insufficient
"Expected to materialize 100 values in batch 1 but materialized 2"
)
})
+
+test_that("convert array stream works for dictionaries of structs of
dictionaries", {
+ skip_if_not_installed("arrow")
+ skip_if_not_installed("tibble")
+
+ # Create a record batch with a dictionary inside a struct. Use tibble
+ # for slightly easier nested data frame construction.
+ df <- tibble::tibble(
+ idx = 1:26,
+ letter = letters,
+ nested = data.frame(idx2 = 1:26, LETTER = LETTERS)
+ )
+
+ schema <- arrow::schema(
+ idx = arrow::int32(),
+ letter = arrow::utf8(),
+ nested = arrow::struct(
+ idx2 = arrow::int32(),
+ LETTER = arrow::dictionary(
+ index_type = arrow::int32(),
+ value_type = arrow::large_utf8()
+ )
+ )
+ )
+
+ batch <- arrow::record_batch(df)$cast(schema)
+
+ # Modify the batch so that the inner struct is the values member of a dict
+ na_batch <- as_nanoarrow_array(batch)
+ na_batch$children$nested <- nanoarrow_array_modify(
+ nanoarrow_array_init(
+ as_nanoarrow_schema(
+ arrow::dictionary(
+ index_type = arrow::int32(),
+ value_type = arrow::struct(
+ idx2 = arrow::int32(),
+ LETTER = arrow::dictionary(
+ index_type = arrow::int8(),
+ value_type = arrow::large_utf8()
+ )
+ )
+ )
+ )
+ ),
+ list(
+ length = 26,
+ null_count = 0,
+ buffers = list(
+ NULL,
+ 0:25
+ ),
+ dictionary = na_batch$children$nested
+ ),
+ )
+
+ # Split into multiple batches (first 13 rows and last 13 rows)
+ batch_with_nested_dictionary <- arrow::as_record_batch(na_batch)
+ batch1 <- batch_with_nested_dictionary$Take(0:12)
+ batch2 <- batch_with_nested_dictionary$Take(13:25)
+
+ # Create a stream with multiple batches
+ stream <- basic_array_stream(
+ list(
+ as_nanoarrow_array(batch1),
+ as_nanoarrow_array(batch2)
+ )
+ )
+
+ # Ensure the stream converts back to the original
+ expect_identical(
+ convert_array_stream(stream),
+ as.data.frame(df)
+ )
+
+ # Also test with reversed batches
+ reversed_batch1 <- batch_with_nested_dictionary$Take(12:0)
+ reversed_batch2 <- batch_with_nested_dictionary$Take(25:13)
+
+ reversed_stream <- basic_array_stream(
+ list(
+ as_nanoarrow_array(reversed_batch1),
+ as_nanoarrow_array(reversed_batch2)
+ )
+ )
+
+ expected_reversed <- rbind(
+ as.data.frame(df[13:1, ]),
+ as.data.frame(df[26:14, ])
+ )
+ row.names(expected_reversed) <- 1:26
+
+ expect_identical(
+ convert_array_stream(reversed_stream),
+ expected_reversed
+ )
+})
diff --git a/r/tests/testthat/test-convert-array.R
b/r/tests/testthat/test-convert-array.R
index 7b7be069..8ceca779 100644
--- a/r/tests/testthat/test-convert-array.R
+++ b/r/tests/testthat/test-convert-array.R
@@ -1332,3 +1332,78 @@ test_that("convert to vector works for lists nested in
data frames", {
df_in_list_in_df
)
})
+
+test_that("convert to vector works for dictionaries of structs of
dictionaries", {
+ skip_if_not_installed("arrow")
+ skip_if_not_installed("tibble")
+
+ # Create a record batch with a dictionary inside a struct. Use tibble
+ # for slightly easier nested data frame construction.
+ df <- tibble::tibble(
+ idx = 1:26,
+ letter = letters,
+ nested = data.frame(idx2 = 1:26, LETTER = LETTERS)
+ )
+
+ schema <- arrow::schema(
+ idx = arrow::int32(),
+ letter = arrow::utf8(),
+ nested = arrow::struct(
+ idx2 = arrow::int32(),
+ LETTER = arrow::dictionary(
+ index_type = arrow::int32(),
+ value_type = arrow::large_utf8()
+ )
+ )
+ )
+
+ batch <- arrow::record_batch(df)$cast(schema)
+
+ # Modify the batch so that the inner struct is the values member of a dict
+ na_batch <- as_nanoarrow_array(batch)
+ na_batch$children$nested <- nanoarrow_array_modify(
+ nanoarrow_array_init(
+ as_nanoarrow_schema(
+ arrow::dictionary(
+ index_type = arrow::int32(),
+ value_type = arrow::struct(
+ idx2 = arrow::int32(),
+ LETTER = arrow::dictionary(
+ index_type = arrow::int8(),
+ value_type = arrow::large_utf8()
+ )
+ )
+ )
+ )
+ ),
+ list(
+ length = 26,
+ null_count = 0,
+ buffers = list(
+ NULL,
+ 0:25
+ ),
+ dictionary = na_batch$children$nested
+ ),
+ )
+
+ # Ensure the batch converts back to the original
+ expect_identical(
+ convert_array(na_batch),
+ as.data.frame(df)
+ )
+
+ # Ensure a reversed batch converts back to the original
+ batch_with_nested_dictionary <- arrow::as_record_batch(na_batch)
+ reversed_batch_with_nested_dictionary <- batch_with_nested_dictionary$Take(
+ (batch$num_rows - 1):0
+ )
+
+ converted_reversed <- convert_array(
+ as_nanoarrow_array(reversed_batch_with_nested_dictionary)
+ )
+ expect_identical(
+ converted_reversed,
+ as.data.frame(df[rev(seq_len(nrow(df))), ])
+ )
+})
diff --git a/r/tests/testthat/test-ipc.R b/r/tests/testthat/test-ipc.R
index 69f96aa2..1daab86b 100644
--- a/r/tests/testthat/test-ipc.R
+++ b/r/tests/testthat/test-ipc.R
@@ -433,3 +433,92 @@ test_that("read_nanoarrow() works for dictionary arrays
produced by arrow", {
df$col <- as.character(df$col)
expect_identical(result, do.call(rbind, rep(list(df), 10)))
})
+
+test_that("read_nanoarrow() works for dictionaries of structs of
dictionaries", {
+ skip_if_not_installed("arrow")
+ skip_if_not_installed("tibble")
+
+ # Create a record batch with a dictionary inside a struct. Use tibble
+ # for slightly easier nested data frame construction.
+ df <- tibble::tibble(
+ idx = 1:26,
+ letter = letters,
+ nested = data.frame(idx2 = 1:26, LETTER = LETTERS)
+ )
+
+ schema <- arrow::schema(
+ idx = arrow::int32(),
+ letter = arrow::utf8(),
+ nested = arrow::struct(
+ idx2 = arrow::int32(),
+ LETTER = arrow::dictionary(
+ index_type = arrow::int32(),
+ value_type = arrow::large_utf8()
+ )
+ )
+ )
+
+ batch <- arrow::record_batch(df)$cast(schema)
+
+ # Modify the batch so that the inner struct is the values member of a dict
+ na_batch <- as_nanoarrow_array(batch)
+ na_batch$children$nested <- nanoarrow_array_modify(
+ nanoarrow_array_init(
+ as_nanoarrow_schema(
+ arrow::dictionary(
+ index_type = arrow::int32(),
+ value_type = arrow::struct(
+ idx2 = arrow::int32(),
+ LETTER = arrow::dictionary(
+ index_type = arrow::int8(),
+ value_type = arrow::large_utf8()
+ )
+ )
+ )
+ )
+ ),
+ list(
+ length = 26,
+ null_count = 0,
+ buffers = list(
+ NULL,
+ 0:25
+ ),
+ dictionary = na_batch$children$nested
+ ),
+ )
+
+ # Create a reversed batch as well (same dictionary, different indices)
+ batch_with_nested_dictionary <- arrow::as_record_batch(na_batch)
+ reversed_batch_with_nested_dictionary <- batch_with_nested_dictionary$Take(
+ (batch$num_rows - 1):0
+ )
+
+ # Test IPC roundtrip with multiple batches
+ tf <- tempfile()
+ on.exit(unlink(tf))
+
+ # Write multiple batches using RecordBatchStreamWriter
+ sink <- arrow::FileOutputStream$create(tf)
+ writer <- arrow::RecordBatchStreamWriter$create(
+ sink,
+ batch_with_nested_dictionary$schema
+ )
+
+ # Write the forward batch and the reversed batch
+ writer$write_batch(batch_with_nested_dictionary)
+ writer$write_batch(reversed_batch_with_nested_dictionary)
+ writer$close()
+ sink$close()
+
+ # Read back via nanoarrow and verify result
+ result <- convert_array_stream(read_nanoarrow(tf))
+
+ expected <- rbind(
+ as.data.frame(df),
+ as.data.frame(df[rev(seq_len(nrow(df))), ])
+ )
+ row.names(expected) <- seq_len(nrow(expected))
+
+ expect_identical(result, expected)
+})