From a6b8b6f479ce1653812eff7516610b363308ca90 Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Fri, 5 Jun 2026 17:54:35 +0200 Subject: [PATCH 1/3] [ntuple] fix late record extension with buffered sink --- tree/ntuple/src/RPageSinkBuf.cxx | 11 +++++++++- tree/ntuple/test/ntuple_modelext.cxx | 31 ++++++++++++++-------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/tree/ntuple/src/RPageSinkBuf.cxx b/tree/ntuple/src/RPageSinkBuf.cxx index a4b715e99dcbf..1fed3a0138ced 100644 --- a/tree/ntuple/src/RPageSinkBuf.cxx +++ b/tree/ntuple/src/RPageSinkBuf.cxx @@ -108,7 +108,16 @@ void ROOT::Internal::RPageSinkBuf::UpdateSchema(const ROOT::Internal::RNTupleMod auto cloneAddField = [&](const ROOT::RFieldBase *field) { auto cloned = field->Clone(field->GetFieldName()); auto p = &(*cloned); - fInnerModel->AddField(std::move(cloned)); + + auto parent = field->GetParent(); + assert(parent); + if (typeid(*parent) != typeid(RFieldZero)) { + auto &innerParent = fInnerModel->GetMutableField(parent->GetQualifiedFieldName()); + assert(dynamic_cast(&innerParent)); + AddItemToRecord(static_cast(innerParent), std::move(cloned)); + } else { + fInnerModel->AddField(std::move(cloned)); + } return p; }; auto cloneAddProjectedField = [&](ROOT::RFieldBase *field) { diff --git a/tree/ntuple/test/ntuple_modelext.cxx b/tree/ntuple/test/ntuple_modelext.cxx index 9ac90f2446af8..7fd211f5a8b96 100644 --- a/tree/ntuple/test/ntuple_modelext.cxx +++ b/tree/ntuple/test/ntuple_modelext.cxx @@ -737,33 +737,33 @@ TEST(RNTuple, ModelExtensionRecordSimple) std::string expect{ R"({ "r": { - "pt": 1 - }, - "vec": [] + "pt": 1, + "vec": [] + } } { "r": { - "pt": 2 - }, - "vec": [] + "pt": 2, + "vec": [] + } } { "r": { - "pt": 3 - }, - "vec": [10, 11] + "pt": 3, + "vec": [10, 11] + } } { "r": { - "pt": 4 - }, - "vec": [] + "pt": 4, + "vec": [] + } } { "r": { - "pt": 5 - }, - "vec": [12] + "pt": 5, + "vec": [12] + } } )" }; // clang-format on @@ -782,7 +782,6 @@ TEST(RNTuple, ModelExtensionRecordNested) model->AddField(std::move(recordField)); RNTupleWriteOptions opts; - opts.SetUseBufferedWrite(false); auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath(), opts); auto entry = writer->CreateEntry(); From 277554d9ca2768e4ef1ba0fa6c47976896713652 Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Fri, 5 Jun 2026 17:28:40 +0200 Subject: [PATCH 2/3] [ntuple] fix late model extension of nested records --- tree/ntuple/src/RField.cxx | 20 ++++++++++++++++++++ tree/ntuple/test/ntuple_modelext.cxx | 2 ++ 2 files changed, 22 insertions(+) diff --git a/tree/ntuple/src/RField.cxx b/tree/ntuple/src/RField.cxx index e994b3851b073..f2b55f957b712 100644 --- a/tree/ntuple/src/RField.cxx +++ b/tree/ntuple/src/RField.cxx @@ -643,6 +643,26 @@ void ROOT::Internal::AddItemToRecord(RRecordField &record, std::unique_ptr(parent) && parent->GetTypeName().empty()); + + auto precord = static_cast(parent); + std::vector> subfields; + std::swap(precord->fSubfields, subfields); + precord->fTraits = ROOT::RFieldBase::kTraitExtensible; + precord->fSubfieldNames.clear(); + precord->fMaxAlignment = 1; + precord->fSize = 0; + precord->fOffsets.clear(); + precord->AttachItemFields(std::move(subfields)); + + parent = precord->fParent; + } } std::size_t ROOT::RRecordField::GetItemPadding(std::size_t baseOffset, std::size_t itemAlignment) const diff --git a/tree/ntuple/test/ntuple_modelext.cxx b/tree/ntuple/test/ntuple_modelext.cxx index 7fd211f5a8b96..9a88c3a0a03a5 100644 --- a/tree/ntuple/test/ntuple_modelext.cxx +++ b/tree/ntuple/test/ntuple_modelext.cxx @@ -784,6 +784,7 @@ TEST(RNTuple, ModelExtensionRecordNested) RNTupleWriteOptions opts; auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath(), opts); + EXPECT_EQ(1u, writer->GetModel().GetConstField("r1").GetValueSize()); auto entry = writer->CreateEntry(); writer->Fill(*entry); writer->CommitCluster(); @@ -799,6 +800,7 @@ TEST(RNTuple, ModelExtensionRecordNested) modelUpdater->AddField(std::move(recordField), "r1.r2"); modelUpdater->CommitUpdate(); + EXPECT_EQ(sizeof(float), writer->GetModel().GetConstField("r1").GetValueSize()); entry = writer->CreateEntry(); auto ptrFloat = static_cast(entry->GetPtr("r1").get()); *ptrFloat = 1.0; From 55252f5687f0107531e5f32437ba5610c3b718d2 Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Sat, 6 Jun 2026 20:59:39 +0200 Subject: [PATCH 3/3] [ntuple] extend late record extension test --- tree/ntuple/test/ntuple_modelext.cxx | 40 +++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/tree/ntuple/test/ntuple_modelext.cxx b/tree/ntuple/test/ntuple_modelext.cxx index 9a88c3a0a03a5..8ec3886f9216d 100644 --- a/tree/ntuple/test/ntuple_modelext.cxx +++ b/tree/ntuple/test/ntuple_modelext.cxx @@ -805,15 +805,32 @@ TEST(RNTuple, ModelExtensionRecordNested) auto ptrFloat = static_cast(entry->GetPtr("r1").get()); *ptrFloat = 1.0; writer->Fill(*entry); + + modelUpdater->BeginUpdate(); + modelUpdater->AddField(std::make_unique>("ptHP"), "r1.r2.r3.r4"); + modelUpdater->CommitUpdate(); + + EXPECT_EQ(2 * sizeof(double), writer->GetModel().GetConstField("r1").GetValueSize()); + entry = writer->CreateEntry(); + struct FloatAndDouble { + float pt; + double ptHP; + }; + + auto ptrFloatAndDouble = static_cast(entry->GetPtr("r1").get()); + ptrFloatAndDouble->pt = 2.0; + ptrFloatAndDouble->ptHP = 3.0; + writer->Fill(*entry); } auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath()); - EXPECT_EQ(3u, reader->GetNEntries()); + EXPECT_EQ(4u, reader->GetNEntries()); std::ostringstream os; reader->Show(0, os); reader->Show(1, os); reader->Show(2, os); + reader->Show(3, os); // clang-format off std::string expect{ R"({ @@ -821,7 +838,20 @@ R"({ "r2": { "r3": { "r4": { - "pt": 0 + "pt": 0, + "ptHP": 0 + } + } + } + } +} +{ + "r1": { + "r2": { + "r3": { + "r4": { + "pt": 0, + "ptHP": 0 } } } @@ -832,7 +862,8 @@ R"({ "r2": { "r3": { "r4": { - "pt": 0 + "pt": 1, + "ptHP": 0 } } } @@ -843,7 +874,8 @@ R"({ "r2": { "r3": { "r4": { - "pt": 1 + "pt": 2, + "ptHP": 3 } } }