This is an automated email from the ASF dual-hosted git repository.
Fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro.git
The following commit(s) were added to refs/heads/main by this push:
new 209a3735ec AVRO-4351: [C++] Preserve primitive custom attributes
(#4001)
209a3735ec is described below
commit 209a3735ec330679790824da54b7558db55e4e7f
Author: Gang Wu <[email protected]>
AuthorDate: Mon Sep 21 20:53:46 2026 +0800
AVRO-4351: [C++] Preserve primitive custom attributes (#4001)
Preserve unknown properties when parsing and serializing primitive
schema objects. This keeps extensions such as Iceberg's adjust-to-utc
annotation intact across schema JSON round trips.
---
lang/c++/impl/Compiler.cc | 7 +++++++
lang/c++/impl/NodeImpl.cc | 9 ++++++++-
lang/c++/test/SchemaTests.cc | 25 +++++++++++++++++++++++++
3 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/lang/c++/impl/Compiler.cc b/lang/c++/impl/Compiler.cc
index c0b39f9e74..d69fd6548f 100644
--- a/lang/c++/impl/Compiler.cc
+++ b/lang/c++/impl/Compiler.cc
@@ -552,6 +552,13 @@ static NodePtr makeNode(const Entity &e, const Object &m,
result = makeMapNode(e, m, st, ns);
} else {
result = makePrimitive(type);
+ if (result) {
+ CustomAttributes customAttributes;
+ getCustomAttributes(m, customAttributes);
+ if (!customAttributes.attributes().empty()) {
+ result->addCustomAttributesForField(customAttributes);
+ }
+ }
}
if (result) {
diff --git a/lang/c++/impl/NodeImpl.cc b/lang/c++/impl/NodeImpl.cc
index e68eb365e2..01596a5057 100644
--- a/lang/c++/impl/NodeImpl.cc
+++ b/lang/c++/impl/NodeImpl.cc
@@ -220,8 +220,10 @@ NodeSymbolic::resolve(const Node &reader) const {
void NodePrimitive::printJson(std::ostream &os, size_t depth) const {
bool hasLogicalType = logicalType().type() != LogicalType::NONE;
+ bool hasCustomAttributes = customAttributes_.size() != 0;
+ bool printAsObject = hasLogicalType || hasCustomAttributes;
- if (hasLogicalType) {
+ if (printAsObject) {
os << "{\n"
<< indent(depth) << "\"type\": ";
}
@@ -232,6 +234,11 @@ void NodePrimitive::printJson(std::ostream &os, size_t
depth) const {
os << ",\n"
<< indent(depth);
logicalType().printJson(os);
+ }
+ for (size_t i = 0; i != customAttributes_.size(); ++i) {
+ printCustomAttributes(customAttributes_.get(i), depth, os);
+ }
+ if (printAsObject) {
os << "\n}";
}
if (!getDoc().empty()) {
diff --git a/lang/c++/test/SchemaTests.cc b/lang/c++/test/SchemaTests.cc
index 2aa39d4146..13fe0121d2 100644
--- a/lang/c++/test/SchemaTests.cc
+++ b/lang/c++/test/SchemaTests.cc
@@ -890,6 +890,30 @@ static void testCustomAttributesJson2Schema2Json() {
BOOST_CHECK_EQUAL(removeWhitespaceFromSchema(json),
removeWhitespaceFromSchema(schema));
}
+static void testPrimitiveCustomAttributesJsonRoundTrip() {
+ const std::vector<std::string> schemas = {
+ R"({
+ "type": "long",
+ "logicalType": "timestamp-micros",
+ "adjust-to-utc": true
+ })",
+ R"({
+ "type": "long",
+ "custom-property": "value"
+ })",
+ };
+
+ // Iceberg relies on adjust-to-utc to distinguish timestamp from
timestamptz.
+ // Primitive schema properties must also round-trip without a logical type.
+ for (const auto &schema : schemas) {
+ ValidSchema compiledSchema = compileJsonSchemaFromString(schema);
+ BOOST_REQUIRE_EQUAL(compiledSchema.root()->customAttributes(), 1);
+
+ std::string json = compiledSchema.toJson();
+ BOOST_CHECK_EQUAL(removeWhitespaceFromSchema(json),
removeWhitespaceFromSchema(schema));
+ }
+}
+
static void testCustomAttributesSchema2Json2Schema() {
const std::string expected = R"({
"type": "record",
@@ -951,6 +975,7 @@ init_unit_test_suite(int /*argc*/, char * /*argv*/[]) {
ts->add(BOOST_TEST_CASE(&avro::schema::testParseCustomAttributes));
ts->add(BOOST_TEST_CASE(&avro::schema::testAddCustomAttributes));
ts->add(BOOST_TEST_CASE(&avro::schema::testCustomAttributesJson2Schema2Json));
+
ts->add(BOOST_TEST_CASE(&avro::schema::testPrimitiveCustomAttributesJsonRoundTrip));
ts->add(BOOST_TEST_CASE(&avro::schema::testCustomAttributesSchema2Json2Schema));
return ts;
}