Skip to content
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
17 changes: 16 additions & 1 deletion tree/ntuple/src/RFieldMeta.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ std::unique_ptr<ROOT::RFieldBase> ROOT::RClassField::BeforeConnectPageSource(ROO
// On-disk members that are not targeted by an I/O rule; all other sub fields of the in-memory class
// will be marked as artificial (added member in a new class version or member set by rule).
std::unordered_set<std::string> regularSubfields;
// We generally don't support changing the number of base classes, with the exception of changing from/to zero
// base classes. The variable stores the number of on-disk base classes.
int nOnDiskBaseClasses = 0;

if (GetOnDiskId() == kInvalidDescriptorId) {
// This can happen for added base classes or added members of class type
Expand All @@ -468,6 +471,8 @@ std::unique_ptr<ROOT::RFieldBase> ROOT::RClassField::BeforeConnectPageSource(ROO
for (auto linkId : fieldDesc.GetLinkIds()) {
const auto &subFieldDesc = desc.GetFieldDescriptor(linkId);
regularSubfields.insert(subFieldDesc.GetFieldName());
if (!subFieldDesc.GetFieldName().empty() && subFieldDesc.GetFieldName()[0] == ':')
nOnDiskBaseClasses++;
}

rules = FindRules(&fieldDesc);
Expand Down Expand Up @@ -510,10 +515,20 @@ std::unique_ptr<ROOT::RFieldBase> ROOT::RClassField::BeforeConnectPageSource(ROO
}

// Iterate over all sub fields in memory and mark those as missing that are not in the descriptor.
int nInMemoryBaseClasses = 0;
for (auto &field : fSubfields) {
if (regularSubfields.count(field->GetFieldName()) == 0) {
const auto &fieldName = field->GetFieldName();
if (regularSubfields.count(fieldName) == 0) {
CallSetArtificialOn(*field);
}
if (!fieldName.empty() && fieldName[0] == ':')
nInMemoryBaseClasses++;
}

if (nInMemoryBaseClasses && nOnDiskBaseClasses && nInMemoryBaseClasses != nOnDiskBaseClasses) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe being explicit would make the condition even clearer?

Suggested change
if (nInMemoryBaseClasses && nOnDiskBaseClasses && nInMemoryBaseClasses != nOnDiskBaseClasses) {
if (nInMemoryBaseClasses != 0 && nOnDiskBaseClasses != 0 && nInMemoryBaseClasses != nOnDiskBaseClasses) {

throw RException(R__FAIL(std::string("incompatible number of base classes for field ") + GetFieldName() + ": " +
GetTypeName() + ", " + std::to_string(nInMemoryBaseClasses) + " vs. " +
std::to_string(nOnDiskBaseClasses)));
Comment on lines +530 to +531
Copy link
Member

Choose a reason for hiding this comment

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

Consider extending the text to clarify which number is for in memory and which number is for on file. It might also help to add version number (or checksum) of the class onfile layout.

}

return nullptr;
Expand Down
77 changes: 62 additions & 15 deletions tree/ntuple/test/ntuple_evolution_shape.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -593,20 +593,12 @@ struct AddedSecondBaseDerived : public AddedSecondBaseFirst, public AddedSecondB
)"));

auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath());
ASSERT_EQ(2, reader->GetNEntries());

void *ptr = reader->GetModel().GetDefaultEntry().GetPtr<void>("f").get();
DeclarePointer("AddedSecondBaseDerived", "ptrAddedSecondBaseDerived", ptr);

reader->LoadEntry(0);
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fFirstBase", 1);
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fSecondBase", 2);
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fDerived", 3);

reader->LoadEntry(1);
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fFirstBase", 71);
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fSecondBase", 2);
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fDerived", 93);
try {
reader->GetModel();
FAIL() << "model reconstruction should fail";
} catch (const ROOT::RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("incompatible number of base classes"));
}
}

TEST(RNTupleEvolution, PrependSecondBaseClass)
Expand Down Expand Up @@ -662,7 +654,7 @@ struct PrependSecondBaseDerived : public PrependSecondBaseSecond, public Prepend
reader->GetModel();
FAIL() << "model reconstruction should fail";
} catch (const ROOT::RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("incompatible type name for field"));
EXPECT_THAT(err.what(), testing::HasSubstr("incompatible number of base classes"));
}
}

Expand Down Expand Up @@ -844,6 +836,61 @@ struct RemovedIntermediateDerived : public RemovedIntermediateBase {
}
}

TEST(RNTupleEvolution, RemovedSecondBaseClass)
{
FileRaii fileGuard("test_ntuple_evolution_removed_second_base_class.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 RemovedSecondBaseFirst {
int fFirstBase = 1;
};
struct RemovedSecondBaseSecond {
int fSecondBase = 2;
};
struct RemovedSecondBaseDerived : public RemovedSecondBaseFirst, public RemovedSecondBaseSecond {
int fDerived = 3;
};
)"));

auto model = RNTupleModel::Create();
model->AddField(RFieldBase::Create("f", "RemovedSecondBaseDerived").Unwrap());

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

// Reset / close the writer and flush the file.
{
// TStreamerInfo::Build will report a warning for interpreted classes (but only for base classes).
// See also https://github.com/root-project/root/issues/9371
ROOT::TestSupport::CheckDiagsRAII diagRAII;
diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary",
/*matchFullMessage=*/false);
writer.reset();
}
});

ASSERT_TRUE(gInterpreter->Declare(R"(
struct RemovedSecondBaseFirst {
int fFirstBase = 1;
};
struct RemovedSecondBaseDerived : public RemovedSecondBaseFirst {
int fDerived = 3;
};
)"));

auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath());
try {
reader->GetModel();
FAIL() << "model reconstruction should fail";
} catch (const ROOT::RException &err) {
EXPECT_THAT(err.what(), testing::HasSubstr("incompatible number of base classes"));
}
}

TEST(RNTupleEvolution, RenamedBaseClass)
{
// RNTuple currently does not support automatic schema evolution when a class is renamed.
Expand Down
Loading