diff --git a/tree/ntuple/inc/ROOT/RField/RFieldSoA.hxx b/tree/ntuple/inc/ROOT/RField/RFieldSoA.hxx index 0861c68a1a60c..1f7ff8a16089c 100644 --- a/tree/ntuple/inc/ROOT/RField/RFieldSoA.hxx +++ b/tree/ntuple/inc/ROOT/RField/RFieldSoA.hxx @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -63,9 +64,12 @@ class RSoAField : public RFieldBase { }; TClass *fSoAClass = nullptr; - std::vector fRecordMemberFields; ///< Direct access to the member fields of the underlying record - /// The offset of the RVec members in the SoA type in the order of subfields of the underlying record type. - /// In particular, the order is not necessarily the same then the order of RVec members in the SoA class. + /// Direct access to the member fields of the underlying record. In case of a nested SoA type, this vector + /// contains the contents of the inner fRecordMemberFields, too. Effectively, this record will contain all the + /// fields of the underlying record type that correspond to terminal RVec members in a nested SoA type. + /// Pointers in this vector point into the field hierarchy owned by fSubfields[0]. + std::vector fRecordMemberFields; + /// The offset of the RVec members in the SoA type in the same order as fRecordMemberFields. std::vector fSoAMemberOffsets; ///< A deleter returned by each record member's GetDeleter() std::vector> fRecordMemberDeleters; @@ -75,12 +79,26 @@ class RSoAField : public RFieldBase { /// SoA object are used directly with the subfields of the underlying record type. For splitting a SoA class object /// (SplitValue()), however, we need actual RRVecFields so that we can recursively split the in-memory SoA value. /// The split fields are created only when SplitField() is called. - mutable std::unique_ptr>> fSplitFields; + /// In general, the number of split fields is different from the number of record member fields because there is + /// one split field for every _direct_ member of the SoA class. Because nested SoA structs can have transient + /// members, we can also not reuse fSoAMemberOffsets but we need to store the data member offsets. + mutable std::unique_ptr>> fSplitFields; + mutable std::unique_ptr> fSplitOffsets; mutable std::unique_ptr fLockSplitFields; ///< protects the fSplitFields member. RSoAField(std::string_view fieldName, const RSoAField &source); ///< Used by CloneImpl RSoAField(std::string_view fieldName, TClass *clSoA); + /// Called during construction, picks up the (nested) member fields of the underlying record type(s) and its + /// base classes. + void CollectRecordMemberFields(); + /// For a nested SoA struct (either as a member of as a base class), use their fRecordMemberFields in this class, + /// i.e. "unroll" the vectors in the nested SoA struct into the SoA base class. + void GraftNestedMemberFields(const RSoAField &nestedSoA, std::size_t offsetInParent, + std::function fnRecordFieldFinder); + + void ReconstructSplitFields() const; + protected: std::unique_ptr CloneImpl(std::string_view newName) const final; diff --git a/tree/ntuple/src/RFieldMeta.cxx b/tree/ntuple/src/RFieldMeta.cxx index 5b97f86383703..a5533d47a0ce6 100644 --- a/tree/ntuple/src/RFieldMeta.cxx +++ b/tree/ntuple/src/RFieldMeta.cxx @@ -686,6 +686,171 @@ ROOT::Experimental::RSoAField::RSoAField(std::string_view fieldName, std::string { } +void ROOT::Experimental::RSoAField::GraftNestedMemberFields( + const RSoAField &nestedSoA, std::size_t offsetInParent, + std::function fnRecordFieldFinder) +{ + const std::size_t nNestedRecordMemberFields = nestedSoA.fRecordMemberFields.size(); + + // The qualified field name of fields in nestedSoA->fRecordMemberFields will have a "._0." + // prefix because these fields are rooted in a collection named after the nested SoA field. + // + // E.g., in the following example: + // + // struct SoA_A { struct Record_A { + // SoA_B fB; Record_B fB; + // }; }; + // + // struct SoA_B { struct Record_B { + // ROOT::RVec fX; float fX; + // }; }; + // + // The on-disk schema of SoA_A is "collection of Record_A", and the on-disk schema of SoA_B is + // "collection of Record_B". + // The qualified field name of fX in the subfield hiararchy of SoA_A is ._0.fB.fX. + // The qualified field name of fX in the subfield hiararchy of SoA_B is ._0.fX. + const auto lenPrefix = nestedSoA.GetFieldName().length() + strlen("._0."); + + for (std::size_t i = 0; i < nNestedRecordMemberFields; ++i) { + const auto fieldNameForMatching = + nestedSoA.GetFieldName() + "." + nestedSoA.fRecordMemberFields[i]->GetQualifiedFieldName().substr(lenPrefix); + + fRecordMemberFields.emplace_back(fnRecordFieldFinder(fieldNameForMatching)); + fRecordMemberDeleters.emplace_back(GetDeleterOf(*nestedSoA.fRecordMemberFields[i])); + fSoAMemberOffsets.emplace_back(offsetInParent + nestedSoA.fSoAMemberOffsets[i]); + } +} + +void ROOT::Experimental::RSoAField::CollectRecordMemberFields() +{ + // Build a map of all subfields (nested) of the underlying record type. Map the fully qualified name of the + // subfields to their field pointer, so that we can later match the subfields of the SoA class to their corresponding + // fields in the underlying record type. Note that the members of the SoA class and the underlying record type + // can have different ordering. However, the base classes of the SoA class and the underlying record type must match + // in order. + + std::vector realRecordMemberFields; // Contains all subfields of the underlying record type + // Qualified field name --> index in realRecordMemberFields + std::unordered_map recordFieldNameToIdx; + + // Count the top-level subfields of the underlying record type for cross-check with the SoA type + unsigned int nDirectRecordSubfields = 0; + unsigned int nDirectRecordBases = 0; + + for (auto itr = fSubfields[0]->begin(), iEnd = fSubfields[0]->end(); itr != iEnd; ++itr) { + if (itr->GetParent() == fSubfields[0].get()) { + (itr->GetFieldName()[0] == ':') ? nDirectRecordBases++ : nDirectRecordSubfields++; + } + + // Build the qualified field name for matching. We root the qualified field name at the underlying record type. + auto qualifiedName = itr->GetFieldName(); + auto parent = itr->GetParent(); + while (parent != fSubfields[0].get()) { + qualifiedName = parent->GetFieldName() + "." + qualifiedName; + parent = parent->GetParent(); + } + recordFieldNameToIdx[qualifiedName] = realRecordMemberFields.size(); + + realRecordMemberFields.emplace_back(&(*itr)); + } + + // Base classes are treated as unrolled nested SoA classes + const auto *soaBases = fSoAClass->GetListOfBases(); + if (soaBases->GetSize() != static_cast(nDirectRecordBases)) { + throw RException(R__FAIL(std::string("number of base classes don't match between SoA class ") + GetFieldName() + + " and its underlying record type")); + } + for (unsigned int i = 0; i < static_cast(soaBases->GetSize()); ++i) { + auto base = static_cast(soaBases->At(i)); + if (base->GetDelta() < 0) { + throw RException(R__FAIL(std::string("virtual inheritance is not supported: ") + GetTypeName() + + " virtually inherits from " + base->GetName())); + } + TClass *cl = base->GetClassPointer(); + + const auto baseFieldName = std::string(":_") + std::to_string(i); + + // SoA class `A` inherits from a SoA class `B` whose underlying record type is `X` if and only if the underlying + // record type of `A` inherits from a type `X`. + const auto underlyingBaseTypeName = ROOT::Internal::GetRNTupleSoARecord(cl); + auto recordBaseField = realRecordMemberFields[recordFieldNameToIdx[baseFieldName]]; + if (underlyingBaseTypeName != recordBaseField->GetTypeName()) { + throw RException(R__FAIL(std::string("inheritance of SoA class ") + GetFieldName() + + " does not match its underlying record type")); + } + + std::unique_ptr soaBaseField; + try { + soaBaseField = std::make_unique(baseFieldName, cl->GetName()); + } catch (const RException &e) { + throw RException(R__FAIL(std::string("invalid field type in base class: ") + cl->GetName() + " of SoA field " + + GetFieldName() + " (" + e.what() + ")")); + } + + GraftNestedMemberFields(*soaBaseField, base->GetDelta(), [&](const std::string &name) { + return realRecordMemberFields[recordFieldNameToIdx[name]]; + }); + } + + unsigned int nMembers = 0; + for (auto dataMember : ROOT::Detail::TRangeStaticCast(*fSoAClass->GetListOfDataMembers())) { + // NOTE: ReconstructSplitFields() will also traverse the data members and need to apply the same rules for + // skipping members + + if ((dataMember->Property() & kIsStatic) || !dataMember->IsPersistent()) + continue; + + if (dataMember->Property() & kIsArray) { + throw RException(R__FAIL(std::string("unsupported array type in SoA class: ") + dataMember->GetName())); + } + + const std::string typeName{dataMember->GetTrueTypeName()}; + auto dmField = RFieldBase::Create(dataMember->GetName(), typeName).Unwrap(); + + auto itr = recordFieldNameToIdx.find(dmField->GetFieldName()); + if (itr == recordFieldNameToIdx.end()) { + throw RException(R__FAIL(std::string("unexpected SoA member: ") + dmField->GetFieldName())); + } + auto underlyingField = realRecordMemberFields[itr->second]; + assert(dmField->GetFieldName() == underlyingField->GetFieldName()); + + if (auto soaField = dynamic_cast(dmField.get())) { + if (ROOT::Internal::GetRNTupleSoARecord(soaField->fSoAClass) != underlyingField->GetTypeName()) { + throw RException(R__FAIL(std::string("nested SoA field ") + soaField->GetQualifiedFieldName() + " [" + + soaField->GetTypeName() + "] does not match underlying type " + + underlyingField->GetTypeName())); + } + + GraftNestedMemberFields(*soaField, dataMember->GetOffset(), [&](const std::string &name) { + return realRecordMemberFields[recordFieldNameToIdx[name]]; + }); + } else if (auto vecField = dynamic_cast(dmField.get())) { + if (vecField->begin()->GetTypeName() != underlyingField->GetTypeName() || + vecField->begin()->GetTypeAlias() != underlyingField->GetTypeAlias()) { + const std::string leftType = + vecField->begin()->GetTypeName() + + (vecField->begin()->GetTypeAlias().empty() ? "" : " [" + vecField->begin()->GetTypeAlias() + "]"); + const std::string rightType = + underlyingField->GetTypeName() + + (underlyingField->GetTypeAlias().empty() ? "" : " [" + underlyingField->GetTypeAlias() + "]"); + throw RException(R__FAIL(std::string("SoA member type mismatch: ") + vecField->GetFieldName() + " (" + + leftType + " vs. " + rightType + ")")); + } + + fRecordMemberFields.emplace_back(underlyingField); + fRecordMemberDeleters.emplace_back(GetDeleterOf(*underlyingField)); + fSoAMemberOffsets.emplace_back(dataMember->GetOffset()); + } else { + throw RException(R__FAIL("invalid field type in SoA class: " + dmField->GetTypeName())); + } + + nMembers++; + } + if (nDirectRecordSubfields != nMembers) { + throw RException(R__FAIL("missing SoA members")); + } +} + ROOT::Experimental::RSoAField::RSoAField(std::string_view fieldName, TClass *clSoA) : ROOT::RFieldBase(fieldName, GetRenormalizedTypeName(clSoA->GetName()), ROOT::ENTupleStructure::kCollection, false /* isSimple */), @@ -714,75 +879,8 @@ ROOT::Experimental::RSoAField::RSoAField(std::string_view fieldName, TClass *clS std::to_string(fSoAClass->GetClassVersion()) + " vs. " + std::to_string(fSubfields[0]->GetTypeVersion()))); } - fRecordMemberFields = fSubfields[0]->GetMutableSubfields(); - std::unordered_map recordFieldNameToIdx; - fRecordMemberDeleters.reserve(fRecordMemberFields.size()); - recordFieldNameToIdx.reserve(fRecordMemberFields.size()); - for (std::size_t i = 0; i < fRecordMemberFields.size(); ++i) { - const RFieldBase *f = fRecordMemberFields[i]; - assert(!f->GetFieldName().empty()); - if (f->GetFieldName()[0] == ':') { - throw RException(R__FAIL("SoA fields with inheritance are currently unsupported")); - } - recordFieldNameToIdx[f->GetFieldName()] = i; - fRecordMemberDeleters.emplace_back(GetDeleterOf(*f)); - } - - const auto *bases = fSoAClass->GetListOfBases(); - assert(bases); - for (auto baseClass : ROOT::Detail::TRangeStaticCast(*bases)) { - if (baseClass->GetDelta() < 0) { - throw RException(R__FAIL(std::string("virtual inheritance is not supported: ") + GetTypeName() + - " virtually inherits from " + baseClass->GetName())); - } - // At a later point, we will support inheritance - throw RException(R__FAIL("SoA fields with inheritance are currently unsupported")); - } - - fSoAMemberOffsets.resize(fRecordMemberFields.size()); - unsigned int nMembers = 0; - for (auto dataMember : ROOT::Detail::TRangeStaticCast(*fSoAClass->GetListOfDataMembers())) { - if ((dataMember->Property() & kIsStatic) || !dataMember->IsPersistent()) - continue; - - if (dataMember->Property() & kIsArray) { - throw RException(R__FAIL(std::string("unsupported array type in SoA class: ") + dataMember->GetName())); - } - - const std::string typeName{dataMember->GetTrueTypeName()}; - auto subField = RFieldBase::Create(dataMember->GetName(), typeName).Unwrap(); - auto vecFieldPtr = dynamic_cast(subField.get()); - if (!vecFieldPtr) { - throw RException(R__FAIL("invalid field type in SoA class: " + subField->GetTypeName())); - } - subField.release(); - auto vecField = std::unique_ptr(vecFieldPtr); - - auto itr = recordFieldNameToIdx.find(vecField->GetFieldName()); - if (itr == recordFieldNameToIdx.end()) { - throw RException(R__FAIL(std::string("unexpected SoA member: ") + vecField->GetFieldName())); - } - const RFieldBase *memberField = fRecordMemberFields[itr->second]; - if (vecField->begin()->GetTypeName() != memberField->GetTypeName() || - vecField->begin()->GetTypeAlias() != memberField->GetTypeAlias()) { - const std::string leftType = - vecField->begin()->GetTypeName() + - (vecField->begin()->GetTypeAlias().empty() ? "" : " [" + vecField->begin()->GetTypeAlias() + "]"); - const std::string rightType = - memberField->GetTypeName() + - (memberField->GetTypeAlias().empty() ? "" : " [" + memberField->GetTypeAlias() + "]"); - throw RException(R__FAIL(std::string("SoA member type mismatch: ") + vecField->GetFieldName() + " (" + - leftType + " vs. " + rightType + ")")); - } - - assert(itr->second < fSoAMemberOffsets.size()); - fSoAMemberOffsets[itr->second] = dataMember->GetOffset(); - nMembers++; - } - if (recordFieldNameToIdx.size() != nMembers) { - throw RException(R__FAIL("missing SoA members")); - } + CollectRecordMemberFields(); std::string renormalizedAlias; if (ROOT::Internal::NeedsMetaNameAsAlias(fSoAClass->GetName(), renormalizedAlias)) @@ -898,29 +996,46 @@ void ROOT::Experimental::RSoAField::RSoADeleter::operator()(void *objPtr, bool d RDeleter::operator()(objPtr, dtorOnly); } -std::vector ROOT::Experimental::RSoAField::SplitValue(const RValue &value) const +void ROOT::Experimental::RSoAField::ReconstructSplitFields() const { - const auto nSoAMembers = fSoAMemberOffsets.size(); + std::lock_guard lockGuard(*fLockSplitFields); + if (fSplitFields) + return; - { - std::lock_guard lockGuard(*fLockSplitFields); - if (!fSplitFields) { - fSplitFields = std::make_unique>>(); - fSplitFields->reserve(nSoAMembers); - for (std::size_t i = 0; i < nSoAMembers; ++i) { - const auto itemField = fRecordMemberFields[i]; - fSplitFields->emplace_back(std::make_unique(itemField->GetFieldName(), itemField->Clone("_0"))); - } - } + fSplitFields = std::make_unique>>(); + fSplitOffsets = std::make_unique>(); + + unsigned int iBase = 0; + for (auto base : ROOT::Detail::TRangeStaticCast(*fSoAClass->GetListOfBases())) { + TClass *cl = base->GetClassPointer(); + auto baseField = RFieldBase::Create(std::string(":_" + std::to_string(iBase)), cl->GetName()).Unwrap(); + fSplitFields->emplace_back(std::move(baseField)); + fSplitOffsets->emplace_back(base->GetDelta()); + iBase++; } + for (auto dataMember : ROOT::Detail::TRangeStaticCast(*fSoAClass->GetListOfDataMembers())) { + if ((dataMember->Property() & kIsStatic) || !dataMember->IsPersistent()) + continue; + + const std::string typeName{dataMember->GetTrueTypeName()}; + auto dmField = RFieldBase::Create(dataMember->GetName(), typeName).Unwrap(); + fSplitFields->emplace_back(std::move(dmField)); + fSplitOffsets->emplace_back(dataMember->GetOffset()); + } +} + +std::vector ROOT::Experimental::RSoAField::SplitValue(const RValue &value) const +{ + ReconstructSplitFields(); + const auto nSplitFields = fSplitFields->size(); + auto valuePtr = value.GetPtr(); auto soaPtr = static_cast(valuePtr.get()); std::vector values; - values.reserve(nSoAMembers); - for (std::size_t i = 0; i < nSoAMembers; ++i) { - values.emplace_back( - (*fSplitFields)[i]->BindValue(std::shared_ptr(valuePtr, soaPtr + fSoAMemberOffsets[i]))); + values.reserve(nSplitFields); + for (std::size_t i = 0; i < nSplitFields; ++i) { + values.emplace_back((*fSplitFields)[i]->BindValue(std::shared_ptr(valuePtr, soaPtr + (*fSplitOffsets)[i]))); } return values; } diff --git a/tree/ntuple/test/SoAField.hxx b/tree/ntuple/test/SoAField.hxx index 44143d787135c..e74192aa84c88 100644 --- a/tree/ntuple/test/SoAField.hxx +++ b/tree/ntuple/test/SoAField.hxx @@ -23,26 +23,6 @@ struct SoAVersionMismatch { ClassDefNV(SoAVersionMismatch, 4); }; -struct RecordBase { - ClassDefNV(RecordBase, 2); -}; - -struct RecordDerived : public RecordBase { - ClassDefNV(RecordDerived, 2); -}; - -struct SoABase { - ClassDefNV(SoABase, 2); -}; - -struct SoAOnDerivedRecord { - ClassDefNV(SoAOnDerivedRecord, 2); -}; - -struct SoADerivedOnBaseRecord : public SoABase { - ClassDefNV(SoADerivedOnBaseRecord, 2); -}; - struct RecordSimple { float fX; float fY; @@ -114,4 +94,80 @@ struct SoAComplex { ClassDefNV(SoAComplex, 2); }; +struct RecordProperties { + int fColor; + float fSize; + + ClassDefNV(RecordProperties, 2); +}; + +struct SoAProperties { + ROOT::RVec fColor; + ROOT::RVec fSize; + + ClassDefNV(SoAProperties, 2); +}; + +struct RecordDot { + float fX; + float fY; + RecordProperties fProperties; + + ClassDefNV(RecordDot, 2); +}; + +struct SoADot { + ROOT::RVec fX; + ROOT::RVec fY; + SoAProperties fProperties; + + ClassDefNV(SoADot, 2); +}; + +struct SoADotBadNestedType { + ROOT::RVec fX; + ROOT::RVec fY; + SoA fProperties; + + ClassDefNV(SoADotBadNestedType, 2); +}; + +struct RecordBase { + float fBase; + ClassDefNV(RecordBase, 2); +}; + +struct SoABase { + ROOT::RVec fBase; + ClassDefNV(SoABase, 2); +}; + +struct RecordDerived : public RecordBase { + float fDerived; + ClassDefNV(RecordDerived, 2); +}; + +struct SoADerived : public SoABase { + ROOT::RVec fDerived; + ClassDefNV(SoADerived, 2); +}; + +struct RecordDerivedMulti : public RecordDerived, RecordDot { + float fMulti; + ClassDefNV(RecordDerivedMulti, 2); +}; + +struct SoADerivedMulti : public SoADerived, SoADot { + ROOT::RVec fMulti; + ClassDefNV(SoADerivedMulti, 2); +}; + +struct SoADerivedFail1 : public RecordDerived { + ClassDefNV(SoADerivedFail1, 2); +}; + +struct SoADerivedFail2 : public SoABase { + ClassDefNV(SoADerivedFail2, 2); +}; + #endif // ROOT_RNTuple_Test_SoAField diff --git a/tree/ntuple/test/SoAFieldLinkDef.h b/tree/ntuple/test/SoAFieldLinkDef.h index 970b5e6f02c75..0af69cec4189a 100644 --- a/tree/ntuple/test/SoAFieldLinkDef.h +++ b/tree/ntuple/test/SoAFieldLinkDef.h @@ -6,12 +6,6 @@ #pragma link C++ options=rntupleSoARecord(xyz) class SoAUnknownRecord+; #pragma link C++ options=rntupleSoARecord(Record) class SoAVersionMismatch+; -#pragma link C++ class RecordBase+; -#pragma link C++ class RecordDerived+; -#pragma link C++ options=rntupleSoARecord(RecordBase) class SoABase+; -#pragma link C++ options=rntupleSoARecord(RecordDerived) class SoAOnDerivedRecord+; -#pragma link C++ options=rntupleSoARecord(RecordBase) class SoADerivedOnBaseRecord+; - #pragma link C++ class RecordSimple+; #pragma link C++ options=rntupleSoARecord(RecordSimple) class SoASimple+; #pragma link C++ options=rntupleSoARecord(RecordSimple) class SoASimpleSwapped+; @@ -25,4 +19,19 @@ #pragma link C++ class RecordComplex+; #pragma link C++ options=rntupleSoARecord(RecordComplex) class SoAComplex+; +#pragma link C++ class RecordProperties+; +#pragma link C++ class RecordDot+; +#pragma link C++ options=rntupleSoARecord(RecordProperties) class SoAProperties+; +#pragma link C++ options=rntupleSoARecord(RecordDot) class SoADot+; +#pragma link C++ options=rntupleSoARecord(RecordDot) class SoADotBadNestedType+; + +#pragma link C++ class RecordBase+; +#pragma link C++ class RecordDerived+; +#pragma link C++ class RecordDerivedMulti+; +#pragma link C++ options=rntupleSoARecord(RecordBase) class SoABase+; +#pragma link C++ options=rntupleSoARecord(RecordDerived) class SoADerived+; +#pragma link C++ options=rntupleSoARecord(RecordDerivedMulti) class SoADerivedMulti+; +#pragma link C++ options=rntupleSoARecord(RecordDerived) class SoADerivedFail1+; +#pragma link C++ options=rntupleSoARecord(RecordDerived) class SoADerivedFail2+; + #endif // __CLING__ diff --git a/tree/ntuple/test/ntuple_soa.cxx b/tree/ntuple/test/ntuple_soa.cxx index 8f6ceb5c05d3b..c4a5bc272ba59 100644 --- a/tree/ntuple/test/ntuple_soa.cxx +++ b/tree/ntuple/test/ntuple_soa.cxx @@ -70,22 +70,6 @@ TEST(RNTuple, SoACheck) EXPECT_THAT(e.what(), ::testing::HasSubstr("version mismatch between SoA type and underlying record type")); } - try { - auto f = std::make_unique("f", "SoAOnDerivedRecord"); - FAIL() << "creating SoA field on derived record should fail"; - } catch (const ROOT::RException &e) { - EXPECT_THAT(e.what(), ::testing::HasSubstr("SoA fields with inheritance are currently unsupported")); - } - try { - auto f = std::make_unique("f", "SoADerivedOnBaseRecord"); - FAIL() << "creating a derived SoA field should fail"; - } catch (const ROOT::RException &e) { - EXPECT_THAT(e.what(), ::testing::HasSubstr("SoA fields with inheritance are currently unsupported")); - } - { - EXPECT_NO_THROW(auto f = std::make_unique("f", "SoABase")); - } - try { auto f = std::make_unique("f", "SoASimpleBadArray"); FAIL() << "creating SoA field with arrays fail"; @@ -468,3 +452,127 @@ R"({ // clang-format on EXPECT_EQ(expected, os.str()); } + +TEST(RNTuple, SoANested) +{ + ROOT::TestSupport::FileRaii fileGuard("test_rntuple_soa_nested.root"); + + { + auto model = ROOT::RNTupleModel::Create(); + try { + model->AddField(std::make_unique("dot", "SoADotBadNestedType")); + FAIL() << "SoADotBadNestedType should fail"; + } catch (const ROOT::RException &e) { + EXPECT_THAT(e.what(), testing::HasSubstr("nested SoA field fProperties [SoA] does not match underlying type " + "RecordProperties")); + } + + model->AddField(std::make_unique("dot", "SoADot")); + auto writer = ROOT::RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); + auto dotSoA = writer->GetModel().GetDefaultEntry().GetPtr("dot"); + dotSoA->fX = {1.0, 2.0}; + dotSoA->fY = {3.0, 4.0}; + dotSoA->fProperties.fColor = {5, 6}; + dotSoA->fProperties.fSize = {7.0, 8.0}; + writer->Fill(); + } + + auto reader = ROOT::RNTupleReader::Open("ntpl", fileGuard.GetPath()); + + std::ostringstream os; + reader->Show(0, os); + + // clang-format off + std::string expected{ +R"({ + "dot": { + "fX": [1, 2], + "fY": [3, 4], + "fProperties": { + "fColor": [5, 6], + "fSize": [7, 8] + } + } +} +)" }; + // clang-format on + EXPECT_EQ(expected, os.str()); +} + +TEST(RNTuple, SoADerived) +{ + ROOT::TestSupport::FileRaii fileGuard("test_rntuple_soa_derived.root"); + + { + auto model = ROOT::RNTupleModel::Create(); + + try { + model->AddField(std::make_unique("x", "SoADerivedFail1")); + FAIL() << "SoADerivedFail1 should fail"; + } catch (const ROOT::RException &e) { + EXPECT_THAT(e.what(), + testing::HasSubstr("inheritance of SoA class x does not match its underlying record type")); + } + try { + model->AddField(std::make_unique("x", "SoADerivedFail2")); + FAIL() << "SoADerivedFail2 should fail"; + } catch (const ROOT::RException &e) { + EXPECT_THAT(e.what(), testing::HasSubstr("missing SoA members")); + } + + model->AddField(std::make_unique("derived", "SoADerived")); + model->AddField(std::make_unique("multi", "SoADerivedMulti")); + auto writer = ROOT::RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); + + auto derivedSoA = writer->GetModel().GetDefaultEntry().GetPtr("derived"); + derivedSoA->fBase = {1.0, 2.0}; + derivedSoA->fDerived = {3.0, 4.0}; + + auto derivedMultiSoA = writer->GetModel().GetDefaultEntry().GetPtr("multi"); + derivedMultiSoA->fBase = {5.0, 6.0}; + derivedMultiSoA->fDerived = {7.0, 8.0}; + derivedMultiSoA->fX = {9.0, 10.0}; + derivedMultiSoA->fY = {11.0, 12.0}; + derivedMultiSoA->fProperties.fColor = {13, 14}; + derivedMultiSoA->fProperties.fSize = {15.0, 16.0}; + derivedMultiSoA->fMulti = {17.0, 18.0}; + + writer->Fill(); + } + + auto reader = ROOT::RNTupleReader::Open("ntpl", fileGuard.GetPath()); + + std::ostringstream os; + reader->Show(0, os); + + // clang-format off + std::string expected{ +R"({ + "derived": { + ":_0": { + "fBase": [1, 2] + }, + "fDerived": [3, 4] + }, + "multi": { + ":_0": { + ":_0": { + "fBase": [5, 6] + }, + "fDerived": [7, 8] + }, + ":_1": { + "fX": [9, 10], + "fY": [11, 12], + "fProperties": { + "fColor": [13, 14], + "fSize": [15, 16] + } + }, + "fMulti": [17, 18] + } +} +)" }; + // clang-format on + EXPECT_EQ(expected, os.str()); +}