Skip to content

[ntuple] fix schema evolution with streamer fields #18451

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

Open
wants to merge 7 commits 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
46 changes: 46 additions & 0 deletions roottest/root/ntuple/streamerfield/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
ROOTTEST_GENERATE_DICTIONARY(
event_v2_dict
${CMAKE_CURRENT_SOURCE_DIR}/Event_v2.hxx
LINKDEF ${CMAKE_CURRENT_SOURCE_DIR}/Event_v2_LinkDef.h
NO_ROOTMAP NO_CXXMODULE
FIXTURES_SETUP generated_event_v2_dictionary
)

ROOTTEST_GENERATE_EXECUTABLE(
write_event
LIBRARIES Core RIO ROOTNTuple
FIXTURES_REQUIRED generated_event_v2_dictionary
FIXTURES_SETUP write_event_excutable)

target_sources(
write_event
PRIVATE write_event.cxx event_v2_dict.cxx
)

ROOTTEST_ADD_TEST(write_event
EXEC ./write_event
FIXTURES_REQUIRED write_event_excutable
FIXTURES_SETUP written_event)

ROOTTEST_GENERATE_DICTIONARY(
event_v3_dict
${CMAKE_CURRENT_SOURCE_DIR}/Event_v3.hxx
LINKDEF ${CMAKE_CURRENT_SOURCE_DIR}/Event_v3_LinkDef.h
NO_ROOTMAP NO_CXXMODULE
FIXTURES_SETUP generated_event_v3_dictionary
)

ROOTTEST_GENERATE_EXECUTABLE(
read_event
LIBRARIES Core RIO ROOTNTuple
FIXTURES_REQUIRED generated_event_v3_dictionary
FIXTURES_SETUP read_event_executable)

target_sources(
read_event
PRIVATE read_event.cxx event_v3_dict.cxx
)

ROOTTEST_ADD_TEST(read_event
EXEC ./read_event
FIXTURES_REQUIRED read_event_executable written_event)
36 changes: 36 additions & 0 deletions roottest/root/ntuple/streamerfield/Event_v2.hxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#ifndef EVENT_V2_H
#define EVENT_V2_H

#include <RtypesCore.h>

#include <memory>

struct StreamerBase {
int fBase = 0;
virtual ~StreamerBase() = default;

ClassDef(StreamerBase, 2)
};

struct StreamerDerived : public StreamerBase {
int fFirst = 1;
int fSecond = 2;
virtual ~StreamerDerived() = default;

ClassDef(StreamerDerived, 2)
};

struct StreamerContainer {
std::unique_ptr<StreamerBase> fPtr;

ClassDefNV(StreamerContainer, 2)
};

struct Event {
int fX = 137;
StreamerContainer fField;

ClassDefNV(Event, 2)
};

#endif // EVENT_V2_H
8 changes: 8 additions & 0 deletions roottest/root/ntuple/streamerfield/Event_v2_LinkDef.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#ifdef __ROOTCLING__

#pragma link C++ class StreamerBase+;
#pragma link C++ class StreamerDerived+;
#pragma link C++ options = rntupleStreamerMode(true) class StreamerContainer+;
#pragma link C++ class Event+;

#endif
36 changes: 36 additions & 0 deletions roottest/root/ntuple/streamerfield/Event_v3.hxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#ifndef EVENT_V3_H
#define EVENT_V3_H

#include <RtypesCore.h>

#include <memory>

struct StreamerBase {
int fBase = 0;
virtual ~StreamerBase() = default;

ClassDef(StreamerBase, 2)
};

struct StreamerDerived : public StreamerBase {
int fSecond = 2;
int fFirst = 1;
virtual ~StreamerDerived() = default;

ClassDef(StreamerDerived, 3)
};

struct StreamerContainer {
std::unique_ptr<StreamerBase> fPtr;

ClassDefNV(StreamerContainer, 2)
};

struct Event {
StreamerContainer fField;
int fY = 42;

ClassDefNV(Event, 3)
};

#endif // EVENT_V3_H
12 changes: 12 additions & 0 deletions roottest/root/ntuple/streamerfield/Event_v3_LinkDef.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#ifdef __ROOTCLING__

#pragma link C++ class StreamerBase+;
#pragma link C++ class StreamerDerived+;
#pragma link C++ options = rntupleStreamerMode(true) class StreamerContainer+;
#pragma link C++ class Event+;

#pragma read sourceClass = "StreamerBase" source = "int fBase;" version = "[2]" \
targetClass = "StreamerBase" target = "fBase" \
code = "{ fBase = onfile.fBase + 1000; }"

#endif
34 changes: 34 additions & 0 deletions roottest/root/ntuple/streamerfield/read_event.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include <ROOT/RNTupleReader.hxx>

#include "Event_v3.hxx"

#include <cstdio>

int main()
{
auto reader = ROOT::RNTupleReader::Open("ntpl", "root_test_streamerfield.root");

auto ptrEvent = reader->GetModel().GetDefaultEntry().GetPtr<Event>("event");

reader->LoadEntry(0);

if (!dynamic_cast<const ROOT::RStreamerField *>(&reader->GetModel().GetConstField("event.fField")))
return 10;

if (!ptrEvent->fField.fPtr)
return 1;
auto derived = dynamic_cast<StreamerDerived *>(ptrEvent->fField.fPtr.get());
if (!derived)
return 2;
if (derived->fBase != 1000)
return 3;
if (derived->fFirst != 1)
return 4;
if (derived->fSecond != 2)
return 5;
if (ptrEvent->fY != 42)
return 6;

std::remove("root_test_streamerfield.root");
return 0;
}
18 changes: 18 additions & 0 deletions roottest/root/ntuple/streamerfield/write_event.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include <ROOT/RNTupleModel.hxx>
#include <ROOT/RNTupleWriter.hxx>

#include "Event_v2.hxx"

#include <memory>

int main()
{
auto model = ROOT::RNTupleModel::Create();
auto field = model->MakeField<Event>("event");
auto writer = ROOT::RNTupleWriter::Recreate(std::move(model), "ntpl", "root_test_streamerfield.root");

field->fField.fPtr = std::unique_ptr<StreamerBase>(new StreamerDerived());
writer->Fill();

return 0;
}
2 changes: 2 additions & 0 deletions tree/ntuple/inc/ROOT/RField.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ protected:
// Returns the list of seen streamer infos
ROOT::RExtraTypeInfoDescriptor GetExtraTypeInfo() const final;

void BeforeConnectPageSource(ROOT::Internal::RPageSource &pageSource) final;

public:
RStreamerField(std::string_view fieldName, std::string_view className, std::string_view typeAlias = "");
RStreamerField(RStreamerField &&other) = default;
Expand Down
5 changes: 5 additions & 0 deletions tree/ntuple/inc/ROOT/RPageStorage.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ private:
REntryRange fEntryRange; ///< Used by the cluster pool to prevent reading beyond the given range
bool fHasStructure = false; ///< Set to true once `LoadStructure()` is called
bool fIsAttached = false; ///< Set to true once `Attach()` is called
bool fHasStreamerInfosRegistered = false; ///< Set to true when RegisterStreamerInfos() is called.

/// Remembers the last cluster id from which a page was requested
ROOT::DescriptorId_t fLastUsedCluster = ROOT::kInvalidDescriptorId;
Expand Down Expand Up @@ -817,6 +818,10 @@ public:
// TODO(gparolini): for symmetry with SealPage(), we should either make this private or SealPage() public.
RResult<ROOT::Internal::RPage>
UnsealPage(const RSealedPage &sealedPage, const ROOT::Internal::RColumnElementBase &element);

/// Builds the streamer info records from the descriptor's extra type info section. This is necessary when
/// connecting streamer fields so that emulated classes can be read.
void RegisterStreamerInfos();
}; // class RPageSource

} // namespace Internal
Expand Down
5 changes: 5 additions & 0 deletions tree/ntuple/src/RFieldMeta.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,11 @@ ROOT::RStreamerField::RStreamerField(std::string_view fieldName, TClass *classp)
fTraits |= kTraitTriviallyDestructible;
}

void ROOT::RStreamerField::BeforeConnectPageSource(ROOT::Internal::RPageSource &pageSource)
{
pageSource.RegisterStreamerInfos();
}

std::unique_ptr<ROOT::RFieldBase> ROOT::RStreamerField::CloneImpl(std::string_view newName) const
{
return std::unique_ptr<RStreamerField>(new RStreamerField(newName, GetTypeName(), GetTypeAlias()));
Expand Down
3 changes: 1 addition & 2 deletions tree/ntuple/src/RNTupleSerialize.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -2146,13 +2146,12 @@ ROOT::Internal::RNTupleSerializer::DeserializeStreamerInfos(const std::string &e
TBufferFile buffer(TBuffer::kRead, extraTypeInfoContent.length(), const_cast<char *>(extraTypeInfoContent.data()),
false /* adopt */);
auto infoList = reinterpret_cast<TList *>(buffer.ReadObject(TList::Class()));
infoList->SetOwner(); // delete the TStreamerInfo items of the list

TObjLink *lnk = infoList->FirstLink();
while (lnk) {
auto info = reinterpret_cast<TStreamerInfo *>(lnk->GetObject());
info->BuildCheck();
infoMap[info->GetNumber()] = info->GetClass()->GetStreamerInfo();
infoMap[info->GetNumber()] = info->GetClass()->GetStreamerInfo(info->GetClassVersion());
assert(info->GetNumber() == infoMap[info->GetNumber()]->GetNumber());
lnk = lnk->Next();
}
Expand Down
16 changes: 16 additions & 0 deletions tree/ntuple/src/RPageStorage.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,22 @@ ROOT::RResult<ROOT::Internal::RPage> ROOT::Internal::RPageSource::UnsealPage(con
return page;
}

void ROOT::Internal::RPageSource::RegisterStreamerInfos()
{
if (fHasStreamerInfosRegistered)
return;

for (const auto &extraTypeInfo : fDescriptor.GetExtraTypeInfoIterable()) {
if (extraTypeInfo.GetContentId() != EExtraTypeInfoIds::kStreamerInfo)
continue;
// We don't need the result, it's enough that during deserialization, BuildCheck() is called for every
// streamer info record.
RNTupleSerializer::DeserializeStreamerInfos(extraTypeInfo.GetContent()).Unwrap();
}

fHasStreamerInfosRegistered = true;
}

//------------------------------------------------------------------------------

bool ROOT::Internal::RWritePageMemoryManager::RColumnInfo::operator>(const RColumnInfo &other) const
Expand Down
18 changes: 18 additions & 0 deletions tree/ntuple/test/StreamerField.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,22 @@ struct PolyContainer {
std::unique_ptr<PolyBase> fPoly;
};

template <typename T>
struct OldStreamerName {
T fValue;
};

template <typename T>
struct NewStreamerName {
T fValue;
};

struct TemperatureCelsius {
float fValue;
};

struct TemperatureKelvin {
float fValue;
};

#endif // ROOT_RNTuple_Test_StreamerField
7 changes: 7 additions & 0 deletions tree/ntuple/test/StreamerFieldLinkDef.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@
#pragma link C++ class PolyB + ;
#pragma link C++ options = rntupleStreamerMode(true) class PolyContainer + ;

#pragma link C++ options = rntupleStreamerMode(true), version(3) class OldStreamerName < int> + ;
#pragma link C++ options = rntupleStreamerMode(true), version(3) class NewStreamerName < int> + ;
#pragma read sourceClass = "OldStreamerName<int>" targetClass = "NewStreamerName<int>" version = "[3]"

#pragma link C++ options = rntupleStreamerMode(true) class TemperatureCelsius + ;
#pragma link C++ options = rntupleStreamerMode(true) class TemperatureKelvin + ;

#endif
57 changes: 57 additions & 0 deletions tree/ntuple/test/ntuple_evolution.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -863,3 +863,60 @@ struct RenamedIntermediateDerived : public RenamedIntermediate2 {
EXPECT_THAT(err.what(), testing::HasSubstr("incompatible type name for field"));
}
}

TEST(RNTupleEvolution, StreamerField)
{
FileRaii fileGuard("test_ntuple_evolution_streamer_field.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 StreamerField {
int fInt = 1;
int fAnotherInt = 3;

ClassDefNV(StreamerField, 2)
};
)"));

auto model = RNTupleModel::Create();
model->AddField(std::make_unique<ROOT::RStreamerField>("f", "StreamerField"));

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

void *ptr = writer->GetModel().GetDefaultEntry().GetPtr<void>("f").get();
DeclarePointer("StreamerField", "ptrStreamerField", ptr);
ProcessLine("ptrStreamerField->fInt = 2;");
writer->Fill();

// Reset / close the writer and flush the file.
writer.reset();
});

ASSERT_TRUE(gInterpreter->Declare(R"(
struct StreamerField {
int fInt = 0;
int fAdded = 137;
Comment on lines +901 to +902
Copy link
Member

Choose a reason for hiding this comment

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

To test the indirect collection of StreamerInfo, there should be here a pointer to another class (possibly initialized with an instance of a derived class).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we already test that with

TEST(RField, StreamerPoly)

Copy link
Member

Choose a reason for hiding this comment

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

Technically the test linked will succeed whether or not the StreamerInfo are stored ... since they are still in memory from the already loaded dictionary.

Copy link
Member

Choose a reason for hiding this comment

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

i.e. like here to test the feature, one need 2 separate processes and 2 distinct schemas/versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I left the unit tests unchanged but I added this as an integration test.

// removed fAnotherInt

ClassDefNV(StreamerField, 3)
};
)"));

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

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

reader->LoadEntry(0);
EXPECT_EVALUATE_EQ("ptrStreamerField->fInt", 1);
EXPECT_EVALUATE_EQ("ptrStreamerField->fAdded", 137);

reader->LoadEntry(1);
EXPECT_EVALUATE_EQ("ptrStreamerField->fInt", 2);
EXPECT_EVALUATE_EQ("ptrStreamerField->fAdded", 137);
}
Loading
Loading