diff --git a/tree/dataframe/src/RDFSnapshotHelpers.cxx b/tree/dataframe/src/RDFSnapshotHelpers.cxx index ec06e521cb935..ce5c339a0511e 100644 --- a/tree/dataframe/src/RDFSnapshotHelpers.cxx +++ b/tree/dataframe/src/RDFSnapshotHelpers.cxx @@ -912,7 +912,18 @@ void ROOT::Internal::RDF::UntypedSnapshotRNTupleHelper::Initialize() ? ROOT::Internal::RDF::GetTypeNameWithOpts(*fInputLoopManager->GetDataSource(), fInputFieldNames[i], fOptions.fVector2RVec) : ROOT::Internal::RDF::TypeID2TypeName(*fInputColumnTypeIDs[i]); - model->AddField(ROOT::RFieldBase::Create(fOutputFieldNames[i], typeName).Unwrap()); + + // Cardinality fields are read-only, so instead we snapshot them as their inner type. + if (typeName.substr(0, 25) == "ROOT::RNTupleCardinality<") { + // Get "T" from "ROOT::RNTupleCardinality". + std::string cardinalityType = typeName.substr(25, typeName.size() - 26); + Warning("Snapshot", + "Column \"%s\" is a read-only \"%s\" column. It will be snapshot as its inner type \"%s\" instead.", + fInputFieldNames[i].c_str(), typeName.c_str(), cardinalityType.c_str()); + model->AddField(ROOT::RFieldBase::Create(fOutputFieldNames[i], cardinalityType).Unwrap()); + } else { + model->AddField(ROOT::RFieldBase::Create(fOutputFieldNames[i], typeName).Unwrap()); + } fFieldTokens[i] = model->GetToken(fOutputFieldNames[i]); } model->Freeze(); diff --git a/tree/dataframe/test/dataframe_snapshot_ntuple.cxx b/tree/dataframe/test/dataframe_snapshot_ntuple.cxx index 69fad452f0165..0d5fdd6acf7ed 100644 --- a/tree/dataframe/test/dataframe_snapshot_ntuple.cxx +++ b/tree/dataframe/test/dataframe_snapshot_ntuple.cxx @@ -538,6 +538,40 @@ TEST(RDFSnapshotRNTuple, TDirectory) EXPECT_EQ(expected, sdf.GetColumnNames()); } +TEST(RDFSnapshotRNTuple, CardinalityColumns) +{ + FileRAII fileGuard{"RDFSnapshotRNTuple_cardinality_columns.root"}; + + { + auto model = ROOT::RNTupleModel::Create(); + + model->MakeField>("electron"); + + auto cardinalityFld = std::make_unique>>("nElectrons"); + model->AddProjectedField(std::move(cardinalityFld), [](const std::string &) { return "electron"; }); + + auto writer = ROOT::RNTupleWriter::Recreate(std::move(model), "ntuple", fileGuard.GetPath()); + auto electron = writer->GetModel().GetDefaultEntry().GetPtr>("electron"); + + for (unsigned i = 0; i < 5; ++i) { + *electron = {Electron{1.f * i}, Electron{2.f * i}, Electron{3.f * i}}; + writer->Fill(); + } + } + + ROOT::RDF::RSnapshotOptions opts; + opts.fMode = "UPDATE"; + opts.fOutputFormat = ROOT::RDF::ESnapshotOutputFormat::kRNTuple; + ROOT::RDataFrame df("ntuple", fileGuard.GetPath()); + + ROOT_EXPECT_WARNING(df.Snapshot("ntuple_snap", fileGuard.GetPath(), "", opts), "Snapshot", + "Column \"nElectrons\" is a read-only \"ROOT::RNTupleCardinality\" column. It " + "will be snapshot as its inner type \"std::uint32_t\" instead."); + + ROOT::RDataFrame sdf("ntuple_snap", fileGuard.GetPath()); + EXPECT_EQ("std::uint32_t", sdf.GetColumnType("nElectrons")); +} + class RDFSnapshotRNTupleFromTTreeTest : public ::testing::Test { protected: const std::string fFileName = "RDFSnapshotRNTuple_ttree_fixture.root";