diff --git a/roottest/root/io/hadd/merge_changeComp_check_output.C b/roottest/root/io/hadd/merge_changeComp_check_output.C index 9cf41f980619e..cad0053cf1792 100644 --- a/roottest/root/io/hadd/merge_changeComp_check_output.C +++ b/roottest/root/io/hadd/merge_changeComp_check_output.C @@ -28,7 +28,7 @@ int merge_changeComp_check_output(int expectedCompressionRNT, int expectedCompre const auto expCompAlgo = ROOT::RCompressionSetting::AlgorithmFromCompressionSettings(expectedCompressionRNT); const auto &desc = source.GetSharedDescriptorGuard(); - auto clusterIter = desc->GetClusterIterable(); + auto clusterIter = desc->GetActiveClusterIterable(); for (const auto &clusterDesc : clusterIter) { // check advertised compression int advertisedCompression = clusterDesc.GetColumnRange(0).GetCompressionSettings().value(); diff --git a/tree/ntuple/inc/ROOT/RColumn.hxx b/tree/ntuple/inc/ROOT/RColumn.hxx index 023452fe50418..b6dbd35a352d7 100644 --- a/tree/ntuple/inc/ROOT/RColumn.hxx +++ b/tree/ntuple/inc/ROOT/RColumn.hxx @@ -53,7 +53,7 @@ private: ROOT::Internal::RPage fWritePage; /// The initial number of elements in a page ROOT::NTupleSize_t fInitialNElements = 1; - /// The number of elements written resp. available in the column + /// The number of elements written ROOT::NTupleSize_t fNElements = 0; /// The currently mapped page for reading ROOT::Internal::RPageRef fReadPageRef; @@ -334,7 +334,6 @@ public: void MergeTeams(RColumn &other); - ROOT::NTupleSize_t GetNElements() const { return fNElements; } ROOT::Internal::RColumnElementBase *GetElement() const { return fElement.get(); } ROOT::ENTupleColumnType GetType() const { return fType; } std::uint16_t GetBitsOnStorage() const diff --git a/tree/ntuple/inc/ROOT/RNTupleDescriptor.hxx b/tree/ntuple/inc/ROOT/RNTupleDescriptor.hxx index f3a7773d3ed75..c791456011bba 100644 --- a/tree/ntuple/inc/ROOT/RNTupleDescriptor.hxx +++ b/tree/ntuple/inc/ROOT/RNTupleDescriptor.hxx @@ -880,7 +880,13 @@ public: RClusterGroupDescriptorIterable GetClusterGroupIterable() const; - RClusterDescriptorIterable GetClusterIterable() const; + RClusterDescriptorIterable + R__DEPRECATED(6, 46, + "This function is unsafe to use because, in general, not all cluster " + "metadata are available (there may be cluster groups with unloaded cluster group details). Use the " + "'GetActiveClusterIterable()' method instead if you are aware of this limitation.") + GetClusterIterable() const; + RClusterDescriptorIterable GetActiveClusterIterable() const; RExtraTypeInfoDescriptorIterable GetExtraTypeInfoIterable() const; @@ -900,7 +906,11 @@ public: /// We know the number of entries from adding the cluster summaries ROOT::NTupleSize_t GetNEntries() const { return fNEntries; } - ROOT::NTupleSize_t GetNElements(ROOT::DescriptorId_t physicalColumnId) const; + ROOT::NTupleSize_t + R__DEPRECATED(6, 46, + "This function is ill-conceived in the descriptor " + "as not all cluster descriptors may be present. This interface is no longer publicly exposed.") + GetNElements(ROOT::DescriptorId_t physicalColumnId) const; /// Returns the logical parent of all top-level RNTuple data fields. ROOT::DescriptorId_t GetFieldZeroId() const { return fFieldZeroId; } @@ -1087,7 +1097,7 @@ public: /** \class ROOT::RNTupleDescriptor::RClusterGroupDescriptorIterable \ingroup NTuple -\brief Used to loop over all the cluster groups of an RNTuple (in unspecified order) +\brief Used to loop over all the cluster groups of an RNTuple in order of entry ranges Enumerate all cluster group IDs from the descriptor. No specific order can be assumed. */ @@ -1099,10 +1109,18 @@ private: public: class RIterator final { + friend class RNTupleDescriptor::RClusterGroupDescriptorIterable; + private: - using Iter_t = std::unordered_map::const_iterator; - /// The wrapped map iterator - Iter_t fIter; + const RNTupleDescriptor *fNTuple = nullptr; + const std::vector *fSortedClusterGroupIds = nullptr; + std::size_t fIdx = 0; + + RIterator(const RNTupleDescriptor *ntuple, const std::vector *sortedClusterGroupIds, + std::size_t idx) + : fNTuple(ntuple), fSortedClusterGroupIds(sortedClusterGroupIds), fIdx(idx) + { + } public: using iterator_category = std::forward_iterator_tag; @@ -1113,27 +1131,36 @@ public: using reference = const RClusterGroupDescriptor &; RIterator() = default; - explicit RIterator(Iter_t iter) : fIter(iter) {} iterator &operator++() /* prefix */ { - ++fIter; + ++fIdx; return *this; } iterator operator++(int) /* postfix */ { auto old = *this; - operator++(); + fIdx++; return old; } - reference operator*() const { return fIter->second; } - pointer operator->() const { return &fIter->second; } - bool operator!=(const iterator &rh) const { return fIter != rh.fIter; } - bool operator==(const iterator &rh) const { return fIter == rh.fIter; } + iterator &operator+=(std::size_t n) + { + fIdx += n; + return *this; + } + reference operator*() const { return fNTuple->GetClusterGroupDescriptor((*fSortedClusterGroupIds)[fIdx]); } + pointer operator->() const { return &fNTuple->GetClusterGroupDescriptor((*fSortedClusterGroupIds)[fIdx]); } + bool operator==(const iterator &rh) const { return (fNTuple == rh.fNTuple) && (fIdx == rh.fIdx); } + bool operator!=(const iterator &rh) const { return !(*this == rh); } }; RClusterGroupDescriptorIterable(const RNTupleDescriptor &ntuple) : fNTuple(ntuple) {} - RIterator begin() { return RIterator(fNTuple.fClusterGroupDescriptors.cbegin()); } - RIterator end() { return RIterator(fNTuple.fClusterGroupDescriptors.cend()); } + RIterator begin() { return RIterator(&fNTuple, &fNTuple.fSortedClusterGroupIds, 0); } + RIterator end() + { + return RIterator(&fNTuple, &fNTuple.fSortedClusterGroupIds, fNTuple.fSortedClusterGroupIds.size()); + } + std::size_t size() const { return fNTuple.fSortedClusterGroupIds.size(); } + bool empty() const { return fNTuple.fSortedClusterGroupIds.empty(); } }; // clang-format off diff --git a/tree/ntuple/inc/ROOT/RNTupleView.hxx b/tree/ntuple/inc/ROOT/RNTupleView.hxx index 54a030f65cd0f..f46a584e336e7 100644 --- a/tree/ntuple/inc/ROOT/RNTupleView.hxx +++ b/tree/ntuple/inc/ROOT/RNTupleView.hxx @@ -37,7 +37,7 @@ namespace Internal { /// by the number of elements of the first principal column found in the subfields searched by BFS. /// If the field hierarchy is empty on columns, the returned field range is invalid (start and end set to /// kInvalidNTupleIndex). An attempt to use such a field range in RNTupleViewBase::GetFieldRange will throw. -ROOT::RNTupleGlobalRange GetFieldRange(const ROOT::RFieldBase &field, const ROOT::Internal::RPageSource &pageSource); +ROOT::RNTupleGlobalRange GetFieldRange(const ROOT::RFieldBase &field, ROOT::Internal::RPageSource &pageSource); } // namespace Internal @@ -145,7 +145,8 @@ public: ROOT::RNTupleGlobalRange GetFieldRange() const { if (!fFieldRange.IsValid()) { - throw RException(R__FAIL("field iteration over empty fields is unsupported: " + fField->GetFieldName())); + throw RException(R__FAIL("field iteration over empty fields in vectors or variants is unsupported: " + + fField->GetFieldName())); } return fFieldRange; } diff --git a/tree/ntuple/inc/ROOT/RPageStorage.hxx b/tree/ntuple/inc/ROOT/RPageStorage.hxx index dfcf2d11d411c..824c1dc229a81 100644 --- a/tree/ntuple/inc/ROOT/RPageStorage.hxx +++ b/tree/ntuple/inc/ROOT/RPageStorage.hxx @@ -853,7 +853,7 @@ public: void Attach(ROOT::Internal::RNTupleSerializer::EDescriptorDeserializeMode mode = ROOT::Internal::RNTupleSerializer::EDescriptorDeserializeMode::kForReading); ROOT::NTupleSize_t GetNEntries(); - ROOT::NTupleSize_t GetNElements(ColumnHandle_t columnHandle); + ROOT::NTupleSize_t GetNElements(ROOT::DescriptorId_t physicalColumnId); /// Promise to only read from the given entry range. If set, prevents the cluster pool from reading-ahead beyond /// the given range. The range needs to be within `[0, GetNEntries())`. diff --git a/tree/ntuple/src/RColumn.cxx b/tree/ntuple/src/RColumn.cxx index ce964e4c8f992..64be7b1a8aae7 100644 --- a/tree/ntuple/src/RColumn.cxx +++ b/tree/ntuple/src/RColumn.cxx @@ -57,7 +57,6 @@ void ROOT::Internal::RColumn::ConnectPageSource(ROOT::DescriptorId_t fieldId, RP { fPageSource = &pageSource; fHandleSource = fPageSource->AddColumn(fieldId, *this); - fNElements = fPageSource->GetNElements(fHandleSource); fOnDiskId = fPageSource->GetColumnId(fHandleSource); { auto descriptorGuard = fPageSource->GetSharedDescriptorGuard(); diff --git a/tree/ntuple/src/RNTupleDescriptor.cxx b/tree/ntuple/src/RNTupleDescriptor.cxx index 3092b8a4fe7b1..e2616c05b25ac 100644 --- a/tree/ntuple/src/RNTupleDescriptor.cxx +++ b/tree/ntuple/src/RNTupleDescriptor.cxx @@ -1483,6 +1483,11 @@ ROOT::RNTupleDescriptor::RClusterGroupDescriptorIterable ROOT::RNTupleDescriptor } ROOT::RNTupleDescriptor::RClusterDescriptorIterable ROOT::RNTupleDescriptor::GetClusterIterable() const +{ + return GetActiveClusterIterable(); +} + +ROOT::RNTupleDescriptor::RClusterDescriptorIterable ROOT::RNTupleDescriptor::GetActiveClusterIterable() const { return RClusterDescriptorIterable(*this); } diff --git a/tree/ntuple/src/RNTupleMerger.cxx b/tree/ntuple/src/RNTupleMerger.cxx index c23750f74181a..f29fd7640539f 100644 --- a/tree/ntuple/src/RNTupleMerger.cxx +++ b/tree/ntuple/src/RNTupleMerger.cxx @@ -194,10 +194,9 @@ try { // Get the compression of this RNTuple and use it as the output compression. // We currently assume all column ranges have the same compression, so we just peek at the first one. source->Attach(RNTupleSerializer::EDescriptorDeserializeMode::kRaw); - auto descriptor = source->GetSharedDescriptorGuard(); - auto clusterIter = descriptor->GetClusterIterable(); - auto firstCluster = clusterIter.begin(); - if (firstCluster == clusterIter.end()) { + auto descGuard = source->GetSharedDescriptorGuard(); + auto clusterGroupIterable = descGuard->GetClusterGroupIterable(); + if (clusterGroupIterable.empty()) { R__LOG_ERROR(NTupleMergeLog()) << "Asked to use the first source's compression as the output compression, but the " "first source (file '" @@ -206,7 +205,10 @@ try { "determined."; return -1; } - auto colRangeIter = (*firstCluster).GetColumnRangeIterable(); + const auto firstClusterGroup = clusterGroupIterable.begin(); + R__ASSERT(firstClusterGroup->HasClusterDetails()); + const auto &firstCluster = descGuard->GetClusterDescriptor(firstClusterGroup->GetClusterIds()[0]); + auto colRangeIter = firstCluster.GetColumnRangeIterable(); auto firstColRange = colRangeIter.begin(); if (firstColRange == colRangeIter.end()) { R__LOG_ERROR(NTupleMergeLog()) diff --git a/tree/ntuple/src/RNTupleView.cxx b/tree/ntuple/src/RNTupleView.cxx index 8440b1dc94c07..b9694e7bf97f7 100644 --- a/tree/ntuple/src/RNTupleView.cxx +++ b/tree/ntuple/src/RNTupleView.cxx @@ -19,38 +19,60 @@ #include ROOT::RNTupleGlobalRange -ROOT::Internal::GetFieldRange(const ROOT::RFieldBase &field, const ROOT::Internal::RPageSource &pageSource) +ROOT::Internal::GetFieldRange(const ROOT::RFieldBase &field, ROOT::Internal::RPageSource &pageSource) { - const auto &desc = pageSource.GetSharedDescriptorGuard().GetRef(); - - auto fnGetPrincipalColumnId = [&desc](ROOT::DescriptorId_t fieldId) -> ROOT::DescriptorId_t { - R__ASSERT(fieldId != ROOT::kInvalidDescriptorId); - auto columnIterable = desc.GetColumnIterable(fieldId); - return (columnIterable.size() > 0) ? columnIterable.begin()->GetPhysicalId() : ROOT::kInvalidDescriptorId; - }; - - auto columnId = fnGetPrincipalColumnId(field.GetOnDiskId()); - if (columnId == ROOT::kInvalidDescriptorId) { - // We need to iterate the field descriptor tree, not the sub fields of `field`, because in the presence of - // read rules, the in-memory sub fields may be artificial and not have valid on-disk IDs. - const auto &linkIds = desc.GetFieldDescriptor(field.GetOnDiskId()).GetLinkIds(); - std::deque subFields(linkIds.begin(), linkIds.end()); - while (!subFields.empty()) { - auto subFieldId = subFields.front(); - subFields.pop_front(); - columnId = fnGetPrincipalColumnId(subFieldId); - if (columnId != ROOT::kInvalidDescriptorId) + std::uint64_t arraySize = 1; + DescriptorId_t columnId = ROOT::kInvalidDescriptorId; + + { + auto descGuard = pageSource.GetSharedDescriptorGuard(); + const auto &desc = descGuard.GetRef(); + + auto parentId = desc.GetFieldDescriptor(field.GetOnDiskId()).GetParentId(); + while (parentId != desc.GetFieldZeroId()) { + const auto &fd = desc.GetFieldDescriptor(parentId); + if ((fd.GetStructure() != ROOT::ENTupleStructure::kPlain) && + (fd.GetStructure() != ROOT::ENTupleStructure::kRecord)) { break; + } + parentId = fd.GetParentId(); + } + if (parentId == desc.GetFieldZeroId()) { + // This field is structurally a top-level field, hence we can avoid the calculation of column elements + // and instead return the number of entries. + return ROOT::RNTupleGlobalRange(0, desc.GetNEntries()); + } + + auto fnGetPrincipalColumnId = [&desc](ROOT::DescriptorId_t fieldId) -> ROOT::DescriptorId_t { + R__ASSERT(fieldId != ROOT::kInvalidDescriptorId); + auto columnIterable = desc.GetColumnIterable(fieldId); + return (columnIterable.size() > 0) ? columnIterable.begin()->GetPhysicalId() : ROOT::kInvalidDescriptorId; + }; - const auto &subLinkIds = desc.GetFieldDescriptor(subFieldId).GetLinkIds(); - subFields.insert(subFields.end(), subLinkIds.begin(), subLinkIds.end()); + columnId = fnGetPrincipalColumnId(field.GetOnDiskId()); + if (columnId == ROOT::kInvalidDescriptorId) { + // We need to iterate the field descriptor tree, not the sub fields of `field`, because in the presence of + // read rules, the in-memory sub fields may be artificial and not have valid on-disk IDs. + const auto &linkIds = desc.GetFieldDescriptor(field.GetOnDiskId()).GetLinkIds(); + std::deque subFields(linkIds.begin(), linkIds.end()); + while (!subFields.empty()) { + auto subFieldId = subFields.front(); + subFields.pop_front(); + columnId = fnGetPrincipalColumnId(subFieldId); + if (columnId != ROOT::kInvalidDescriptorId) + break; + + const auto &subLinkIds = desc.GetFieldDescriptor(subFieldId).GetLinkIds(); + subFields.insert(subFields.end(), subLinkIds.begin(), subLinkIds.end()); + } + } + + if (columnId == ROOT::kInvalidDescriptorId) { + return ROOT::RNTupleGlobalRange(ROOT::kInvalidNTupleIndex, ROOT::kInvalidNTupleIndex); } - } - if (columnId == ROOT::kInvalidDescriptorId) { - return ROOT::RNTupleGlobalRange(ROOT::kInvalidNTupleIndex, ROOT::kInvalidNTupleIndex); + arraySize = std::max(arraySize, desc.GetFieldDescriptor(field.GetOnDiskId()).GetNRepetitions()); } - auto arraySize = std::max(std::uint64_t(1), desc.GetFieldDescriptor(field.GetOnDiskId()).GetNRepetitions()); - return ROOT::RNTupleGlobalRange(0, desc.GetNElements(columnId) / arraySize); + return ROOT::RNTupleGlobalRange(0, pageSource.GetNElements(columnId) / arraySize); } diff --git a/tree/ntuple/src/RPageStorage.cxx b/tree/ntuple/src/RPageStorage.cxx index 0e4f0501bca84..565fd4639c4e3 100644 --- a/tree/ntuple/src/RPageStorage.cxx +++ b/tree/ntuple/src/RPageStorage.cxx @@ -269,9 +269,25 @@ ROOT::NTupleSize_t ROOT::Internal::RPageSource::GetNEntries() return GetSharedDescriptorGuard()->GetNEntries(); } -ROOT::NTupleSize_t ROOT::Internal::RPageSource::GetNElements(ColumnHandle_t columnHandle) +ROOT::NTupleSize_t ROOT::Internal::RPageSource::GetNElements(ROOT::DescriptorId_t physicalColumnId) { - return GetSharedDescriptorGuard()->GetNElements(columnHandle.fPhysicalId); + auto descGuard = GetSharedDescriptorGuard(); + if (descGuard->GetNClusters() == 0) + return 0; + + auto itr = descGuard->GetClusterGroupIterable().begin(); + itr += descGuard->GetNClusterGroups() - 1; + R__ASSERT(itr->HasClusterDetails()); + + ROOT::NTupleSize_t result = 0; + for (const auto &clusterId : itr->GetClusterIds()) { + const auto &cd = descGuard->GetClusterDescriptor(clusterId); + if (!cd.ContainsColumn(physicalColumnId)) + continue; + auto columnRange = cd.GetColumnRange(physicalColumnId); + result = std::max(result, columnRange.GetFirstElementIndex() + columnRange.GetNElements()); + } + return result; } void ROOT::Internal::RPageSource::UnzipCluster(RCluster *cluster) diff --git a/tree/ntuple/test/ntuple_descriptor.cxx b/tree/ntuple/test/ntuple_descriptor.cxx index 4c26a6866bfaa..b8b029d31ba38 100644 --- a/tree/ntuple/test/ntuple_descriptor.cxx +++ b/tree/ntuple/test/ntuple_descriptor.cxx @@ -598,6 +598,31 @@ TEST(RColumnDescriptorIterable, IterateOverColumns) EXPECT_EQ(desc.GetNLogicalColumns(), counter); } +TEST(RClusterGroupDescriptorIterable, Ordering) +{ + auto model = RNTupleModel::Create(); + + ROOT::TestSupport::FileRaii fileGuard("test_rntuple_cluster_group_iterable.root"); + { + auto writer = RNTupleWriter::Recreate(std::move(model), "ntuple", fileGuard.GetPath()); + writer->Fill(); + writer->CommitCluster(true /* commitClusterGroup */); + writer->Fill(); + writer->CommitCluster(true /* commitClusterGroup */); + writer->Fill(); + } + + auto reader = RNTupleReader::Open("ntuple", fileGuard.GetPath()); + const auto &desc = reader->GetDescriptor(); + + EXPECT_EQ(3u, desc.GetNClusterGroups()); + int entryIdx = -1; + for (const auto &cg : desc.GetClusterGroupIterable()) { + EXPECT_LT(entryIdx, static_cast(cg.GetMinEntry())); + entryIdx = cg.GetMinEntry(); + } +} + TEST(RClusterDescriptor, GetNBytesOnStorage) { auto model = RNTupleModel::Create(); diff --git a/tree/ntuple/test/ntuple_serialize.cxx b/tree/ntuple/test/ntuple_serialize.cxx index 71181c5a007fb..df1b8f6d957a1 100644 --- a/tree/ntuple/test/ntuple_serialize.cxx +++ b/tree/ntuple/test/ntuple_serialize.cxx @@ -797,8 +797,9 @@ TEST(RNTuple, SerializeFooter) context = RNTupleSerializer::SerializeHeader(bufHeader.get(), desc).Unwrap(); std::vector physClusterIDs; - for (const auto &c : desc.GetClusterIterable()) { - physClusterIDs.emplace_back(context.MapClusterId(c.GetId())); + for (const auto &cg : desc.GetClusterGroupIterable()) { + for (const auto cid : cg.GetClusterIds()) + physClusterIDs.emplace_back(context.MapClusterId(cid)); } EXPECT_EQ(desc.GetNClusters(), physClusterIDs.size()); context.MapClusterGroupId(256); diff --git a/tree/ntuple/test/ntuple_view.cxx b/tree/ntuple/test/ntuple_view.cxx index 80a2ca7134ce6..0e8826aedd07d 100644 --- a/tree/ntuple/test/ntuple_view.cxx +++ b/tree/ntuple/test/ntuple_view.cxx @@ -505,6 +505,7 @@ TEST(RNTuple, ViewFieldIteration) model->MakeField>("array"); model->MakeField("struct"); model->MakeField("empty"); + model->MakeField>("vecOfEmpty"); auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); writer->Fill(); @@ -524,13 +525,15 @@ TEST(RNTuple, ViewFieldIteration) EXPECT_EQ(1u, viewStruct.GetFieldRange().size()); auto viewArray = reader->GetView("array"); EXPECT_EQ(1u, viewArray.GetFieldRange().size()); - auto viewEmpty = reader->GetView("empty"); + EXPECT_EQ(1u, viewArray.GetFieldRange().size()); + + auto viewVecOfEmpty = reader->GetView("vecOfEmpty._0"); try { - viewEmpty.GetFieldRange(); - FAIL() << "accessing the field range of a view on an empty field should throw"; + viewVecOfEmpty.GetFieldRange(); + FAIL() << "accessing the field range of a view on an empty vector item should throw"; } catch (const ROOT::RException &err) { - EXPECT_THAT(err.what(), testing::HasSubstr("field iteration over empty fields is unsupported")); + EXPECT_THAT(err.what(), testing::HasSubstr("field iteration over empty fields")); } } diff --git a/tree/ntupleutil/src/RNTupleInspector.cxx b/tree/ntupleutil/src/RNTupleInspector.cxx index a3287a1d4d2ff..23d0565635b6e 100644 --- a/tree/ntupleutil/src/RNTupleInspector.cxx +++ b/tree/ntupleutil/src/RNTupleInspector.cxx @@ -50,6 +50,12 @@ void ROOT::Experimental::RNTupleInspector::CollectColumnInfo() fCompressedSize = 0; fUncompressedSize = 0; + std::vector clusterIds; + for (const auto &cgDesc : fDescriptor.GetClusterGroupIterable()) { + R__ASSERT(cgDesc.HasClusterDetails()); + clusterIds.insert(clusterIds.end(), cgDesc.GetClusterIds().begin(), cgDesc.GetClusterIds().end()); + } + for (const auto &colDesc : fDescriptor.GetColumnIterable()) { if (colDesc.IsAliasColumn()) continue; @@ -62,7 +68,8 @@ void ROOT::Experimental::RNTupleInspector::CollectColumnInfo() std::uint64_t nElems = 0; std::vector compressedPageSizes{}; - for (const auto &clusterDescriptor : fDescriptor.GetClusterIterable()) { + for (auto cid : clusterIds) { + const auto &clusterDescriptor = fDescriptor.GetClusterDescriptor(cid); if (!clusterDescriptor.ContainsColumn(colId)) { continue; } diff --git a/tree/ntupleutil/test/ntuple_inspector.cxx b/tree/ntupleutil/test/ntuple_inspector.cxx index edc67ee957cf7..67c385b82a1f9 100644 --- a/tree/ntupleutil/test/ntuple_inspector.cxx +++ b/tree/ntupleutil/test/ntuple_inspector.cxx @@ -559,8 +559,10 @@ TEST(RNTupleInspector, ColumnTypeInfoHist) EXPECT_STREQ("Number of elements by column type", nElemsHist->GetTitle()); EXPECT_EQ(4U, nElemsHist->GetNbinsX()); std::uint64_t nTotalElems = 0; + auto firstClusterId = inspector->GetDescriptor().FindClusterId(0, 0); + const auto &clusterDesc = inspector->GetDescriptor().GetClusterDescriptor(firstClusterId); for (const auto &col : inspector->GetDescriptor().GetColumnIterable()) { - nTotalElems += inspector->GetDescriptor().GetNElements(col.GetPhysicalId()); + nTotalElems += clusterDesc.GetColumnRange(col.GetPhysicalId()).GetNElements(); } EXPECT_EQ(nTotalElems, nElemsHist->Integral());