From 986ac79e6ee2c7894b78dd7a9fef2f5cabe3c200 Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Sun, 21 Sep 2025 21:20:10 +0200 Subject: [PATCH 1/8] [ntuple] add RFieldZero::fAllowFieldSubstitutions --- tree/ntuple/inc/ROOT/RField.hxx | 19 +++++++++++++++++++ tree/ntuple/src/RField.cxx | 5 +++++ 2 files changed, 24 insertions(+) diff --git a/tree/ntuple/inc/ROOT/RField.hxx b/tree/ntuple/inc/ROOT/RField.hxx index 28699049683da..b5ed106fdfca9 100644 --- a/tree/ntuple/inc/ROOT/RField.hxx +++ b/tree/ntuple/inc/ROOT/RField.hxx @@ -49,9 +49,26 @@ namespace Detail { class RFieldVisitor; } // namespace Detail +class RFieldZero; +namespace Internal { +void SetAllowFieldSubstitutions(RFieldZero &fieldZero, bool val); +} + /// The container field for an ntuple model, which itself has no physical representation. /// Therefore, the zero field must not be connected to a page source or sink. class RFieldZero final : public RFieldBase { + friend void ROOT::Internal::SetAllowFieldSubstitutions(RFieldZero &, bool); + + /// If field substitutions are allowed, upon connecting to a page source the field hierarchy will replace created + /// fields by fields that match the on-disk schema. This happens for + /// - Vector fields (RVectorField, RRVecField) that connect to an on-disk fixed-size array + /// - Streamer fields that connect to an on-disk class field + /// Field substitutions must not be enabled when the field hierarchy already handed out RValue objects because + /// they would leave dangling field pointers to the replaced fields. This is used in cases when the field/model + /// is created by RNTuple (not imposed), before it is made available to the user. + /// This flag is reset on Clone(). + bool fAllowFieldSubstitutions = false; + protected: std::unique_ptr CloneImpl(std::string_view newName) const final; void ConstructValue(void *) const final {} @@ -64,6 +81,8 @@ public: size_t GetAlignment() const final { return 0; } void AcceptVisitor(ROOT::Detail::RFieldVisitor &visitor) const final; + + bool GetAllowFieldSubstitutions() const { return fAllowFieldSubstitutions; } }; /// Used in RFieldBase::Check() to record field creation failures. diff --git a/tree/ntuple/src/RField.cxx b/tree/ntuple/src/RField.cxx index c59d63e03e7cc..ca74bc0f942c5 100644 --- a/tree/ntuple/src/RField.cxx +++ b/tree/ntuple/src/RField.cxx @@ -31,6 +31,11 @@ #include #include +void ROOT::Internal::SetAllowFieldSubstitutions(RFieldZero &fieldZero, bool val) +{ + fieldZero.fAllowFieldSubstitutions = val; +} + std::unique_ptr ROOT::RFieldZero::CloneImpl(std::string_view /*newName*/) const { auto result = std::make_unique(); From 0e081de8eff117c1803f03000ea7b90bee3723f4 Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Sun, 21 Sep 2025 21:32:55 +0200 Subject: [PATCH 2/8] [ntuple] add RFieldZero::ReleaseSubfields() --- tree/ntuple/inc/ROOT/RField.hxx | 2 ++ tree/ntuple/inc/ROOT/RFieldBase.hxx | 1 + tree/ntuple/src/RField.cxx | 9 +++++++++ 3 files changed, 12 insertions(+) diff --git a/tree/ntuple/inc/ROOT/RField.hxx b/tree/ntuple/inc/ROOT/RField.hxx index b5ed106fdfca9..3c3237c25dbb5 100644 --- a/tree/ntuple/inc/ROOT/RField.hxx +++ b/tree/ntuple/inc/ROOT/RField.hxx @@ -83,6 +83,8 @@ public: void AcceptVisitor(ROOT::Detail::RFieldVisitor &visitor) const final; bool GetAllowFieldSubstitutions() const { return fAllowFieldSubstitutions; } + /// Moves all subfields into the returned vector. + std::vector> ReleaseSubfields(); }; /// Used in RFieldBase::Check() to record field creation failures. diff --git a/tree/ntuple/inc/ROOT/RFieldBase.hxx b/tree/ntuple/inc/ROOT/RFieldBase.hxx index 725d9163dadf9..b172d2f0d1521 100644 --- a/tree/ntuple/inc/ROOT/RFieldBase.hxx +++ b/tree/ntuple/inc/ROOT/RFieldBase.hxx @@ -85,6 +85,7 @@ This is and can only be partially enforced through C++. */ // clang-format on class RFieldBase { + friend class RFieldZero; // to reset fParent pointer in ReleaseSubfields() friend class ROOT::Experimental::Detail::RRawPtrWriteEntry; // to call Append() friend struct ROOT::Internal::RFieldCallbackInjector; // used for unit tests friend struct ROOT::Internal::RFieldRepresentationModifier; // used for unit tests diff --git a/tree/ntuple/src/RField.cxx b/tree/ntuple/src/RField.cxx index ca74bc0f942c5..e7e32ca2856bb 100644 --- a/tree/ntuple/src/RField.cxx +++ b/tree/ntuple/src/RField.cxx @@ -44,6 +44,15 @@ std::unique_ptr ROOT::RFieldZero::CloneImpl(std::string_view / return result; } +std::vector> ROOT::RFieldZero::ReleaseSubfields() +{ + std::vector> result; + std::swap(fSubfields, result); + for (auto &f : result) + f->fParent = nullptr; + return result; +} + void ROOT::RFieldZero::AcceptVisitor(ROOT::Detail::RFieldVisitor &visitor) const { visitor.VisitFieldZero(*this); From 33af4208209b8482100f93bf36f730f79d174ec4 Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Sun, 21 Sep 2025 21:42:24 +0200 Subject: [PATCH 3/8] [ntuple] allow field substitutions for own models We can safely apply field substitutions in a field hierarchy if we control the time between field creation and connecting the field to the page source, i.e. if we can ensure no field values have been created in-between. This is the case for - the RNTupleReader - views - the RNTupleProcessor - RDF - source member fields used in schema evolution Only imposed models created by users cannot apply field substitutions. --- tree/dataframe/src/RNTupleDS.cxx | 7 ++++++- tree/ntuple/inc/ROOT/RNTupleReader.hxx | 2 +- tree/ntuple/inc/ROOT/RNTupleView.hxx | 7 +++++-- tree/ntuple/src/RFieldBase.cxx | 8 ++++++-- tree/ntuple/src/RFieldMeta.cxx | 20 +++++++++++++------- tree/ntuple/src/RNTupleProcessor.cxx | 2 ++ tree/ntuple/src/RNTupleReader.cxx | 8 +++++--- 7 files changed, 38 insertions(+), 16 deletions(-) diff --git a/tree/dataframe/src/RNTupleDS.cxx b/tree/dataframe/src/RNTupleDS.cxx index 8afc0dadee311..d887bb7f43ce2 100644 --- a/tree/dataframe/src/RNTupleDS.cxx +++ b/tree/dataframe/src/RNTupleDS.cxx @@ -197,15 +197,20 @@ class RNTupleColumnReader : public ROOT::Detail::RDF::RColumnReaderBase { } } + RFieldZero fieldZero; + ROOT::Internal::SetAllowFieldSubstitutions(fieldZero, true); + fieldZero.Attach(std::move(fField)); try { - ROOT::Internal::CallConnectPageSourceOnField(*fField, source); + ROOT::Internal::CallConnectPageSourceOnField(fieldZero, source); } catch (const ROOT::RException &err) { + fField = std::move(fieldZero.ReleaseSubfields()[0]); auto onDiskType = source.GetSharedDescriptorGuard()->GetFieldDescriptor(fField->GetOnDiskId()).GetTypeName(); std::string msg = "RNTupleDS: invalid type \"" + fField->GetTypeName() + "\" for column \"" + fDataSource->fFieldId2QualifiedName[fField->GetOnDiskId()] + "\" with on-disk type \"" + onDiskType + "\""; throw std::runtime_error(msg); } + fField = std::move(fieldZero.ReleaseSubfields()[0]); if (fValuePtr) { // When the reader reconnects to a new file, the fValuePtr is already set diff --git a/tree/ntuple/inc/ROOT/RNTupleReader.hxx b/tree/ntuple/inc/ROOT/RNTupleReader.hxx index b8c68b20339a3..f0aafef634e48 100644 --- a/tree/ntuple/inc/ROOT/RNTupleReader.hxx +++ b/tree/ntuple/inc/ROOT/RNTupleReader.hxx @@ -93,7 +93,7 @@ private: /// The model is generated from the RNTuple metadata on storage. explicit RNTupleReader(std::unique_ptr source, const ROOT::RNTupleReadOptions &options); - void ConnectModel(ROOT::RNTupleModel &model); + void ConnectModel(ROOT::RNTupleModel &model, bool allowFieldSubstitutions); RNTupleReader *GetDisplayReader(); void InitPageSource(bool enableMetrics); diff --git a/tree/ntuple/inc/ROOT/RNTupleView.hxx b/tree/ntuple/inc/ROOT/RNTupleView.hxx index 0e4e59751b715..3052b0e4b2f43 100644 --- a/tree/ntuple/inc/ROOT/RNTupleView.hxx +++ b/tree/ntuple/inc/ROOT/RNTupleView.hxx @@ -89,6 +89,8 @@ protected: static std::unique_ptr CreateField(ROOT::DescriptorId_t fieldId, Internal::RPageSource &pageSource, std::string_view typeName = "") { + RFieldZero fieldZero; + Internal::SetAllowFieldSubstitutions(fieldZero, true); std::unique_ptr field; { const auto &desc = pageSource.GetSharedDescriptorGuard().GetRef(); @@ -103,8 +105,9 @@ protected: } } field->SetOnDiskId(fieldId); - ROOT::Internal::CallConnectPageSourceOnField(*field, pageSource); - return field; + fieldZero.Attach(std::move(field)); + ROOT::Internal::CallConnectPageSourceOnField(fieldZero, pageSource); + return std::move(fieldZero.ReleaseSubfields()[0]); } RNTupleViewBase(std::unique_ptr field, ROOT::RNTupleGlobalRange range) diff --git a/tree/ntuple/src/RFieldBase.cxx b/tree/ntuple/src/RFieldBase.cxx index a9c9d046038b5..012922c2ae43e 100644 --- a/tree/ntuple/src/RFieldBase.cxx +++ b/tree/ntuple/src/RFieldBase.cxx @@ -945,8 +945,12 @@ void ROOT::RFieldBase::ConnectPageSink(ROOT::Internal::RPageSink &pageSink, ROOT void ROOT::RFieldBase::ConnectPageSource(ROOT::Internal::RPageSource &pageSource) { - if (dynamic_cast(this)) - throw RException(R__FAIL("invalid attempt to connect zero field to page source")); + if (dynamic_cast(this)) { + for (auto &f : fSubfields) + f->ConnectPageSource(pageSource); + return; + } + if (fState != EState::kUnconnected) throw RException(R__FAIL("invalid attempt to connect an already connected field to a page source")); diff --git a/tree/ntuple/src/RFieldMeta.cxx b/tree/ntuple/src/RFieldMeta.cxx index 3e3cd1f58001a..d87e822b97897 100644 --- a/tree/ntuple/src/RFieldMeta.cxx +++ b/tree/ntuple/src/RFieldMeta.cxx @@ -393,17 +393,20 @@ void ROOT::RClassField::PrepareStagingArea(const std::vectorGetName())); } - const auto &memberFieldDesc = desc.GetFieldDescriptor(memberFieldId); auto memberType = source->GetTypeForDeclaration() + source->GetDimensions(); - stagingItem.fField = Create("" /* we don't need a field name */, std::string(memberType)).Unwrap(); - stagingItem.fField->SetOnDiskId(memberFieldDesc.GetId()); + auto memberField = Create("" /* we don't need a field name */, std::string(memberType)).Unwrap(); + memberField->SetOnDiskId(memberFieldId); + auto fieldZero = std::make_unique(); + Internal::SetAllowFieldSubstitutions(*fieldZero, true); + fieldZero->Attach(std::move(memberField)); + stagingItem.fField = std::move(fieldZero); stagingItem.fOffset = fStagingClass->GetDataMemberOffset(source->GetName()); // Since we successfully looked up the source member in the RNTuple on-disk metadata, we expect it // to be present in the TClass instance, too. R__ASSERT(stagingItem.fOffset != TVirtualStreamerInfo::kMissing); - stagingAreaSize = std::max(stagingAreaSize, stagingItem.fOffset + stagingItem.fField->GetValueSize()); + stagingAreaSize = std::max(stagingAreaSize, stagingItem.fOffset + stagingItem.fField->begin()->GetValueSize()); } } @@ -413,8 +416,9 @@ void ROOT::RClassField::PrepareStagingArea(const std::vector(stagingAreaSize); for (const auto &[_, si] : fStagingItems) { - if (!(si.fField->GetTraits() & kTraitTriviallyConstructible)) { - CallConstructValueOn(*si.fField, fStagingArea.get() + si.fOffset); + const auto &memberField = *si.fField->cbegin(); + if (!(memberField.GetTraits() & kTraitTriviallyConstructible)) { + CallConstructValueOn(memberField, fStagingArea.get() + si.fOffset); } } } @@ -476,8 +480,10 @@ void ROOT::RClassField::BeforeConnectPageSource(ROOT::Internal::RPageSource &pag if (!rules.empty()) { SetStagingClass(fieldDesc.GetTypeName(), fieldDesc.GetTypeVersion()); PrepareStagingArea(rules, desc, fieldDesc); - for (auto &[_, si] : fStagingItems) + for (auto &[_, si] : fStagingItems) { Internal::CallConnectPageSourceOnField(*si.fField, pageSource); + si.fField = std::move(static_cast(si.fField.get())->ReleaseSubfields()[0]); + } // Remove target member of read rules from the list of regular members of the underlying on-disk field for (const auto rule : rules) { diff --git a/tree/ntuple/src/RNTupleProcessor.cxx b/tree/ntuple/src/RNTupleProcessor.cxx index 014439dfb4696..d1cdcce3ea67d 100644 --- a/tree/ntuple/src/RNTupleProcessor.cxx +++ b/tree/ntuple/src/RNTupleProcessor.cxx @@ -208,6 +208,7 @@ void ROOT::Experimental::RNTupleSingleProcessor::Connect() auto &fieldZero = ROOT::Internal::GetFieldZeroOfModel(*fModel); auto fieldZeroId = desc->GetFieldZeroId(); fieldZero.SetOnDiskId(fieldZeroId); + ROOT::Internal::SetAllowFieldSubstitutions(fieldZero, true); for (auto &field : fieldZero.GetMutableSubfields()) { auto onDiskId = desc->FindFieldId(field->GetQualifiedFieldName(), fieldZeroId); @@ -223,6 +224,7 @@ void ROOT::Experimental::RNTupleSingleProcessor::Connect() ROOT::Internal::CallConnectPageSourceOnField(*field, *fPageSource); } + ROOT::Internal::SetAllowFieldSubstitutions(fieldZero, false); } void ROOT::Experimental::RNTupleSingleProcessor::AddEntriesToJoinTable(Internal::RNTupleJoinTable &joinTable, diff --git a/tree/ntuple/src/RNTupleReader.cxx b/tree/ntuple/src/RNTupleReader.cxx index 39f819182496b..28b0fb94f1316 100644 --- a/tree/ntuple/src/RNTupleReader.cxx +++ b/tree/ntuple/src/RNTupleReader.cxx @@ -22,9 +22,10 @@ #include -void ROOT::RNTupleReader::ConnectModel(ROOT::RNTupleModel &model) +void ROOT::RNTupleReader::ConnectModel(ROOT::RNTupleModel &model, bool allowFieldSubstitutions) { auto &fieldZero = ROOT::Internal::GetFieldZeroOfModel(model); + ROOT::Internal::SetAllowFieldSubstitutions(fieldZero, allowFieldSubstitutions); // We must not use the descriptor guard to prevent recursive locking in field.ConnectPageSource ROOT::DescriptorId_t fieldZeroId = fSource->GetSharedDescriptorGuard()->GetFieldZeroId(); fieldZero.SetOnDiskId(fieldZeroId); @@ -38,6 +39,7 @@ void ROOT::RNTupleReader::ConnectModel(ROOT::RNTupleModel &model) } ROOT::Internal::CallConnectPageSourceOnField(*field, *fSource); } + ROOT::Internal::SetAllowFieldSubstitutions(fieldZero, false); } void ROOT::RNTupleReader::InitPageSource(bool enableMetrics) @@ -68,7 +70,7 @@ ROOT::RNTupleReader::RNTupleReader(std::unique_ptr model, } fModel->Freeze(); InitPageSource(options.GetEnableMetrics()); - ConnectModel(*fModel); + ConnectModel(*fModel, false /* allowFieldSubstitutions */); } ROOT::RNTupleReader::RNTupleReader(std::unique_ptr source, @@ -136,7 +138,7 @@ const ROOT::RNTupleModel &ROOT::RNTupleReader::GetModel() if (!fModel) { fModel = fSource->GetSharedDescriptorGuard()->CreateModel( fCreateModelOptions.value_or(ROOT::RNTupleDescriptor::RCreateModelOptions{})); - ConnectModel(*fModel); + ConnectModel(*fModel, true /* allowFieldSubstitutions */); } return *fModel; } From 4a36f6eb64a5c91aaa9f516b437718b3151f9538 Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Sun, 21 Sep 2025 21:51:04 +0200 Subject: [PATCH 4/8] [ntuple] allow for returning field substitute in BeforeConnectPageSource() --- tree/ntuple/inc/ROOT/RField.hxx | 4 ++-- tree/ntuple/inc/ROOT/RFieldBase.hxx | 8 +++++++- tree/ntuple/src/RFieldMeta.cxx | 7 +++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/tree/ntuple/inc/ROOT/RField.hxx b/tree/ntuple/inc/ROOT/RField.hxx index 3c3237c25dbb5..a9398d1ede906 100644 --- a/tree/ntuple/inc/ROOT/RField.hxx +++ b/tree/ntuple/inc/ROOT/RField.hxx @@ -208,7 +208,7 @@ protected: void ReadGlobalImpl(ROOT::NTupleSize_t globalIndex, void *to) final; void ReadInClusterImpl(RNTupleLocalIndex localIndex, void *to) final; - void BeforeConnectPageSource(ROOT::Internal::RPageSource &pageSource) final; + std::unique_ptr BeforeConnectPageSource(ROOT::Internal::RPageSource &pageSource) final; void ReconcileOnDiskField(const RNTupleDescriptor &desc) final; public: @@ -263,7 +263,7 @@ protected: // Returns the list of seen streamer infos ROOT::RExtraTypeInfoDescriptor GetExtraTypeInfo() const final; - void BeforeConnectPageSource(ROOT::Internal::RPageSource &source) final; + std::unique_ptr BeforeConnectPageSource(ROOT::Internal::RPageSource &source) final; void ReconcileOnDiskField(const RNTupleDescriptor &desc) final; public: diff --git a/tree/ntuple/inc/ROOT/RFieldBase.hxx b/tree/ntuple/inc/ROOT/RFieldBase.hxx index b172d2f0d1521..c7243dfc1bd63 100644 --- a/tree/ntuple/inc/ROOT/RFieldBase.hxx +++ b/tree/ntuple/inc/ROOT/RFieldBase.hxx @@ -514,7 +514,13 @@ protected: /// Called by ConnectPageSource() before connecting; derived classes may override this as appropriate, e.g. /// for the application of I/O rules. In the process, the field at hand or its subfields may be marked as /// "artifical", i.e. introduced by schema evolution and not backed by on-disk information. - virtual void BeforeConnectPageSource(ROOT::Internal::RPageSource & /* source */) {} + /// May return a field substitute that fits the on-disk schema as a replacement for the field at hand. + /// A field substitute must read into the same in-memory layout than the original field and field substitutions + /// must not be cyclic. + virtual std::unique_ptr BeforeConnectPageSource(ROOT::Internal::RPageSource & /* source */) + { + return nullptr; + } /// For non-artificial fields, check compatibility of the in-memory field and the on-disk field. In the process, /// the field at hand may change its on-disk ID or perform other tasks related to automatic schema evolution. diff --git a/tree/ntuple/src/RFieldMeta.cxx b/tree/ntuple/src/RFieldMeta.cxx index d87e822b97897..6b03feff5e8bc 100644 --- a/tree/ntuple/src/RFieldMeta.cxx +++ b/tree/ntuple/src/RFieldMeta.cxx @@ -440,7 +440,7 @@ void ROOT::RClassField::AddReadCallbacksFromIORule(const TSchemaRule *rule) }); } -void ROOT::RClassField::BeforeConnectPageSource(ROOT::Internal::RPageSource &pageSource) +std::unique_ptr ROOT::RClassField::BeforeConnectPageSource(ROOT::Internal::RPageSource &pageSource) { std::vector rules; // On-disk members that are not targeted by an I/O rule; all other sub fields of the in-memory class @@ -507,6 +507,8 @@ void ROOT::RClassField::BeforeConnectPageSource(ROOT::Internal::RPageSource &pag CallSetArtificialOn(*field); } } + + return nullptr; } void ROOT::RClassField::ReconcileOnDiskField(const RNTupleDescriptor &desc) @@ -979,9 +981,10 @@ void ROOT::RStreamerField::GenerateColumns(const ROOT::RNTupleDescriptor &desc) GenerateColumnsImpl(desc); } -void ROOT::RStreamerField::BeforeConnectPageSource(ROOT::Internal::RPageSource &source) +std::unique_ptr ROOT::RStreamerField::BeforeConnectPageSource(ROOT::Internal::RPageSource &source) { source.RegisterStreamerInfos(); + return nullptr; } void ROOT::RStreamerField::ReconcileOnDiskField(const RNTupleDescriptor &desc) From caaa240fd617c269680da4bb276eae410d4051ac Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Sun, 21 Sep 2025 22:01:32 +0200 Subject: [PATCH 5/8] [ntuple] apply field substitutions in ConnectPageSource() --- tree/ntuple/src/RFieldBase.cxx | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tree/ntuple/src/RFieldBase.cxx b/tree/ntuple/src/RFieldBase.cxx index 012922c2ae43e..0d754a017f6a0 100644 --- a/tree/ntuple/src/RFieldBase.cxx +++ b/tree/ntuple/src/RFieldBase.cxx @@ -959,7 +959,26 @@ void ROOT::RFieldBase::ConnectPageSource(ROOT::Internal::RPageSource &pageSource if (!fDescription.empty()) throw RException(R__FAIL("setting description only valid when connecting to a page sink")); - BeforeConnectPageSource(pageSource); + auto substitute = BeforeConnectPageSource(pageSource); + if (substitute) { + const RFieldBase *itr = this; + while (itr->GetParent()) { + itr = itr->GetParent(); + } + if (typeid(*itr) == typeid(RFieldZero) && static_cast(itr)->GetAllowFieldSubstitutions()) { + for (auto &f : fParent->fSubfields) { + if (f.get() != this) + continue; + + f = std::move(substitute); + f->ConnectPageSource(pageSource); + return; + } + R__ASSERT(false); // never here + } else { + throw RException(R__FAIL("invalid attempt to substitute field " + GetQualifiedFieldName())); + } + } if (!fIsArtificial) { R__ASSERT(fOnDiskId != kInvalidDescriptorId); From 9135850fb21c0a167d327170d2d2d934e17647d7 Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Mon, 22 Sep 2025 10:49:01 +0200 Subject: [PATCH 6/8] [ntuple] Fix RArrayAsRVecField for use outside RDF This field used to expect a pre-contructed RVec of a certain size, which we can ensure in RDF but not elsewhere. Factor out the logic to resize RVecs in a type-erased way and use it in both this field and the RRVecField. --- .../ROOT/RField/RFieldSequenceContainer.hxx | 7 + tree/ntuple/src/RFieldSequenceContainer.cxx | 140 +++++++----------- 2 files changed, 60 insertions(+), 87 deletions(-) diff --git a/tree/ntuple/inc/ROOT/RField/RFieldSequenceContainer.hxx b/tree/ntuple/inc/ROOT/RField/RFieldSequenceContainer.hxx index 473fa2e3c8484..ef2ca613093a4 100644 --- a/tree/ntuple/inc/ROOT/RField/RFieldSequenceContainer.hxx +++ b/tree/ntuple/inc/ROOT/RField/RFieldSequenceContainer.hxx @@ -111,6 +111,13 @@ public: /// The type-erased field for a RVec class RRVecField : public RFieldBase { + friend class RArrayAsRVecField; // to call ResizeRVec() + + // Ensures that the RVec pointed to by rvec has at least nItems valid elements + // Returns the possibly new "begin pointer" of the RVec, i.e. the pointer to the data area. + static unsigned char * + ResizeRVec(void *rvec, std::size_t nItems, std::size_t itemSize, const RFieldBase *itemField, RDeleter *itemDeleter); + public: /// the RRVecDeleter is also used by RArrayAsRVecField and therefore declared public class RRVecDeleter : public RDeleter { diff --git a/tree/ntuple/src/RFieldSequenceContainer.cxx b/tree/ntuple/src/RFieldSequenceContainer.cxx index 8a3cae108c485..063069dd0e15f 100644 --- a/tree/ntuple/src/RFieldSequenceContainer.cxx +++ b/tree/ntuple/src/RFieldSequenceContainer.cxx @@ -9,6 +9,7 @@ #include #include // for malloc, free +#include #include #include // hardware_destructive_interference_size @@ -134,19 +135,20 @@ namespace { /// Retrieve the addresses of the data members of a generic RVec from a pointer to the beginning of the RVec object. /// Returns pointers to fBegin, fSize and fCapacity in a std::tuple. -std::tuple GetRVecDataMembers(void *rvecPtr) +std::tuple GetRVecDataMembers(void *rvecPtr) { - void **begin = reinterpret_cast(rvecPtr); + unsigned char **beginPtr = reinterpret_cast(rvecPtr); // int32_t fSize is the second data member (after 1 void*) - std::int32_t *size = reinterpret_cast(begin + 1); + std::int32_t *size = reinterpret_cast(beginPtr + 1); R__ASSERT(*size >= 0); // int32_t fCapacity is the third data member (1 int32_t after fSize) std::int32_t *capacity = size + 1; R__ASSERT(*capacity >= -1); - return {begin, size, capacity}; + return {beginPtr, size, capacity}; } -std::tuple GetRVecDataMembers(const void *rvecPtr) +std::tuple +GetRVecDataMembers(const void *rvecPtr) { return {GetRVecDataMembers(const_cast(rvecPtr))}; } @@ -198,7 +200,7 @@ std::size_t EvalRVecAlignment(std::size_t alignOfSubfield) return std::max({alignof(void *), alignof(std::int32_t), alignOfSubfield}); } -void DestroyRVecWithChecks(std::size_t alignOfT, void **beginPtr, char *begin, std::int32_t *capacityPtr) +void DestroyRVecWithChecks(std::size_t alignOfT, unsigned char **beginPtr, std::int32_t *capacityPtr) { // figure out if we are in the small state, i.e. begin == &inlineBuffer // there might be padding between fCapacity and the inline buffer, so we compute it here @@ -206,11 +208,11 @@ void DestroyRVecWithChecks(std::size_t alignOfT, void **beginPtr, char *begin, s auto paddingMiddle = dataMemberSz % alignOfT; if (paddingMiddle != 0) paddingMiddle = alignOfT - paddingMiddle; - const bool isSmall = (begin == (reinterpret_cast(beginPtr) + dataMemberSz + paddingMiddle)); + const bool isSmall = (*beginPtr == (reinterpret_cast(beginPtr) + dataMemberSz + paddingMiddle)); const bool owns = (*capacityPtr != -1); if (!isSmall && owns) - free(begin); + free(*beginPtr); } } // anonymous namespace @@ -242,9 +244,8 @@ std::size_t ROOT::RRVecField::AppendImpl(const void *from) GetPrincipalColumnOf(*fSubfields[0])->AppendV(*beginPtr, *sizePtr); nbytes += *sizePtr * GetPrincipalColumnOf(*fSubfields[0])->GetElement()->GetPackedSize(); } else { - auto begin = reinterpret_cast(*beginPtr); // for pointer arithmetics for (std::int32_t i = 0; i < *sizePtr; ++i) { - nbytes += CallAppendOn(*fSubfields[0], begin + i * fItemSize); + nbytes += CallAppendOn(*fSubfields[0], *beginPtr + i * fItemSize); } } @@ -253,30 +254,27 @@ std::size_t ROOT::RRVecField::AppendImpl(const void *from) return nbytes + fPrincipalColumn->GetElement()->GetPackedSize(); } -void ROOT::RRVecField::ReadGlobalImpl(ROOT::NTupleSize_t globalIndex, void *to) -{ - // TODO as a performance optimization, we could assign values to elements of the inline buffer: - // if size < inline buffer size: we save one allocation here and usage of the RVec skips a pointer indirection +unsigned char *ROOT::RRVecField::ResizeRVec(void *rvec, std::size_t nItems, std::size_t itemSize, + const RFieldBase *itemField, RDeleter *itemDeleter) - auto [beginPtr, sizePtr, capacityPtr] = GetRVecDataMembers(to); +{ + if (nItems > static_cast(std::numeric_limits::max())) { + throw RException(R__FAIL("RVec too large: " + std::to_string(nItems))); + } - // Read collection info for this entry - ROOT::NTupleSize_t nItems; - RNTupleLocalIndex collectionStart; - fPrincipalColumn->GetCollectionInfo(globalIndex, &collectionStart, &nItems); - char *begin = reinterpret_cast(*beginPtr); // for pointer arithmetics + auto [beginPtr, sizePtr, capacityPtr] = GetRVecDataMembers(rvec); const std::size_t oldSize = *sizePtr; // See "semantics of reading non-trivial objects" in RNTuple's Architecture.md for details // on the element construction/destrution. const bool owns = (*capacityPtr != -1); - const bool needsConstruct = !(fSubfields[0]->GetTraits() & kTraitTriviallyConstructible); - const bool needsDestruct = owns && fItemDeleter; + const bool needsConstruct = !(itemField->GetTraits() & kTraitTriviallyConstructible); + const bool needsDestruct = owns && itemDeleter; // Destroy excess elements, if any if (needsDestruct) { for (std::size_t i = nItems; i < oldSize; ++i) { - fItemDeleter->operator()(begin + (i * fItemSize), true /* dtorOnly */); + itemDeleter->operator()(*beginPtr + (i * itemSize), true /* dtorOnly */); } } @@ -286,7 +284,7 @@ void ROOT::RRVecField::ReadGlobalImpl(ROOT::NTupleSize_t globalIndex, void *to) // allocates memory we need to release it here to avoid memleaks (e.g. if this is an RVec>) if (needsDestruct) { for (std::size_t i = 0u; i < oldSize; ++i) { - fItemDeleter->operator()(begin + (i * fItemSize), true /* dtorOnly */); + itemDeleter->operator()(*beginPtr + (i * itemSize), true /* dtorOnly */); } } @@ -297,15 +295,14 @@ void ROOT::RRVecField::ReadGlobalImpl(ROOT::NTupleSize_t globalIndex, void *to) } // We trust that malloc returns a buffer with large enough alignment. // This might not be the case if T in RVec is over-aligned. - *beginPtr = malloc(nItems * fItemSize); + *beginPtr = static_cast(malloc(nItems * itemSize)); R__ASSERT(*beginPtr != nullptr); - begin = reinterpret_cast(*beginPtr); *capacityPtr = nItems; // Placement new for elements that were already there before the resize if (needsConstruct) { for (std::size_t i = 0u; i < oldSize; ++i) - CallConstructValueOn(*fSubfields[0], begin + (i * fItemSize)); + CallConstructValueOn(*itemField, *beginPtr + (i * itemSize)); } } *sizePtr = nItems; @@ -313,9 +310,24 @@ void ROOT::RRVecField::ReadGlobalImpl(ROOT::NTupleSize_t globalIndex, void *to) // Placement new for new elements, if any if (needsConstruct) { for (std::size_t i = oldSize; i < nItems; ++i) - CallConstructValueOn(*fSubfields[0], begin + (i * fItemSize)); + CallConstructValueOn(*itemField, *beginPtr + (i * itemSize)); } + return *beginPtr; +} + +void ROOT::RRVecField::ReadGlobalImpl(ROOT::NTupleSize_t globalIndex, void *to) +{ + // TODO as a performance optimization, we could assign values to elements of the inline buffer: + // if size < inline buffer size: we save one allocation here and usage of the RVec skips a pointer indirection + + // Read collection info for this entry + ROOT::NTupleSize_t nItems; + RNTupleLocalIndex collectionStart; + fPrincipalColumn->GetCollectionInfo(globalIndex, &collectionStart, &nItems); + + auto begin = ResizeRVec(to, nItems, fItemSize, fSubfields[0].get(), fItemDeleter.get()); + if (fSubfields[0]->IsSimple() && nItems) { GetPrincipalColumnOf(*fSubfields[0])->ReadV(collectionStart, nItems, begin); return; @@ -430,14 +442,13 @@ void ROOT::RRVecField::RRVecDeleter::operator()(void *objPtr, bool dtorOnly) { auto [beginPtr, sizePtr, capacityPtr] = GetRVecDataMembers(objPtr); - char *begin = reinterpret_cast(*beginPtr); // for pointer arithmetics if (fItemDeleter) { for (std::int32_t i = 0; i < *sizePtr; ++i) { - fItemDeleter->operator()(begin + i * fItemSize, true /* dtorOnly */); + fItemDeleter->operator()(*beginPtr + i * fItemSize, true /* dtorOnly */); } } - DestroyRVecWithChecks(fItemAlignment, beginPtr, begin, capacityPtr); + DestroyRVecWithChecks(fItemAlignment, beginPtr, capacityPtr); RDeleter::operator()(objPtr, dtorOnly); } @@ -453,10 +464,10 @@ std::vector ROOT::RRVecField::SplitValue(const RValue auto [beginPtr, sizePtr, _] = GetRVecDataMembers(value.GetPtr().get()); std::vector result; - char *begin = reinterpret_cast(*beginPtr); // for pointer arithmetics result.reserve(*sizePtr); for (std::int32_t i = 0; i < *sizePtr; ++i) { - result.emplace_back(fSubfields[0]->BindValue(std::shared_ptr(value.GetPtr(), begin + i * fItemSize))); + result.emplace_back( + fSubfields[0]->BindValue(std::shared_ptr(value.GetPtr(), *beginPtr + i * fItemSize))); } return result; } @@ -748,52 +759,10 @@ std::unique_ptr ROOT::RArrayAsRVecField::CloneImpl(std::string void ROOT::RArrayAsRVecField::ConstructValue(void *where) const { // initialize data members fBegin, fSize, fCapacity + // currently the inline buffer is left uninitialized void **beginPtr = new (where)(void *)(nullptr); - std::int32_t *sizePtr = new (reinterpret_cast(beginPtr + 1)) std::int32_t(0); - std::int32_t *capacityPtr = new (sizePtr + 1) std::int32_t(0); - - // Create the RVec with the known fixed size, do it once here instead of - // every time the value is read in `Read*Impl` functions - char *begin = reinterpret_cast(*beginPtr); // for pointer arithmetics - - // Early return if the RVec has already been allocated. - if (*sizePtr == std::int32_t(fArrayLength)) - return; - - // Need to allocate the RVec if it is the first time the value is being created. - // See "semantics of reading non-trivial objects" in RNTuple's Architecture.md for details - // on the element construction. - const bool owns = (*capacityPtr != -1); // RVec is adopting the memory - const bool needsConstruct = !(fSubfields[0]->GetTraits() & kTraitTriviallyConstructible); - const bool needsDestruct = owns && fItemDeleter; - - // Destroy old elements: useless work for trivial types, but in case the element type's constructor - // allocates memory we need to release it here to avoid memleaks (e.g. if this is an RVec>) - if (needsDestruct) { - for (std::int32_t i = 0; i < *sizePtr; ++i) { - fItemDeleter->operator()(begin + (i * fItemSize), true /* dtorOnly */); - } - } - - // TODO: Isn't the RVec always owning in this case? - if (owns) { - // *beginPtr points to the array of item values (allocated in an earlier call by the following malloc()) - free(*beginPtr); - } - - *beginPtr = malloc(fArrayLength * fItemSize); - R__ASSERT(*beginPtr != nullptr); - // Re-assign begin pointer after allocation - begin = reinterpret_cast(*beginPtr); - // Size and capacity are equal since the field data type is std::array - *sizePtr = fArrayLength; - *capacityPtr = fArrayLength; - - // Placement new for the array elements - if (needsConstruct) { - for (std::size_t i = 0; i < fArrayLength; ++i) - CallConstructValueOn(*fSubfields[0], begin + (i * fItemSize)); - } + std::int32_t *sizePtr = new (static_cast(beginPtr + 1)) std::int32_t(0); + new (sizePtr + 1) std::int32_t(-1); } std::unique_ptr ROOT::RArrayAsRVecField::GetDeleter() const @@ -807,39 +776,36 @@ std::unique_ptr ROOT::RArrayAsRVecField::GetDeleter( void ROOT::RArrayAsRVecField::ReadGlobalImpl(ROOT::NTupleSize_t globalIndex, void *to) { - - auto [beginPtr, _, __] = GetRVecDataMembers(to); - auto rvecBeginPtr = reinterpret_cast(*beginPtr); // for pointer arithmetics + auto begin = RRVecField::ResizeRVec(to, fArrayLength, fItemSize, fSubfields[0].get(), fItemDeleter.get()); if (fSubfields[0]->IsSimple()) { - GetPrincipalColumnOf(*fSubfields[0])->ReadV(globalIndex * fArrayLength, fArrayLength, rvecBeginPtr); + GetPrincipalColumnOf(*fSubfields[0])->ReadV(globalIndex * fArrayLength, fArrayLength, begin); return; } // Read the new values into the collection elements for (std::size_t i = 0; i < fArrayLength; ++i) { - CallReadOn(*fSubfields[0], globalIndex * fArrayLength + i, rvecBeginPtr + (i * fItemSize)); + CallReadOn(*fSubfields[0], globalIndex * fArrayLength + i, begin + (i * fItemSize)); } } void ROOT::RArrayAsRVecField::ReadInClusterImpl(RNTupleLocalIndex localIndex, void *to) { - auto [beginPtr, _, __] = GetRVecDataMembers(to); - auto rvecBeginPtr = reinterpret_cast(*beginPtr); // for pointer arithmetics + auto begin = RRVecField::ResizeRVec(to, fArrayLength, fItemSize, fSubfields[0].get(), fItemDeleter.get()); const auto &clusterId = localIndex.GetClusterId(); const auto &indexInCluster = localIndex.GetIndexInCluster(); if (fSubfields[0]->IsSimple()) { GetPrincipalColumnOf(*fSubfields[0]) - ->ReadV(RNTupleLocalIndex(clusterId, indexInCluster * fArrayLength), fArrayLength, rvecBeginPtr); + ->ReadV(RNTupleLocalIndex(clusterId, indexInCluster * fArrayLength), fArrayLength, begin); return; } // Read the new values into the collection elements for (std::size_t i = 0; i < fArrayLength; ++i) { CallReadOn(*fSubfields[0], RNTupleLocalIndex(clusterId, indexInCluster * fArrayLength + i), - rvecBeginPtr + (i * fItemSize)); + begin + (i * fItemSize)); } } From 79c23df8d799986518bfa53d85ada6b5c71f8f35 Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Sun, 21 Sep 2025 22:52:37 +0200 Subject: [PATCH 7/8] [ntuple] automatically evolve fixed size array into RVec --- .../ROOT/RField/RFieldSequenceContainer.hxx | 1 + tree/ntuple/src/RFieldSequenceContainer.cxx | 16 +++++++ tree/ntuple/test/ntuple_evolution_shape.cxx | 44 +++++++++++++++++++ tree/ntuple/test/ntuple_evolution_type.cxx | 22 ++++++++++ 4 files changed, 83 insertions(+) diff --git a/tree/ntuple/inc/ROOT/RField/RFieldSequenceContainer.hxx b/tree/ntuple/inc/ROOT/RField/RFieldSequenceContainer.hxx index ef2ca613093a4..950f442f1d85f 100644 --- a/tree/ntuple/inc/ROOT/RField/RFieldSequenceContainer.hxx +++ b/tree/ntuple/inc/ROOT/RField/RFieldSequenceContainer.hxx @@ -154,6 +154,7 @@ protected: void ReadGlobalImpl(ROOT::NTupleSize_t globalIndex, void *to) final; std::size_t ReadBulkImpl(const RBulkSpec &bulkSpec) final; + std::unique_ptr BeforeConnectPageSource(ROOT::Internal::RPageSource &pageSource) final; void ReconcileOnDiskField(const RNTupleDescriptor &desc) final; void CommitClusterImpl() final { fNWritten = 0; } diff --git a/tree/ntuple/src/RFieldSequenceContainer.cxx b/tree/ntuple/src/RFieldSequenceContainer.cxx index 063069dd0e15f..b6db8dd1dcd34 100644 --- a/tree/ntuple/src/RFieldSequenceContainer.cxx +++ b/tree/ntuple/src/RFieldSequenceContainer.cxx @@ -424,6 +424,22 @@ void ROOT::RRVecField::GenerateColumns(const ROOT::RNTupleDescriptor &desc) GenerateColumnsImpl(desc); } +std::unique_ptr ROOT::RRVecField::BeforeConnectPageSource(Internal::RPageSource &pageSource) +{ + if (GetOnDiskId() == kInvalidDescriptorId) + return nullptr; + + const auto descGuard = pageSource.GetSharedDescriptorGuard(); + const auto &fieldDesc = descGuard->GetFieldDescriptor(GetOnDiskId()); + if (fieldDesc.GetTypeName().rfind("std::array<", 0) == 0) { + auto substitute = std::make_unique( + GetFieldName(), fSubfields[0]->Clone(fSubfields[0]->GetFieldName()), fieldDesc.GetNRepetitions()); + substitute->SetOnDiskId(GetOnDiskId()); + return substitute; + } + return nullptr; +} + void ROOT::RRVecField::ReconcileOnDiskField(const RNTupleDescriptor &desc) { EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName); diff --git a/tree/ntuple/test/ntuple_evolution_shape.cxx b/tree/ntuple/test/ntuple_evolution_shape.cxx index 5b81f0ccd111b..0c19b02f22a22 100644 --- a/tree/ntuple/test/ntuple_evolution_shape.cxx +++ b/tree/ntuple/test/ntuple_evolution_shape.cxx @@ -956,6 +956,50 @@ struct RenamedIntermediateDerived : public RenamedIntermediate2 { } } +TEST(RNTupleEvolution, ArrayAsVector) +{ + FileRaii fileGuard("test_ntuple_evolution_array_as_vector.root"); + + ExecInFork([&] { + // The child process writes the file and exits, but the file must be preserved to be read by the parent. + fileGuard.PreserveFile(); + + ASSERT_TRUE(gInterpreter->Declare(R"( +struct ArrayAsVector { + std::array fArr = {1, 2}; + int x = 3; +}; +)")); + + auto model = RNTupleModel::Create(); + model->AddField(RFieldBase::Create("f", "ArrayAsVector").Unwrap()); + + auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); + writer->Fill(); + + writer.reset(); + }); + + ASSERT_TRUE(gInterpreter->Declare(R"( +struct ArrayAsVector { + ROOT::RVec fArr; + int x; +}; +)")); + + auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath()); + ASSERT_EQ(1, reader->GetNEntries()); + + void *ptr = reader->GetModel().GetDefaultEntry().GetPtr("f").get(); + DeclarePointer("ArrayAsVector", "ptrArrayAsVector", ptr); + + reader->LoadEntry(0); + EXPECT_EVALUATE_EQ("ptrArrayAsVector->x", 3); + EXPECT_EVALUATE_EQ("ptrArrayAsVector->fArr.size()", 2); + EXPECT_EVALUATE_EQ("ptrArrayAsVector->fArr[0]", 1); + EXPECT_EVALUATE_EQ("ptrArrayAsVector->fArr[1]", 2); +} + TEST(RNTupleEvolution, StreamerField) { FileRaii fileGuard("test_ntuple_evolution_streamer_field.root"); diff --git a/tree/ntuple/test/ntuple_evolution_type.cxx b/tree/ntuple/test/ntuple_evolution_type.cxx index c7700f6b0e2c5..c70bb30dfda4d 100644 --- a/tree/ntuple/test/ntuple_evolution_type.cxx +++ b/tree/ntuple/test/ntuple_evolution_type.cxx @@ -229,3 +229,25 @@ TEST(RNTupleEvolution, Enum) EXPECT_EQ(42, ve1(0)); EXPECT_EQ(137, ve2(0)); } + +TEST(RNTupleEvolution, ArrayAsRVec) +{ + FileRaii fileGuard("test_ntuple_evolution_array_as_rvec.root"); + { + auto model = ROOT::RNTupleModel::Create(); + auto a = model->MakeField>("a"); + auto writer = ROOT::RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath()); + + *a = {1, 2}; + writer->Fill(); + } + + auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath()); + + auto a = reader->GetView>("a"); + const auto &f = a.GetField(); // necessary to silence clang warning + EXPECT_EQ(typeid(f), typeid(ROOT::RArrayAsRVecField)); + EXPECT_EQ(2u, a(0).size()); + EXPECT_EQ(1, a(0)[0]); + EXPECT_EQ(2, a(0)[1]); +} From 90b5d4b7570aba6cbb112a5d290a4d6ed00341e5 Mon Sep 17 00:00:00 2001 From: Jakob Blomer Date: Tue, 23 Sep 2025 11:43:21 +0200 Subject: [PATCH 8/8] [ntuple] test size check in RVec reading --- tree/ntuple/test/CMakeLists.txt | 1 + tree/ntuple/test/ntuple_largevector.cxx | 45 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tree/ntuple/test/ntuple_largevector.cxx diff --git a/tree/ntuple/test/CMakeLists.txt b/tree/ntuple/test/CMakeLists.txt index 47ab2cf34f87e..9d090c68a1059 100644 --- a/tree/ntuple/test/CMakeLists.txt +++ b/tree/ntuple/test/CMakeLists.txt @@ -92,6 +92,7 @@ ROOT_ADD_GTEST(ntuple_extended ntuple_extended.cxx LIBRARIES ROOTNTuple MathCore if(NOT MSVC OR win_broken_tests) ROOT_ADD_GTEST(ntuple_largefile1 ntuple_largefile1.cxx LIBRARIES ROOTNTuple MathCore) ROOT_ADD_GTEST(ntuple_largefile2 ntuple_largefile2.cxx LIBRARIES ROOTNTuple MathCore) + ROOT_ADD_GTEST(ntuple_largevector ntuple_largevector.cxx LIBRARIES ROOTNTuple) endif() ROOT_ADD_GTEST(ntuple_randomaccess ntuple_randomaccess.cxx LIBRARIES ROOTNTuple MathCore) diff --git a/tree/ntuple/test/ntuple_largevector.cxx b/tree/ntuple/test/ntuple_largevector.cxx new file mode 100644 index 0000000000000..f5c7721317d6f --- /dev/null +++ b/tree/ntuple/test/ntuple_largevector.cxx @@ -0,0 +1,45 @@ +#include "ntuple_test.hxx" + +#include + +TEST(RNTuple, DISABLED_LargeVector) +{ + FileRaii fileGuard("test_ntuple_large_vector.root"); + + // write out a vector too large for RVec + { + auto m = RNTupleModel::Create(); + auto vec = m->MakeField>("v"); + auto writer = RNTupleWriter::Recreate(std::move(m), "r", fileGuard.GetPath()); + vec->push_back(1); + writer->Fill(); + vec->resize(std::numeric_limits::max()); + writer->Fill(); + vec->push_back(2); + writer->Fill(); + vec->clear(); + writer->Fill(); + } + + ROOT::RNTupleReadOptions options; + options.SetClusterCache(ROOT::RNTupleReadOptions::EClusterCache::kOff); + auto reader = RNTupleReader::Open("r", fileGuard.GetPath(), options); + ASSERT_EQ(4u, reader->GetNEntries()); + + auto viewRVec = reader->GetView>("v"); + EXPECT_EQ(1u, viewRVec(0).size()); + EXPECT_EQ(1, viewRVec(0).at(0)); + const auto &v1 = viewRVec(1); + EXPECT_EQ(std::numeric_limits::max(), v1.size()); + EXPECT_EQ(1, v1.at(0)); + EXPECT_EQ(0, v1.at(1000)); + EXPECT_THROW(viewRVec(2), ROOT::RException); + EXPECT_TRUE(viewRVec(3).empty()); + + auto viewVector = reader->GetView>("v"); + const auto &v3 = viewVector(2); + EXPECT_EQ(static_cast(std::numeric_limits::max()) + 1, v3.size()); + EXPECT_EQ(1, v3.at(0)); + EXPECT_EQ(0, v3.at(1000)); + EXPECT_EQ(2, v3.at(std::numeric_limits::max())); +}