Skip to content

Commit 8c5860e

Browse files
committed
[df] Lift template requirement for RNTuple Snapshot
A new action helper is introduced to allow calling Snapshot without template arguments and without the need for JITting to write RNTuple data to disk. Unlike TTree, RNTuple is actually able to write certain types to disk even without explicit dictionary information or RTTI. Notably, all the class of composed types such as `std::vector<std::vector<...>>` and similar, for which RField is able to create recursively the inner fields if all the components are known. This required a slight modification in the logic. A new tag type `UseNativeDataType` is used in contexts that require `std::type_info` such as `GetColumnReaders` to signal that we let the RNTuple data source figure out the type. The tag is also used in the new action helper to retrieve the correct type name to crete the RField with.
1 parent 6a2d50b commit 8c5860e

File tree

8 files changed

+255
-45
lines changed

8 files changed

+255
-45
lines changed

tree/dataframe/inc/ROOT/RDF/ActionHelpers.hxx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2247,6 +2247,57 @@ public:
22472247
}
22482248
};
22492249

2250+
class R__CLING_PTRCHECK(off) UntypedSnapshotRNTupleHelper final : public RActionImpl<UntypedSnapshotRNTupleHelper> {
2251+
std::string fFileName;
2252+
std::string fDirName;
2253+
std::string fNTupleName;
2254+
2255+
std::unique_ptr<TFile> fOutputFile{nullptr};
2256+
2257+
RSnapshotOptions fOptions;
2258+
ROOT::Detail::RDF::RLoopManager *fInputLoopManager;
2259+
ROOT::Detail::RDF::RLoopManager *fOutputLoopManager;
2260+
ColumnNames_t fInputFieldNames; // This contains the resolved aliases
2261+
ColumnNames_t fOutputFieldNames;
2262+
std::unique_ptr<ROOT::RNTupleWriter> fWriter{nullptr};
2263+
2264+
ROOT::REntry *fOutputEntry;
2265+
2266+
std::vector<bool> fIsDefine;
2267+
2268+
std::vector<const std::type_info *> fInputColumnTypeIDs; // Types for the input columns
2269+
2270+
public:
2271+
UntypedSnapshotRNTupleHelper(std::string_view filename, std::string_view dirname, std::string_view ntuplename,
2272+
const ColumnNames_t &vfnames, const ColumnNames_t &fnames,
2273+
const RSnapshotOptions &options, ROOT::Detail::RDF::RLoopManager *inputLM,
2274+
ROOT::Detail::RDF::RLoopManager *outputLM, std::vector<bool> &&isDefine,
2275+
const std::vector<const std::type_info *> &colTypeIDs);
2276+
2277+
UntypedSnapshotRNTupleHelper(const UntypedSnapshotRNTupleHelper &) = delete;
2278+
UntypedSnapshotRNTupleHelper &operator=(const UntypedSnapshotRNTupleHelper &) = delete;
2279+
UntypedSnapshotRNTupleHelper(UntypedSnapshotRNTupleHelper &&) = default;
2280+
UntypedSnapshotRNTupleHelper &operator=(UntypedSnapshotRNTupleHelper &&) = default;
2281+
~UntypedSnapshotRNTupleHelper() final;
2282+
2283+
void InitTask(TTreeReader *, unsigned int /* slot */) {}
2284+
2285+
void Exec(unsigned int /* slot */, const std::vector<void *> &values);
2286+
2287+
void Initialize();
2288+
2289+
void Finalize();
2290+
2291+
std::string GetActionName() { return "Snapshot"; }
2292+
2293+
ROOT::RDF::SampleCallback_t GetSampleCallback() final
2294+
{
2295+
return [](unsigned int, const RSampleInfo &) mutable {};
2296+
}
2297+
2298+
UntypedSnapshotRNTupleHelper MakeNew(void *newName);
2299+
};
2300+
22502301
class R__CLING_PTRCHECK(off) UntypedSnapshotTTreeHelper final : public RActionImpl<UntypedSnapshotTTreeHelper> {
22512302
std::string fFileName;
22522303
std::string fDirName;

tree/dataframe/inc/ROOT/RDF/InterfaceUtils.hxx

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ BuildAction(const ColumnNames_t &colNames, const std::shared_ptr<SnapshotHelperA
345345
const auto &treename = snapHelperArgs->fTreeName;
346346
const auto &outputColNames = snapHelperArgs->fOutputColNames;
347347
const auto &options = snapHelperArgs->fOptions;
348-
const auto &lmPtr = snapHelperArgs->fOutputLoopManager;
348+
const auto &outputLM = snapHelperArgs->fOutputLoopManager;
349349
const auto &inputLM = snapHelperArgs->fInputLoopManager;
350350

351351
auto sz = colNames.size();
@@ -354,21 +354,36 @@ BuildAction(const ColumnNames_t &colNames, const std::shared_ptr<SnapshotHelperA
354354
isDefine[i] = colRegister.IsDefineOrAlias(colNames[i]);
355355

356356
std::unique_ptr<RActionBase> actionPtr;
357+
if (snapHelperArgs->fToNTuple) {
358+
if (!ROOT::IsImplicitMTEnabled()) {
359+
// single-thread snapshot
360+
using Helper_t = UntypedSnapshotRNTupleHelper;
361+
using Action_t = RActionSnapshot<Helper_t, PrevNodeType>;
357362

358-
if (!ROOT::IsImplicitMTEnabled()) {
359-
// single-thread snapshot
360-
using Helper_t = UntypedSnapshotTTreeHelper;
361-
using Action_t = RActionSnapshot<Helper_t, PrevNodeType>;
362-
actionPtr.reset(new Action_t(Helper_t(filename, dirname, treename, colNames, outputColNames, options,
363-
std::move(isDefine), lmPtr, inputLM, colTypeIDs),
364-
colNames, colTypeIDs, prevNode, colRegister));
363+
actionPtr.reset(new Action_t(Helper_t(filename, dirname, treename, colNames, outputColNames, options, inputLM,
364+
outputLM, std::move(isDefine), colTypeIDs),
365+
colNames, colTypeIDs, prevNode, colRegister));
366+
} else {
367+
// multi-thread snapshot to RNTuple is not yet supported
368+
// TODO(fdegeus) Add MT snapshotting
369+
throw std::runtime_error("Snapshot: Snapshotting to RNTuple with IMT enabled is not supported yet.");
370+
}
365371
} else {
366-
// multi-thread snapshot
367-
using Helper_t = UntypedSnapshotTTreeHelperMT;
368-
using Action_t = RActionSnapshot<Helper_t, PrevNodeType>;
369-
actionPtr.reset(new Action_t(Helper_t(nSlots, filename, dirname, treename, colNames, outputColNames, options,
370-
std::move(isDefine), lmPtr, inputLM, colTypeIDs),
371-
colNames, colTypeIDs, prevNode, colRegister));
372+
if (!ROOT::IsImplicitMTEnabled()) {
373+
// single-thread snapshot
374+
using Helper_t = UntypedSnapshotTTreeHelper;
375+
using Action_t = RActionSnapshot<Helper_t, PrevNodeType>;
376+
actionPtr.reset(new Action_t(Helper_t(filename, dirname, treename, colNames, outputColNames, options,
377+
std::move(isDefine), outputLM, inputLM, colTypeIDs),
378+
colNames, colTypeIDs, prevNode, colRegister));
379+
} else {
380+
// multi-thread snapshot
381+
using Helper_t = UntypedSnapshotTTreeHelperMT;
382+
using Action_t = RActionSnapshot<Helper_t, PrevNodeType>;
383+
actionPtr.reset(new Action_t(Helper_t(nSlots, filename, dirname, treename, colNames, outputColNames, options,
384+
std::move(isDefine), outputLM, inputLM, colTypeIDs),
385+
colNames, colTypeIDs, prevNode, colRegister));
386+
}
372387
}
373388

374389
return actionPtr;

tree/dataframe/inc/ROOT/RDF/RInterface.hxx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,10 +1344,14 @@ public:
13441344

13451345
RResultPtr<RInterface<RLoopManager>> resPtr;
13461346

1347-
auto retrieveTypeID = [](const std::string &colName, const std::string &colTypeName) -> const std::type_info * {
1347+
auto retrieveTypeID = [](const std::string &colName, const std::string &colTypeName,
1348+
bool isRNTuple = false) -> const std::type_info * {
13481349
try {
13491350
return &ROOT::Internal::RDF::TypeName2TypeID(colTypeName);
13501351
} catch (const std::runtime_error &err) {
1352+
if (isRNTuple)
1353+
return &typeid(ROOT::Internal::RDF::UseNativeDataType);
1354+
13511355
if (std::string(err.what()).find("Cannot extract type_info of type") != std::string::npos) {
13521356
// We could not find RTTI for this column, thus we cannot write it out at the moment.
13531357
std::string trueTypeName{colTypeName};
@@ -1380,12 +1384,26 @@ public:
13801384
std::string(filename), std::string(dirname), std::string(treename), colListWithAliasesAndSizeBranches,
13811385
options, newRDF->GetLoopManager(), GetLoopManager(), true /* fToNTuple */});
13821386

1383-
// The Snapshot helper will use colListNoAliasesWithSizeBranches (with aliases resolved) as input columns, and
1384-
// colListWithAliasesAndSizeBranches (still with aliases in it, passed through snapHelperArgs) as output column
1385-
// names.
1386-
resPtr = CreateAction<RDFInternal::ActionTags::Snapshot, RDFDetail::RInferredType>(
1387-
colListNoAliasesWithSizeBranches, newRDF, snapHelperArgs, fProxiedPtr,
1388-
colListNoAliasesWithSizeBranches.size());
1387+
auto &&nColumns = colListNoAliasesWithSizeBranches.size();
1388+
const auto validColumnNames = GetValidatedColumnNames(nColumns, colListNoAliasesWithSizeBranches);
1389+
1390+
const auto nSlots = fLoopManager->GetNSlots();
1391+
std::vector<const std::type_info *> colTypeIDs;
1392+
colTypeIDs.reserve(nColumns);
1393+
for (decltype(nColumns) i{}; i < nColumns; i++) {
1394+
const auto &colName = validColumnNames[i];
1395+
const auto colTypeName = ROOT::Internal::RDF::ColumnName2ColumnTypeName(
1396+
colName, /*tree*/ nullptr, GetDataSource(), fColRegister.GetDefine(colName), options.fVector2RVec);
1397+
const std::type_info *colTypeID = retrieveTypeID(colName, colTypeName, /*isRNTuple*/ true);
1398+
colTypeIDs.push_back(colTypeID);
1399+
}
1400+
// Crucial e.g. if the column names do not correspond to already-available column readers created by the data
1401+
// source
1402+
CheckAndFillDSColumns(validColumnNames, colTypeIDs);
1403+
1404+
auto action =
1405+
RDFInternal::BuildAction(validColumnNames, snapHelperArgs, nSlots, fProxiedPtr, fColRegister, colTypeIDs);
1406+
resPtr = MakeResultPtr(newRDF, *GetLoopManager(), std::move(action));
13891407
} else {
13901408
if (RDFInternal::GetDataSourceLabel(*this) == "RNTupleDS" &&
13911409
options.fOutputFormat == ESnapshotOutputFormat::kDefault) {

tree/dataframe/inc/ROOT/RDF/Utils.hxx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,13 @@ auto MakeAliasedSharedPtr(T *rawPtr)
336336
*/
337337
ROOT::RDF::Experimental::RDatasetSpec RetrieveSpecFromJson(const std::string &jsonFile);
338338

339+
/**
340+
* Tag to let data sources use the native data type when creating a column reader.
341+
*
342+
* See usage of this in RNTupleDS
343+
*/
344+
struct UseNativeDataType {};
345+
339346
} // end NS RDF
340347
} // end NS Internal
341348
} // end NS ROOT

tree/dataframe/inc/ROOT/RNTupleDS.hxx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ class RNTupleDS final : public ROOT::RDF::RDataSource {
170170

171171
explicit RNTupleDS(std::unique_ptr<ROOT::Internal::RPageSource> pageSource);
172172

173+
ROOT::RFieldBase *GetFieldWithTypeChecks(std::string_view fieldName, const std::type_info &tid);
174+
173175
public:
174176
RNTupleDS(std::string_view ntupleName, std::string_view fileName);
175177
RNTupleDS(std::string_view ntupleName, const std::vector<std::string> &fileNames);

tree/dataframe/src/RDFActionHelpers.cxx

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,3 +892,101 @@ ROOT::Internal::RDF::UntypedSnapshotTTreeHelperMT::MakeNew(void *newName, std::s
892892
fInputLoopManager,
893893
fInputColumnTypeIDs};
894894
}
895+
896+
ROOT::Internal::RDF::UntypedSnapshotRNTupleHelper::UntypedSnapshotRNTupleHelper(
897+
std::string_view filename, std::string_view dirname, std::string_view ntuplename, const ColumnNames_t &vfnames,
898+
const ColumnNames_t &fnames, const RSnapshotOptions &options, ROOT::Detail::RDF::RLoopManager *inputLM,
899+
ROOT::Detail::RDF::RLoopManager *outputLM, std::vector<bool> &&isDefine,
900+
const std::vector<const std::type_info *> &colTypeIDs)
901+
: fFileName(filename),
902+
fDirName(dirname),
903+
fNTupleName(ntuplename),
904+
fOptions(options),
905+
fInputLoopManager(inputLM),
906+
fOutputLoopManager(outputLM),
907+
fInputFieldNames(vfnames),
908+
fOutputFieldNames(ReplaceDotWithUnderscore(fnames)),
909+
fIsDefine(std::move(isDefine)),
910+
fInputColumnTypeIDs(colTypeIDs)
911+
{
912+
EnsureValidSnapshotRNTupleOutput(fOptions, fNTupleName, fFileName);
913+
}
914+
915+
ROOT::Internal::RDF::UntypedSnapshotRNTupleHelper::~UntypedSnapshotRNTupleHelper()
916+
{
917+
if (!fNTupleName.empty() && !fOutputLoopManager->GetDataSource() && fOptions.fLazy)
918+
Warning("Snapshot", "A lazy Snapshot action was booked but never triggered.");
919+
}
920+
921+
void ROOT::Internal::RDF::UntypedSnapshotRNTupleHelper::Exec(unsigned int /* slot */, const std::vector<void *> &values)
922+
{
923+
assert(values.size() == fOutputFieldNames.size());
924+
for (decltype(values.size()) i = 0; i < values.size(); i++) {
925+
fOutputEntry->BindRawPtr(fOutputFieldNames[i], values[i]);
926+
}
927+
fWriter->Fill();
928+
}
929+
930+
void ROOT::Internal::RDF::UntypedSnapshotRNTupleHelper::Initialize()
931+
{
932+
auto model = ROOT::RNTupleModel::Create();
933+
auto nFields = fOutputFieldNames.size();
934+
for (decltype(nFields) i = 0; i < nFields; i++) {
935+
// Need to retrieve the type of every field to create as a string
936+
// If the input type for a field does not have RTTI, internally we store it as the tag UseNativeDataType. When
937+
// that is detected, we need to ask the data source which is the type name based on the on-disk information.
938+
const auto typeName = *fInputColumnTypeIDs[i] == typeid(ROOT::Internal::RDF::UseNativeDataType)
939+
? ROOT::Internal::RDF::GetTypeNameWithOpts(*fInputLoopManager->GetDataSource(),
940+
fInputFieldNames[i], fOptions.fVector2RVec)
941+
: ROOT::Internal::RDF::TypeID2TypeName(*fInputColumnTypeIDs[i]);
942+
model->AddField(ROOT::RFieldBase::Create(fOutputFieldNames[i], typeName).Unwrap());
943+
}
944+
fOutputEntry = &model->GetDefaultEntry();
945+
946+
ROOT::RNTupleWriteOptions writeOptions;
947+
writeOptions.SetCompression(fOptions.fCompressionAlgorithm, fOptions.fCompressionLevel);
948+
949+
fOutputFile.reset(TFile::Open(fFileName.c_str(), fOptions.fMode.c_str()));
950+
if (!fOutputFile)
951+
throw std::runtime_error("Snapshot: could not create output file " + fFileName);
952+
953+
TDirectory *outputDir = fOutputFile.get();
954+
if (!fDirName.empty()) {
955+
TString checkupdate = fOptions.fMode;
956+
checkupdate.ToLower();
957+
if (checkupdate == "update")
958+
outputDir = fOutputFile->mkdir(fDirName.c_str(), "", true); // do not overwrite existing directory
959+
else
960+
outputDir = fOutputFile->mkdir(fDirName.c_str());
961+
}
962+
963+
fWriter = ROOT::RNTupleWriter::Append(std::move(model), fNTupleName, *outputDir, writeOptions);
964+
}
965+
966+
void ROOT::Internal::RDF::UntypedSnapshotRNTupleHelper::Finalize()
967+
{
968+
fWriter.reset();
969+
// We can now set the data source of the loop manager for the RDataFrame that is returned by the Snapshot call.
970+
fOutputLoopManager->SetDataSource(std::make_unique<ROOT::RDF::RNTupleDS>(fDirName + "/" + fNTupleName, fFileName));
971+
}
972+
973+
/**
974+
* Create a new UntypedSnapshotRNTupleHelper with a different output file name.
975+
*
976+
* \param[in] newName A type-erased string with the output file name
977+
* \return UntypedSnapshotRNTupleHelper
978+
*
979+
* This MakeNew implementation is tied to the cloning feature of actions
980+
* of the computation graph. In particular, cloning a Snapshot node usually
981+
* also involves changing the name of the output file, otherwise the cloned
982+
* Snapshot would overwrite the same file.
983+
*/
984+
ROOT::Internal::RDF::UntypedSnapshotRNTupleHelper
985+
ROOT::Internal::RDF::UntypedSnapshotRNTupleHelper::MakeNew(void *newName)
986+
{
987+
const std::string finalName = *reinterpret_cast<const std::string *>(newName);
988+
return UntypedSnapshotRNTupleHelper{finalName, fDirName, fNTupleName,
989+
fInputFieldNames, fOutputFieldNames, fOptions,
990+
fInputLoopManager, fOutputLoopManager, std::vector<bool>(fIsDefine),
991+
fInputColumnTypeIDs};
992+
}

tree/dataframe/src/RNTupleDS.cxx

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -430,40 +430,59 @@ ROOT::RDF::RNTupleDS::GetColumnReadersImpl(std::string_view /* name */, const st
430430
return {};
431431
}
432432

433-
std::unique_ptr<ROOT::Detail::RDF::RColumnReaderBase>
434-
ROOT::RDF::RNTupleDS::GetColumnReaders(unsigned int slot, std::string_view name, const std::type_info &tid)
433+
ROOT::RFieldBase *ROOT::RDF::RNTupleDS::GetFieldWithTypeChecks(std::string_view fieldName, const std::type_info &tid)
435434
{
436435
// At this point we can assume that `name` will be found in fColumnNames
437-
const auto index = std::distance(fColumnNames.begin(), std::find(fColumnNames.begin(), fColumnNames.end(), name));
436+
const auto index =
437+
std::distance(fColumnNames.begin(), std::find(fColumnNames.begin(), fColumnNames.end(), fieldName));
438+
439+
// A reader was requested but we don't have RTTI for it, this is encoded with the tag UseNativeDataType. We can just
440+
// return the available protofield
441+
if (tid == typeid(ROOT::Internal::RDF::UseNativeDataType)) {
442+
return fProtoFields[index].get();
443+
}
444+
445+
// The user explicitly requested a type
438446
const auto requestedType = ROOT::Internal::GetRenormalizedTypeName(ROOT::Internal::RDF::TypeID2TypeName(tid));
439447

440-
ROOT::RFieldBase *field;
441448
// If the field corresponding to the provided name is not a cardinality column and the requested type is different
442449
// from the proto field that was created when the data source was constructed, we first have to create an
443450
// alternative proto field for the column reader. Otherwise, we can directly use the existing proto field.
444-
if (name.substr(0, 13) != "R_rdf_sizeof_" && requestedType != fColumnTypes[index]) {
451+
if (fieldName.substr(0, 13) != "R_rdf_sizeof_" && requestedType != fColumnTypes[index]) {
445452
auto &altProtoFields = fAlternativeProtoFields[index];
446-
auto altProtoField = std::find_if(altProtoFields.begin(), altProtoFields.end(),
447-
[&requestedType](const std::unique_ptr<ROOT::RFieldBase> &fld) {
448-
return fld->GetTypeName() == requestedType;
449-
});
450-
if (altProtoField != altProtoFields.end()) {
451-
field = altProtoField->get();
452-
} else {
453-
auto newAltProtoFieldOrException = ROOT::RFieldBase::Create(std::string(name), requestedType);
454-
if (!newAltProtoFieldOrException) {
455-
throw std::runtime_error("RNTupleDS: Could not create field with type \"" + requestedType +
456-
"\" for column \"" + std::string(name));
457-
}
458-
auto newAltProtoField = newAltProtoFieldOrException.Unwrap();
459-
newAltProtoField->SetOnDiskId(fProtoFields[index]->GetOnDiskId());
460-
field = newAltProtoField.get();
461-
altProtoFields.emplace_back(std::move(newAltProtoField));
453+
454+
// If we can find the requested type in the registered alternative protofields, return the corresponding field
455+
if (auto altProtoField = std::find_if(altProtoFields.begin(), altProtoFields.end(),
456+
[&requestedType](const std::unique_ptr<ROOT::RFieldBase> &fld) {
457+
return fld->GetTypeName() == requestedType;
458+
});
459+
altProtoField != altProtoFields.end()) {
460+
return altProtoField->get();
462461
}
463-
} else {
464-
field = fProtoFields[index].get();
462+
463+
// Otherwise, create a new protofield and register it in the alternatives before returning
464+
auto newAltProtoFieldOrException = ROOT::RFieldBase::Create(std::string(fieldName), requestedType);
465+
if (!newAltProtoFieldOrException) {
466+
throw std::runtime_error("RNTupleDS: Could not create field with type \"" + requestedType +
467+
"\" for column \"" + std::string(fieldName));
468+
}
469+
auto newAltProtoField = newAltProtoFieldOrException.Unwrap();
470+
newAltProtoField->SetOnDiskId(fProtoFields[index]->GetOnDiskId());
471+
auto *newField = newAltProtoField.get();
472+
altProtoFields.emplace_back(std::move(newAltProtoField));
473+
return newField;
465474
}
466475

476+
// General case: there was a correspondence between the user-requested type and the corresponding column type
477+
return fProtoFields[index].get();
478+
}
479+
480+
std::unique_ptr<ROOT::Detail::RDF::RColumnReaderBase>
481+
ROOT::RDF::RNTupleDS::GetColumnReaders(unsigned int slot, std::string_view name, const std::type_info &tid)
482+
{
483+
ROOT::RFieldBase *field = GetFieldWithTypeChecks(name, tid);
484+
assert(field != nullptr);
485+
467486
// Map the field's and subfields' IDs to qualified names so that we can later connect the fields to
468487
// other page sources from the chain
469488
fFieldId2QualifiedName[field->GetOnDiskId()] = fPrincipalDescriptor.GetQualifiedFieldName(field->GetOnDiskId());

tree/dataframe/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ ROOT_ADD_GTEST(dataframe_snapshot_emptyoutput dataframe_snapshot_emptyoutput.cxx
5959
ROOT_GENERATE_DICTIONARY(DummyDict ${CMAKE_CURRENT_SOURCE_DIR}/DummyHeader.hxx
6060
MODULE dataframe_snapshot_emptyoutput LINKDEF DummyHeaderLinkDef.hxx OPTIONS -inlineInputHeader
6161
DEPENDENCIES ROOTVecOps GenVector)
62-
ROOT_ADD_GTEST(dataframe_snapshot_ntuple dataframe_snapshot_ntuple.cxx LIBRARIES ROOTDataFrame ROOTNTuple)
62+
ROOT_ADD_GTEST(dataframe_snapshot_ntuple dataframe_snapshot_ntuple.cxx LIBRARIES ROOTDataFrame ROOTNTuple NTupleStruct)
6363
endif()
6464

6565
ROOT_ADD_GTEST(dataframe_datasetspec dataframe_datasetspec.cxx LIBRARIES ROOTDataFrame)

0 commit comments

Comments
 (0)