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 9dd6c4b0 feat: Add DictionaryBatch write support to IPC encoder and 
writer (#926)
9dd6c4b0 is described below

commit 9dd6c4b0b84a347039e109d24f67c3692269a0c9
Author: NIEK VERWEIJ <[email protected]>
AuthorDate: Tue Sep 8 13:35:22 2026 -0400

    feat: Add DictionaryBatch write support to IPC encoder and writer (#926)
    
    Hi, I had implemented dictionary batch writing for a project that I was
    working on, I noticed a thread
    (https://github.com/apache/arrow-nanoarrow/issues/622) about it, so I
    just wanted to share what I had in case it is useful. I needed this for
    a project that emits dictionary-encoded columns over IPC. I use
    ArrowIpcWriterWriteDictionaryBatch directly, for the PR I wired it into
    WriteArrayStream so dictionaries get emitted automatically on the
    high-level path and added a write→read round-trip test. Happy to also
    adjust.
    
    ---------
    
    Co-authored-by: niekverw <[email protected]>
    Co-authored-by: Dewey Dunnington <[email protected]>
    Co-authored-by: Dewey Dunnington <[email protected]>
---
 src/nanoarrow/ipc/encoder.c       | 116 +++++++++++++++++++++-
 src/nanoarrow/ipc/encoder_test.cc |  71 ++++++++++++++
 src/nanoarrow/ipc/writer.c        |  85 ++++++++++++++++
 src/nanoarrow/ipc/writer_test.cc  | 201 ++++++++++++++++++++++++++++++++++++++
 src/nanoarrow/nanoarrow_ipc.h     |  29 ++++++
 5 files changed, 500 insertions(+), 2 deletions(-)

diff --git a/src/nanoarrow/ipc/encoder.c b/src/nanoarrow/ipc/encoder.c
index 2c12a1b5..d1435f49 100644
--- a/src/nanoarrow/ipc/encoder.c
+++ b/src/nanoarrow/ipc/encoder.c
@@ -445,6 +445,13 @@ static ArrowErrorCode ArrowIpcEncodeField(
     flatcc_builder_t* builder, const struct ArrowSchema* schema,
     const struct ArrowIpcDictionaryEncodings* dictionary_encodings,
     struct ArrowError* error) {
+  // Check before ArrowSchemaViewInit(), which assumes dictionary values are 
not
+  // themselves dictionary-encoded.
+  if (schema->dictionary != NULL && schema->dictionary->dictionary != NULL) {
+    ArrowErrorSet(error, "IPC encoding of nested dictionary values 
unsupported");
+    return ENOTSUP;
+  }
+
   FLATCC_RETURN_UNLESS_0(Field_name_create_str(builder, schema->name), error);
   FLATCC_RETURN_UNLESS_0(
       Field_nullable_add(builder, (schema->flags & ARROW_FLAG_NULLABLE) != 0), 
error);
@@ -519,6 +526,15 @@ static ArrowErrorCode ArrowIpcEncodeField(
     // Add the dictionary encoding to the field
     FLATCC_RETURN_UNLESS_0(Field_dictionary_add(builder, dict_encoding_ref), 
error);
 
+    // Support dictionary values with children by encoding children from
+    // schema->dictionary (and add a roundtrip test for a nested value type).
+    // Using schema below would encode the index type's children instead and
+    // produce a Field whose type and children do not agree.
+    if (schema->dictionary->n_children != 0) {
+      ArrowErrorSet(error, "IPC encoding of dictionary values with children 
unsupported");
+      return ENOTSUP;
+    }
+
     NANOARROW_RETURN_NOT_OK(ArrowSchemaViewInit(&schema_view, 
schema->dictionary, error));
   }
 
@@ -689,8 +705,10 @@ static ArrowErrorCode ArrowIpcEncoderEncodeRecordBatchImpl(
   }
 
   if (array_view->dictionary != NULL) {
-    ArrowErrorSet(error, "Cannot encode dictionary arrays");
-    return ENOTSUP;
+    // Values live in a separate DictionaryBatch message per the Arrow IPC 
spec;
+    // the parent's index node + buffers were already emitted by the caller 
loop,
+    // so stop recursing here.
+    return NANOARROW_OK;
   }
 
   for (int64_t c = 0; c < array_view->n_children; ++c) {
@@ -783,6 +801,81 @@ ArrowErrorCode ArrowIpcEncoderEncodeSimpleRecordBatch(
   return ArrowIpcEncoderEncodeRecordBatch(encoder, &buffer_encoder, 
array_view, error);
 }
 
+static ArrowErrorCode ArrowIpcEncoderEncodeDictionaryBatch(
+    struct ArrowIpcEncoder* encoder, struct ArrowIpcBufferEncoder* 
buffer_encoder,
+    int64_t dictionary_id, char is_delta, const struct ArrowArrayView* 
values_view,
+    struct ArrowError* error) {
+  NANOARROW_DCHECK(encoder != NULL && encoder->private_data != NULL &&
+                   buffer_encoder != NULL && buffer_encoder->encode_buffer != 
NULL);
+  if (values_view->dictionary != NULL) {
+    ArrowErrorSet(error,
+                  "DictionaryBatch values array must not itself be 
dictionary-encoded");
+    return EINVAL;
+  }
+
+  struct ArrowIpcEncoderPrivate* private =
+      (struct ArrowIpcEncoderPrivate*)encoder->private_data;
+  flatcc_builder_t* builder = &private->builder;
+
+  FLATCC_RETURN_UNLESS_0(Message_start_as_root(builder), error);
+  FLATCC_RETURN_UNLESS_0(Message_version_add(builder, ns(MetadataVersion_V5)), 
error);
+
+  FLATCC_RETURN_UNLESS_0(Message_header_DictionaryBatch_start(builder), error);
+  FLATCC_RETURN_UNLESS_0(DictionaryBatch_id_add(builder, dictionary_id), 
error);
+  FLATCC_RETURN_UNLESS_0(DictionaryBatch_data_start(builder), error);
+  FLATCC_RETURN_UNLESS_0(RecordBatch_length_add(builder, values_view->length), 
error);
+
+  NANOARROW_ASSERT_OK(ArrowBufferResize(&private->buffers, 0, 0));
+  NANOARROW_ASSERT_OK(ArrowBufferResize(&private->nodes, 0, 0));
+
+  // The values array is a single top-level column. Emit the top-level node +
+  // buffers here, then descend into any nested children.
+  struct ns(FieldNode) top_node = {values_view->length, 
values_view->null_count};
+  NANOARROW_RETURN_NOT_OK_WITH_ERROR(
+      ArrowBufferAppend(&private->nodes, &top_node, sizeof(top_node)), error);
+  for (int64_t b = 0; b < values_view->array->n_buffers; ++b) {
+    struct ns(Buffer) buffer;
+    NANOARROW_RETURN_NOT_OK(buffer_encoder->encode_buffer(
+        values_view->buffer_views[b], encoder, buffer_encoder, &buffer.offset,
+        &buffer.length, error));
+    NANOARROW_RETURN_NOT_OK_WITH_ERROR(
+        ArrowBufferAppend(&private->buffers, &buffer, sizeof(buffer)), error);
+  }
+  NANOARROW_RETURN_NOT_OK(ArrowIpcEncoderEncodeRecordBatchImpl(
+      encoder, buffer_encoder, values_view, &private->buffers, 
&private->nodes, error));
+
+  FLATCC_RETURN_UNLESS_0(
+      RecordBatch_nodes_create(builder, (struct 
ns(FieldNode)*)private->nodes.data,
+                               private->nodes.size_bytes / sizeof(struct 
ns(FieldNode))),
+      error);
+  FLATCC_RETURN_UNLESS_0(
+      RecordBatch_buffers_create(builder, (struct 
ns(Buffer)*)private->buffers.data,
+                                 private->buffers.size_bytes / sizeof(struct 
ns(Buffer))),
+      error);
+  FLATCC_RETURN_UNLESS_0(DictionaryBatch_data_end(builder), error);
+  FLATCC_RETURN_UNLESS_0(DictionaryBatch_isDelta_add(builder, is_delta ? 1 : 
0), error);
+  FLATCC_RETURN_UNLESS_0(Message_header_DictionaryBatch_end(builder), error);
+  FLATCC_RETURN_UNLESS_0(Message_bodyLength_add(builder, 
buffer_encoder->body_length),
+                         error);
+  FLATCC_RETURN_IF_NULL(ns(Message_end_as_root(builder)), error);
+  return NANOARROW_OK;
+}
+
+ArrowErrorCode ArrowIpcEncoderEncodeSimpleDictionaryBatch(
+    struct ArrowIpcEncoder* encoder, int64_t dictionary_id, char is_delta,
+    const struct ArrowArrayView* values_view, struct ArrowBuffer* body_buffer,
+    struct ArrowError* error) {
+  NANOARROW_DCHECK(encoder != NULL && encoder->private_data != NULL &&
+                   body_buffer != NULL);
+  struct ArrowIpcBufferEncoder buffer_encoder = {
+      .encode_buffer = &ArrowIpcEncoderBuildContiguousBodyBufferCallback,
+      .encode_buffer_state = body_buffer,
+      .body_length = 0,
+  };
+  return ArrowIpcEncoderEncodeDictionaryBatch(encoder, &buffer_encoder, 
dictionary_id,
+                                              is_delta, values_view, error);
+}
+
 void ArrowIpcFooterInit(struct ArrowIpcFooter* footer) {
   footer->schema.release = NULL;
   ArrowBufferInit(&footer->record_batch_blocks);
@@ -837,6 +930,25 @@ ArrowErrorCode ArrowIpcEncoderEncodeFooter(struct 
ArrowIpcEncoder* encoder,
   }
   FLATCC_RETURN_UNLESS_0(Footer_recordBatches_end(builder), error);
 
+  const struct ArrowIpcFileBlock* dict_blocks =
+      (struct ArrowIpcFileBlock*)footer->dictionary_blocks.data;
+  int64_t n_dict_blocks =
+      footer->dictionary_blocks.size_bytes / sizeof(struct ArrowIpcFileBlock);
+
+  FLATCC_RETURN_UNLESS_0(Footer_dictionaries_start(builder), error);
+  struct ns(Block)* flatcc_dict_blocks =
+      ns(Footer_dictionaries_extend(builder, n_dict_blocks));
+  FLATCC_RETURN_IF_NULL(flatcc_dict_blocks, error);
+  for (int64_t i = 0; i < n_dict_blocks; i++) {
+    struct ns(Block) block = {
+        dict_blocks[i].offset,
+        dict_blocks[i].metadata_length,
+        dict_blocks[i].body_length,
+    };
+    flatcc_dict_blocks[i] = block;
+  }
+  FLATCC_RETURN_UNLESS_0(Footer_dictionaries_end(builder), error);
+
   FLATCC_RETURN_IF_NULL(ns(Footer_end_as_root(builder)), error);
   return NANOARROW_OK;
 }
diff --git a/src/nanoarrow/ipc/encoder_test.cc 
b/src/nanoarrow/ipc/encoder_test.cc
index 3eeff1ca..4fa0e4fe 100644
--- a/src/nanoarrow/ipc/encoder_test.cc
+++ b/src/nanoarrow/ipc/encoder_test.cc
@@ -106,6 +106,29 @@ TEST(NanoarrowIpcTest, NanoarrowIpcFooterEncoding) {
   EXPECT_GT(footer_buffer->size_bytes, raw_schema_buffer->size_bytes);
 }
 
+TEST(NanoarrowIpcTest, NanoarrowIpcEncoderRejectsNestedDictionary) {
+  nanoarrow::UniqueSchema schema;
+  ASSERT_EQ(ArrowSchemaInitFromType(schema.get(), NANOARROW_TYPE_STRUCT), 
NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateChildren(schema.get(), 1), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInitFromType(schema->children[0], NANOARROW_TYPE_INT32),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateDictionary(schema->children[0]), NANOARROW_OK);
+  ASSERT_EQ(
+      ArrowSchemaInitFromType(schema->children[0]->dictionary, 
NANOARROW_TYPE_INT32),
+      NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateDictionary(schema->children[0]->dictionary), 
NANOARROW_OK);
+  
ASSERT_EQ(ArrowSchemaInitFromType(schema->children[0]->dictionary->dictionary,
+                                    NANOARROW_TYPE_STRING),
+            NANOARROW_OK);
+
+  nanoarrow::ipc::UniqueEncoder encoder;
+  ASSERT_EQ(ArrowIpcEncoderInit(encoder.get()), NANOARROW_OK);
+
+  struct ArrowError error;
+  EXPECT_EQ(ArrowIpcEncoderEncodeSchema(encoder.get(), schema.get(), &error), 
ENOTSUP);
+  EXPECT_STREQ(error.message, "IPC encoding of nested dictionary values 
unsupported");
+}
+
 using KeyValues = std::vector<std::pair<std::string, std::string>>;
 
 // Unpack nanoarrow's metadata representation into something comparable
@@ -430,3 +453,51 @@ TEST(NanoarrowIpcTest, 
NanoarrowIpcVisitMessageMetadataError) {
   EXPECT_EQ(visited, (KeyValues{{"key1", "value1"}}));
   EXPECT_STREQ(error.message, "visitor stopped at key1");
 }
+
+TEST(NanoarrowIpcTest, NanoarrowIpcEncoderDictionaryBatch) {
+  nanoarrow::ipc::UniqueEncoder encoder;
+  ASSERT_EQ(ArrowIpcEncoderInit(encoder.get()), NANOARROW_OK);
+
+  // Build a simple Utf8 values array
+  nanoarrow::UniqueSchema values_schema;
+  ASSERT_EQ(ArrowSchemaInitFromType(values_schema.get(), 
NANOARROW_TYPE_STRING),
+            NANOARROW_OK);
+
+  nanoarrow::UniqueArray values_array;
+  ASSERT_EQ(ArrowArrayInitFromSchema(values_array.get(), values_schema.get(), 
nullptr),
+            NANOARROW_OK);
+
+  struct ArrowError error;
+  ASSERT_EQ(ArrowArrayStartAppending(values_array.get()), NANOARROW_OK);
+  ASSERT_EQ(ArrowArrayAppendString(values_array.get(), ArrowCharView("foo")),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowArrayAppendString(values_array.get(), ArrowCharView("bar")),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowArrayFinishBuildingDefault(values_array.get(), &error), 
NANOARROW_OK)
+      << error.message;
+
+  nanoarrow::UniqueArrayView values_view;
+  ASSERT_EQ(ArrowArrayViewInitFromSchema(values_view.get(), 
values_schema.get(), &error),
+            NANOARROW_OK)
+      << error.message;
+  ASSERT_EQ(ArrowArrayViewSetArray(values_view.get(), values_array.get(), 
&error),
+            NANOARROW_OK)
+      << error.message;
+
+  // Encode a non-delta DictionaryBatch with dictionary_id=0
+  nanoarrow::UniqueBuffer body_buffer;
+  EXPECT_EQ(ArrowIpcEncoderEncodeSimpleDictionaryBatch(encoder.get(), 
/*dictionary_id=*/0,
+                                                       /*is_delta=*/0, 
values_view.get(),
+                                                       body_buffer.get(), 
&error),
+            NANOARROW_OK)
+      << error.message;
+
+  nanoarrow::UniqueBuffer message_buffer;
+  EXPECT_EQ(ArrowIpcEncoderFinalizeBuffer(encoder.get(), /*encapsulate=*/1,
+                                          message_buffer.get()),
+            NANOARROW_OK);
+
+  // The encapsulated message must be non-empty and 8-byte aligned
+  EXPECT_GT(message_buffer->size_bytes, 8);
+  EXPECT_EQ(message_buffer->size_bytes % 8, 0);
+}
diff --git a/src/nanoarrow/ipc/writer.c b/src/nanoarrow/ipc/writer.c
index 1f100865..00ea93ec 100644
--- a/src/nanoarrow/ipc/writer.c
+++ b/src/nanoarrow/ipc/writer.c
@@ -317,6 +317,86 @@ ArrowErrorCode ArrowIpcWriterWriteArrayView(struct 
ArrowIpcWriter* writer,
   return NANOARROW_OK;
 }
 
+ArrowErrorCode ArrowIpcWriterWriteDictionaryBatch(
+    struct ArrowIpcWriter* writer, int64_t dictionary_id, char is_delta,
+    const struct ArrowArrayView* values_view, struct ArrowError* error) {
+  NANOARROW_DCHECK(writer != NULL && writer->private_data != NULL && 
values_view != NULL);
+  struct ArrowIpcWriterPrivate* private =
+      (struct ArrowIpcWriterPrivate*)writer->private_data;
+
+  // This check is intentionally minimal: we're allowed to write one dictionary
+  // batch per ID in a file but we would need to add bookkeeping to keep track
+  // of written IDs (and usefully a fingerprint or reference to the dictionary
+  // so we can check if we need to emit it again).
+  if (private->writing_file &&
+      (is_delta || private->footer.dictionary_blocks.size_bytes != 0)) {
+    ArrowErrorSet(error,
+                  "IPC file writing supports exactly one non-delta dictionary 
batch");
+    return ENOTSUP;
+  }
+
+  NANOARROW_ASSERT_OK(ArrowBufferResize(&private->buffer, 0, 0));
+  NANOARROW_ASSERT_OK(ArrowBufferResize(&private->body_buffer, 0, 0));
+
+  NANOARROW_RETURN_NOT_OK(ArrowIpcEncoderEncodeSimpleDictionaryBatch(
+      &private->encoder, dictionary_id, is_delta, values_view, 
&private->body_buffer,
+      error));
+  NANOARROW_RETURN_NOT_OK_WITH_ERROR(
+      ArrowIpcEncoderFinalizeBuffer(&private->encoder, /*encapsulate=*/1,
+                                    &private->buffer),
+      error);
+
+  if (private->writing_file) {
+    _NANOARROW_CHECK_RANGE(private->buffer.size_bytes, 0, INT32_MAX);
+    struct ArrowIpcFileBlock block = {
+        .offset = private->bytes_written,
+        .metadata_length = (int32_t) private->buffer.size_bytes,
+        .body_length = private->body_buffer.size_bytes,
+    };
+    NANOARROW_RETURN_NOT_OK_WITH_ERROR(
+        ArrowBufferAppend(&private->footer.dictionary_blocks, &block, 
sizeof(block)),
+        error);
+  }
+  private->bytes_written += private->buffer.size_bytes;
+  private->bytes_written += private->body_buffer.size_bytes;
+
+  NANOARROW_RETURN_NOT_OK(ArrowIpcOutputStreamWrite(
+      &private->output_stream, ArrowBufferToBufferView(&private->buffer), 
error));
+  NANOARROW_RETURN_NOT_OK(ArrowIpcOutputStreamWrite(
+      &private->output_stream, ArrowBufferToBufferView(&private->body_buffer), 
error));
+  return NANOARROW_OK;
+}
+
+// Walk the array in the same depth-first order the schema encoder uses to 
assign
+// dictionary ids (see ArrowIpcDictionaryEncodingsAppendSchema): a 
dictionary-encoded
+// node claims the next id before descending into its children and then its 
values.
+// Emitting a full (non-delta) DictionaryBatch for each dictionary before every
+// RecordBatch keeps each batch's indices valid against the dictionary that 
precedes
+// it, which is required because each array in the stream carries its own 
dictionary.
+// In the future we can reduce the number of dictionaries emitted by checking 
for
+// identical dictionary arrays.
+static ArrowErrorCode ArrowIpcWriterWriteDictionariesForArrayView(
+    struct ArrowIpcWriter* writer, const struct ArrowArrayView* array_view,
+    int64_t* next_id, struct ArrowError* error) {
+  if (array_view->dictionary != NULL) {
+    int64_t dictionary_id = (*next_id)++;
+    NANOARROW_RETURN_NOT_OK(ArrowIpcWriterWriteDictionaryBatch(
+        writer, dictionary_id, /*is_delta=*/0, array_view->dictionary, error));
+  }
+
+  for (int64_t i = 0; i < array_view->n_children; i++) {
+    NANOARROW_RETURN_NOT_OK(ArrowIpcWriterWriteDictionariesForArrayView(
+        writer, array_view->children[i], next_id, error));
+  }
+
+  if (array_view->dictionary != NULL) {
+    NANOARROW_RETURN_NOT_OK(ArrowIpcWriterWriteDictionariesForArrayView(
+        writer, array_view->dictionary, next_id, error));
+  }
+
+  return NANOARROW_OK;
+}
+
 static ArrowErrorCode ArrowIpcWriterWriteArrayStreamImpl(
     struct ArrowIpcWriter* writer, struct ArrowArrayStream* in,
     struct ArrowSchema* schema, struct ArrowArray* array,
@@ -332,6 +412,11 @@ static ArrowErrorCode ArrowIpcWriterWriteArrayStreamImpl(
     }
 
     NANOARROW_RETURN_NOT_OK(ArrowArrayViewSetArray(array_view, array, error));
+
+    int64_t next_dictionary_id = 0;
+    NANOARROW_RETURN_NOT_OK(ArrowIpcWriterWriteDictionariesForArrayView(
+        writer, array_view, &next_dictionary_id, error));
+
     NANOARROW_RETURN_NOT_OK(ArrowIpcWriterWriteArrayView(writer, array_view, 
error));
     ArrowArrayRelease(array);
   }
diff --git a/src/nanoarrow/ipc/writer_test.cc b/src/nanoarrow/ipc/writer_test.cc
index a07ae516..c30dd603 100644
--- a/src/nanoarrow/ipc/writer_test.cc
+++ b/src/nanoarrow/ipc/writer_test.cc
@@ -204,3 +204,204 @@ TEST(NanoarrowIpcWriter, FileWriting) {
   auto after_footer = p->bytes_written;
   EXPECT_GT(after_footer, after_eos);
 }
+
+TEST(NanoarrowIpcWriter, WriteDictionaryBatch) {
+  struct ArrowError error;
+
+  nanoarrow::UniqueBuffer output;
+  nanoarrow::ipc::UniqueOutputStream stream;
+  ASSERT_EQ(ArrowIpcOutputStreamInitBuffer(stream.get(), output.get()), 
NANOARROW_OK);
+
+  nanoarrow::ipc::UniqueWriter writer;
+  ASSERT_EQ(ArrowIpcWriterInit(writer.get(), stream.get()), NANOARROW_OK);
+
+  auto* p = static_cast<struct ArrowIpcWriterPrivate*>(writer->private_data);
+
+  // Build a simple Utf8 values array
+  nanoarrow::UniqueSchema values_schema;
+  ASSERT_EQ(ArrowSchemaInitFromType(values_schema.get(), 
NANOARROW_TYPE_STRING),
+            NANOARROW_OK);
+
+  nanoarrow::UniqueArray values_array;
+  ASSERT_EQ(ArrowArrayInitFromSchema(values_array.get(), values_schema.get(), 
nullptr),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowArrayStartAppending(values_array.get()), NANOARROW_OK);
+  ASSERT_EQ(ArrowArrayAppendString(values_array.get(), ArrowCharView("foo")),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowArrayAppendString(values_array.get(), ArrowCharView("bar")),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowArrayFinishBuildingDefault(values_array.get(), &error), 
NANOARROW_OK)
+      << error.message;
+
+  nanoarrow::UniqueArrayView values_view;
+  ASSERT_EQ(ArrowArrayViewInitFromSchema(values_view.get(), 
values_schema.get(), &error),
+            NANOARROW_OK)
+      << error.message;
+  ASSERT_EQ(ArrowArrayViewSetArray(values_view.get(), values_array.get(), 
&error),
+            NANOARROW_OK)
+      << error.message;
+
+  // stream mode: write a DictionaryBatch — bytes are emitted but no block is 
tracked
+  EXPECT_EQ(p->bytes_written, 0);
+  EXPECT_EQ(p->footer.dictionary_blocks.size_bytes, 0);
+
+  EXPECT_EQ(ArrowIpcWriterWriteDictionaryBatch(writer.get(), 
/*dictionary_id=*/0,
+                                               /*is_delta=*/0, 
values_view.get(), &error),
+            NANOARROW_OK)
+      << error.message;
+
+  auto after_dict_stream = p->bytes_written;
+  EXPECT_GT(after_dict_stream, 0);
+  // no block tracked in stream mode
+  EXPECT_EQ(p->footer.dictionary_blocks.size_bytes, 0);
+
+  // file mode: the block is tracked in the footer
+  nanoarrow::ipc::UniqueOutputStream stream2;
+  nanoarrow::UniqueBuffer output2;
+  ASSERT_EQ(ArrowIpcOutputStreamInitBuffer(stream2.get(), output2.get()), 
NANOARROW_OK);
+
+  nanoarrow::ipc::UniqueWriter writer2;
+  ASSERT_EQ(ArrowIpcWriterInit(writer2.get(), stream2.get()), NANOARROW_OK);
+
+  auto* p2 = static_cast<struct ArrowIpcWriterPrivate*>(writer2->private_data);
+
+  ASSERT_EQ(ArrowIpcWriterStartFile(writer2.get(), &error), NANOARROW_OK)
+      << error.message;
+  EXPECT_EQ(p2->footer.dictionary_blocks.size_bytes, 0);
+
+  EXPECT_EQ(ArrowIpcWriterWriteDictionaryBatch(writer2.get(), 
/*dictionary_id=*/0,
+                                               /*is_delta=*/0, 
values_view.get(), &error),
+            NANOARROW_OK)
+      << error.message;
+
+  // one block tracked in file mode
+  EXPECT_EQ(p2->footer.dictionary_blocks.size_bytes, sizeof(struct 
ArrowIpcFileBlock));
+
+  int64_t bytes_written = p2->bytes_written;
+  EXPECT_EQ(ArrowIpcWriterWriteDictionaryBatch(writer2.get(), 
/*dictionary_id=*/0,
+                                               /*is_delta=*/0, 
values_view.get(), &error),
+            ENOTSUP);
+  EXPECT_STREQ(error.message,
+               "IPC file writing supports exactly one non-delta dictionary 
batch");
+  EXPECT_EQ(p2->bytes_written, bytes_written);
+  EXPECT_EQ(p2->footer.dictionary_blocks.size_bytes, sizeof(struct 
ArrowIpcFileBlock));
+
+  nanoarrow::ipc::UniqueOutputStream stream3;
+  nanoarrow::UniqueBuffer output3;
+  ASSERT_EQ(ArrowIpcOutputStreamInitBuffer(stream3.get(), output3.get()), 
NANOARROW_OK);
+
+  nanoarrow::ipc::UniqueWriter writer3;
+  ASSERT_EQ(ArrowIpcWriterInit(writer3.get(), stream3.get()), NANOARROW_OK);
+  ASSERT_EQ(ArrowIpcWriterStartFile(writer3.get(), &error), NANOARROW_OK)
+      << error.message;
+  EXPECT_EQ(ArrowIpcWriterWriteDictionaryBatch(writer3.get(), 
/*dictionary_id=*/0,
+                                               /*is_delta=*/1, 
values_view.get(), &error),
+            ENOTSUP);
+  EXPECT_STREQ(error.message,
+               "IPC file writing supports exactly one non-delta dictionary 
batch");
+}
+
+// Build a struct array with a single dictionary-encoded (int32 -> utf8) child.
+static void MakeDictionaryStructArray(struct ArrowArray* array,
+                                      struct ArrowSchema* schema) {
+  ASSERT_EQ(ArrowSchemaInitFromType(schema, NANOARROW_TYPE_STRUCT), 
NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateChildren(schema, 1), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInitFromType(schema->children[0], NANOARROW_TYPE_INT32),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaSetName(schema->children[0], "dict_col"), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateDictionary(schema->children[0]), NANOARROW_OK);
+  ASSERT_EQ(
+      ArrowSchemaInitFromType(schema->children[0]->dictionary, 
NANOARROW_TYPE_STRING),
+      NANOARROW_OK);
+
+  ASSERT_EQ(ArrowArrayInitFromSchema(array, schema, nullptr), NANOARROW_OK);
+  struct ArrowArray* indices = array->children[0];
+  struct ArrowArray* values = indices->dictionary;
+
+  ASSERT_EQ(ArrowArrayStartAppending(array), NANOARROW_OK);
+  ASSERT_EQ(ArrowArrayAppendString(values, ArrowCharView("foo")), 
NANOARROW_OK);
+  ASSERT_EQ(ArrowArrayAppendString(values, ArrowCharView("bar")), 
NANOARROW_OK);
+
+  ASSERT_EQ(ArrowArrayAppendInt(indices, 0), NANOARROW_OK);
+  ASSERT_EQ(ArrowArrayAppendInt(indices, 1), NANOARROW_OK);
+  ASSERT_EQ(ArrowArrayAppendInt(indices, 0), NANOARROW_OK);
+  array->length = 3;
+
+  ASSERT_EQ(ArrowArrayFinishBuildingDefault(array, nullptr), NANOARROW_OK);
+}
+
+// Write a dictionary-encoded stream through the high-level WriteArrayStream 
path
+// and read it back through the IPC reader, confirming the DictionaryBatch is
+// emitted automatically and the decoded values match.
+TEST(NanoarrowIpcWriter, RoundtripDictionaryStream) {
+  struct ArrowError error;
+
+  nanoarrow::UniqueSchema schema;
+  nanoarrow::UniqueArray array;
+  MakeDictionaryStructArray(array.get(), schema.get());
+
+  nanoarrow::UniqueArrayStream array_stream;
+  ASSERT_EQ(ArrowBasicArrayStreamInit(array_stream.get(), schema.get(), 1), 
NANOARROW_OK);
+  ArrowBasicArrayStreamSetArray(array_stream.get(), 0, array.get());
+
+  nanoarrow::UniqueBuffer output;
+  nanoarrow::ipc::UniqueOutputStream out_stream;
+  ASSERT_EQ(ArrowIpcOutputStreamInitBuffer(out_stream.get(), output.get()), 
NANOARROW_OK);
+
+  nanoarrow::ipc::UniqueWriter writer;
+  ASSERT_EQ(ArrowIpcWriterInit(writer.get(), out_stream.get()), NANOARROW_OK);
+  ASSERT_EQ(ArrowIpcWriterWriteArrayStream(writer.get(), array_stream.get(), 
&error),
+            NANOARROW_OK)
+      << error.message;
+
+  // Read the encoded bytes back
+  struct ArrowIpcInputStream input;
+  ASSERT_EQ(ArrowIpcInputStreamInitBuffer(&input, output.get()), NANOARROW_OK);
+
+  nanoarrow::UniqueArrayStream reader;
+  ASSERT_EQ(ArrowIpcArrayStreamReaderInit(reader.get(), &input, nullptr), 
NANOARROW_OK);
+
+  nanoarrow::UniqueSchema roundtrip_schema;
+  ASSERT_EQ(ArrowArrayStreamGetSchema(reader.get(), roundtrip_schema.get(), 
&error),
+            NANOARROW_OK)
+      << error.message;
+  ASSERT_EQ(roundtrip_schema->n_children, 1);
+  ASSERT_NE(roundtrip_schema->children[0]->dictionary, nullptr);
+  EXPECT_STREQ(roundtrip_schema->children[0]->dictionary->format, "u");
+
+  nanoarrow::UniqueArray roundtrip_array;
+  ASSERT_EQ(ArrowArrayStreamGetNext(reader.get(), roundtrip_array.get(), 
&error),
+            NANOARROW_OK)
+      << error.message;
+  ASSERT_EQ(roundtrip_array->length, 3);
+  ASSERT_EQ(roundtrip_array->n_children, 1);
+  ASSERT_NE(roundtrip_array->children[0]->dictionary, nullptr);
+  EXPECT_EQ(roundtrip_array->children[0]->dictionary->length, 2);
+
+  // Validate the decoded indices resolve to the original values
+  nanoarrow::UniqueArrayView view;
+  ASSERT_EQ(ArrowArrayViewInitFromSchema(view.get(), roundtrip_schema.get(), 
&error),
+            NANOARROW_OK)
+      << error.message;
+  ASSERT_EQ(ArrowArrayViewSetArray(view.get(), roundtrip_array.get(), &error),
+            NANOARROW_OK)
+      << error.message;
+
+  struct ArrowArrayView* indices_view = view->children[0];
+  struct ArrowArrayView* values_view = indices_view->dictionary;
+  ASSERT_NE(values_view, nullptr);
+  EXPECT_EQ(ArrowArrayViewGetIntUnsafe(indices_view, 0), 0);
+  EXPECT_EQ(ArrowArrayViewGetIntUnsafe(indices_view, 1), 1);
+  EXPECT_EQ(ArrowArrayViewGetIntUnsafe(indices_view, 2), 0);
+
+  struct ArrowStringView v0 = ArrowArrayViewGetStringUnsafe(values_view, 0);
+  struct ArrowStringView v1 = ArrowArrayViewGetStringUnsafe(values_view, 1);
+  EXPECT_EQ(std::string(v0.data, v0.size_bytes), "foo");
+  EXPECT_EQ(std::string(v1.data, v1.size_bytes), "bar");
+
+  roundtrip_array.reset();
+  ASSERT_EQ(ArrowArrayStreamGetNext(reader.get(), roundtrip_array.get(), 
&error),
+            NANOARROW_OK)
+      << error.message;
+  EXPECT_EQ(roundtrip_array->release, nullptr);
+}
diff --git a/src/nanoarrow/nanoarrow_ipc.h b/src/nanoarrow/nanoarrow_ipc.h
index fb38750c..0523ac38 100644
--- a/src/nanoarrow/nanoarrow_ipc.h
+++ b/src/nanoarrow/nanoarrow_ipc.h
@@ -96,6 +96,8 @@
   NANOARROW_SYMBOL(NANOARROW_NAMESPACE, ArrowIpcEncoderEncodeSchema)
 #define ArrowIpcEncoderEncodeSimpleRecordBatch \
   NANOARROW_SYMBOL(NANOARROW_NAMESPACE, ArrowIpcEncoderEncodeSimpleRecordBatch)
+#define ArrowIpcEncoderEncodeSimpleDictionaryBatch \
+  NANOARROW_SYMBOL(NANOARROW_NAMESPACE, 
ArrowIpcEncoderEncodeSimpleDictionaryBatch)
 #define ArrowIpcOutputStreamInitBuffer \
   NANOARROW_SYMBOL(NANOARROW_NAMESPACE, ArrowIpcOutputStreamInitBuffer)
 #define ArrowIpcOutputStreamInitFile \
@@ -110,6 +112,8 @@
   NANOARROW_SYMBOL(NANOARROW_NAMESPACE, ArrowIpcWriterWriteSchema)
 #define ArrowIpcWriterWriteArrayView \
   NANOARROW_SYMBOL(NANOARROW_NAMESPACE, ArrowIpcWriterWriteArrayView)
+#define ArrowIpcWriterWriteDictionaryBatch \
+  NANOARROW_SYMBOL(NANOARROW_NAMESPACE, ArrowIpcWriterWriteDictionaryBatch)
 #define ArrowIpcWriterWriteArrayStream \
   NANOARROW_SYMBOL(NANOARROW_NAMESPACE, ArrowIpcWriterWriteArrayStream)
 #define ArrowIpcWriterStartFile \
@@ -881,6 +885,20 @@ NANOARROW_DLL ArrowErrorCode 
ArrowIpcEncoderEncodeSimpleRecordBatch(
     struct ArrowIpcEncoder* encoder, const struct ArrowArrayView* array_view,
     struct ArrowBuffer* body_buffer, struct ArrowError* error);
 
+/// \brief Encode an ArrayView as a DictionaryBatch flatbuffer, embedded in a 
Message.
+///
+/// dictionary_id must match the id assigned to the dictionary-encoded field 
in the
+/// schema. is_delta selects DictionaryBatch.isDelta. values_view must not 
itself be
+/// dictionary-encoded. Body buffers are concatenated into a contiguous, padded
+/// body_buffer.
+///
+/// Returns ENOMEM if allocation fails, EINVAL if values_view is 
dictionary-encoded,
+/// NANOARROW_OK otherwise.
+NANOARROW_DLL ArrowErrorCode ArrowIpcEncoderEncodeSimpleDictionaryBatch(
+    struct ArrowIpcEncoder* encoder, int64_t dictionary_id, char is_delta,
+    const struct ArrowArrayView* values_view, struct ArrowBuffer* body_buffer,
+    struct ArrowError* error);
+
 /// \brief An user-extensible output data sink
 struct ArrowIpcOutputStream {
   /// \brief Write up to buf_size_bytes from stream into buf
@@ -964,6 +982,17 @@ NANOARROW_DLL ArrowErrorCode 
ArrowIpcWriterWriteArrayView(struct ArrowIpcWriter*
                                                           const struct 
ArrowArrayView* in,
                                                           struct ArrowError* 
error);
 
+/// \brief Write a DictionaryBatch message to the output byte stream
+///
+/// dictionary_id must match the id assigned to the dictionary-encoded field 
in the
+/// schema. is_delta selects DictionaryBatch.isDelta. values_view must not 
itself be
+/// dictionary-encoded. The writer does not check that a schema was already 
written.
+///
+/// Errors are propagated from the underlying encoder and output byte stream.
+NANOARROW_DLL ArrowErrorCode ArrowIpcWriterWriteDictionaryBatch(
+    struct ArrowIpcWriter* writer, int64_t dictionary_id, char is_delta,
+    const struct ArrowArrayView* values_view, struct ArrowError* error);
+
 /// \brief Write an entire stream (including EOS) to the output byte stream
 ///
 /// Errors are propagated from the underlying encoder, array stream, and 
output byte

Reply via email to