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 e675fc7b refactor: Narrow message metadata API (#937)
e675fc7b is described below
commit e675fc7befd80e9b7848c557e29987d5f37a5e16
Author: Rusty Conover <[email protected]>
AuthorDate: Thu Sep 10 13:36:57 2026 -0400
refactor: Narrow message metadata API (#937)
Follow-up to #924.
This removes ArrowIpcDecoderGetMessageMetadataValue() before the API
ships in a release. Callers that need one key can use
ArrowMetadataGetValue() with the packed metadata returned by
ArrowIpcDecoderGetMessageMetadata(); callers that need zero-copy
iteration can continue using ArrowIpcDecoderVisitMessageMetadata().
It also scopes the visitor callback type as
ArrowIpcMessageMetadataVisitFunction and replaces the hand-generated
keyless KeyValue FlatBuffer test fixture with a flatcc-built message.
Tests:
- IPC-enabled CMake build
- ctest: 365 tests passed (16 codec-dependent tests skipped)
- git diff --check
- clang-format on changed lines
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
src/nanoarrow/ipc/decoder.c | 67 +++---------------------
src/nanoarrow/ipc/decoder_test.cc | 106 ++++++++++++++++++++++++++------------
src/nanoarrow/ipc/encoder_test.cc | 41 +++++++++------
src/nanoarrow/nanoarrow_ipc.h | 34 ++++--------
4 files changed, 113 insertions(+), 135 deletions(-)
diff --git a/src/nanoarrow/ipc/decoder.c b/src/nanoarrow/ipc/decoder.c
index 00dbcf2c..9826b759 100644
--- a/src/nanoarrow/ipc/decoder.c
+++ b/src/nanoarrow/ipc/decoder.c
@@ -644,19 +644,14 @@ static inline int32_t ArrowIpcReadInt32LE(struct
ArrowBufferView* data, int swap
return value;
}
-// Returned by an internal ArrowIpcMetadataVisitFunction to stop iterating
early.
-// This is never returned to a caller of the public API.
-#define _NANOARROW_IPC_VISIT_STOP (-1)
-
// Visits each key/value pair in a flatbuffers vector of KeyValue.
//
// Keys and values point into the message and are passed with an explicit size
because
// both are optional fields whose content may contain embedded nulls; a
KeyValue with no
// key or no value is visited with an empty string view.
-static ArrowErrorCode ArrowIpcDecoderVisitMetadata(ns(KeyValue_vec_t) kv_vec,
-
ArrowIpcMetadataVisitFunction visit,
- void* private_data,
- struct ArrowError* error) {
+static ArrowErrorCode ArrowIpcDecoderVisitMetadata(
+ ns(KeyValue_vec_t) kv_vec, ArrowIpcMessageMetadataVisitFunction visit,
+ void* private_data, struct ArrowError* error) {
int64_t n_pairs = ns(KeyValue_vec_len(kv_vec));
for (int64_t i = 0; i < n_pairs; i++) {
@@ -1767,59 +1762,9 @@ ArrowErrorCode ArrowIpcDecoderGetMessageMetadata(struct
ArrowIpcDecoder* decoder
return ArrowIpcDecoderBuildMetadata(private_data->last_message_metadata,
out, error);
}
-struct ArrowIpcMetadataValueLookup {
- struct ArrowStringView key;
- struct ArrowStringView* value_out;
-};
-
-static ArrowErrorCode ArrowIpcDecoderMatchMetadataKey(struct ArrowStringView
key,
- struct ArrowStringView
value,
- void* private_data,
- struct ArrowError*
error) {
- NANOARROW_UNUSED(error);
- struct ArrowIpcMetadataValueLookup* lookup =
- (struct ArrowIpcMetadataValueLookup*)private_data;
-
- if (key.size_bytes != lookup->key.size_bytes) {
- return NANOARROW_OK;
- }
-
- if (key.size_bytes > 0 &&
- memcmp(key.data, lookup->key.data, (size_t)key.size_bytes) != 0) {
- return NANOARROW_OK;
- }
-
- *lookup->value_out = value;
- return _NANOARROW_IPC_VISIT_STOP;
-}
-
-ArrowErrorCode ArrowIpcDecoderGetMessageMetadataValue(struct ArrowIpcDecoder*
decoder,
- struct ArrowStringView
key,
- struct ArrowStringView*
value_out,
- struct ArrowError*
error) {
- NANOARROW_DCHECK(decoder != NULL && decoder->private_data != NULL &&
value_out != NULL);
- struct ArrowIpcDecoderPrivate* private_data =
- (struct ArrowIpcDecoderPrivate*)decoder->private_data;
-
- struct ArrowIpcMetadataValueLookup lookup;
- lookup.key = key;
- lookup.value_out = value_out;
-
- int result =
- ArrowIpcDecoderVisitMetadata(private_data->last_message_metadata,
- &ArrowIpcDecoderMatchMetadataKey, &lookup,
error);
- if (result == _NANOARROW_IPC_VISIT_STOP) {
- // key was found and value_out was set
- return NANOARROW_OK;
- }
-
- return result;
-}
-
-ArrowErrorCode ArrowIpcDecoderVisitMessageMetadata(struct ArrowIpcDecoder*
decoder,
-
ArrowIpcMetadataVisitFunction visit,
- void* private_data,
- struct ArrowError* error) {
+ArrowErrorCode ArrowIpcDecoderVisitMessageMetadata(
+ struct ArrowIpcDecoder* decoder, ArrowIpcMessageMetadataVisitFunction
visit,
+ void* private_data, struct ArrowError* error) {
NANOARROW_DCHECK(decoder != NULL && decoder->private_data != NULL && visit
!= NULL);
struct ArrowIpcDecoderPrivate* decoder_private =
(struct ArrowIpcDecoderPrivate*)decoder->private_data;
diff --git a/src/nanoarrow/ipc/decoder_test.cc
b/src/nanoarrow/ipc/decoder_test.cc
index 6adf1e36..dcdde176 100644
--- a/src/nanoarrow/ipc/decoder_test.cc
+++ b/src/nanoarrow/ipc/decoder_test.cc
@@ -33,9 +33,13 @@
// For bswap32()
#include "flatcc/portable/pendian.h"
+#include "flatcc/flatcc_builder.h"
+#include "flatcc_generated.h"
#include "nanoarrow/nanoarrow_gtest_util.hpp"
#include "nanoarrow/nanoarrow_ipc.hpp"
+#define ns(x) FLATBUFFERS_WRAP_NAMESPACE(org_apache_arrow_flatbuf, x)
+
#if defined(NANOARROW_BUILD_TESTS_WITH_ARROW)
using namespace arrow;
#endif
@@ -1382,28 +1386,69 @@ void AssertArrayViewIdentical(const struct
ArrowArrayView* actual,
}
}
-// A Message whose Message.custom_metadata and Schema.custom_metadata each
contain a
-// KeyValue with no key. Both `key` and `value` are optional fields of
KeyValue in the
-// IPC format, so this passes flatbuffer verification and must not crash the
decoder.
-// Generated with flatcc: a Schema message with no fields, whose two metadata
vectors
-// each contain KeyValue{value: "message_value" / "schema_value"} and no key.
-alignas(8) static uint8_t kKeylessMetadataSchema[] = {
- 0xff, 0xff, 0xff, 0xff, 0x90, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0x86, 0xff,
- 0xff, 0xff, 0x04, 0x00, 0x01, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x04, 0x00,
0x00, 0x00,
- 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xb0, 0xff, 0xff, 0xff,
0x04, 0x00,
- 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x5f,
- 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00, 0x00, 0x00, 0xc4, 0xff, 0xff, 0xff,
0x2c, 0x00,
- 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00,
0x00, 0x00,
- 0xe0, 0xff, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00,
0x73, 0x63,
- 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00, 0x00,
0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00,
0x0a, 0x00,
- 0x0c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0x0e, 0x00, 0x10, 0x00,
0x04, 0x00,
- 0x06, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00};
+// Builds an encapsulated Schema message whose Message.custom_metadata and
+// Schema.custom_metadata each contain a KeyValue with no key. Both `key` and
`value`
+// are optional fields of KeyValue in the IPC format, so this passes flatbuffer
+// verification and must not crash the decoder. The nanoarrow encoder always
writes
+// both, so this message has to be built with flatcc directly.
+static void MakeKeylessMetadataSchemaMessage(struct ArrowBuffer* out) {
+ flatcc_builder_t builder;
+ flatcc_builder_init(&builder);
+
+ ASSERT_EQ(ns(Message_start_as_root(&builder)), 0);
+ ASSERT_EQ(ns(Message_version_add(&builder, ns(MetadataVersion_V5))), 0);
+
+ ASSERT_EQ(ns(Message_header_Schema_start(&builder)), 0);
+ ASSERT_EQ(ns(Schema_endianness_add(&builder, ns(Endianness_Little))), 0);
+ ASSERT_EQ(ns(Schema_fields_start(&builder)), 0);
+ ASSERT_EQ(ns(Schema_fields_end(&builder)), 0);
+ ASSERT_EQ(ns(Schema_custom_metadata_start(&builder)), 0);
+ ASSERT_EQ(ns(Schema_custom_metadata_push_start(&builder)), 0);
+ ASSERT_EQ(ns(KeyValue_value_create_str(&builder, "schema_value")), 0);
+ ASSERT_NE(ns(Schema_custom_metadata_push_end(&builder)), nullptr);
+ ASSERT_EQ(ns(Schema_custom_metadata_end(&builder)), 0);
+ ASSERT_EQ(ns(Message_header_Schema_end(&builder)), 0);
+
+ ASSERT_EQ(ns(Message_custom_metadata_start(&builder)), 0);
+ ASSERT_EQ(ns(Message_custom_metadata_push_start(&builder)), 0);
+ ASSERT_EQ(ns(KeyValue_value_create_str(&builder, "message_value")), 0);
+ ASSERT_NE(ns(Message_custom_metadata_push_end(&builder)), nullptr);
+ ASSERT_EQ(ns(Message_custom_metadata_end(&builder)), 0);
+
+ ASSERT_EQ(ns(Message_bodyLength_add(&builder, 0)), 0);
+ ASSERT_NE(ns(Message_end_as_root(&builder)), 0);
+
+ // Encapsulate: continuation, little endian header size, header, padding to
8 bytes
+ size_t size = flatcc_builder_get_buffer_size(&builder);
+ int64_t padded_size = _ArrowRoundUpToMultipleOf8(static_cast<int64_t>(size));
+ ASSERT_EQ(
+ ArrowBufferReserve(out, 2 * static_cast<int64_t>(sizeof(int32_t)) +
padded_size),
+ NANOARROW_OK);
+ ASSERT_EQ(ArrowBufferAppendInt32(out, -1), NANOARROW_OK);
+#if defined(__BIG_ENDIAN__)
+ ASSERT_EQ(ArrowBufferAppendInt32(
+ out,
static_cast<int32_t>(bswap32(static_cast<uint32_t>(padded_size)))),
+ NANOARROW_OK);
+#else
+ ASSERT_EQ(ArrowBufferAppendInt32(out, static_cast<int32_t>(padded_size)),
NANOARROW_OK);
+#endif
+ ASSERT_NE(flatcc_builder_copy_buffer(&builder, out->data + out->size_bytes,
size),
+ nullptr);
+ out->size_bytes += size;
+ while (out->size_bytes % 8 != 0) {
+ out->data[out->size_bytes++] = 0;
+ }
+
+ flatcc_builder_clear(&builder);
+}
TEST(NanoarrowIpcTest, NanoarrowIpcDecodeMetadataWithoutKey) {
+ nanoarrow::UniqueBuffer message;
+ ASSERT_NO_FATAL_FAILURE(MakeKeylessMetadataSchemaMessage(message.get()));
+
struct ArrowBufferView data;
- data.data.as_uint8 = kKeylessMetadataSchema;
- data.size_bytes = sizeof(kKeylessMetadataSchema);
+ data.data.as_uint8 = message->data;
+ data.size_bytes = message->size_bytes;
struct ArrowError error;
nanoarrow::ipc::UniqueDecoder decoder;
@@ -1419,15 +1464,11 @@ TEST(NanoarrowIpcTest,
NanoarrowIpcDecodeMetadataWithoutKey) {
ArrowIpcDecoderGetMessageMetadata(decoder.get(), message_metadata.get(),
&error),
NANOARROW_OK)
<< error.message;
- EXPECT_EQ(
- ArrowSchemaMetadataToString(reinterpret_cast<const
char*>(message_metadata->data)),
- "=message_value");
+ const char* packed = reinterpret_cast<const char*>(message_metadata->data);
+ EXPECT_EQ(ArrowSchemaMetadataToString(packed), "=message_value");
struct ArrowStringView value = ArrowCharView(nullptr);
- ASSERT_EQ(ArrowIpcDecoderGetMessageMetadataValue(decoder.get(),
ArrowCharView(""),
- &value, &error),
- NANOARROW_OK)
- << error.message;
+ ASSERT_EQ(ArrowMetadataGetValue(packed, ArrowCharView(""), &value),
NANOARROW_OK);
EXPECT_EQ(std::string(value.data, value.size_bytes), "message_value");
// ...including the metadata of the schema the message contains
@@ -1885,17 +1926,14 @@ TEST(NanoarrowIpcTest,
NanoarrowIpcMessageMetadataArrowInterop) {
&error),
NANOARROW_OK)
<< error.message;
- EXPECT_EQ(ArrowSchemaMetadataToString(
- reinterpret_cast<const char*>(batch_message_metadata->data)),
- "key1=value1, key2=value2");
+ const char* packed = reinterpret_cast<const
char*>(batch_message_metadata->data);
+ EXPECT_EQ(ArrowSchemaMetadataToString(packed), "key1=value1, key2=value2");
for (int64_t i = 0; i < custom_metadata->size(); i++) {
struct ArrowStringView value = ArrowCharView(nullptr);
- ASSERT_EQ(ArrowIpcDecoderGetMessageMetadataValue(
- decoder.get(),
ArrowCharView(custom_metadata->key(i).c_str()), &value,
- &error),
- NANOARROW_OK)
- << error.message;
+ ASSERT_EQ(ArrowMetadataGetValue(
+ packed, ArrowCharView(custom_metadata->key(i).c_str()),
&value),
+ NANOARROW_OK);
EXPECT_EQ(std::string(value.data, value.size_bytes),
custom_metadata->value(i));
}
}
diff --git a/src/nanoarrow/ipc/encoder_test.cc
b/src/nanoarrow/ipc/encoder_test.cc
index 6c79babd..00fd278e 100644
--- a/src/nanoarrow/ipc/encoder_test.cc
+++ b/src/nanoarrow/ipc/encoder_test.cc
@@ -248,28 +248,29 @@ TEST(NanoarrowIpcTest,
NanoarrowIpcEncoderMessageMetadataRoundtrip) {
EXPECT_EQ(DecodeMessageMetadata(message.get(), decoder.get()), key_values);
- // Values can also be read in place, without copying
- struct ArrowStringView value = ArrowCharView(nullptr);
- ASSERT_EQ(ArrowIpcDecoderGetMessageMetadataValue(
- decoder.get(), ArrowCharView("cache-control"), &value, &error),
+ // Single keys are read from the decoded metadata with
ArrowMetadataGetValue()
+ nanoarrow::UniqueBuffer packed;
+ ASSERT_EQ(ArrowIpcDecoderGetMessageMetadata(decoder.get(), packed.get(),
&error),
NANOARROW_OK)
<< error.message;
+ const char* packed_metadata = reinterpret_cast<const char*>(packed->data);
+
+ struct ArrowStringView value = ArrowCharView(nullptr);
+ ASSERT_EQ(
+ ArrowMetadataGetValue(packed_metadata, ArrowCharView("cache-control"),
&value),
+ NANOARROW_OK);
EXPECT_EQ(std::string(value.data, value.size_bytes), "no-store");
// A key that isn't present leaves value_out untouched
value = ArrowCharView(nullptr);
- ASSERT_EQ(ArrowIpcDecoderGetMessageMetadataValue(
- decoder.get(), ArrowCharView("not-a-key"), &value, &error),
- NANOARROW_OK)
- << error.message;
+ ASSERT_EQ(ArrowMetadataGetValue(packed_metadata, ArrowCharView("not-a-key"),
&value),
+ NANOARROW_OK);
EXPECT_EQ(value.data, nullptr);
// A key which is a prefix of a present key is not a match
value = ArrowCharView(nullptr);
- ASSERT_EQ(ArrowIpcDecoderGetMessageMetadataValue(decoder.get(),
ArrowCharView("cache"),
- &value, &error),
- NANOARROW_OK)
- << error.message;
+ ASSERT_EQ(ArrowMetadataGetValue(packed_metadata, ArrowCharView("cache"),
&value),
+ NANOARROW_OK);
EXPECT_EQ(value.data, nullptr);
// The metadata applied to exactly one message: the next one has none
@@ -395,12 +396,20 @@ TEST(NanoarrowIpcTest,
NanoarrowIpcEncoderMessageMetadataEmpty) {
ASSERT_EQ(message->size_bytes, baseline->size_bytes);
EXPECT_EQ(memcmp(message->data, baseline->data, message->size_bytes), 0);
- // Reading in place from a message without metadata finds nothing
- struct ArrowStringView value = ArrowCharView(nullptr);
- ASSERT_EQ(ArrowIpcDecoderGetMessageMetadataValue(decoder.get(),
ArrowCharView("key"),
- &value, &error),
+ // The decoded metadata of a message without any is empty, and is still safe
to
+ // hand to ArrowMetadataGetValue()
+ EXPECT_EQ(DecodeMessageMetadata(message.get(), decoder.get()), KeyValues{});
+
+ nanoarrow::UniqueBuffer packed;
+ ASSERT_EQ(ArrowIpcDecoderGetMessageMetadata(decoder.get(), packed.get(),
&error),
NANOARROW_OK)
<< error.message;
+ ASSERT_EQ(packed->data, nullptr);
+
+ struct ArrowStringView value = ArrowCharView(nullptr);
+ ASSERT_EQ(ArrowMetadataGetValue(reinterpret_cast<const char*>(packed->data),
+ ArrowCharView("key"), &value),
+ NANOARROW_OK);
EXPECT_EQ(value.data, nullptr);
}
diff --git a/src/nanoarrow/nanoarrow_ipc.h b/src/nanoarrow/nanoarrow_ipc.h
index 8a29cf5f..7f555975 100644
--- a/src/nanoarrow/nanoarrow_ipc.h
+++ b/src/nanoarrow/nanoarrow_ipc.h
@@ -82,8 +82,6 @@
NANOARROW_SYMBOL(NANOARROW_NAMESPACE, ArrowIpcDecoderSetEndianness)
#define ArrowIpcDecoderGetMessageMetadata \
NANOARROW_SYMBOL(NANOARROW_NAMESPACE, ArrowIpcDecoderGetMessageMetadata)
-#define ArrowIpcDecoderGetMessageMetadataValue \
- NANOARROW_SYMBOL(NANOARROW_NAMESPACE, ArrowIpcDecoderGetMessageMetadataValue)
#define ArrowIpcDecoderVisitMessageMetadata \
NANOARROW_SYMBOL(NANOARROW_NAMESPACE, ArrowIpcDecoderVisitMessageMetadata)
#define ArrowIpcDecoderPeekFooter \
@@ -623,10 +621,9 @@ NANOARROW_DLL ArrowErrorCode
ArrowIpcDecoderSetDecompressor(
///
/// Returning any value other than NANOARROW_OK will stop the visit and cause
that
/// value to be returned by ArrowIpcDecoderVisitMessageMetadata().
-typedef ArrowErrorCode (*ArrowIpcMetadataVisitFunction)(struct ArrowStringView
key,
- struct ArrowStringView
value,
- void* private_data,
- struct ArrowError*
error);
+typedef ArrowErrorCode (*ArrowIpcMessageMetadataVisitFunction)(
+ struct ArrowStringView key, struct ArrowStringView value, void*
private_data,
+ struct ArrowError* error);
/// \brief Peek at a message header
///
@@ -691,36 +688,25 @@ NANOARROW_DLL ArrowErrorCode
ArrowIpcDecoderDecodeHeader(struct ArrowIpcDecoder*
/// (const char*)out->data can be passed to ArrowSchemaSetMetadata() or
/// ArrowMetadataReaderInit().
///
+/// To read a single key, pass (const char*)out->data to
ArrowMetadataGetValue(); to
+/// read every pair without copying, use ArrowIpcDecoderVisitMessageMetadata().
+///
/// Returns ENOMEM if allocation fails, EINVAL if the metadata cannot be
decoded, or
/// NANOARROW_OK otherwise.
NANOARROW_DLL ArrowErrorCode ArrowIpcDecoderGetMessageMetadata(
struct ArrowIpcDecoder* decoder, struct ArrowBuffer* out, struct
ArrowError* error);
-/// \brief Get one value from the custom metadata of the most recently decoded
message
-///
-/// Unlike ArrowIpcDecoderGetMessageMetadata(), this does not copy: the value
returned
-/// points into the message header passed to ArrowIpcDecoderVerifyHeader() or
-/// ArrowIpcDecoderDecodeHeader() and is only valid until that data is
invalidated or
-/// another message header is decoded.
-///
-/// If key occurs more than once, the first value is returned. If key does not
occur,
-/// value_out is left unmodified: initialize it with ArrowCharView(NULL) and
check
-/// value_out->data for NULL to detect a missing key.
-NANOARROW_DLL ArrowErrorCode ArrowIpcDecoderGetMessageMetadataValue(
- struct ArrowIpcDecoder* decoder, struct ArrowStringView key,
- struct ArrowStringView* value_out, struct ArrowError* error);
-
/// \brief Visit each key/value pair in the most recently decoded message's
metadata
///
-/// Like ArrowIpcDecoderGetMessageMetadataValue(), the keys and values passed
to visit
-/// point into the message header passed to ArrowIpcDecoderVerifyHeader() or
-/// ArrowIpcDecoderDecodeHeader() and must not be retained beyond the lifetime
of that
+/// Unlike ArrowIpcDecoderGetMessageMetadata(), this does not copy: the keys
and values
+/// passed to visit point into the message header passed to
ArrowIpcDecoderVerifyHeader()
+/// or ArrowIpcDecoderDecodeHeader() and must not be retained beyond the
lifetime of that
/// data. private_data and error are passed to each invocation of visit.
///
/// Returns the first non-NANOARROW_OK value returned by visit, or
NANOARROW_OK if all
/// pairs were visited.
NANOARROW_DLL ArrowErrorCode ArrowIpcDecoderVisitMessageMetadata(
- struct ArrowIpcDecoder* decoder, ArrowIpcMetadataVisitFunction visit,
+ struct ArrowIpcDecoder* decoder, ArrowIpcMessageMetadataVisitFunction
visit,
void* private_data, struct ArrowError* error);
/// \brief Decode an ArrowSchema