diff --git a/tree/ntuple/CMakeLists.txt b/tree/ntuple/CMakeLists.txt index 6e5b2d0fcdcc6..56061a1a9216c 100644 --- a/tree/ntuple/CMakeLists.txt +++ b/tree/ntuple/CMakeLists.txt @@ -23,6 +23,7 @@ HEADERS ROOT/RFieldVisitor.hxx ROOT/RMiniFile.hxx ROOT/RNTuple.hxx + ROOT/RNTupleAttrReading.hxx ROOT/RNTupleAttrUtils.hxx ROOT/RNTupleAttrWriting.hxx ROOT/RNTupleDescriptor.hxx @@ -68,6 +69,7 @@ SOURCES src/RFieldVisitor.cxx src/RMiniFile.cxx src/RNTuple.cxx + src/RNTupleAttrReading.cxx src/RNTupleAttrWriting.cxx src/RNTupleDescriptor.cxx src/RNTupleDescriptorFmt.cxx diff --git a/tree/ntuple/inc/ROOT/REntry.hxx b/tree/ntuple/inc/ROOT/REntry.hxx index 7028ff09c925d..845749be2f46e 100644 --- a/tree/ntuple/inc/ROOT/REntry.hxx +++ b/tree/ntuple/inc/ROOT/REntry.hxx @@ -34,9 +34,13 @@ namespace ROOT { class RNTupleFillContext; class RNTupleReader; -namespace Experimental::Internal { +namespace Experimental { +class RNTupleAttrSetReader; + +namespace Internal { struct RNTupleAttrEntry; } +} // namespace Experimental // clang-format off /** @@ -52,6 +56,7 @@ class REntry { friend class RNTupleFillContext; friend class RNTupleModel; friend class RNTupleReader; + friend class Experimental::RNTupleAttrSetReader; friend struct Experimental::Internal::RNTupleAttrEntry; private: diff --git a/tree/ntuple/inc/ROOT/RFieldBase.hxx b/tree/ntuple/inc/ROOT/RFieldBase.hxx index a6892edcb4923..f82e83ea7061a 100644 --- a/tree/ntuple/inc/ROOT/RFieldBase.hxx +++ b/tree/ntuple/inc/ROOT/RFieldBase.hxx @@ -44,6 +44,10 @@ class RFieldVisitor; class RRawPtrWriteEntry; } // namespace Detail +namespace Experimental { +class RNTupleAttrSetReader; +} + namespace Internal { class RPageSink; @@ -84,6 +88,7 @@ This is and can only be partially enforced through C++. class RFieldBase { friend class RFieldZero; // to reset fParent pointer in ReleaseSubfields() friend class ROOT::Detail::RRawPtrWriteEntry; // to call Append() + friend class ROOT::Experimental::RNTupleAttrSetReader; // for field->Read() in LoadEntry() friend struct ROOT::Internal::RFieldCallbackInjector; // used for unit tests friend struct ROOT::Internal::RFieldRepresentationModifier; // used for unit tests friend void Internal::CallFlushColumnsOnField(RFieldBase &); diff --git a/tree/ntuple/inc/ROOT/RNTupleAttrReading.hxx b/tree/ntuple/inc/ROOT/RNTupleAttrReading.hxx new file mode 100644 index 0000000000000..46c707ba45f9a --- /dev/null +++ b/tree/ntuple/inc/ROOT/RNTupleAttrReading.hxx @@ -0,0 +1,250 @@ +/// \file ROOT/RNTupleAttrReading.hxx +/// \ingroup NTuple +/// \author Giacomo Parolini +/// \date 2026-04-01 +/// \warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback +/// is welcome! + +#ifndef ROOT7_RNTuple_Attr_Reading +#define ROOT7_RNTuple_Attr_Reading + +#include +#include +#include +#include + +#include +#include +#include + +namespace ROOT { + +class REntry; +class RNTupleDescriptor; +class RNTupleModel; + +namespace Experimental { + +class RNTupleAttrEntryIterable; + +// clang-format off +/** +\class ROOT::Experimental::RNTupleAttrRange +\ingroup NTuple +\brief A range of main entries referred to by an attribute entry + +Each attribute entry contains a set of values referring to 0 or more contiguous entries in the main RNTuple. +This class represents that contiguous range of entries. +*/ +// clang-format on +class RNTupleAttrRange final { + ROOT::NTupleSize_t fStart = 0; + ROOT::NTupleSize_t fLength = 0; + + RNTupleAttrRange(ROOT::NTupleSize_t start, ROOT::NTupleSize_t length) : fStart(start), fLength(length) {} + +public: + static RNTupleAttrRange FromStartLength(ROOT::NTupleSize_t start, ROOT::NTupleSize_t length) + { + return RNTupleAttrRange{start, length}; + } + + /// Creates an AttributeRange from [start, end), where `end` is one past the last valid entry of the range + /// (`FromStartEnd(0, 10)` will create a range whose last valid index is 9). + static RNTupleAttrRange FromStartEnd(ROOT::NTupleSize_t start, ROOT::NTupleSize_t end) + { + R__ASSERT(end >= start); + return RNTupleAttrRange{start, end - start}; + } + + RNTupleAttrRange() = default; + + /// Returns the first valid entry index in the range. Returns nullopt if the range has zero length. + std::optional GetFirst() const { return fLength ? std::make_optional(fStart) : std::nullopt; } + /// Returns the beginning of the range. Note that this is *not* a valid index in the range if the range has zero + /// length. + ROOT::NTupleSize_t GetStart() const { return fStart; } + /// Returns the last valid entry index in the range. Returns nullopt if the range has zero length. + std::optional GetLast() const + { + return fLength ? std::make_optional(fStart + fLength - 1) : std::nullopt; + } + /// Returns one past the last valid index of the range, equal to `GetStart() + GetLength()`. + ROOT::NTupleSize_t GetEnd() const { return fStart + fLength; } + ROOT::NTupleSize_t GetLength() const { return fLength; } + + /// Returns the pair { firstEntryIdx, lastEntryIdx } (inclusive). Returns nullopt if the range has zero length. + std::optional> GetFirstLast() const + { + return fLength ? std::make_optional(std::make_pair(fStart, fStart + fLength - 1)) : std::nullopt; + } + /// Returns the pair { start, length }. + std::pair GetStartLength() const { return {fStart, fLength}; } +}; + +// clang-format off +/** +\class ROOT::Experimental::RNTupleAttrSetReader +\ingroup NTuple +\brief Class used to read a RNTupleAttrSet in the context of a RNTupleReader + +An RNTupleAttrSetReader is created via RNTupleReader::OpenAttributeSet. Once created, it may outlive its parent Reader. +Reading Attributes works similarly to reading regular RNTuple entries: you can either create entries or just use the +AttrSetReader Model's default entry and load data into it via LoadEntry. + +~~ {.cpp} +// Reading Attributes via RNTupleAttrSetReader +// ------------------------------------------- + +// Assuming `reader` is a RNTupleReader: +auto attrSet = reader->OpenAttributeSet("MyAttrSet"); + +// Just like how you would read a regular RNTuple, first get the pointer to the fields you want to read: +auto &attrEntry = attrSet->GetModel().GetDefaultEntry(); +auto pAttr = attrEntry->GetPtr("myAttr"); + +// Then select which attributes you want to read. E.g. read all attributes linked to the entry at index 10: +for (auto idx : attrSet->GetAttributes(10)) { + attrSet->LoadEntry(idx); + cout << "entry " << idx << " has attribute " << *pAttr << "\n"; +} +~~ +*/ +// clang-format on +class RNTupleAttrSetReader final { + friend class ROOT::RNTupleReader; + friend class RNTupleAttrEntryIterable; + + /// List containing pairs { entryRange, entryIndex }, used to quickly find out which entries in the Attribute + /// RNTuple contain entries that overlap a given range. The list is sorted by range start, i.e. + /// entryRange.first.Start(). + std::vector> fEntryRanges; + /// The internal Reader used to read the AttributeSet RNTuple + std::unique_ptr fReader; + /// The reconstructed user model + std::unique_ptr fUserModel; + + static bool EntryRangesAreSorted(const decltype(fEntryRanges) &ranges); + + explicit RNTupleAttrSetReader(std::unique_ptr reader); + + std::vector + GetAttributesRangeInternal(NTupleSize_t startEntry, NTupleSize_t endEntry, bool rangeIsContained); + +public: + RNTupleAttrSetReader(const RNTupleAttrSetReader &) = delete; + RNTupleAttrSetReader &operator=(const RNTupleAttrSetReader &) = delete; + RNTupleAttrSetReader(RNTupleAttrSetReader &&) = default; + RNTupleAttrSetReader &operator=(RNTupleAttrSetReader &&) = default; + ~RNTupleAttrSetReader() = default; + + /// Returns the read-only descriptor of this attribute set + const ROOT::RNTupleDescriptor &GetDescriptor() const; + /// Returns the read-only model of this attribute set + const ROOT::RNTupleModel &GetModel() const { return *fUserModel; } + + /// Creates an entry suitable for use with LoadEntry. + /// This is a convenience method equivalent to GetModel().CreateEntry(). + std::unique_ptr CreateEntry(); + + /// Loads the attribute entry at position `index` into the default entry. + /// Returns the range of main RNTuple entries that the loaded set of attributes refers to. + RNTupleAttrRange LoadEntry(NTupleSize_t index); + /// Loads the attribute entry at position `index` into the given entry. + /// Returns the range of main RNTuple entries that the loaded set of attributes refers to. + RNTupleAttrRange LoadEntry(NTupleSize_t index, REntry &entry); + + /// Returns the number of all attribute entries in this attribute set. + std::size_t GetNEntries() const { return fEntryRanges.size(); } + + /// Returns all the attributes in this Set. The returned attributes are sorted by entry range start. + RNTupleAttrEntryIterable GetAttributes(); + /// Returns all the attributes whose range contains index `entryIndex`. + RNTupleAttrEntryIterable GetAttributes(NTupleSize_t entryIndex); + /// Returns all the attributes whose range fully contains `[startEntry, endEntry)` + RNTupleAttrEntryIterable GetAttributesContainingRange(NTupleSize_t startEntry, NTupleSize_t endEntry); + /// Returns all the attributes whose range is fully contained in `[startEntry, endEntry)` + RNTupleAttrEntryIterable GetAttributesInRange(NTupleSize_t startEntry, NTupleSize_t endEntry); +}; + +// clang-format off +/** +\class ROOT::Experimental::RNTupleAttrEntryIterable +\ingroup NTuple +\brief Iterable class used to loop over attribute entries. + +This class allows to perform range-for iteration on some set of attributes, typically returned by the +RNTupleAttrSetReader::GetAttributes family of methods. + +See the documentation of RNTupleAttrSetReader for example usage. +*/ +// clang-format on +class RNTupleAttrEntryIterable final { +public: + struct RFilter { + RNTupleAttrRange fRange; + bool fIsContained; + }; + +private: + RNTupleAttrSetReader &fReader; + std::optional fFilter; + +public: + class RIterator final { + private: + using Iter_t = decltype(std::declval().fEntryRanges.begin()); + Iter_t fCur, fEnd; + std::optional fFilter; + + Iter_t Next() const; + bool FullyContained(RNTupleAttrRange range) const; + + public: + using iterator_category = std::forward_iterator_tag; + using iterator = RIterator; + using value_type = NTupleSize_t; + using difference_type = std::ptrdiff_t; + using pointer = const value_type *; + using reference = const value_type &; + + RIterator(Iter_t iter, Iter_t end, std::optional filter) : fCur(iter), fEnd(end), fFilter(filter) + { + if (fFilter) { + if (fFilter->fRange.GetLength() == 0) + fCur = end; + else + fCur = Next(); + } + } + iterator operator++() + { + ++fCur; + fCur = Next(); + return *this; + } + iterator operator++(int) + { + iterator it = *this; + ++fCur; + fCur = Next(); + return it; + } + reference operator*() { return fCur->second; } + bool operator!=(const iterator &rh) const { return !operator==(rh); } + bool operator==(const iterator &rh) const { return fCur == rh.fCur; } + }; + + explicit RNTupleAttrEntryIterable(RNTupleAttrSetReader &reader, std::optional filter = {}) + : fReader(reader), fFilter(filter) + { + } + + RIterator begin() { return RIterator{fReader.fEntryRanges.begin(), fReader.fEntryRanges.end(), fFilter}; } + RIterator end() { return RIterator{fReader.fEntryRanges.end(), fReader.fEntryRanges.end(), fFilter}; } +}; + +} // namespace Experimental +} // namespace ROOT + +#endif diff --git a/tree/ntuple/inc/ROOT/RNTupleAttrUtils.hxx b/tree/ntuple/inc/ROOT/RNTupleAttrUtils.hxx index 1de0d1ea19926..8997b6b85afc5 100644 --- a/tree/ntuple/inc/ROOT/RNTupleAttrUtils.hxx +++ b/tree/ntuple/inc/ROOT/RNTupleAttrUtils.hxx @@ -39,13 +39,15 @@ The order and name of the meta Model's fields is defined by the schema version. inline const std::uint16_t kSchemaVersionMajor = 1; inline const std::uint16_t kSchemaVersionMinor = 0; -inline const char *const kRangeStartName = "_rangeStart"; -inline const char *const kRangeLenName = "_rangeLen"; -inline const char *const kUserDataName = "_userData"; - -inline constexpr std::size_t kRangeStartIndex = 0; -inline constexpr std::size_t kRangeLenIndex = 1; -inline constexpr std::size_t kUserDataIndex = 2; +enum : std::size_t { + kMetaFieldIndex_RangeStart, + kMetaFieldIndex_RangeLen, + kMetaFieldIndex_UserData, + + kMetaFieldIndex_Count +}; +inline constexpr const char *kMetaFieldNames[] = {"_rangeStart", "_rangeLen", "_userData"}; +static_assert(kMetaFieldIndex_Count == sizeof(kMetaFieldNames) / sizeof(kMetaFieldNames[0])); } // namespace ROOT::Experimental::Internal::RNTupleAttributes diff --git a/tree/ntuple/inc/ROOT/RNTupleReader.hxx b/tree/ntuple/inc/ROOT/RNTupleReader.hxx index 73f211ece0a2c..7d44eee12dbdd 100644 --- a/tree/ntuple/inc/ROOT/RNTupleReader.hxx +++ b/tree/ntuple/inc/ROOT/RNTupleReader.hxx @@ -527,6 +527,12 @@ public: /// ~~~ void EnableMetrics() { fMetrics.Enable(); } const Experimental::Detail::RNTupleMetrics &GetMetrics() const { return fMetrics; } + + /// Looks for an attribute set with the given name and creates an RNTupleAttrSetReader for it, with the provided + /// read options. + /// The returned reader has an independent lifetime from this RNTupleReader. + std::unique_ptr + OpenAttributeSet(std::string_view attrSetName, const ROOT::RNTupleReadOptions &options = {}); }; // class RNTupleReader } // namespace ROOT diff --git a/tree/ntuple/inc/ROOT/RPageStorage.hxx b/tree/ntuple/inc/ROOT/RPageStorage.hxx index 9c5e7470839ab..a7c0441e3597e 100644 --- a/tree/ntuple/inc/ROOT/RPageStorage.hxx +++ b/tree/ntuple/inc/ROOT/RPageStorage.hxx @@ -854,6 +854,11 @@ public: /// Forces the loading of ROOT StreamerInfo from the underlying file. This currently only has an effect for /// TFile-backed sources. virtual void LoadStreamerInfo() = 0; + + /// Creates a new PageSource using the same underlying file as this but referring to a different RNTuple, + /// described by `anchorLink`. + virtual std::unique_ptr OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &anchorLink, + const ROOT::RNTupleReadOptions &options = {}) = 0; }; // class RPageSource } // namespace Internal diff --git a/tree/ntuple/inc/ROOT/RPageStorageDaos.hxx b/tree/ntuple/inc/ROOT/RPageStorageDaos.hxx index 3a462411867cf..64b37e0fdd275 100644 --- a/tree/ntuple/inc/ROOT/RPageStorageDaos.hxx +++ b/tree/ntuple/inc/ROOT/RPageStorageDaos.hxx @@ -183,6 +183,9 @@ public: std::string GetObjectClass() const; void LoadStreamerInfo() final; + + std::unique_ptr OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &anchorLink, + const ROOT::RNTupleReadOptions &options = {}) final; }; // class RPageSourceDaos } // namespace Internal diff --git a/tree/ntuple/inc/ROOT/RPageStorageFile.hxx b/tree/ntuple/inc/ROOT/RPageStorageFile.hxx index 361da4c45b467..00c8eef44a1ca 100644 --- a/tree/ntuple/inc/ROOT/RPageStorageFile.hxx +++ b/tree/ntuple/inc/ROOT/RPageStorageFile.hxx @@ -188,10 +188,8 @@ public: RPageSourceFile &operator=(RPageSourceFile &&) = delete; ~RPageSourceFile() override; - /// Creates a new PageSourceFile using the same underlying file as this but referring to a different RNTuple, - /// represented by `anchor`. - std::unique_ptr - OpenWithDifferentAnchor(const RNTuple &anchor, const ROOT::RNTupleReadOptions &options = ROOT::RNTupleReadOptions()); + std::unique_ptr OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &anchorLink, + const ROOT::RNTupleReadOptions &options = {}) final; void LoadSealedPage(ROOT::DescriptorId_t physicalColumnId, RNTupleLocalIndex localIndex, RSealedPage &sealedPage) final; diff --git a/tree/ntuple/src/RNTupleAttrReading.cxx b/tree/ntuple/src/RNTupleAttrReading.cxx new file mode 100644 index 0000000000000..cb0c2efa5d936 --- /dev/null +++ b/tree/ntuple/src/RNTupleAttrReading.cxx @@ -0,0 +1,242 @@ +/// \file RNTupleAttrReading.cxx +/// \ingroup NTuple +/// \author Giacomo Parolini +/// \date 2026-04-01 +/// \warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback +/// is welcome! + +#include +#include +#include +#include + +using namespace ROOT::Experimental::Internal::RNTupleAttributes; + +ROOT::Experimental::RNTupleAttrSetReader::RNTupleAttrSetReader(std::unique_ptr reader) + : fReader(std::move(reader)) +{ + // Initialize user model + fUserModel = RNTupleModel::Create(); + + // Validate meta model format + const auto metaFields = fReader->GetModel().GetConstFieldZero().GetConstSubfields(); + if (metaFields.size() != kMetaFieldIndex_Count) { + throw ROOT::RException(R__FAIL("invalid number of attribute meta-fields: expected " + + std::to_string(kMetaFieldIndex_Count) + ", got " + + std::to_string(metaFields.size()))); + } + for (std::size_t i = 0; i < kMetaFieldIndex_Count; ++i) { + if (metaFields[i]->GetFieldName() != kMetaFieldNames[i]) { + throw ROOT::RException(R__FAIL(std::string("invalid attribute meta-field name: expected '") + + kMetaFieldNames[i] + "', got '" + metaFields[i]->GetFieldName() + "'")); + } + } + + const auto *userFieldRoot = metaFields[kMetaFieldIndex_UserData]; + for (const auto *field : userFieldRoot->GetConstSubfields()) { + fUserModel->AddField(field->Clone(field->GetFieldName())); + } + fUserModel->Freeze(); + + // Collect all entry ranges + auto entryRangeStartView = fReader->GetView(kMetaFieldNames[kMetaFieldIndex_RangeStart]); + auto entryRangeLenView = fReader->GetView(kMetaFieldNames[kMetaFieldIndex_RangeLen]); + fEntryRanges.reserve(fReader->GetNEntries()); + for (auto i : fReader->GetEntryRange()) { + auto start = entryRangeStartView(i); + auto len = entryRangeLenView(i); + fEntryRanges.push_back({RNTupleAttrRange::FromStartLength(start, len), i}); + } + + std::stable_sort(fEntryRanges.begin(), fEntryRanges.end(), + [](const auto &a, const auto &b) { return a.first.GetStart() < b.first.GetStart(); }); + + R__LOG_INFO(ROOT::Internal::NTupleLog()) << "Loaded " << fEntryRanges.size() << " attribute entries."; +} + +const ROOT::RNTupleDescriptor &ROOT::Experimental::RNTupleAttrSetReader::GetDescriptor() const +{ + return fReader->GetDescriptor(); +} + +ROOT::Experimental::RNTupleAttrRange +ROOT::Experimental::RNTupleAttrSetReader::LoadEntry(ROOT::NTupleSize_t index, REntry &entry) +{ + auto &metaModel = const_cast(fReader->GetModel()); + auto &metaEntry = metaModel.GetDefaultEntry(); + + if (R__unlikely(entry.GetModelId() != fUserModel->GetModelId())) + throw RException(R__FAIL("mismatch between entry and model")); + + // Load the meta fields + metaEntry.fValues[kMetaFieldIndex_RangeStart].Read(index); + metaEntry.fValues[kMetaFieldIndex_RangeLen].Read(index); + + // Load the user fields into `entry` + auto *userRootField = ROOT::Internal::GetFieldZeroOfModel(metaModel).GetMutableSubfields()[kMetaFieldIndex_UserData]; + const auto userFields = userRootField->GetMutableSubfields(); + assert(entry.fValues.size() == userFields.size()); + for (std::size_t i = 0; i < userFields.size(); ++i) { + auto *field = userFields[i]; + field->Read(index, entry.fValues[i].GetPtr().get()); + } + + auto pStart = metaEntry.fValues[kMetaFieldIndex_RangeStart].GetPtr(); + auto pLen = metaEntry.fValues[kMetaFieldIndex_RangeLen].GetPtr(); + + return RNTupleAttrRange::FromStartLength(*pStart, *pLen); +} + +ROOT::Experimental::RNTupleAttrRange ROOT::Experimental::RNTupleAttrSetReader::LoadEntry(ROOT::NTupleSize_t index) +{ + auto &entry = fUserModel->GetDefaultEntry(); + return LoadEntry(index, entry); +} + +std::unique_ptr ROOT::Experimental::RNTupleAttrSetReader::CreateEntry() +{ + return fUserModel->CreateEntry(); +} + +// Entry ranges should be sorted with respect to GetStart by construction. +bool ROOT::Experimental::RNTupleAttrSetReader::EntryRangesAreSorted(const decltype(fEntryRanges) &ranges) +{ + ROOT::NTupleSize_t prevStart = 0; + for (const auto &[range, _] : ranges) { + if (range.GetStart() < prevStart) + return false; + prevStart = range.GetStart(); + } + return true; +}; + +std::vector +ROOT::Experimental::RNTupleAttrSetReader::GetAttributesRangeInternal(NTupleSize_t startEntry, NTupleSize_t endEntry, + bool rangeIsContained) +{ + std::vector result; + + if (endEntry < startEntry) { + R__LOG_WARNING(ROOT::Internal::NTupleLog()) + << "end < start when getting attributes from Attribute Set '" << GetDescriptor().GetName() + << "' (range given: [" << startEntry << ", " << endEntry << "]."; + return result; + } + + assert(EntryRangesAreSorted(fEntryRanges)); + + const auto FullyContained = [rangeIsContained](auto startInner, auto endInner, auto startOuter, auto endOuter) { + if (rangeIsContained) { + std::swap(startOuter, startInner); + std::swap(endOuter, endInner); + } + return startOuter <= startInner && endInner <= endOuter; + }; + + // TODO: consider using binary search, since fEntryRanges is sorted + // (maybe it should be done only if the size of the list is bigger than a threshold). + for (const auto &[range, index] : fEntryRanges) { + const auto &firstLast = range.GetFirstLast(); + if (!firstLast) + continue; + + const auto &[first, last] = *firstLast; + if (first >= endEntry) + break; // We can break here because fEntryRanges is sorted. + + if (FullyContained(startEntry, endEntry, first, last + 1)) { + result.push_back(index); + } + } + + return result; +} + +ROOT::Experimental::RNTupleAttrEntryIterable +ROOT::Experimental::RNTupleAttrSetReader::GetAttributesContainingRange(NTupleSize_t startEntry, NTupleSize_t endEntry) +{ + RNTupleAttrRange range; + if (endEntry <= startEntry) { + R__LOG_WARNING(ROOT::Internal::NTupleLog()) + << "empty range given when getting attributes from Attribute Set '" << GetDescriptor().GetName() + << "' (range given: [" << startEntry << ", " << endEntry << "))."; + // Make sure we find 0 entries + range = RNTupleAttrRange::FromStartLength(startEntry, 0); + } else { + range = RNTupleAttrRange::FromStartEnd(startEntry, endEntry); + } + RNTupleAttrEntryIterable::RFilter filter{range, false}; + return RNTupleAttrEntryIterable{*this, filter}; +} + +ROOT::Experimental::RNTupleAttrEntryIterable +ROOT::Experimental::RNTupleAttrSetReader::GetAttributesInRange(NTupleSize_t startEntry, NTupleSize_t endEntry) +{ + RNTupleAttrRange range; + if (endEntry <= startEntry) { + R__LOG_WARNING(ROOT::Internal::NTupleLog()) + << "empty range given when getting attributes from Attribute Set '" << GetDescriptor().GetName() + << "' (range given: [" << startEntry << ", " << endEntry << "))."; + // Make sure we find 0 entries + range = RNTupleAttrRange::FromStartLength(startEntry, 0); + } else { + range = RNTupleAttrRange::FromStartEnd(startEntry, endEntry); + } + RNTupleAttrEntryIterable::RFilter filter{range, true}; + return RNTupleAttrEntryIterable{*this, filter}; +} + +ROOT::Experimental::RNTupleAttrEntryIterable +ROOT::Experimental::RNTupleAttrSetReader::GetAttributes(NTupleSize_t entryIndex) +{ + RNTupleAttrEntryIterable::RFilter filter{RNTupleAttrRange::FromStartEnd(entryIndex, entryIndex + 1), false}; + return RNTupleAttrEntryIterable{*this, filter}; +} + +ROOT::Experimental::RNTupleAttrEntryIterable ROOT::Experimental::RNTupleAttrSetReader::GetAttributes() +{ + return RNTupleAttrEntryIterable{*this}; +} + +// +// RNTupleAttrEntryIterable +// +bool ROOT::Experimental::RNTupleAttrEntryIterable::RIterator::FullyContained(RNTupleAttrRange range) const +{ + assert(fFilter); + if (fFilter->fIsContained) { + return fFilter->fRange.GetStart() <= range.GetStart() && range.GetEnd() <= fFilter->fRange.GetEnd(); + } else { + return range.GetStart() <= fFilter->fRange.GetStart() && fFilter->fRange.GetEnd() <= range.GetEnd(); + } +} + +ROOT::Experimental::RNTupleAttrEntryIterable::RIterator::Iter_t +ROOT::Experimental::RNTupleAttrEntryIterable::RIterator::Next() const +{ + // TODO: consider using binary search, since fEntryRanges is sorted + // (maybe it should be done only if the size of the list is bigger than a threshold). + for (auto it = fCur; it != fEnd; ++it) { + const auto &[range, index] = *it; + // If we have no filter, every entry is valid. + if (!fFilter) + return it; + + const auto &firstLast = range.GetFirstLast(); + // If this is nullopt it means this is a zero-length entry: we always skip those except + // for the "catch-all" GetAttributes() (which is when fFilter is also nullopt). + if (!firstLast) + continue; + + const auto &[first, last] = *firstLast; + if (first >= fFilter->fRange.GetEnd()) { + // Since fEntryRanges is sorted we know we are at the end of the iteration + // TODO: tweak fEnd to directly pass the last entry? + return fEnd; + } + + if (FullyContained(RNTupleAttrRange::FromStartEnd(first, last + 1))) + return it; + } + return fEnd; +} diff --git a/tree/ntuple/src/RNTupleAttrWriting.cxx b/tree/ntuple/src/RNTupleAttrWriting.cxx index 7427654a218a6..ed3fe69bbfb39 100644 --- a/tree/ntuple/src/RNTupleAttrWriting.cxx +++ b/tree/ntuple/src/RNTupleAttrWriting.cxx @@ -1,5 +1,5 @@ /// \file RNTupleAttrWriting.cxx -/// \ingroup NTuple ROOT7 +/// \ingroup NTuple /// \author Giacomo Parolini /// \date 2026-01-27 /// \warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback @@ -35,12 +35,13 @@ std::size_t ROOT::Experimental::Internal::RNTupleAttrEntry::Append() { std::size_t bytesWritten = 0; // Write the meta entry values - bytesWritten += fMetaEntry.fValues[kRangeStartIndex].Append(); - bytesWritten += fMetaEntry.fValues[kRangeLenIndex].Append(); + bytesWritten += fMetaEntry.fValues[kMetaFieldIndex_RangeStart].Append(); + bytesWritten += fMetaEntry.fValues[kMetaFieldIndex_RangeLen].Append(); // Bind the user model's memory to the meta model's subfields - const auto &userFields = - ROOT::Internal::GetFieldZeroOfModel(fMetaModel).GetMutableSubfields()[kUserDataIndex]->GetMutableSubfields(); + const auto &userFields = ROOT::Internal::GetFieldZeroOfModel(fMetaModel) + .GetMutableSubfields()[kMetaFieldIndex_UserData] + ->GetMutableSubfields(); assert(userFields.size() == fScopedEntry.fValues.size()); for (std::size_t i = 0; i < fScopedEntry.fValues.size(); ++i) { std::shared_ptr userPtr = fScopedEntry.fValues[i].GetPtr(); @@ -70,15 +71,16 @@ ROOT::Experimental::RNTupleAttrSetWriter::Create(const RNTupleFillContext &mainF // the user model and store them under the meta model's fields (see RNTupleAttrEntry::Append()) auto metaModel = RNTupleModel::Create(); metaModel->SetDescription(userModel->GetDescription()); - auto rangeStartPtr = metaModel->MakeField(kRangeStartName); - auto rangeLenPtr = metaModel->MakeField(kRangeLenName); + auto rangeStartPtr = metaModel->MakeField(kMetaFieldNames[kMetaFieldIndex_RangeStart]); + auto rangeLenPtr = metaModel->MakeField(kMetaFieldNames[kMetaFieldIndex_RangeLen]); std::vector> fields; const auto subfields = userModel->GetConstFieldZero().GetConstSubfields(); fields.reserve(subfields.size()); for (const auto *field : subfields) { fields.push_back(field->Clone(field->GetFieldName())); } - auto userRootField = std::make_unique(kUserDataName, std::move(fields)); + auto userRootField = + std::make_unique(kMetaFieldNames[kMetaFieldIndex_UserData], std::move(fields)); metaModel->AddField(std::move(userRootField)); metaModel->Freeze(); diff --git a/tree/ntuple/src/RNTupleReader.cxx b/tree/ntuple/src/RNTupleReader.cxx index 007dffcef7de1..81e9d332a329c 100644 --- a/tree/ntuple/src/RNTupleReader.cxx +++ b/tree/ntuple/src/RNTupleReader.cxx @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -366,3 +367,20 @@ ROOT::DescriptorId_t ROOT::RNTupleReader::RetrieveFieldId(std::string_view field } return fieldId; } + +std::unique_ptr +ROOT::RNTupleReader::OpenAttributeSet(std::string_view attrSetName, const ROOT::RNTupleReadOptions &readOpts) +{ + auto attrSets = GetDescriptor().GetAttrSetIterable(); + const auto it = + std::find_if(attrSets.begin(), attrSets.end(), [&](const auto &d) { return d.GetName() == attrSetName; }); + if (it == attrSets.end()) + throw ROOT::RException(R__FAIL(std::string("No such attribute set: ") + std::string(attrSetName))); + + auto attrSource = fSource->OpenWithDifferentAnchor({it->GetAnchorLocator(), it->GetAnchorLength()}, readOpts); + auto newReader = std::unique_ptr(new RNTupleReader(std::move(attrSource), readOpts)); + R__ASSERT(newReader); + auto attrSetReader = std::unique_ptr( + new ROOT::Experimental::RNTupleAttrSetReader(std::move(newReader))); + return attrSetReader; +} diff --git a/tree/ntuple/src/RPageStorageDaos.cxx b/tree/ntuple/src/RPageStorageDaos.cxx index 6080acbade472..29a0c423c2dbc 100644 --- a/tree/ntuple/src/RPageStorageDaos.cxx +++ b/tree/ntuple/src/RPageStorageDaos.cxx @@ -716,3 +716,10 @@ void ROOT::Experimental::Internal::RPageSourceDaos::LoadStreamerInfo() { R__LOG_WARNING(ROOT::Internal::NTupleLog()) << "DAOS-backed sources have no associated StreamerInfo to load."; } + +std::unique_ptr +ROOT::Experimental::Internal::RPageSourceDaos::OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &, + const ROOT::RNTupleReadOptions &) +{ + throw ROOT::RException(R__FAIL("method not implemented")); +} diff --git a/tree/ntuple/src/RPageStorageFile.cxx b/tree/ntuple/src/RPageStorageFile.cxx index 006e06648fc22..ecacdae49495f 100644 --- a/tree/ntuple/src/RPageStorageFile.cxx +++ b/tree/ntuple/src/RPageStorageFile.cxx @@ -363,9 +363,15 @@ ROOT::Internal::RPageSourceFile::~RPageSourceFile() fClusterPool.StopBackgroundThread(); } -std::unique_ptr -ROOT::Internal::RPageSourceFile::OpenWithDifferentAnchor(const RNTuple &anchor, const ROOT::RNTupleReadOptions &options) +std::unique_ptr +ROOT::Internal::RPageSourceFile::OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &anchorLink, + const ROOT::RNTupleReadOptions &options) { + assert(anchorLink.fLocator.GetType() == RNTupleLocator::kTypeFile); + + const auto anchorPos = anchorLink.fLocator.GetPosition(); + auto anchor = + fReader.GetNTupleProperAtOffset(anchorPos, anchorLink.fLocator.GetNBytesOnStorage(), anchorLink.fLength).Unwrap(); auto pageSource = std::make_unique("", fFile->Clone(), options); pageSource->fAnchor = anchor; // NOTE: fNTupleName gets set only upon Attach(). diff --git a/tree/ntuple/test/ntuple_attributes.cxx b/tree/ntuple/test/ntuple_attributes.cxx index 85c9d3153db5f..1d7405a05654a 100644 --- a/tree/ntuple/test/ntuple_attributes.cxx +++ b/tree/ntuple/test/ntuple_attributes.cxx @@ -1,5 +1,6 @@ #include "ntuple_test.hxx" #include +#include TEST(RNTupleAttributes, CreateWriter) { @@ -63,33 +64,36 @@ TEST(RNTupleAttributes, AttributeSetDuplicateName) } } -TEST(RNTupleAttributes, BasicWriting) +TEST(RNTupleAttributes, BasicReadingWriting) { - FileRaii fileGuard("ntuple_attr_basic_writing.root"); + FileRaii fileGuard("ntuple_attr_basic_readwriting.root"); ROOT::TestSupport::CheckDiagsRAII diagsRaii; diagsRaii.requiredDiag(kWarning, "ROOT.NTuple", "RNTuple Attributes are experimental", false); - auto file = std::unique_ptr(TFile::Open(fileGuard.GetPath().c_str(), "RECREATE")); - auto model = RNTupleModel::Create(); - auto pInt = model->MakeField("int"); - auto writer = RNTupleWriter::Append(std::move(model), "ntuple", *file); + /// Writing + { + auto file = std::unique_ptr(TFile::Open(fileGuard.GetPath().c_str(), "RECREATE")); + auto model = RNTupleModel::Create(); + auto pInt = model->MakeField("int"); + auto writer = RNTupleWriter::Append(std::move(model), "ntuple", *file); - auto attrModel = RNTupleModel::Create(); - auto pAttr = attrModel->MakeField("attr"); - auto attrSetWriter = writer->CreateAttributeSet(std::move(attrModel), "AttrSet1"); + auto attrModel = RNTupleModel::Create(); + auto pAttr = attrModel->MakeField("attr"); + auto attrSetWriter = writer->CreateAttributeSet(std::move(attrModel), "AttrSet1"); - auto attrRange = attrSetWriter->BeginRange(); - *pAttr = "My Attribute"; - for (int i = 0; i < 100; ++i) { - *pInt = i; - writer->Fill(); - } - attrSetWriter->CommitRange(std::move(attrRange)); - writer.reset(); + auto attrRange = attrSetWriter->BeginRange(); + *pAttr = "My Attribute"; + for (int i = 0; i < 100; ++i) { + *pInt = i; + writer->Fill(); + } + attrSetWriter->CommitRange(std::move(attrRange)); + writer.reset(); - // Cannot create new ranges after closing the main writer - EXPECT_THROW((attrRange = attrSetWriter->BeginRange()), ROOT::RException); + // Cannot create new ranges after closing the main writer + EXPECT_THROW((attrRange = attrSetWriter->BeginRange()), ROOT::RException); + } // Cannot directly fetch the attribute RNTuple from the TFile { @@ -98,11 +102,70 @@ TEST(RNTupleAttributes, BasicWriting) EXPECT_EQ(ntuple, nullptr); } + /// Reading auto reader = RNTupleReader::Open("ntuple", fileGuard.GetPath()); EXPECT_EQ(reader->GetDescriptor().GetNAttributeSets(), 1); for (const auto &attrSetIt : reader->GetDescriptor().GetAttrSetIterable()) { EXPECT_EQ(attrSetIt.GetName(), "AttrSet1"); } + + auto attrSetReader = reader->OpenAttributeSet("AttrSet1"); + EXPECT_EQ(attrSetReader->GetNEntries(), 1); + auto pAttr = attrSetReader->GetModel().GetDefaultEntry().GetPtr("attr"); + { + int nAttrs = 0; + // iterate all attributes + for (auto idx : attrSetReader->GetAttributes()) { + attrSetReader->LoadEntry(idx); + EXPECT_EQ(*pAttr, "My Attribute"); + nAttrs += 1; + } + EXPECT_EQ(nAttrs, 1); + } + { + int nAttrs = 0; + // attributes containing entry 99 + for (auto idx : attrSetReader->GetAttributes(99)) { + attrSetReader->LoadEntry(idx); + EXPECT_EQ(*pAttr, "My Attribute"); + nAttrs += 1; + } + EXPECT_EQ(nAttrs, 1); + } + { + int nAttrs = 0; + // attributes containing entry 100 (no entry) + for (auto _ : attrSetReader->GetAttributes(100)) { + nAttrs += 1; + } + EXPECT_EQ(nAttrs, 0); + } + { + int nAttrs = 0; + // attributes contained in entry range 50-200 (no entry) + for (auto _ : attrSetReader->GetAttributesInRange(50, 200)) { + nAttrs += 1; + } + EXPECT_EQ(nAttrs, 0); + } + { + int nAttrs = 0; + // attributes contained in entry range 0-1000 + for (auto idx : attrSetReader->GetAttributesInRange(0, 1000)) { + attrSetReader->LoadEntry(idx); + EXPECT_EQ(*pAttr, "My Attribute"); + nAttrs += 1; + } + EXPECT_EQ(nAttrs, 1); + } + { + int nAttrs = 0; + // attributes containing entry range 200-300 (no entry) + for (auto _ : attrSetReader->GetAttributesContainingRange(200, 300)) { + nAttrs += 1; + } + EXPECT_EQ(nAttrs, 0); + } } TEST(RNTupleAttributes, BasicWritingWithExplicitEntry) @@ -147,6 +210,9 @@ TEST(RNTupleAttributes, BasicWritingWithExplicitEntry) for (const auto &attrSetIt : reader->GetDescriptor().GetAttrSetIterable()) { EXPECT_EQ(attrSetIt.GetName(), "AttrSet1"); } + + auto attrSetReader = reader->OpenAttributeSet("AttrSet1"); + EXPECT_EQ(attrSetReader->GetNEntries(), 1); } TEST(RNTupleAttributes, NoCommitRange) @@ -185,6 +251,7 @@ TEST(RNTupleAttributes, MultipleSets) ROOT::TestSupport::CheckDiagsRAII diagsRaii; diagsRaii.requiredDiag(kWarning, "ROOT.NTuple", "RNTuple Attributes are experimental", false); + /// Writing { auto model = RNTupleModel::Create(); auto pInt = model->MakeField("int"); @@ -193,11 +260,11 @@ TEST(RNTupleAttributes, MultipleSets) auto attrModel1 = RNTupleModel::Create(); auto pInt1 = attrModel1->MakeField("int"); - auto attrSet1 = writer->CreateAttributeSet(attrModel1->Clone(), "MyAttrSet1"); + auto attrSet1 = writer->CreateAttributeSet(std::move(attrModel1), "MyAttrSet1"); auto attrModel2 = RNTupleModel::Create(); auto pString2 = attrModel2->MakeField("string"); - auto attrSet2 = writer->CreateAttributeSet(attrModel2->Clone(), "MyAttrSet2"); + auto attrSet2 = writer->CreateAttributeSet(std::move(attrModel2), "MyAttrSet2"); auto attrRange2 = attrSet2->BeginRange(); for (int i = 0; i < 100; ++i) { @@ -211,12 +278,66 @@ TEST(RNTupleAttributes, MultipleSets) attrSet2->CommitRange(std::move(attrRange2)); } + /// Reading auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath()); EXPECT_EQ(reader->GetDescriptor().GetNAttributeSets(), 2); - int n = 1; - for (const auto &attrSetIt : reader->GetDescriptor().GetAttrSetIterable()) { - EXPECT_EQ(attrSetIt.GetName(), "MyAttrSet" + std::to_string(n)); - ++n; + auto sets = reader->GetDescriptor().GetAttrSetIterable(); + // NOTE: there is no guaranteed order in which the attribute sets appear in the iterable + EXPECT_NE(std::find_if(sets.begin(), sets.end(), [](auto &&s) { return s.GetName() == "MyAttrSet1"; }), sets.end()); + EXPECT_NE(std::find_if(sets.begin(), sets.end(), [](auto &&s) { return s.GetName() == "MyAttrSet2"; }), sets.end()); + + auto attrSetReader1 = reader->OpenAttributeSet("MyAttrSet1"); + EXPECT_EQ(attrSetReader1->GetNEntries(), 100); + auto attrSetReader2 = reader->OpenAttributeSet("MyAttrSet2"); + EXPECT_EQ(attrSetReader2->GetNEntries(), 1); + + auto attrEntry1 = attrSetReader1->CreateEntry(); + auto pAttrInt = attrEntry1->GetPtr("int"); + auto attrEntry2 = attrSetReader2->CreateEntry(); + auto pAttrString = attrEntry2->GetPtr("string"); + { + int nAttrs = 0; + for (auto idx : attrSetReader1->GetAttributesInRange(0, 1000)) { + auto range = attrSetReader1->LoadEntry(idx, *attrEntry1); + EXPECT_EQ(*pAttrInt, idx); + EXPECT_EQ(range.GetStart(), idx); + EXPECT_EQ(range.GetLength(), 1); + nAttrs += 1; + } + EXPECT_EQ(nAttrs, 100); + } + { + int nAttrs = 0; + for (auto idx : attrSetReader1->GetAttributes(42)) { + auto range = attrSetReader1->LoadEntry(idx, *attrEntry1); + EXPECT_EQ(*pAttrInt, 42); + EXPECT_EQ(range.GetStart(), 42); + EXPECT_EQ(range.GetLength(), 1); + nAttrs += 1; + } + EXPECT_EQ(nAttrs, 1); + } + { + int nAttrs = 0; + for (auto idx : attrSetReader2->GetAttributes()) { + auto range = attrSetReader2->LoadEntry(idx, *attrEntry2); + EXPECT_EQ(*pAttrString, "Run 1"); + EXPECT_EQ(range.GetStart(), 0); + EXPECT_EQ(range.GetLength(), 100); + nAttrs += 1; + } + EXPECT_EQ(nAttrs, 1); + } + { + for (auto idx : attrSetReader2->GetAttributes()) { + // Reading into the wrong entry + try { + attrSetReader2->LoadEntry(idx, *attrEntry1); + FAIL() << "reading into an unrelated entry should fail"; + } catch (const ROOT::RException &ex) { + EXPECT_THAT(ex.what(), testing::HasSubstr("mismatch between entry and model")); + } + } } } diff --git a/tree/ntuple/test/ntuple_cluster.cxx b/tree/ntuple/test/ntuple_cluster.cxx index 40c7f8999dd66..eef2444c5b992 100644 --- a/tree/ntuple/test/ntuple_cluster.cxx +++ b/tree/ntuple/test/ntuple_cluster.cxx @@ -43,6 +43,11 @@ class RPageSourceMock : public RPageSource { std::unique_ptr CloneImpl() const final { return nullptr; } RPageRef LoadPageImpl(ColumnHandle_t, const RClusterInfo &, ROOT::NTupleSize_t) final { return RPageRef(); } void LoadStreamerInfo() final {} + std::unique_ptr + OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &, const ROOT::RNTupleReadOptions &) + { + throw ROOT::RException(R__FAIL("method not implemented")); + } public: /// Records the cluster IDs requests by LoadClusters() calls diff --git a/tree/ntuple/test/ntuple_endian.cxx b/tree/ntuple/test/ntuple_endian.cxx index 8ae880a03ae02..4208e09a095c9 100644 --- a/tree/ntuple/test/ntuple_endian.cxx +++ b/tree/ntuple/test/ntuple_endian.cxx @@ -66,6 +66,12 @@ class RPageSinkMock : public RPageSink { throw ROOT::RException(R__FAIL("cannot clone sink")); } + std::unique_ptr + OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &, const ROOT::RNTupleReadOptions &) + { + throw ROOT::RException(R__FAIL("method not implemented")); + } + public: RPageSinkMock(const RColumnElementBase &elt) : RPageSink("test", ROOT::RNTupleWriteOptions()), fElement(elt) { @@ -96,6 +102,12 @@ class RPageSourceMock : public RPageSource { RPageRef LoadPageImpl(ColumnHandle_t, const RClusterInfo &, ROOT::NTupleSize_t) final { return RPageRef(); } void LoadStreamerInfo() final {} + std::unique_ptr + OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &, const ROOT::RNTupleReadOptions &) + { + throw ROOT::RException(R__FAIL("method not implemented")); + } + public: RPageSourceMock(const std::vector &pages, const RColumnElementBase &elt) : RPageSource("test", ROOT::RNTupleReadOptions()), fElement(elt), fPages(pages) diff --git a/tree/ntuple/test/ntuple_pages.cxx b/tree/ntuple/test/ntuple_pages.cxx index da56a6cbcd3a8..787f2ec355484 100644 --- a/tree/ntuple/test/ntuple_pages.cxx +++ b/tree/ntuple/test/ntuple_pages.cxx @@ -13,6 +13,11 @@ class RPageSourceMock : public RPageSource { std::unique_ptr CloneImpl() const final { return nullptr; } RPageRef LoadPageImpl(ColumnHandle_t, const RClusterInfo &, ROOT::NTupleSize_t) final { return RPageRef(); } void LoadStreamerInfo() final {} + std::unique_ptr + OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &, const ROOT::RNTupleReadOptions &) + { + throw ROOT::RException(R__FAIL("method not implemented")); + } public: RPageSourceMock() : RPageSource("test", RNTupleReadOptions()) {} diff --git a/tree/ntuple/test/ntuple_storage.cxx b/tree/ntuple/test/ntuple_storage.cxx index 846fe56fa7f5e..1530626461892 100644 --- a/tree/ntuple/test/ntuple_storage.cxx +++ b/tree/ntuple/test/ntuple_storage.cxx @@ -1,4 +1,5 @@ #include "ntuple_test.hxx" +#include #include #include #include @@ -61,6 +62,12 @@ class RPageSinkMock : public RPageSink { throw ROOT::RException(R__FAIL("cannot clone sink")); } + std::unique_ptr + OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &, const ROOT::RNTupleReadOptions &) + { + throw ROOT::RException(R__FAIL("method not implemented")); + } + public: RPageSinkMock(const ROOT::RNTupleWriteOptions &options) : RPageSink("test", options) {} }; @@ -1187,9 +1194,14 @@ TEST(RPageSourceFile, OpenDifferentAnchor) EXPECT_NE(desc->FindFieldId("f"), ROOT::kInvalidDescriptorId); } + TKey *anchor2Key = file->GetKey("ntpl2"); + ROOT::RNTupleLocator anchorLoc; + anchorLoc.SetPosition(anchor2Key->GetSeekKey() + anchor2Key->GetKeylen()); + anchorLoc.SetNBytesOnStorage(anchor2Key->GetNbytes() - anchor2Key->GetKeylen()); + ROOT::Internal::RNTupleLink anchorLink{anchorLoc, static_cast(anchor2Key->GetObjlen())}; auto anchor2 = file->Get("ntpl2"); ASSERT_NE(anchor2, nullptr); - auto source2 = source->OpenWithDifferentAnchor(*anchor2); + auto source2 = source->OpenWithDifferentAnchor(anchorLink); source2->Attach(); EXPECT_EQ(source2->GetNTupleName(), "ntpl2"); EXPECT_EQ(source2->GetNEntries(), 20);