Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ntuple] Enable subfield access in REntry #16693

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions tree/ntuple/v7/inc/ROOT/REntry.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ private:

void AddValue(RFieldBase::RValue &&value)
{
fFieldName2Token[value.GetField().GetFieldName()] = fValues.size();
fFieldName2Token[value.GetField().GetQualifiedFieldName()] = fValues.size();
fValues.emplace_back(std::move(value));
}

/// While building the entry, adds a new value to the list and return the value's shared pointer
template <typename T, typename... ArgsT>
std::shared_ptr<T> AddValue(RField<T> &field, ArgsT &&...args)
{
fFieldName2Token[field.GetFieldName()] = fValues.size();
fFieldName2Token[field.GetQualifiedFieldName()] = fValues.size();
auto ptr = std::make_shared<T>(std::forward<ArgsT>(args)...);
fValues.emplace_back(field.BindValue(ptr));
return ptr;
Expand Down Expand Up @@ -136,7 +136,7 @@ private:
if constexpr (!std::is_void_v<T>) {
const auto &v = fValues[token.fIndex];
if (v.GetField().GetTypeName() != RField<T>::TypeName()) {
throw RException(R__FAIL("type mismatch for field " + v.GetField().GetFieldName() + ": " +
throw RException(R__FAIL("type mismatch for field " + v.GetField().GetQualifiedFieldName() + ": " +
v.GetField().GetTypeName() + " vs. " + RField<T>::TypeName()));
}
}
Expand Down
9 changes: 9 additions & 0 deletions tree/ntuple/v7/inc/ROOT/RFieldBase.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ namespace ROOT {
namespace Experimental {

class RFieldBase;
class RNTupleModel;

namespace Internal {
struct RFieldCallbackInjector;
Expand Down Expand Up @@ -65,6 +66,7 @@ This is and can only be partially enforced through C++.
*/
// clang-format on
class RFieldBase {
friend class RNTupleModel;
friend struct ROOT::Experimental::Internal::RFieldCallbackInjector; // used for unit tests
friend struct ROOT::Experimental::Internal::RFieldRepresentationModifier; // used for unit tests
friend void Internal::CallFlushColumnsOnField(RFieldBase &);
Expand Down Expand Up @@ -406,6 +408,13 @@ protected:
/// Fields may need direct access to the principal column of their sub fields, e.g. in RRVecField::ReadBulk
static Internal::RColumn *GetPrincipalColumnOf(const RFieldBase &other) { return other.fPrincipalColumn; }

/// Set the field's parent
void SetParent(RFieldBase *parent) { fParent = parent; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be public? Considering it doesn't do any sanity check I'm wondering if it's not something we want to prevent users to call directly..unless I'm missing something. (same consideration for SetSubField)


/// Set a field's subfield pointer to the one provided. This subfield must already be present in fSubFields, i.e., it
/// is not possible to add brand new subfields and change the structure of the field this way.
void SetSubField(std::unique_ptr<RFieldBase> subField);

/// Set a user-defined function to be called after reading a value, giving a chance to inspect and/or modify the
/// value object.
/// Returns an index that can be used to remove the callback.
Expand Down
6 changes: 6 additions & 0 deletions tree/ntuple/v7/inc/ROOT/RNTupleModel.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,12 @@ public:
/// Throws an exception if the field is null.
void AddField(std::unique_ptr<RFieldBase> field);

/// Adds a subfield belonging to the provided parent, whose type is not known at compile time. `field` must be a
/// valid, existing subfield of `parent` (i.e., it is not possible to alter the parent's structure).
///
/// Throws an exception if the field is null, or if it not a valid subfield of the provided parent.
void AddSubField(std::unique_ptr<RFieldBase> field, RFieldBase &parent);

/// Adds a top-level field based on existing fields.
RResult<void> AddProjectedField(std::unique_ptr<RFieldBase> field, FieldMappingFunc_t mapping);
const RProjectedFields &GetProjectedFields() const { return *fProjectedFields; }
Expand Down
16 changes: 16 additions & 0 deletions tree/ntuple/v7/src/RField.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,22 @@ ROOT::Experimental::RFieldBase::EnsureCompatibleColumnTypes(const RNTupleDescrip
"(representation index: " + std::to_string(representationIndex) + ")"));
}

void ROOT::Experimental::RFieldBase::SetSubField(std::unique_ptr<RFieldBase> subField)
{
if (fState != EState::kUnconnected)
throw RException(R__FAIL("invalid attempt to attach subfield to already connected field"));

for (auto &currSubField : fSubFields) {
if (currSubField->GetFieldName() == subField->GetFieldName() &&
currSubField->GetTypeName() == subField->GetTypeName()) {
std::swap(currSubField, subField);
return;
}
}

throw RException(R__FAIL("subfield \"" + subField->GetFieldName() + "\" not found"));
}

size_t ROOT::Experimental::RFieldBase::AddReadCallback(ReadCallback_t func)
{
fReadCallbacks.push_back(func);
Expand Down
16 changes: 16 additions & 0 deletions tree/ntuple/v7/src/RNTupleModel.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,22 @@ void ROOT::Experimental::RNTupleModel::AddField(std::unique_ptr<RFieldBase> fiel
fFieldZero->Attach(std::move(field));
}

void ROOT::Experimental::RNTupleModel::AddSubField(std::unique_ptr<RFieldBase> field, RFieldBase &parent)
{
EnsureNotFrozen();
if (!field)
throw RException(R__FAIL("null field"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this error a bit more explicative

EnsureValidFieldName(field->GetFieldName());

field->SetParent(&parent);

if (fDefaultEntry)
fDefaultEntry->AddValue(field->CreateValue());
fFieldNames.insert(field->GetFieldName());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this collide between a top-level field a and a subfield struct.a?


parent.SetSubField(std::move(field));
}

ROOT::Experimental::RResult<void>
ROOT::Experimental::RNTupleModel::AddProjectedField(std::unique_ptr<RFieldBase> field, FieldMappingFunc_t mapping)
{
Expand Down
52 changes: 52 additions & 0 deletions tree/ntuple/v7/test/ntuple_basics.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,58 @@ TEST(REntry, Basics)
EXPECT_NE(&pt, e->GetPtr<void>("pt").get());
}

TEST(REntry, SubFields)
{
FileRaii fileGuard("test_rentry_subfields");
{
auto model = RNTupleModel::Create();
model->MakeField<CustomStruct>("struct", CustomStruct{1.f, {2.f, 3.f}, {{4.f}, {5.f, 6.f, 7.f}}, "foo"});

auto ntuple = RNTupleWriter::Recreate(std::move(model), "ntuple", fileGuard.GetPath());
ntuple->Fill();
}

auto model = RNTupleModel::Create();
auto fldStruct = RFieldBase::Create("struct", "CustomStruct").Unwrap();
auto fldA = RFieldBase::Create("a", "float").Unwrap();

model->AddSubField(std::move(fldA), *fldStruct);

try {
auto tmpModel = model->Clone();
auto fldInvalidSubField = RFieldBase::Create("doesnotexist", "float").Unwrap();
tmpModel->AddSubField(std::move(fldInvalidSubField), *fldStruct);
FAIL() << "should not be able to add a subfield not present in the provided parent";
} catch (const RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("subfield \"doesnotexist\" not found"));
}

model->AddField(std::move(fldStruct));

auto &entry = model->GetDefaultEntry();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test should also exercise a non-default entry (doesn't RNTupleModel::CreateEntry need adapting?)

auto ntuple = RNTupleReader::Open(std::move(model), "ntuple", fileGuard.GetPath());
ntuple->LoadEntry(0);

CustomStruct expectedStruct{1.f, {2.f, 3.f}, {{4.f}, {5.f, 6.f, 7.f}}, "foo"};

EXPECT_EQ(expectedStruct, *entry.GetPtr<CustomStruct>("struct"));
EXPECT_FLOAT_EQ(1.f, *entry.GetPtr<float>("struct.a"));

auto tokenStruct = entry.GetToken("struct");
EXPECT_EQ(expectedStruct, *entry.GetPtr<CustomStruct>(tokenStruct));

auto tokenStructA = entry.GetToken("struct.a");
EXPECT_FLOAT_EQ(1.f, *entry.GetPtr<float>(tokenStructA));

// sanity check
try {
*entry.GetPtr<std::vector<float>>("v1");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe also test struct.v1, which is the qualified field name I would expect

FAIL() << "subfields not explicitly added to the model shouldn't be present in the model";
} catch (const RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("invalid field name: v1"));
}
}

TEST(RFieldBase, CreateObject)
{
auto ptrInt = RField<int>("name").CreateObject<int>();
Expand Down
Loading