Skip to content

Commit 379249b

Browse files
committed
[ntuple] fix auto schema evolution of base classes
Forbid changing the number of base classes except for adding base classes where there were previously none and for removing all of the base classes.
1 parent 4731fd5 commit 379249b

File tree

2 files changed

+78
-16
lines changed

2 files changed

+78
-16
lines changed

tree/ntuple/src/RFieldMeta.cxx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,9 @@ std::unique_ptr<ROOT::RFieldBase> ROOT::RClassField::BeforeConnectPageSource(ROO
446446
// On-disk members that are not targeted by an I/O rule; all other sub fields of the in-memory class
447447
// will be marked as artificial (added member in a new class version or member set by rule).
448448
std::unordered_set<std::string> regularSubfields;
449+
// We generally don't support changing the number of base classes, with the exception of changing from/to zero
450+
// base classes. The variable stores the number of on-disk base classes.
451+
int nOnDiskBaseClasses = 0;
449452

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

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

512517
// Iterate over all sub fields in memory and mark those as missing that are not in the descriptor.
518+
int nInMemoryBaseClasses = 0;
513519
for (auto &field : fSubfields) {
514-
if (regularSubfields.count(field->GetFieldName()) == 0) {
520+
const auto &fieldName = field->GetFieldName();
521+
if (regularSubfields.count(fieldName) == 0) {
515522
CallSetArtificialOn(*field);
516523
}
524+
if (!fieldName.empty() && fieldName[0] == ':')
525+
nInMemoryBaseClasses++;
526+
}
527+
528+
if (nInMemoryBaseClasses && nOnDiskBaseClasses && nInMemoryBaseClasses != nOnDiskBaseClasses) {
529+
throw RException(R__FAIL(std::string("incompatible number of base classes for field ") + GetFieldName() + ": " +
530+
GetTypeName() + ", " + std::to_string(nInMemoryBaseClasses) + " vs. " +
531+
std::to_string(nOnDiskBaseClasses)));
517532
}
518533

519534
return nullptr;

tree/ntuple/test/ntuple_evolution_shape.cxx

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -593,20 +593,12 @@ struct AddedSecondBaseDerived : public AddedSecondBaseFirst, public AddedSecondB
593593
)"));
594594

595595
auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath());
596-
ASSERT_EQ(2, reader->GetNEntries());
597-
598-
void *ptr = reader->GetModel().GetDefaultEntry().GetPtr<void>("f").get();
599-
DeclarePointer("AddedSecondBaseDerived", "ptrAddedSecondBaseDerived", ptr);
600-
601-
reader->LoadEntry(0);
602-
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fFirstBase", 1);
603-
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fSecondBase", 2);
604-
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fDerived", 3);
605-
606-
reader->LoadEntry(1);
607-
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fFirstBase", 71);
608-
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fSecondBase", 2);
609-
EXPECT_EVALUATE_EQ("ptrAddedSecondBaseDerived->fDerived", 93);
596+
try {
597+
reader->GetModel();
598+
FAIL() << "model reconstruction should fail";
599+
} catch (const ROOT::RException &err) {
600+
EXPECT_THAT(err.what(), testing::HasSubstr("incompatible number of base classes"));
601+
}
610602
}
611603

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

@@ -844,6 +836,61 @@ struct RemovedIntermediateDerived : public RemovedIntermediateBase {
844836
}
845837
}
846838

839+
TEST(RNTupleEvolution, RemovedSecondBaseClass)
840+
{
841+
FileRaii fileGuard("test_ntuple_evolution_removed_second_base_class.root");
842+
843+
ExecInFork([&] {
844+
// The child process writes the file and exits, but the file must be preserved to be read by the parent.
845+
fileGuard.PreserveFile();
846+
847+
ASSERT_TRUE(gInterpreter->Declare(R"(
848+
struct RemovedSecondBaseFirst {
849+
int fFirstBase = 1;
850+
};
851+
struct RemovedSecondBaseSecond {
852+
int fSecondBase = 2;
853+
};
854+
struct RemovedSecondBaseDerived : public RemovedSecondBaseFirst, public RemovedSecondBaseSecond {
855+
int fDerived = 3;
856+
};
857+
)"));
858+
859+
auto model = RNTupleModel::Create();
860+
model->AddField(RFieldBase::Create("f", "RemovedSecondBaseDerived").Unwrap());
861+
862+
auto writer = RNTupleWriter::Recreate(std::move(model), "ntpl", fileGuard.GetPath());
863+
writer->Fill();
864+
865+
// Reset / close the writer and flush the file.
866+
{
867+
// TStreamerInfo::Build will report a warning for interpreted classes (but only for base classes).
868+
// See also https://github.com/root-project/root/issues/9371
869+
ROOT::TestSupport::CheckDiagsRAII diagRAII;
870+
diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "has no streamer or dictionary",
871+
/*matchFullMessage=*/false);
872+
writer.reset();
873+
}
874+
});
875+
876+
ASSERT_TRUE(gInterpreter->Declare(R"(
877+
struct RemovedSecondBaseFirst {
878+
int fFirstBase = 1;
879+
};
880+
struct RemovedSecondBaseDerived : public RemovedSecondBaseFirst {
881+
int fDerived = 3;
882+
};
883+
)"));
884+
885+
auto reader = RNTupleReader::Open("ntpl", fileGuard.GetPath());
886+
try {
887+
reader->GetModel();
888+
FAIL() << "model reconstruction should fail";
889+
} catch (const ROOT::RException &err) {
890+
EXPECT_THAT(err.what(), testing::HasSubstr("incompatible number of base classes"));
891+
}
892+
}
893+
847894
TEST(RNTupleEvolution, RenamedBaseClass)
848895
{
849896
// RNTuple currently does not support automatic schema evolution when a class is renamed.

0 commit comments

Comments
 (0)