Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
RNTupleMerger.cxx
Go to the documentation of this file.
1/// \file RNTupleMerger.cxx
2/// \author Jakob Blomer <jblomer@cern.ch>, Max Orok <maxwellorok@gmail.com>, Alaettin Serhan Mete <amete@anl.gov>,
3/// Giacomo Parolini <giacomo.parolini@cern.ch>
4/// \date 2020-07-08
5/// \warning This is part of the ROOT 7 prototype! It will
6/// change without notice. It might trigger earthquakes. Feedback is welcome!
7
8/*************************************************************************
9 * Copyright (C) 1995-2020, Rene Brun and Fons Rademakers. *
10 * All rights reserved. *
11 * *
12 * For the licensing terms see $ROOTSYS/LICENSE. *
13 * For the list of contributors see $ROOTSYS/README/CREDITS. *
14 *************************************************************************/
15
16#include <ROOT/RError.hxx>
17#include <ROOT/RNTuple.hxx>
20#include <ROOT/RNTupleModel.hxx>
21#include <ROOT/RNTupleTypes.hxx>
22#include <ROOT/RNTupleUtils.hxx>
25#include <ROOT/RPageStorage.hxx>
26#include <ROOT/RClusterPool.hxx>
28#include <ROOT/RNTupleZip.hxx>
31#include <TROOT.h>
32#include <TFileMergeInfo.h>
33#include <TFile.h>
34#include <TKey.h>
35
36#include <algorithm>
37#include <deque>
38#include <initializer_list>
39#include <unordered_map>
40#include <vector>
41
52
53using namespace ROOT::Internal;
54
56{
57 static ROOT::RLogChannel sLog("ROOT.NTuple.Merge");
58 return sLog;
59}
60
61// TFile options parsing
62// -------------------------------------------------------------------------------------
63static bool BeginsWithDelimitedWord(const TString &str, const char *word)
64{
65 const Ssiz_t wordLen = strlen(word);
66 if (str.Length() < wordLen)
67 return false;
68 if (!str.BeginsWith(word, TString::ECaseCompare::kIgnoreCase))
69 return false;
70 return str.Length() == wordLen || str(wordLen) == ' ';
71}
72
73template <typename T>
74static std::optional<T> ParseStringOption(const TString &opts, const char *pattern,
75 std::initializer_list<std::pair<const char *, T>> validValues)
76{
77 const Ssiz_t patternLen = strlen(pattern);
78 assert(pattern[patternLen - 1] == '='); // we want to parse options with the format `option=Value`
79 if (auto idx = opts.Index(pattern, 0, TString::ECaseCompare::kIgnoreCase);
80 idx >= 0 && opts.Length() > idx + patternLen) {
81 auto sub = TString(opts(idx + patternLen, opts.Length() - idx - patternLen));
82 for (const auto &[name, value] : validValues) {
83 if (BeginsWithDelimitedWord(sub, name)) {
84 return value;
85 }
86 }
87 }
88 return std::nullopt;
89}
90
91static std::optional<ENTupleMergingMode> ParseOptionMergingMode(const TString &opts)
92{
93 return ParseStringOption<ENTupleMergingMode>(opts, "rntuple.MergingMode=",
94 {
95 {"Filter", ENTupleMergingMode::kFilter},
96 {"Union", ENTupleMergingMode::kUnion},
97 {"Strict", ENTupleMergingMode::kStrict},
98 });
99}
100
101static std::optional<ENTupleMergeErrBehavior> ParseOptionErrBehavior(const TString &opts)
102{
103 return ParseStringOption<ENTupleMergeErrBehavior>(opts, "rntuple.ErrBehavior=",
104 {
105 {"Abort", ENTupleMergeErrBehavior::kAbort},
106 {"Skip", ENTupleMergeErrBehavior::kSkip},
107 });
108}
109
110static std::optional<ENTupleMergeVersionBehavior> ParseOptionVersionBehavior(const TString &opts)
111{
112 return ParseStringOption<ENTupleMergeVersionBehavior>(
113 opts, "rntuple.VersionBehavior=",
114 {
115 {"WarnOnHigherVersion", ENTupleMergeVersionBehavior::kWarnOnHigherVersion},
116 {"AbortOnHigherVersion", ENTupleMergeVersionBehavior::kAbortOnHigherVersion},
117 });
118}
119// -------------------------------------------------------------------------------------
120
121// Entry point for TFileMerger. Internally calls RNTupleMerger::Merge().
123// IMPORTANT: this function must not throw, as it is used in exception-unsafe code (TFileMerger).
124try {
125 // Check the inputs
126 if (!inputs || inputs->GetEntries() < 3 || !mergeInfo) {
127 R__LOG_ERROR(NTupleMergeLog()) << "Invalid inputs.";
128 return -1;
129 }
130
131 // Parse the input parameters
132 TIter itr(inputs);
133
134 // First entry is the RNTuple name
135 std::string ntupleName = std::string(itr()->GetName());
136
137 // Second entry is the output file
138 TObject *secondArg = itr();
139 TFile *outFile = dynamic_cast<TFile *>(secondArg);
140 if (!outFile) {
141 R__LOG_ERROR(NTupleMergeLog()) << "Second input parameter should be a TFile, but it's a "
142 << secondArg->ClassName() << ".";
143 return -1;
144 }
145
146 // Check if the output file already has a key with that name
147 TKey *outKey = outFile->FindKey(ntupleName.c_str());
148 ROOT::RNTuple *outNTuple = nullptr;
149 if (outKey) {
150 outNTuple = outKey->ReadObject<ROOT::RNTuple>();
151 if (!outNTuple) {
152 R__LOG_ERROR(NTupleMergeLog()) << "Output file already has key, but not of type RNTuple!";
153 return -1;
154 }
155 // In principle, we should already be working on the RNTuple object from the output file, but just continue with
156 // pointer we just got.
157 }
158
159 const bool defaultComp = mergeInfo->fOptions.Contains("DefaultCompression");
160 const bool firstSrcComp = mergeInfo->fOptions.Contains("FirstSrcCompression");
161 const bool extraVerbose = mergeInfo->fOptions.Contains("rntuple.ExtraVerbose");
162 if (defaultComp && firstSrcComp) {
163 // this should never happen through hadd, but a user may call RNTuple::Merge() from custom code.
164 R__LOG_WARNING(NTupleMergeLog()) << "Passed both options \"DefaultCompression\" and \"FirstSrcCompression\": "
165 "only the latter will apply.";
166 }
167 std::optional<std::uint32_t> compression;
168 if (firstSrcComp) {
169 // user passed -ff or -fk: use the same compression as the first RNTuple we find in the sources.
170 // (do nothing here, the compression will be fetched below)
171 } else if (!defaultComp) {
172 // compression was explicitly passed by the user: use it.
173 compression = outFile->GetCompressionSettings();
174 } else {
175 // user passed no compression-related options: use default
177 R__LOG_INFO(NTupleMergeLog()) << "Using the default compression: " << *compression;
178 }
179
180 // The remaining entries are the input files
181 std::vector<std::unique_ptr<RPageSourceFile>> sources;
182 std::vector<RPageSource *> sourcePtrs;
183
184 while (const auto &pitr = itr()) {
185 TFile *inFile = dynamic_cast<TFile *>(pitr);
186 ROOT::RNTuple *anchor = inFile ? inFile->Get<ROOT::RNTuple>(ntupleName.c_str()) : nullptr;
187 if (!anchor) {
188 R__LOG_INFO(NTupleMergeLog()) << "No RNTuple anchor named '" << ntupleName << "' from file '"
189 << inFile->GetName() << "'";
190 continue;
191 }
192
193 auto source = RPageSourceFile::CreateFromAnchor(*anchor);
194 if (!compression) {
195 // Get the compression of this RNTuple and use it as the output compression.
196 // We currently assume all column ranges have the same compression, so we just peek at the first one.
197 source->Attach(RNTupleSerializer::EDescriptorDeserializeMode::kRaw);
198 DescriptorId_t firstClusterId = kInvalidDescriptorId;
199 auto descGuard = source->FindClusterId(0, firstClusterId);
200 if (firstClusterId == kInvalidDescriptorId) {
202 << "Asked to use the first source's compression as the output compression, but the "
203 "first source (file '"
204 << inFile->GetName()
205 << "') has an empty RNTuple, therefore the output compression could not be "
206 "determined.";
207 return -1;
208 }
209 const auto &firstCluster = descGuard->GetClusterDescriptor(firstClusterId);
210 auto colRangeIter = firstCluster.GetColumnRangeIterable();
211 auto firstColRange = colRangeIter.begin();
212 if (firstColRange == colRangeIter.end()) {
214 << "Asked to use the first source's compression as the output compression, but the "
215 "first source (file '"
216 << inFile->GetName()
217 << "') has an empty RNTuple, therefore the output compression could not be "
218 "determined.";
219 return -1;
220 }
221 compression = (*firstColRange).GetCompressionSettings();
222 R__LOG_INFO(NTupleMergeLog()) << "Using the first RNTuple's compression: " << *compression;
223 }
224 sources.push_back(std::move(source));
225 }
226
227 RNTupleWriteOptions writeOpts;
228 assert(compression);
229 writeOpts.SetCompression(*compression);
230 auto destination = std::make_unique<ROOT::Internal::RPageSinkFile>(ntupleName, *outFile, writeOpts);
231 std::unique_ptr<ROOT::RNTupleModel> model;
232 // If we already have an existing RNTuple, copy over its descriptor to support incremental merging
233 if (outNTuple) {
234 auto outSource = RPageSourceFile::CreateFromAnchor(*outNTuple);
235 outSource->Attach(RNTupleSerializer::EDescriptorDeserializeMode::kForWriting);
236 outSource->LoadAllPageLists();
237 auto desc = outSource->GetSharedDescriptorGuard();
238 model = destination->InitFromDescriptor(desc.GetConstRef(), true /* copyClusters */);
239 }
240
241 // Interface conversion
242 sourcePtrs.reserve(sources.size());
243 for (const auto &s : sources) {
244 sourcePtrs.push_back(s.get());
245 }
246
247 // Now merge
248 RNTupleMerger merger{std::move(destination), std::move(model)};
249 RNTupleMergeOptions mergerOpts;
250 mergerOpts.fCompressionSettings = compression;
251 mergerOpts.fExtraVerbose = extraVerbose;
252 if (auto mergingMode = ParseOptionMergingMode(mergeInfo->fOptions)) {
253 mergerOpts.fMergingMode = *mergingMode;
254 }
255 if (auto errBehavior = ParseOptionErrBehavior(mergeInfo->fOptions)) {
256 mergerOpts.fErrBehavior = *errBehavior;
257 }
258 if (auto versionBehavior = ParseOptionVersionBehavior(mergeInfo->fOptions)) {
259 mergerOpts.fVersionBehavior = *versionBehavior;
260 }
261 merger.Merge(sourcePtrs, mergerOpts).ThrowOnError();
262
263 // Provide the caller with a merged anchor object (even though we've already
264 // written it).
265 *this = *outFile->Get<ROOT::RNTuple>(ntupleName.c_str());
266
267 return 0;
268} catch (const std::exception &ex) {
269 R__LOG_ERROR(NTupleMergeLog()) << "Exception thrown while merging: " << ex.what();
270 return -1;
271}
272
273// =========================================================================================
274//
275// Local types and functions
276//
277// =========================================================================================
278namespace {
279// Functor used to change the compression of a page to `fCompressionSettings`.
280struct RChangeCompressionFunc {
281 const RColumnElementBase &fSrcColElement;
282 std::uint32_t fCompressionSettings;
283 RPageStorage::RSealedPage &fSealedPage;
285 std::byte *fBuffer;
286 std::size_t fBufSize;
287 const ROOT::RNTupleWriteOptions &fWriteOpts;
288
289 void operator()() const
290 {
292
293 const auto bytesPacked = fSrcColElement.GetPackedSize(fSealedPage.GetNElements());
294 // TODO: this buffer could be kept and reused across pages
295 std::unique_ptr<std::byte[]> unzipBufOwned;
296 std::byte *unzipBuf;
297 if (fCompressionSettings != 0) {
298 unzipBufOwned = MakeUninitArray<std::byte>(bytesPacked);
299 unzipBuf = unzipBufOwned.get();
300 } else {
301 unzipBuf = fBuffer;
302 }
303 ROOT::Internal::RNTupleDecompressor::Unzip(fSealedPage.GetBuffer(), fSealedPage.GetDataSize(), bytesPacked,
304 unzipBuf);
305
306 const auto checksumSize = fWriteOpts.GetEnablePageChecksums() * sizeof(std::uint64_t);
307 std::size_t nBytesZipped;
308 if (fCompressionSettings != 0) {
309 assert(fBuffer != unzipBuf);
310 assert(fBufSize >= bytesPacked + checksumSize);
311 nBytesZipped = ROOT::Internal::RNTupleCompressor::Zip(unzipBuf, bytesPacked, fCompressionSettings, fBuffer);
312 } else {
313 nBytesZipped = bytesPacked;
314 }
315 fSealedPage = {fBuffer, nBytesZipped + checksumSize, fSealedPage.GetNElements(), fSealedPage.GetHasChecksum()};
316 fSealedPage.ChecksumIfEnabled();
317 }
318};
319
320struct RTaskVisitor {
321 std::optional<ROOT::Experimental::TTaskGroup> &fGroup;
322
323 template <typename T>
324 void operator()(T &&f)
325 {
326 if (fGroup)
327 fGroup->Run(f);
328 else
329 f();
330 }
331};
332
333struct RCommonField {
334 const ROOT::RFieldDescriptor *fSrc;
335 const ROOT::RFieldDescriptor *fDst;
336
337 RCommonField(const ROOT::RFieldDescriptor &src, const ROOT::RFieldDescriptor &dst) : fSrc(&src), fDst(&dst) {}
338};
339
340/// Maps a column representation from a source to a destination RNTuple.
341/// fSource and fDest are the first representation indices of a specific column.
342///
343/// When we merge fields from different RNTuples, two compatible fields may use different column
344/// representations. When merging their columns we need to make sure that we keep the output
345/// representation coherent, which is what this mapping is here for.
346struct RColReprMapping {
347 std::uint32_t fSource;
348 std::uint32_t fDest;
349};
350
351/// A column extension that needs to be added to an output field.
352/// Note that this also adds a mapping for each new representation, which is why it inherits RColReprMapping.
353struct RColReprExtension : RColReprMapping {
354 /// The new representations to be added
355 std::vector<ROOT::Internal::RColumnFormat> fSourceRepr;
356 /// The first element index that this column had in its source. When adding this representation to the destination,
357 /// the new column will add this amount to the first element index of the 0th-representation column in the
358 /// destination's current cluster.
359 std::uint32_t fOrigFirstElementIndex = 0;
360};
361
362static std::optional<std::uint32_t>
363FindColumnReprMapping(const std::vector<RColReprMapping> &mappings, std::uint32_t sourceReprIndex)
364{
365 for (const auto [src, dst] : mappings)
366 if (src == sourceReprIndex)
367 return dst;
368 return std::nullopt;
369}
370
371struct RColumnOutInfo {
373};
374
375// { ".fully.qualified.fieldName.colInputIndex.colOutputReprIndex" => colOutputInfo }
376using ColumnIdMap_t = std::unordered_map<std::string, RColumnOutInfo>;
377
378struct RColumnMergeInfo {
379 // This column name is built as a dot-separated concatenation of the ancestry of
380 // the columns' parent fields' names plus the index of the column itself.
381 // e.g. "Muon.pt.x._0"
382 std::string fColumnName;
383 // The column id in the source RNTuple
385 // The corresponding column id in the destination RNTuple (the mapping happens in AddColumnsFromField())
387 std::uint16_t fOutputReprIndex = 0;
388 // If nullopt, use the default in-memory type
389 std::optional<std::type_index> fInMemoryType;
390 const ROOT::RFieldDescriptor *fParentFieldDescriptor = nullptr;
391 const ROOT::RNTupleDescriptor *fParentNTupleDescriptor = nullptr;
392};
393
394struct RSealedPageMergeData {
395 // We use a std::deque so that references to the contained SealedPageSequence_t, and its iterators, are
396 // never invalidated.
397 std::deque<RPageStorage::SealedPageSequence_t> fPagesV;
398 std::vector<RPageStorage::RSealedPageGroup> fGroups;
399 std::vector<std::unique_ptr<std::byte[]>> fBuffers;
400};
401
402struct RColumnInfoGroup {
403 std::vector<RColumnMergeInfo> fExtraDstColumns;
404 std::vector<RColumnMergeInfo> fCommonColumns;
405};
406
407template <typename T>
408using FieldCollectionMap_t = std::unordered_map<const ROOT::RFieldDescriptor *, std::vector<T>>;
409
410struct RDescriptorsComparison {
411 std::vector<const ROOT::RFieldDescriptor *> fExtraDstFields;
412 std::vector<const ROOT::RFieldDescriptor *> fExtraSrcFields;
413 std::vector<RCommonField> fCommonFields;
414 // { parentId => subfields }
415 // This is currently only used for the _userData attribute field.
416 std::unordered_map<ROOT::DescriptorId_t, std::vector<const ROOT::RFieldDescriptor *>> fExtraSrcSubfields;
417
418 // Empty if and only if the source can be fast merged.
419 std::string fFastMergeErrors;
420
421 // For each field that has more than 1 column representation in the output model,
422 // maps the column representatives of the source field with those of the destination.
423 // The key is the destination field.
424 FieldCollectionMap_t<RColReprMapping> fColReprMappings;
425 FieldCollectionMap_t<RColReprExtension> fColReprExtensions;
426};
427
428} // namespace
429
430// Subprocedure of CompareDescriptorStructure, extracted for readability.
431// Given two fields, attempts to match their column representations and schedules column extensions if necessary.
433 const ROOT::RFieldDescriptor &srcField, const ROOT::RFieldDescriptor &dstField,
434 RDescriptorsComparison &result, std::vector<std::string> &errors)
435{
436 const auto &srcColumns = srcField.GetLogicalColumnIds();
437 const auto &dstColumns = dstField.GetLogicalColumnIds();
438
439 // Fields must have the same cardinality
440 const std::uint32_t srcColCardinality = srcField.GetColumnCardinality();
441 const std::uint32_t dstColCardinality = dstField.GetColumnCardinality();
442 if (srcColCardinality != dstColCardinality) {
443 std::stringstream ss;
444 ss << "Field `" << srcField.GetFieldName()
445 << "` has a different column cardinality than previously-seen field with the same name (old: "
446 << dstColCardinality << ", new: " << srcColCardinality << ")";
447 errors.push_back(ss.str());
448 return;
449 }
450
451 if (srcColCardinality == 0)
452 return; // no columns to match
453
454 const auto srcNColReprs = srcColumns.size() / srcColCardinality;
455 const auto dstNColReprs = dstColumns.size() / dstColCardinality;
456 std::uint32_t nextDstReprIndex = dstNColReprs;
457
458 // For each column representation of the source, check if it matches one in the descriptor.
459 // If so, and if it doesn't match the destination's repr index, add a mapping for it.
460 // If nothing matches, schedule the column representation to be added later.
461 // NOTE: this has quadratic complexity but the numbers involved are small so it's fine.
462 for (auto srcReprIdx = 0u; srcReprIdx < srcNColReprs; ++srcReprIdx) {
463 std::int64_t matchingRepr = -1;
464 for (auto dstReprIdx = 0u; dstReprIdx < dstNColReprs; ++dstReprIdx) {
465 bool matches = true;
466 for (auto reprColIdx = 0u; reprColIdx < srcColCardinality; ++reprColIdx) {
467 const auto srcColId = srcColumns[srcReprIdx * srcColCardinality + reprColIdx];
468 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
469 const auto dstColId = dstColumns[dstReprIdx * dstColCardinality + reprColIdx];
470 const auto &dstCol = dstDesc.GetColumnDescriptor(dstColId);
471 if (srcCol.GetType() != dstCol.GetType()) {
472 matches = false;
473 break;
474 }
475 }
476
477 if (matches) {
478 // If this column representation matches by column type, we need to make sure that it also has
479 // matching column metadata. Since we currently do not support multiple column representations
480 // that only differ by such metadata, we forbid merging such columns (e.g. we cannot merge two
481 // Real32Trunc columns with different bit widths). This could technically be supported, but it
482 // would require significant effort, so we currently don't.
483 for (auto reprColIdx = 0u; reprColIdx < srcColCardinality; ++reprColIdx) {
484 const auto srcColId = srcColumns[srcReprIdx * srcColCardinality + reprColIdx];
485 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
486 const auto dstColId = dstColumns[dstReprIdx * dstColCardinality + reprColIdx];
487 const auto &dstCol = dstDesc.GetColumnDescriptor(dstColId);
488 if (srcCol.GetType() != dstCol.GetType() || srcCol.GetBitsOnStorage() != dstCol.GetBitsOnStorage() ||
489 srcCol.GetValueRange() != dstCol.GetValueRange()) {
490 matches = false;
491 break;
492 }
493 }
494
495 if (matches) {
496 // We found a valid matching representation.
497 matchingRepr = dstReprIdx;
498 break;
499 }
500 }
501 }
502
503 if (errors.empty()) {
504 if (matchingRepr >= 0 && matchingRepr != srcReprIdx) {
505 // a different matching representation was found
506 assert(matchingRepr < std::numeric_limits<std::uint32_t>::max());
507 result.fColReprMappings[&dstField].push_back(
508 RColReprMapping{srcReprIdx, static_cast<std::uint32_t>(matchingRepr)});
509 } else if (matchingRepr < 0) {
510 // this representation was not found in the destination: add it
511 std::vector<ROOT::Internal::RColumnFormat> newRepr;
512 newRepr.reserve(srcColCardinality);
513 std::uint32_t firstElemIdx = 0;
514 for (auto reprColIdx = 0u; reprColIdx < srcColCardinality; ++reprColIdx) {
515 const auto srcColId = srcColumns[srcReprIdx * srcColCardinality + reprColIdx];
516 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
517 // All added columns are supposed to have the same firstElementIndex
518 assert(firstElemIdx == 0 || firstElemIdx == srcCol.GetFirstElementIndex());
519 firstElemIdx = srcCol.GetFirstElementIndex();
520 auto &reprElement = newRepr.emplace_back();
521 reprElement.fType = srcCol.GetType();
522 reprElement.fBitWidth = srcCol.GetBitsOnStorage();
523 reprElement.fValueRange = srcCol.GetValueRange();
524 }
525 RColReprExtension extension{{srcReprIdx, nextDstReprIndex}, newRepr, firstElemIdx};
526 nextDstReprIndex += newRepr.size();
527 result.fColReprExtensions[&dstField].push_back(extension);
528 result.fColReprMappings[&dstField].push_back(extension);
529 }
530 }
531 }
532}
533
536 bool forAttributeMerging)
537{
538 RDescriptorsComparison res;
539
540 // collect common and extra dst fields
541 for (const auto &dstField : dst.GetTopLevelFields()) {
542 const auto srcFieldId = src.FindFieldId(dstField.GetFieldName());
543 if (srcFieldId != ROOT::kInvalidDescriptorId) {
544 const auto &srcField = src.GetFieldDescriptor(srcFieldId);
545 res.fCommonFields.push_back({srcField, dstField});
546 } else {
547 res.fExtraDstFields.emplace_back(&dstField);
548 }
549 }
550
551 // collect extra src fields
552 for (const auto &srcField : src.GetTopLevelFields()) {
553 const auto dstFieldId = dst.FindFieldId(srcField.GetFieldName());
554 if (dstFieldId == ROOT::kInvalidDescriptorId) {
555 res.fExtraSrcFields.push_back(&srcField);
556 }
557 }
558
559 if (forAttributeMerging) {
560 using namespace ROOT::Internal::RNTupleAttributes;
561 const auto userDataFieldId = src.FindFieldId(kMetaFieldNames[kMetaFieldIndex_UserData]);
562 if (userDataFieldId == ROOT::kInvalidDescriptorId) {
563 return R__FAIL("attribute user data field not found. The RNTuple is corrupted and cannot be merged.");
564 }
565 const auto &userDataField = src.GetFieldDescriptor(userDataFieldId);
566 for (const auto subfieldId : userDataField.GetLinkIds()) {
567 const auto subfieldName = src.GetQualifiedFieldName(subfieldId);
568 const auto dstFieldId = dst.FindFieldId(subfieldName);
569 if (dstFieldId == ROOT::kInvalidDescriptorId) {
570 const auto &subfield = src.GetFieldDescriptor(subfieldId);
571 res.fExtraSrcSubfields[userDataFieldId].push_back(&subfield);
572 }
573 }
574 }
575
576 return res;
577}
578
580 const ROOT::RFieldDescriptor &dstField,
581 const ROOT::RFieldDescriptor &srcField, std::vector<std::string> &errors)
582{
583 bool projCompatible = srcField.IsProjectedField() == dstField.IsProjectedField();
584 if (!projCompatible) {
585 std::stringstream ss;
586 ss << "Field `" << dstField.GetFieldName()
587 << "` is incompatible with previously-seen field with that name because the "
588 << (srcField.IsProjectedField() ? "new" : "old") << " one is projected and the other isn't";
589 errors.push_back(ss.str());
590 } else if (srcField.IsProjectedField()) {
591 // if both fields are projected, verify that they point to the same real field
592 const auto srcName = src.GetQualifiedFieldName(srcField.GetProjectionSourceId());
593 const auto dstName = dst.GetQualifiedFieldName(dstField.GetProjectionSourceId());
594 if (srcName != dstName) {
595 std::stringstream ss;
596 ss << "Field `" << dstField.GetFieldName()
597 << "` is projected to a different field than a previously-seen field with the same name (old: " << dstName
598 << ", new: " << srcName << ")";
599 errors.push_back(ss.str());
600 }
601 }
602}
603
604/// Compares the top level fields of `dst` and `src` and determines whether they can be merged or not.
605/// In addition, returns the differences between `dst` and `src`'s structures
608 bool forAttributeMerging = false)
609{
610 // Cases:
611 // 1. dst == src
612 // 2. dst has fields that src hasn't
613 // 3. src has fields that dst hasn't
614 // 4. dst and src have fields that differ (compatible or incompatible)
615
616 auto descCmpRes = FillCommonAndExtraFields(dst, src, forAttributeMerging);
617 if (!descCmpRes)
618 return R__FORWARD_RESULT(descCmpRes);
619
620 auto res = descCmpRes.Unwrap();
621 std::vector<std::string> slowMergeErrors;
622 std::vector<std::string> errors;
623
624 // Check compatibility of common fields
625 auto fieldsToCheck = res.fCommonFields;
626 const auto nTopLevelFields = fieldsToCheck.size();
627
628 // NOTE: using index-based for loop because the collection may get extended by the iteration
629 for (std::size_t fieldIdx = 0; fieldIdx < fieldsToCheck.size(); ++fieldIdx) {
630 const auto &field = fieldsToCheck[fieldIdx];
631
632 // NOTE: field.fSrc and field.fDst have the same name by construction
633 const auto &fieldName = field.fSrc->GetFieldName();
634
635 // Require that fields are both projected or both not projected
636 auto &errs = (fieldIdx < nTopLevelFields) ? slowMergeErrors : errors;
637 VerifyProjectedFieldsCompatibility(dst, src, *field.fDst, *field.fSrc, errs);
638
639 // Require that fields types match
640 // TODO(gparolini): allow non-identical but compatible types
641 const auto &srcTyName = ROOT::Internal::GetRenormalizedTypeName(field.fSrc->GetTypeName());
642 // This is already renormalized by construction (see RNTupleDescriptorBuilder::SetSchemaFromExisting)
643 const auto &dstTyName = field.fDst->GetTypeName();
644 if (srcTyName != dstTyName) {
645 std::stringstream ss;
646 ss << "Field `" << fieldName
647 << "` has a type incompatible with a previously-seen field with the same name: (old: " << dstTyName
648 << ", new: " << srcTyName << ")";
649 errors.push_back(ss.str());
650 }
651
652 // Require that type checksums match
653 const auto srcTyChk = field.fSrc->GetTypeChecksum();
654 const auto dstTyChk = field.fDst->GetTypeChecksum();
655 if (srcTyChk && dstTyChk && *srcTyChk != *dstTyChk) {
656 std::stringstream ss;
657 ss << "Field `" << field.fSrc->GetFieldName()
658 << "` has a different type checksum than previously-seen field with the same name";
659 errors.push_back(ss.str());
660 }
661
662 // Require that type versions match
663 const auto srcTyVer = field.fSrc->GetTypeVersion();
664 const auto dstTyVer = field.fDst->GetTypeVersion();
665 if (srcTyVer != dstTyVer) {
666 std::stringstream ss;
667 ss << "Field `" << field.fSrc->GetFieldName()
668 << "` has a different type version than previously-seen field with the same name (old: " << dstTyVer
669 << ", new: " << srcTyVer << ")";
670 errors.push_back(ss.str());
671 }
672
673 // Require that field versions match
674 const auto srcFldVer = field.fSrc->GetFieldVersion();
675 const auto dstFldVer = field.fDst->GetFieldVersion();
676 if (srcFldVer != dstFldVer) {
677 std::stringstream ss;
678 ss << "Field `" << field.fSrc->GetFieldName()
679 << "` has a different field version than previously-seen field with the same name (old: " << dstFldVer
680 << ", new: " << srcFldVer << ")";
681 errors.push_back(ss.str());
682 }
683
684 const auto srcRole = field.fSrc->GetStructure();
685 const auto dstRole = field.fDst->GetStructure();
686 if (srcRole != dstRole) {
687 std::stringstream ss;
688 ss << "Field `" << field.fSrc->GetFieldName()
689 << "` has a different structural role than previously-seen field with the same name (old: " << dstRole
690 << ", new: " << srcRole << ")";
691 errors.push_back(ss.str());
692 }
693
694 // Require that column representations match
695 if (!field.fSrc->IsProjectedField()) {
696 MatchColumnRepresentations(src, dst, *field.fSrc, *field.fDst, res, errors);
697 }
698
699 // Require that subfields are compatible
700 const auto &srcLinks = field.fSrc->GetLinkIds();
701 const auto &dstLinks = field.fDst->GetLinkIds();
702 if (srcLinks.size() != dstLinks.size()) {
703 std::stringstream ss;
704 ss << "Field `" << field.fSrc->GetFieldName()
705 << "` has a different number of children than previously-seen field with the same name (old: "
706 << dstLinks.size() << ", new: " << srcLinks.size() << ")";
707 errors.push_back(ss.str());
708 } else {
709 for (std::size_t linkIdx = 0, linkNum = srcLinks.size(); linkIdx < linkNum; ++linkIdx) {
710 const auto &srcSubfield = src.GetFieldDescriptor(srcLinks[linkIdx]);
711 const auto &dstSubfield = dst.GetFieldDescriptor(dstLinks[linkIdx]);
712 fieldsToCheck.push_back(RCommonField{srcSubfield, dstSubfield});
713 }
714 }
715 }
716
717 std::string errMsg;
718 if (!slowMergeErrors.empty()) {
719 // If there are slow merge errors, the source cannot be merged at all.
720 for (const auto &err : slowMergeErrors)
721 errMsg += std::string("\n * ") + err;
722
723 errMsg = errMsg.substr(1); // strip initial newline
724 return R__FAIL(errMsg);
725 }
726
727 // Source can only be slow merged.
728 if (!errors.empty()) {
729 for (const auto &err : errors)
730 errMsg += std::string("\n * ") + err;
731 errMsg = errMsg.substr(1); // strip initial newline
732 res.fFastMergeErrors = errMsg;
733 }
734
735 return ROOT::RResult(res);
736}
737
738// Restores the truncation/quantization parameters (bit width, value range) of a field recreated
739// from an on-disk descriptor, e.g. by ExtendDestinationModel(). RFieldDescriptor::CreateField only
740// rebuilds the type structure; the parameters live in the in-memory field (RRealField::fBitWidth,
741// fValueMin, fValueMax), so pinning the column representative alone leaves stale defaults behind,
742// which are out of range for kReal32Trunc. Recurses into subfields, matched by on-disk ID, so that
743// truncated/quantized members of late records and collections are restored as well. Nodes whose
744// source column is not truncated/quantized, or which span several representations, are left alone.
746 const ROOT::RNTupleDescriptor &srcDesc)
747{
748 const auto &logicalIds = fieldDesc.GetLogicalColumnIds();
749 if (logicalIds.size() == 1) {
750 const auto &column = srcDesc.GetColumnDescriptor(logicalIds[0]);
751 const auto type = column.GetType();
753 auto *realFloat = dynamic_cast<ROOT::RRealField<float> *>(&field);
754 auto *realDouble = dynamic_cast<ROOT::RRealField<double> *>(&field);
755 if (!realFloat && !realDouble) {
756 return R__FAIL("field `" + field.GetQualifiedFieldName() +
757 "` has a Real32Trunc/Real32Quant column but is not an RRealField");
758 }
759 const auto bits = column.GetBitsOnStorage();
760 try {
762 if (realFloat)
763 realFloat->SetTruncated(bits);
764 else
765 realDouble->SetTruncated(bits);
766 } else {
767 const auto valueRange = column.GetValueRange();
768 if (!valueRange) {
769 return R__FAIL("field `" + field.GetQualifiedFieldName() +
770 "` has a Real32Quant column with no value range");
771 }
772 if (realFloat) {
773 realFloat->SetQuantized(bits,
774 {static_cast<float>(valueRange->fMin), static_cast<float>(valueRange->fMax)});
775 } else {
776 realDouble->SetQuantized(bits, {valueRange->fMin, valueRange->fMax});
777 }
778 }
779 } catch (const ROOT::RException &ex) {
780 return R__FAIL(ex.what());
781 }
782 }
783 }
784 for (auto *subfield : field.GetMutableSubfields()) {
785 // CreateField() may materialize in-memory class members that the source descriptor does not
786 // have (e.g. members added to the class after the file was written); those keep an invalid
787 // on-disk ID and carry no source column, so there is nothing to restore for them.
788 if (subfield->GetOnDiskId() == ROOT::kInvalidDescriptorId)
789 continue;
790 auto restored = RestoreRealFieldParams(*subfield, srcDesc.GetFieldDescriptor(subfield->GetOnDiskId()), srcDesc);
791 if (!restored)
792 return R__FORWARD_ERROR(restored);
793 }
795}
796
797// Applies late model extension to `mergeData.fDestination`, adding all `descCmp.fExtraSrcFields` to it.
798[[nodiscard]]
800ExtendDestinationModel(RDescriptorsComparison &descCmp, ROOT::RNTupleModel &dstModel,
801 ROOT::Internal::RPageSink &destination, const ROOT::RNTupleDescriptor &srcDesc,
802 ROOT::NTupleSize_t nEntries, bool verbose, bool forSlowMerge)
803{
804 const auto &newFields = descCmp.fExtraSrcFields;
805
806 dstModel.Unfreeze();
807 ROOT::Internal::RNTupleModelChangeset changeset{dstModel};
808
809 if (verbose) {
810 std::string msg = "destination doesn't contain field";
811 if (newFields.size() > 1)
812 msg += 's';
813 msg += ' ';
814 msg += std::accumulate(newFields.begin(), newFields.end(), std::string{}, [](const auto &acc, const auto *field) {
815 return acc + (acc.length() ? ", " : "") + '`' + field->GetFieldName() + '`';
816 });
817 R__LOG_INFO(NTupleMergeLog()) << msg << ": adding " << (newFields.size() > 1 ? "them" : "it")
818 << " to the destination model (entry #" << nEntries << ").";
819 }
820
822 cfOpts.SetEmulateUnknownTypes(true);
823
824 changeset.fAddedFields.reserve(newFields.size());
825 // First add all non-projected fields...
826 for (const auto *fieldDesc : newFields) {
827 if (fieldDesc->IsProjectedField())
828 continue;
829
830 auto field = fieldDesc->CreateField(srcDesc, cfOpts);
831 if (!forSlowMerge) {
832 // Explicitly set the field representatives. This prevents UpdateSchema() from changing our column
833 // representations via AutoAdjustColumnTypes.
835 for (const auto &colId : fieldDesc->GetLogicalColumnIds()) {
836 const auto &column = srcDesc.GetColumnDescriptor(colId);
837 representatives.push_back(column.GetType());
838 }
839 field->SetColumnRepresentatives({representatives});
840 // Pinning the representative is not enough for truncated/quantized columns: their parameters
841 // are stored in the in-memory field, which CreateField() leaves at defaults.
842 auto restored = RestoreRealFieldParams(*field, *fieldDesc, srcDesc);
843 if (!restored)
844 return R__FORWARD_ERROR(restored);
845 }
846 changeset.AddField(std::move(field));
847 }
848 // ...then add all projected fields...
849 for (const auto *fieldDesc : newFields) {
850 if (!fieldDesc->IsProjectedField())
851 continue;
852
854 auto field = fieldDesc->CreateField(srcDesc, cfOpts);
855 const auto sourceId = fieldDesc->GetProjectionSourceId();
856 const auto &sourceField = dstModel.GetConstField(srcDesc.GetQualifiedFieldName(sourceId));
857 fieldMap[field.get()] = &sourceField;
858
859 for (const auto &subfield : *field) {
860 const auto &subFieldDesc = srcDesc.GetFieldDescriptor(subfield.GetOnDiskId());
861 const auto subSourceId = subFieldDesc.GetProjectionSourceId();
862 const auto &subSourceField = dstModel.GetConstField(srcDesc.GetQualifiedFieldName(subSourceId));
863 fieldMap[&subfield] = &subSourceField;
864 }
865 changeset.fAddedProjectedFields.emplace_back(field.get());
866 ROOT::Internal::GetProjectedFieldsOfModel(dstModel).Add(std::move(field), fieldMap);
867 }
868 // ...finally add all subfields.
869 if (forSlowMerge) { // TODO(gparolini): figure out if this makes sense also in default merging
870 for (const auto &[parentId, subfields] : descCmp.fExtraSrcSubfields) {
871 const auto parentFQName = srcDesc.GetQualifiedFieldName(parentId);
872 // TODO(gparolini): should we RegisterSubfield(parentFQName) here if the parent is not a top-level field?
873 for (const auto *subfield : subfields) {
874 auto field = subfield->CreateField(srcDesc, cfOpts);
875 changeset.AddField(std::move(field), parentFQName);
876 }
877 }
878 }
879 dstModel.Freeze();
880 try {
881 // FIXME: here we are connecting the new fields/columns to the sink!
882 // We should avoid doing that, as all other non-extended fields never get connected (and we don't
883 // need to connect these either in principle).
884 // NOTE: this calls AutoAdjustColumnTypes, but we have set the column representations of all fields
885 // explicitly, so it will not change it under the hood.
886 destination.UpdateSchema(changeset, nEntries);
887 } catch (const ROOT::RException &ex) {
888 return R__FAIL(ex.what());
889 }
890
891 auto &commonFields = descCmp.fCommonFields;
892 commonFields.reserve(commonFields.size() + newFields.size());
893 // NOTE(gparolini): Insert the new fields at the beginning of `commonFields`.
894 // We need to make sure the extended fields appear before all other common fields for the following reason:
895 // in general, when we GatherColumnInfos we (potentially) assign new column output ids in field order; this
896 // assignment happens whenever we find new columns, which happens in 3 cases:
897 // 1. we are in the first source and we're adding the first set of (common) fields;
898 // 2. we are adding a new set of extended common fields (this is done in this function);
899 // 3. we are adding new column representations for fields that we already had before processing this source.
900 //
901 // It's important that the output id assigned to the new columns is coherent with the order of the column descriptors
902 // as they appear in the header and footer of the destination RNTuple.
903 // This is in turn determined by the order by which we append new columns to the dst descriptor during the merging
904 // process.
905 //
906 // Now let's consider the three cases listed above.
907 // Ignoring the trivial case (1), the order of operations for each source is:
908 // - call ExtendDestinationModel (case 2)
909 // (this adds both new fields and column descriptors; see the UpdateSchema call above)
910 // - add new column representations (case 3)
911 // (this only adds column descriptors, see the call to AddColumnRepresentation)
912 //
913 // Since we call ExtendDestinationModel (this function) *before* adding the new column representations,
914 // the dst descriptor always gets updated with the new column descriptors coming from the extended fields before
915 // it gets updated with the extended column representations.
916 //
917 // However, in GatherColumnInfos, the new column output ids are added sequentially in *field* order and the fields
918 // containing the new column representations are already in that list from earlier! So, to make sure the new output
919 // ids are assigned to our extended fields first, we push them in from on the list so they are visited first.
920 const auto &dstDesc = destination.GetDescriptor();
921 for (auto it = newFields.rbegin(); it != newFields.rend(); ++it) {
922 const auto *field = *it;
923 const auto newFieldInDstId = dstDesc.FindFieldId(field->GetFieldName());
924 const auto &newFieldInDst = dstDesc.GetFieldDescriptor(newFieldInDstId);
925 commonFields.insert(commonFields.begin(), RCommonField{*field, newFieldInDst});
926 }
927
929}
930
931static std::optional<std::type_index> ColumnInMemoryType(std::string_view fieldType, ENTupleColumnType onDiskType)
932{
933 if (onDiskType == ENTupleColumnType::kIndex32 || onDiskType == ENTupleColumnType::kSplitIndex32 ||
935 return typeid(ROOT::Internal::RColumnIndex);
936
937 if (onDiskType == ENTupleColumnType::kSwitch)
938 return typeid(ROOT::Internal::RColumnSwitch);
939
940 // clang-format off
941 if (fieldType == "bool") return typeid(bool);
942 if (fieldType == "std::byte") return typeid(std::byte);
943 if (fieldType == "char") return typeid(char);
944 if (fieldType == "std::int8_t") return typeid(std::int8_t);
945 if (fieldType == "std::uint8_t") return typeid(std::uint8_t);
946 if (fieldType == "std::int16_t") return typeid(std::int16_t);
947 if (fieldType == "std::uint16_t") return typeid(std::uint16_t);
948 if (fieldType == "std::int32_t") return typeid(std::int32_t);
949 if (fieldType == "std::uint32_t") return typeid(std::uint32_t);
950 if (fieldType == "std::int64_t") return typeid(std::int64_t);
951 if (fieldType == "std::uint64_t") return typeid(std::uint64_t);
952 if (fieldType == "float") return typeid(float);
953 if (fieldType == "double") return typeid(double);
954 // clang-format on
955
956 // if the type is not one of those above, we use the default in-memory type.
957 return std::nullopt;
958}
959
960static void PrefillColumnMap(const ROOT::RNTupleDescriptor &desc, const ROOT::RFieldDescriptor &fieldDesc,
961 ColumnIdMap_t &colIdMap, const std::string &prefix = "")
962{
963 std::string name = prefix + '.' + fieldDesc.GetFieldName();
964 for (const auto &colId : fieldDesc.GetLogicalColumnIds()) {
965 const auto &colDesc = desc.GetColumnDescriptor(colId);
966 RColumnOutInfo info{};
967 info.fColumnId = colDesc.GetLogicalId();
968 const auto colName =
969 name + '.' + std::to_string(colDesc.GetIndex()) + '.' + std::to_string(colDesc.GetRepresentationIndex());
970 colIdMap[colName] = info;
971 }
972
973 for (const auto &subId : fieldDesc.GetLinkIds()) {
974 const auto &subfield = desc.GetFieldDescriptor(subId);
975 PrefillColumnMap(desc, subfield, colIdMap, name);
976 }
977}
978
980 const ROOT::RFieldDescriptor &field, const ROOT::RNTupleDescriptor &desc,
981 const FieldCollectionMap_t<RColReprExtension> &extensions,
982 std::vector<std::pair<const ROOT::RFieldDescriptor *, std::vector<RColReprExtension>>> &outExtensions,
983 std::unordered_map<ROOT::DescriptorId_t, std::vector<const ROOT::RFieldDescriptor *>> &outProjectionPointees)
984{
985 const auto it = extensions.find(&field);
986 if (it != extensions.end())
987 outExtensions.emplace_back(it->first, it->second);
988
989 if (field.IsProjectedField())
990 outProjectionPointees[field.GetProjectionSourceId()].push_back(&field);
991
992 for (auto childId : field.GetLinkIds()) {
993 const auto &child = desc.GetFieldDescriptor(childId);
994 AddColumnExtensionsInFieldOrder(child, desc, extensions, outExtensions, outProjectionPointees);
995 }
996}
997
998namespace {
999// Collection of common methods and data to be used for any kind of RNTuples merging.
1000class RNTupleMergeStrategy {
1001protected:
1002 // Fixed during the whole merging. Note that these references are owned by the RNTupleMerger and share its lifetime.
1003 std::unique_ptr<ROOT::RNTupleModel> fDstModel;
1005 const ROOT::RNTupleDescriptor &fDstDescriptor;
1006 RNTupleMergeOptions fMergeOpts;
1007
1008 // Changes at every source. InitSource() is supposed to update this right after Attaching the source.
1009 const ROOT::RNTupleDescriptor *fSrcDescriptor = nullptr;
1010
1011public:
1012 RNTupleMergeStrategy(std::unique_ptr<RNTupleModel> dstModel, ROOT::Internal::RPagePersistentSink &dst,
1013 const RNTupleMergeOptions &mergeOpts)
1014 : fDstModel(std::move(dstModel)), fDestination(dst), fDstDescriptor(dst.GetDescriptor()), fMergeOpts(mergeOpts)
1015 {
1016 }
1017
1018 virtual ~RNTupleMergeStrategy() = default;
1019
1020 // Returns the number of entries currently written to the destination.
1021 // This is only accurate before or after a call to MergeSource(), not during it.
1022 virtual ROOT::NTupleSize_t GetNDstEntries() const = 0;
1023
1024 // Invoked as the first thing when we start processing a new source
1025 virtual void InitSource(RPageSource &source) = 0;
1026
1027 // Invoked when the destination model must be late model extended (only invoked in case of union merging)
1028 [[nodiscard]] virtual ROOT::RResult<void> ExtendDestinationModel(RDescriptorsComparison &descCmp) = 0;
1029
1030 // Invoked when merging one source
1031 [[nodiscard]] virtual ROOT::RResult<void> MergeSource(RPageSource &source, RDescriptorsComparison &descCmp) = 0;
1032
1033 // Invoked at the end of the whole process, just before committing the dataset
1034 virtual void BeforeCommit() = 0;
1035};
1036
1037/// Merge Actions used internally to merge attributes.
1038class RNTupleAttrMergeActions final : public RNTupleMergeActions {
1039 /// How much do we need to shift the _rangeStart field by. This changes on each new source (and is equal to the
1040 /// total number of entries committed at the end of the previous source).
1041 ROOT::NTupleSize_t fAttrEntryStartOffset = 0;
1042 const RNTupleMergeStrategy &fParentStrategy;
1043 std::shared_ptr<ROOT::NTupleSize_t> fRangeStartPtr;
1044
1045public:
1046 RNTupleAttrMergeActions(const RNTupleMergeStrategy &parentStrategy, ROOT::NTupleSize_t initialStartOffset)
1047 : fAttrEntryStartOffset(initialStartOffset), fParentStrategy(parentStrategy)
1048 {
1049 }
1050
1052 {
1053 using namespace ROOT::Internal::RNTupleAttributes;
1054
1055 // Cache pointer per source
1056 if (!fRangeStartPtr)
1057 fRangeStartPtr = entry.GetPtr<ROOT::NTupleSize_t>(kMetaFieldNames[kMetaFieldIndex_RangeStart]);
1058
1059 // Shift _rangeStart field by the number of main entries we already written in the destination
1060 *fRangeStartPtr += fAttrEntryStartOffset;
1061 return ENTupleMergeActionResult::kEmit;
1062 }
1063
1065 {
1066 fAttrEntryStartOffset = fParentStrategy.GetNDstEntries();
1067 fRangeStartPtr = nullptr; // invalidate cached ptr every time we switch source
1068 return ENTupleMergeActionResult::kDrop;
1069 }
1070};
1071
1072// =========================================================================================
1073//
1074// MergeStrategyDefault
1075//
1076// =========================================================================================
1077// Strategy used for L1, L2 and L3 merging
1078// (https://github.com/root-project/root/blob/master/tree/ntuple/doc/Merging.md#glossary).
1079class RNTupleMergeStrategyDefault final : public RNTupleMergeStrategy {
1080 std::unique_ptr<ROOT::Internal::RPageAllocator> fPageAlloc = std::make_unique<ROOT::Internal::RPageAllocatorHeap>();
1081 std::optional<ROOT::Experimental::TTaskGroup> fTaskGroup;
1082 std::unordered_map<ROOT::DescriptorId_t, std::vector<const ROOT::RFieldDescriptor *>> fProjectionPointees;
1083
1084 std::vector<RColumnMergeInfo> fColumns;
1085 // Maps input column IDs to output IDs
1086 ColumnIdMap_t fColumnIdMap;
1087
1088 [[nodiscard]] ROOT::RResult<void>
1089 MergeCommonColumns(ROOT::Internal::RClusterPool &clusterPool, const ROOT::RClusterDescriptor &clusterDesc,
1090 std::span<RColumnMergeInfo> commonColumns, const RCluster::ColumnSet_t &commonColumnSet,
1091 RSealedPageMergeData &sealedPageData);
1092 [[nodiscard]]
1093 ROOT::RResult<void> GenerateZeroPagesForColumns(size_t nEntriesToGenerate, std::span<const RColumnMergeInfo> columns,
1094 RSealedPageMergeData &sealedPageData);
1095
1096 void AddColumnsFromField(std::vector<RColumnMergeInfo> &columns, const ROOT::RNTupleDescriptor &srcDesc,
1097 const FieldCollectionMap_t<RColReprMapping> &colReprMappings,
1098 const ROOT::RFieldDescriptor &srcFieldDesc, const ROOT::RFieldDescriptor &dstFieldDesc,
1099 const std::string &prefix = "");
1100
1101 RColumnInfoGroup GatherColumnInfos(const RDescriptorsComparison &descCmp, const ROOT::RNTupleDescriptor &srcDesc);
1102
1103 [[nodiscard]] ROOT::RResult<void> MergeSourceClusters(RPageSource &source, std::span<RColumnMergeInfo> commonColumns,
1104 std::span<const RColumnMergeInfo> extraDstColumns);
1105
1106public:
1107 RNTupleMergeStrategyDefault(std::unique_ptr<RNTupleModel> dstModel, ROOT::Internal::RPagePersistentSink &dst,
1108 const RNTupleMergeOptions &mergeOpts)
1109 : RNTupleMergeStrategy(std::move(dstModel), dst, mergeOpts)
1110 {
1111#ifdef R__USE_IMT
1113 fTaskGroup = ROOT::Experimental::TTaskGroup();
1114#endif
1115
1116 if (fDstModel) {
1117 // If this is an incremental merging, pre-fill the column id map with the existing destination ids.
1118 // Otherwise we would generate new output ids that may not match the ones in the destination!
1119 for (const auto &field : dst.GetDescriptor().GetTopLevelFields()) {
1120 PrefillColumnMap(dst.GetDescriptor(), field, fColumnIdMap);
1121 }
1122 }
1123 }
1124
1125 ROOT::NTupleSize_t GetNDstEntries() const final
1126 {
1127 // Since with default merging we always commit clusters at source boundary, we can simply rely on
1128 // the sink's internal counter.
1129 return fDestination.GetNEntries();
1130 }
1131
1132 void InitSource(RPageSource &source) final
1133 {
1134 source.Attach(RNTupleSerializer::EDescriptorDeserializeMode::kForWriting);
1135 source.LoadAllPageLists();
1136 fSrcDescriptor = &source.GetSharedDescriptorGuard().GetConstRef();
1137
1138 if (!fDstModel) {
1139 constexpr bool copyClusters = false;
1140 fDstModel = fDestination.InitFromDescriptor(*fSrcDescriptor, copyClusters);
1141 }
1142 }
1143
1144 [[nodiscard]] ROOT::RResult<void> ExtendDestinationModel(RDescriptorsComparison &descCmp) final
1145 {
1146 // late model extension for all fExtraSrcFields in Union mode
1147 auto res = ::ExtendDestinationModel(descCmp, *fDstModel, fDestination, *fSrcDescriptor, GetNDstEntries(),
1148 fMergeOpts.fExtraVerbose,
1149 /*forSlowMerge=*/false);
1150 return res;
1151 }
1152
1153 void BeforeCommit() final {}
1154
1155 [[nodiscard]] ROOT::RResult<void> MergeSource(RPageSource &source, RDescriptorsComparison &descCmp) final;
1156};
1157} // namespace
1158
1159// Given a field, fill `columns` and `mergeData.fColumnIdMap` with information about all columns belonging to it and
1160// its subfields. `fColumnIdMap` is used to map matching columns from different sources to the same output
1161// column in the destination. We match columns by their "fully qualified name", which is the concatenation of their
1162// ancestor fields' names and the column index. By this point, since we called `CompareDescriptorStructure()`
1163// earlier, we should be guaranteed that two matching columns will have at least compatible representations.
1164// This function is recursive as it needs to call itself on the entire subfield hierarchy of the source field.
1165// NOTE: srcFieldDesc and dstFieldDesc may alias.
1166void RNTupleMergeStrategyDefault::AddColumnsFromField(std::vector<RColumnMergeInfo> &columns,
1167 const ROOT::RNTupleDescriptor &srcDesc,
1168 const FieldCollectionMap_t<RColReprMapping> &colReprMappings,
1169 const ROOT::RFieldDescriptor &srcFieldDesc,
1170 const ROOT::RFieldDescriptor &dstFieldDesc,
1171 const std::string &prefix)
1172{
1173 std::string name = prefix + '.' + srcFieldDesc.GetFieldName();
1174
1175 // We don't want to try and merge alias columns. Note that subfields of projected fields
1176 // must also be projected, so we don't need to check them.
1177 if (srcFieldDesc.IsProjectedField())
1178 return;
1179
1180 const auto &columnIds = srcFieldDesc.GetLogicalColumnIds();
1181 columns.reserve(columns.size() + columnIds.size());
1182
1183 for (auto i = 0u; i < srcFieldDesc.GetLogicalColumnIds().size(); ++i) {
1184 auto srcColumnId = srcFieldDesc.GetLogicalColumnIds()[i];
1185 const auto &srcColumn = srcDesc.GetColumnDescriptor(srcColumnId);
1186
1187 RColumnMergeInfo info{};
1188 info.fInputId = srcColumn.GetPhysicalId();
1189 // NOTE(gparolini): the parent field is used when synthesizing zero pages, which happens in 2 situations:
1190 // 1. when adding extra dst columns (in which case we need to synthesize zero pages for the incoming src), and
1191 // 2. when merging a deferred column into an existing column (in which case we need to fill the "hole" with
1192 // zeroes). For the first case srcFieldDesc and dstFieldDesc are the same (see the calling site of this
1193 // function), but for the second case they're not, and we need to pick the source field because we will then
1194 // check the column's *input* id inside fParentFieldDescriptor to see if it's a suppressed column (see
1195 // GenerateZeroPagesForColumns()).
1196 info.fParentFieldDescriptor = &srcFieldDesc;
1197 // Save the parent field descriptor since this may be either the source or destination descriptor depending on
1198 // whether this is an extraDstField or a commonField. We will need this in GenerateZeroPagesForColumns() to
1199 // properly walk up the field hierarchy.
1200 info.fParentNTupleDescriptor = &srcDesc;
1201
1202 const auto mappingsIt = colReprMappings.find(&dstFieldDesc);
1203 std::uint16_t reprIndex = srcColumn.GetRepresentationIndex();
1204 if (mappingsIt != colReprMappings.end()) {
1205 if (auto outReprIdx = FindColumnReprMapping(mappingsIt->second, reprIndex); outReprIdx)
1206 reprIndex = *outReprIdx;
1207 }
1208
1209 info.fColumnName = name + '.' + std::to_string(srcColumn.GetIndex()) + '.' + std::to_string(reprIndex);
1210
1212
1213 if (auto it = fColumnIdMap.find(info.fColumnName); it != fColumnIdMap.end()) {
1214 // We had already added this column to the column id map: just copy its data.
1215 info.fOutputId = it->second.fColumnId;
1216 info.fOutputReprIndex = reprIndex;
1217 } else {
1218 // New column: assign it the next ouput id.
1219 info.fOutputId = fColumnIdMap.size();
1220 // NOTE(gparolini): map the representation index of src column to that of dst column.
1221 // This mapping is only relevant for common columns and it's done to ensure we have the correct representation
1222 // index in the output column metadata.
1223 assert(dstFieldDesc.GetColumnCardinality() == srcFieldDesc.GetColumnCardinality());
1224 const auto dstColumnIndex = reprIndex * dstFieldDesc.GetColumnCardinality() + srcColumn.GetIndex();
1225 const auto dstColumnId = dstFieldDesc.GetLogicalColumnIds()[dstColumnIndex];
1226 const auto &dstColumn = fDstDescriptor.GetColumnDescriptor(dstColumnId);
1227 columnType = dstColumn.GetType();
1228 info.fOutputReprIndex = reprIndex;
1229 fColumnIdMap[info.fColumnName] = RColumnOutInfo{info.fOutputId};
1230 }
1231
1232 if (fMergeOpts.fExtraVerbose) {
1233 R__LOG_INFO(NTupleMergeLog()) << "Adding column " << info.fColumnName << " with log.id " << srcColumnId
1234 << ", phys.id " << srcColumn.GetPhysicalId() << ", type "
1235 << RColumnElementBase::GetColumnTypeName(srcColumn.GetType()) << " -> log.id "
1236 << info.fOutputId << ", type "
1238 }
1239
1240 // Since we disallow merging fields of different types, src and dstFieldDesc must have the same type name.
1241 assert(srcDesc.GetTypeNameForComparison(srcFieldDesc) == dstFieldDesc.GetTypeName());
1242 info.fInMemoryType = ColumnInMemoryType(dstFieldDesc.GetTypeName(), columnType);
1243 columns.emplace_back(info);
1244 }
1245
1246 const auto &srcChildrenIds = srcFieldDesc.GetLinkIds();
1247 const auto &dstChildrenIds = dstFieldDesc.GetLinkIds();
1248 assert(srcChildrenIds.size() == dstChildrenIds.size());
1249 for (auto i = 0u; i < srcChildrenIds.size(); ++i) {
1250 const auto &srcChild = srcDesc.GetFieldDescriptor(srcChildrenIds[i]);
1251 const auto &dstChild = fDstDescriptor.GetFieldDescriptor(dstChildrenIds[i]);
1252 AddColumnsFromField(columns, srcDesc, colReprMappings, srcChild, dstChild, name);
1253 }
1254}
1255
1256// Converts the fields comparison data to the corresponding column information.
1257// While doing so, it collects such information in `fColumnIdMap`, which is used by later calls to this
1258// function to map already-seen column names to their chosen outputId, type and so on.
1259RColumnInfoGroup RNTupleMergeStrategyDefault::GatherColumnInfos(const RDescriptorsComparison &descCmp,
1260 const ROOT::RNTupleDescriptor &srcDesc)
1261{
1262 RColumnInfoGroup res;
1263 for (const ROOT::RFieldDescriptor *field : descCmp.fExtraDstFields) {
1264 AddColumnsFromField(res.fExtraDstColumns, fDstDescriptor, descCmp.fColReprMappings, *field, *field);
1265 }
1266 for (const auto &[srcField, dstField] : descCmp.fCommonFields) {
1267 AddColumnsFromField(res.fCommonColumns, srcDesc, descCmp.fColReprMappings, *srcField, *dstField);
1268 }
1269 return res;
1270}
1271
1272// Generates default (zero) values for the given columns
1273ROOT::RResult<void> RNTupleMergeStrategyDefault::GenerateZeroPagesForColumns(size_t nEntriesToGenerate,
1274 std::span<const RColumnMergeInfo> columns,
1275 RSealedPageMergeData &sealedPageData)
1276{
1277 if (!nEntriesToGenerate)
1279
1280 for (const auto &column : columns) {
1281 const ROOT::RFieldDescriptor *field = column.fParentFieldDescriptor;
1282
1283 // Skip all auxiliary columns
1284 assert(!field->GetLogicalColumnIds().empty());
1285 if (field->GetLogicalColumnIds()[0] != column.fInputId)
1286 continue;
1287
1288 // Check if this column is a child of a Collection or a Variant. If so, it has no data
1289 // and can be skipped.
1290 bool skipColumn = false;
1291 auto nRepetitions = std::max<std::uint64_t>(field->GetNRepetitions(), 1);
1292 for (auto parentId = field->GetParentId(); parentId != ROOT::kInvalidDescriptorId;) {
1293 const ROOT::RFieldDescriptor &parent = column.fParentNTupleDescriptor->GetFieldDescriptor(parentId);
1296 skipColumn = true;
1297 break;
1298 }
1299 nRepetitions *= std::max<std::uint64_t>(parent.GetNRepetitions(), 1);
1300 parentId = parent.GetParentId();
1301 }
1302 if (skipColumn)
1303 continue;
1304
1305 const auto structure = field->GetStructure();
1306
1307 if (structure == ROOT::ENTupleStructure::kStreamer) {
1308 return R__FAIL("Destination RNTuple contains a streamer field (" + field->GetFieldName() +
1309 ") that is not present in one of the sources. "
1310 "Creating a default value for a streamer field is ill-defined, therefore the merging "
1311 "process will abort.");
1312 }
1313
1314 // NOTE: we cannot have a Record here because it has no associated columns.
1316 structure == ROOT::ENTupleStructure::kPlain);
1317
1318 const auto &columnDesc = fDstDescriptor.GetColumnDescriptor(column.fOutputId);
1319 const auto colElement = RColumnElementBase::Generate(columnDesc);
1320 const auto nElements = nEntriesToGenerate * nRepetitions;
1321 const auto nBytesOnStorage = colElement->GetPackedSize(nElements);
1322 // TODO(gparolini): make this configurable
1323 constexpr auto kPageSizeLimit = 256 * 1024;
1324 // TODO(gparolini): consider coalescing the last page if its size is less than some threshold
1325 const size_t nPages = nBytesOnStorage / kPageSizeLimit + !!(nBytesOnStorage % kPageSizeLimit);
1326 for (size_t i = 0; i < nPages; ++i) {
1327 const auto pageSize = (i < nPages - 1) ? kPageSizeLimit : nBytesOnStorage - kPageSizeLimit * (nPages - 1);
1328 const auto checksumSize = RPageStorage::kNBytesPageChecksum;
1329 const auto bufSize = pageSize + checksumSize;
1330 assert(pageSize % colElement->GetSize() == 0);
1331 const auto nElementsPerPage = pageSize / colElement->GetSize();
1332 auto page = fPageAlloc->NewPage(colElement->GetSize(), nElementsPerPage);
1333 page.GrowUnchecked(nElementsPerPage);
1334 memset(page.GetBuffer(), 0, page.GetNBytes());
1335
1336 auto &buffer = sealedPageData.fBuffers.emplace_back(new std::byte[bufSize]);
1338 sealConf.fElement = colElement.get();
1339 sealConf.fPage = &page;
1340 sealConf.fBuffer = buffer.get();
1341 sealConf.fCompressionSettings = fMergeOpts.fCompressionSettings.value();
1342 sealConf.fWriteChecksum = fDestination.GetWriteOptions().GetEnablePageChecksums();
1343 auto sealedPage = RPageSink::SealPage(sealConf);
1344
1345 sealedPageData.fPagesV.push_back({sealedPage});
1346 sealedPageData.fGroups.emplace_back(column.fOutputId, sealedPageData.fPagesV.back().cbegin(),
1347 sealedPageData.fPagesV.back().cend());
1348 }
1349 }
1351}
1352
1353// Merges all columns appearing both in the source and destination RNTuples, just copying them if their
1354// compression matches ("fast merge") or by unsealing and resealing them with the proper compression.
1355ROOT::RResult<void> RNTupleMergeStrategyDefault::MergeCommonColumns(ROOT::Internal::RClusterPool &clusterPool,
1356 const ROOT::RClusterDescriptor &clusterDesc,
1357 std::span<RColumnMergeInfo> commonColumns,
1358 const RCluster::ColumnSet_t &commonColumnSet,
1359 RSealedPageMergeData &sealedPageData)
1360{
1361 const auto nCommonColumnsInCluster = commonColumnSet.size();
1362 assert(nCommonColumnsInCluster <= commonColumns.size());
1363
1364 if (nCommonColumnsInCluster == 0)
1366
1367 const RCluster *cluster = clusterPool.GetCluster(clusterDesc.GetId(), commonColumnSet);
1368 // we expect the cluster pool to contain the requested set of columns, since they were
1369 // validated by CompareDescriptorStructure() and MergeSourceClusters().
1370 assert(cluster);
1371
1372 const std::uint32_t outCompression = fMergeOpts.fCompressionSettings.value();
1373
1374 for (size_t colIdx = 0; colIdx < nCommonColumnsInCluster; ++colIdx) {
1375 const auto &column = commonColumns[colIdx];
1376 const auto &columnId = column.fInputId;
1377 R__ASSERT(clusterDesc.ContainsColumn(columnId));
1378
1379 const auto &columnDesc = fSrcDescriptor->GetColumnDescriptor(columnId);
1380 const auto srcColElement = column.fInMemoryType
1381 ? ROOT::Internal::GenerateColumnElement(*column.fInMemoryType, columnDesc)
1382 : RColumnElementBase::Generate(columnDesc);
1383
1384 // Now get the pages for this column in this cluster
1385 const auto &pages = clusterDesc.GetPageRange(columnId);
1386
1388 sealedPages.resize(pages.GetPageInfos().size());
1389
1390 // Each column range potentially has a distinct compression settings
1391 const auto &columnRange = clusterDesc.GetColumnRange(columnId);
1392 assert(!columnRange.IsSuppressed());
1393 const auto colRangeCompressionSettings = columnRange.GetCompressionSettings().value();
1394
1395 // Select "merging level". There are 2 levels, from fastest to slowest, depending on the case:
1396 // L1: compression and encoding of src and dest both match: we can simply copy the page
1397 // L2: compression of dest doesn't match the src we must recompress the page.
1398 // Note that in no case do we need to re-encode the page, as if the encoding differs we simply
1399 // append a new column representation to the field.
1400 const bool needsRecompressing = colRangeCompressionSettings != outCompression;
1401
1402 if (needsRecompressing && fMergeOpts.fExtraVerbose) {
1403 R__LOG_INFO(NTupleMergeLog()) << "Recompressing column " << column.fColumnName
1404 << ": { compression: " << colRangeCompressionSettings << " => "
1405 << fMergeOpts.fCompressionSettings.value() << ", onDiskType: "
1407 srcColElement->GetIdentifier().fOnDiskType)
1408 << "}";
1409 }
1410
1411 const size_t pageBufferBaseIdx = sealedPageData.fBuffers.size();
1412 // If the column range already has the right compression we don't need to allocate any new buffer, so we don't
1413 // bother reserving memory for them.
1414 if (needsRecompressing)
1415 sealedPageData.fBuffers.resize(sealedPageData.fBuffers.size() + pages.GetPageInfos().size());
1416
1417 // If this column is deferred, we may need to fill "holes" until its real start. We fill any missing entry
1418 // with zeroes, like we do for extraDstColumns.
1419 // As an optimization, we don't do this for the first source (since we can rely on the FirstElementIndex and
1420 // deferred column mechanism in that case).
1421 // TODO: also avoid doing this if we added no real page of this column to the destination yet.
1422 if (columnDesc.GetFirstElementIndex() > clusterDesc.GetFirstEntryIndex() && fDestination.GetNEntries() > 0) {
1423 const auto nMissingEntries = columnDesc.GetFirstElementIndex() - clusterDesc.GetFirstEntryIndex();
1424 auto res = GenerateZeroPagesForColumns(nMissingEntries, {&column, 1}, sealedPageData);
1425 if (!res)
1426 return R__FORWARD_ERROR(res);
1427 }
1428
1429 // Loop over the pages
1430 std::uint64_t pageIdx = 0;
1431 for (const auto &pageInfo : pages.GetPageInfos()) {
1432 assert(pageIdx < sealedPages.size());
1433 assert(sealedPageData.fBuffers.size() == 0 || pageIdx < sealedPageData.fBuffers.size());
1434 assert(pageInfo.GetLocator().GetType() != ROOT::RNTupleLocator::kTypePageZero);
1435
1436 ROOT::Internal::ROnDiskPage::Key key{columnId, pageIdx};
1437 auto onDiskPage = cluster->GetOnDiskPage(key);
1438
1439 if (!pageInfo.HasChecksum() && fDestination.GetWriteOptions().GetEnableSamePageMerging()) {
1441 << "one or more pages were found without a checksum: disabling same-page merging.";
1442 const_cast<ROOT::RNTupleWriteOptions &>(fDestination.GetWriteOptions()).SetEnableSamePageMerging(false);
1443 }
1444
1445 const auto checksumSize = pageInfo.HasChecksum() * RPageStorage::kNBytesPageChecksum;
1446 RPageStorage::RSealedPage &sealedPage = sealedPages[pageIdx];
1447 sealedPage.SetNElements(pageInfo.GetNElements());
1448 sealedPage.SetHasChecksum(pageInfo.HasChecksum());
1449 sealedPage.SetBufferSize(pageInfo.GetLocator().GetNBytesOnStorage() + checksumSize);
1450 sealedPage.SetBuffer(onDiskPage->GetAddress());
1451 // TODO(gparolini): more graceful error handling (skip the page?)
1453 R__ASSERT(onDiskPage && (onDiskPage->GetSize() == sealedPage.GetBufferSize()));
1454
1455 if (needsRecompressing) {
1456 const auto uncompressedSize = srcColElement->GetSize() * sealedPage.GetNElements();
1457 auto &buffer = sealedPageData.fBuffers[pageBufferBaseIdx + pageIdx];
1458 const auto bufSize = uncompressedSize + checksumSize;
1459 // NOTE: we currently allocate the max possible size for this buffer and don't shrink it afterward.
1460 // We might want to introduce an option that trades speed for memory usage and shrink the buffer to fit
1461 // the actual data size after recompressing.
1462 buffer = MakeUninitArray<std::byte>(bufSize);
1463
1464 // clang-format off
1465 RTaskVisitor{fTaskGroup}(RChangeCompressionFunc{
1466 *srcColElement,
1467 outCompression,
1468 sealedPage,
1469 *fPageAlloc,
1470 buffer.get(),
1471 bufSize,
1472 fDestination.GetWriteOptions()
1473 });
1474 // clang-format on
1475 }
1476
1477 ++pageIdx;
1478
1479 } // end of loop over pages
1480
1481 if (fTaskGroup)
1482 fTaskGroup->Wait();
1483
1484 sealedPageData.fPagesV.push_back(std::move(sealedPages));
1485 sealedPageData.fGroups.emplace_back(column.fOutputId, sealedPageData.fPagesV.back().cbegin(),
1486 sealedPageData.fPagesV.back().cend());
1487 } // end loop over common columns
1488
1490}
1491
1492// Iterates over all clusters of `source` and merges their pages into `destination`.
1493// It is assumed that all columns in `commonColumns` are present (and compatible) in both the source and
1494// the destination's schemas.
1495// The pages may be "fast-merged" (i.e. simply copied with no decompression/recompression) if the target
1496// compression is unspecified or matches the original compression settings.
1497ROOT::RResult<void> RNTupleMergeStrategyDefault::MergeSourceClusters(RPageSource &source,
1498 std::span<RColumnMergeInfo> commonColumns,
1499 std::span<const RColumnMergeInfo> extraDstColumns)
1500{
1501 ROOT::Internal::RClusterPool clusterPool{source};
1502
1503 std::vector<RColumnMergeInfo> missingColumns{extraDstColumns.begin(), extraDstColumns.end()};
1504
1505 R__ASSERT(fSrcDescriptor->GetNClusters() == fSrcDescriptor->GetNActiveClusters());
1506 for (const auto &clusterDesc : fSrcDescriptor->GetActiveClusterIterable()) {
1507 const auto nClusterEntries = clusterDesc.GetNEntries();
1508 R__ASSERT(nClusterEntries > 0);
1509
1510 // Deduce which columns are suppressed (cluster by cluster) by exclusion, as:
1511 // (columns in the columnIdMap) - (columns in commonColumns which are not suppressed).
1512 // Note that some suppressed columns may not be in commonColumns because they might not appear at all in the
1513 // current source.
1514 FieldCollectionMap_t<ROOT::DescriptorId_t> columnsInCluster;
1515 using ColumnHandle_t = ROOT::Internal::RPageStorage::ColumnHandle_t;
1516
1517 // NOTE: `commonColumns` contains all columns that appear *somewhere* both in the src and in the dst.
1518 // Just because a column is in `commonColumns` it doesn't mean that each cluster in the source contains
1519 // it, as it may be a deferred column that only has real data in a future cluster. We need to figure out which
1520 // columns are actually present in this cluster so we only merge their pages (the missing columns are handled
1521 // by synthesizing zero pages - see below).
1522
1523 // Convert columns to a ColumnSet for the ClusterPool query
1524 RCluster::ColumnSet_t commonColumnSet;
1525 // Collect all common columns appearing in this cluster into commonColumnSet and reorganize commonColumns so
1526 // that those columns are at the start of it (whereas missing columns are at its end).
1527 // NOTE: it's fine if this scrambles the order of columns: the RNTupleSerializer will sort them by physical ID.
1528 int nCommonColumnsInCluster = 0;
1529 std::partition(commonColumns.begin(), commonColumns.end(), [&](const auto &column) {
1530 if (clusterDesc.ContainsColumn(column.fInputId)) {
1531 const auto &colRange = clusterDesc.GetColumnRange(column.fInputId);
1532 ++nCommonColumnsInCluster;
1533 columnsInCluster[column.fParentFieldDescriptor].push_back(column.fOutputId);
1534 if (!colRange.IsSuppressed()) {
1535 commonColumnSet.emplace(column.fInputId);
1536 return true;
1537 }
1538 fDestination.CommitSuppressedColumn(ColumnHandle_t{column.fOutputId});
1539 }
1540 return false;
1541 });
1542
1543 // Commit all suppressed columns.
1544 // This is a fairly involved operation, as we need to commit all known columns that:
1545 // a) do not appear in extraDstColumns (those are "missing", not suppressed), and
1546 // b) do not appear in commonColumnSet (those are the active columns).
1547 // Not that these may or may not appear in commonColumns as suppressed columns, since they may or may not be
1548 // present in the current source.
1549 // The only way to find all the columns is to go and get them from fColumnIdMap, which keeps track of every
1550 // column we added to the destination so far. However, since it also contains the extraDstColumns, we need to
1551 // specifically only query those columns that belong to a field that has at least 1 column in commonColumns
1552 // (remember that commonColumns contains all columns associated to the common fields for this source).
1553 for (const auto &[fieldDesc, columnIds] : columnsInCluster) {
1554 const auto &fieldFQName = fSrcDescriptor->GetQualifiedFieldName(fieldDesc->GetId());
1555 const auto cardinality = fieldDesc->GetColumnCardinality();
1556 for (auto i = 0u; i < fieldDesc->GetLogicalColumnIds().size(); ++i) {
1557 const auto colIndex = i % cardinality;
1558 const auto reprIndex = i / cardinality;
1559 const auto colName = "." + fieldFQName + '.' + std::to_string(colIndex) + '.' + std::to_string(reprIndex);
1560 const auto colIt = fColumnIdMap.find(colName);
1561 assert(colIt != fColumnIdMap.end());
1562 const auto colOutId = colIt->second.fColumnId;
1563 if (std::find(columnIds.begin(), columnIds.end(), colOutId) == columnIds.end()) {
1564 fDestination.CommitSuppressedColumn(ColumnHandle_t{colOutId});
1565 }
1566 }
1567 }
1568
1569 RSealedPageMergeData sealedPageData;
1570 auto res = MergeCommonColumns(clusterPool, clusterDesc, commonColumns, commonColumnSet, sealedPageData);
1571 if (!res)
1572 return R__FORWARD_ERROR(res);
1573
1574 // Generate zero pages for the missing columns.
1575 // For each cluster, the "missing columns" are the union of the extraDstColumns and the common columns
1576 // that are not present in the cluster.
1577 // Note that this does NOT include suppressed columns, for which no pages are synthesized.
1578 missingColumns.resize(extraDstColumns.size()); // NOTE: this clears all common columns of the previous cluster
1579 for (size_t i = nCommonColumnsInCluster; i < commonColumns.size(); ++i)
1580 missingColumns.push_back(commonColumns[i]);
1581
1582 res = GenerateZeroPagesForColumns(nClusterEntries, missingColumns, sealedPageData);
1583 if (!res)
1584 return R__FORWARD_ERROR(res);
1585
1586 // Commit the pages and the clusters
1587 fDestination.CommitSealedPageV(sealedPageData.fGroups);
1588 fDestination.CommitCluster(nClusterEntries);
1589 }
1590
1591 // TODO(gparolini): when we get serious about huge file support (>~ 100GB) we might want to check here
1592 // the size of the running page list and commit a cluster group when it exceeds some threshold,
1593 // which would prevent the page list from getting too large.
1594 // However, as of today, we aren't really handling such huge files, and even relatively big ones
1595 // such as the CMS dataset have a page list size of about only 2 MB.
1596 // So currently we simply merge all cluster groups into one.
1598}
1599
1600ROOT::RResult<void> RNTupleMergeStrategyDefault::MergeSource(RPageSource &source, RDescriptorsComparison &descCmp)
1601{
1602 //// Extend columns if needed
1603 if (!descCmp.fColReprExtensions.empty()) {
1604 for (const auto &field : descCmp.fExtraDstFields) {
1605 if (field->IsProjectedField())
1606 fProjectionPointees[field->GetProjectionSourceId()].push_back(field);
1607 }
1608
1609 // We need to extend the columns in the proper order, i.e. so that they appear in the same order as
1610 // their first representation. This is to ensure that the pages we write to the cluster are in a consistent
1611 // order as their column descriptors. The page creation order is determined by the order of
1612 // columnInfos.fCommonColumns, which in turn depends on the common fields order (see GatherColumnInfos).
1613 // XXX: do we need this separate sort step? Why not just create this vector directly in
1614 // CompareDescriptorStructure?
1615 std::vector<std::pair<const ROOT::RFieldDescriptor *, std::vector<RColReprExtension>>> colExtensions;
1616 colExtensions.reserve(descCmp.fColReprExtensions.size());
1617 for (const auto &commonField : descCmp.fCommonFields) {
1618 const auto *field = commonField.fDst;
1619 AddColumnExtensionsInFieldOrder(*field, fDstDescriptor, descCmp.fColReprExtensions, colExtensions,
1620 fProjectionPointees);
1621 }
1622 for (const auto &field : descCmp.fExtraSrcFields) {
1623 if (field->IsProjectedField())
1624 fProjectionPointees[field->GetProjectionSourceId()].push_back(field);
1625 }
1626
1627 for (const auto &[fieldDesc, extensions] : colExtensions) {
1628 auto &mappings = descCmp.fColReprMappings[fieldDesc];
1629 for (const auto &extension : extensions) {
1630 const auto firstColumnId = fDestination.AddColumnRepresentation(*fieldDesc, extension.fSourceRepr,
1631 extension.fOrigFirstElementIndex);
1632
1633 // When adding new column representations to an existing field which is the source of some projected
1634 // fields, we need to also add new alias columns to those fields so that they can point to the proper
1635 // representation.
1636 if (auto it = fProjectionPointees.find(fieldDesc->GetId()); it != fProjectionPointees.end()) {
1637 for (const auto &projection : it->second) {
1638 for (auto colIdx = 0u; colIdx < extension.fSourceRepr.size(); ++colIdx)
1639 fDestination.AddAliasColumn(fDstDescriptor, *projection, firstColumnId + colIdx);
1640 }
1641 }
1642 mappings.push_back(extension);
1643 }
1644 }
1645 }
1646
1647 // handle extra dst fields & common fields
1648 auto columnInfos = GatherColumnInfos(descCmp, *fSrcDescriptor);
1649 auto res = MergeSourceClusters(source, columnInfos.fCommonColumns, columnInfos.fExtraDstColumns);
1650 return res;
1651}
1652
1653// =========================================================================================
1654//
1655// MergeStrategySlow
1656//
1657// =========================================================================================
1658namespace ROOT::Internal {
1659// Strategy used for L4 merging (https://github.com/root-project/root/blob/master/tree/ntuple/doc/Merging.md#glossary).
1660// This class must be in ROOT::Experimental::Internal because it befriends ROOT::REntry
1661class RNTupleMergeStrategySlow final : public RNTupleMergeStrategy {
1662 std::unique_ptr<ROOT::REntry> fDstEntry;
1663 std::unique_ptr<RNTupleMergeActions> fActions = std::make_unique<RNTupleMergeActions>();
1665
1666public:
1667 RNTupleMergeStrategySlow(std::unique_ptr<RNTupleModel> dstModel, ROOT::Internal::RPagePersistentSink &dst,
1668 const RNTupleMergeOptions &mergeOpts,
1669 std::unique_ptr<RNTupleMergeActions> actions = std::make_unique<RNTupleMergeActions>())
1670 : RNTupleMergeStrategy(std::move(dstModel), dst, mergeOpts), fActions(std::move(actions))
1671 {
1672 // dstModel is non null iff we are doing an incremental merge. Currently we don't support it.
1673 assert(!dstModel);
1674 }
1675
1677 {
1678 // During slow merging clusters are not necessarily committed at source boundary, so we need to
1679 // manually track this number.
1681 }
1682
1683 void InitSource(RPageSource &source) final
1684 {
1685 source.Attach();
1686 fSrcDescriptor = &source.GetSharedDescriptorGuard().GetConstRef();
1687
1688 if (!fDstModel) {
1690 cmOpts.SetCreateBare(true);
1691 cmOpts.SetReconstructProjections(true);
1692 fDstModel = source.GetSharedDescriptorGuard()->CreateModel(cmOpts);
1693 fDestination.Init(*fDstModel);
1694 fDstEntry = fDstModel->CreateEntry();
1695 }
1696 }
1697
1698 [[nodiscard]] ROOT::RResult<void> ExtendDestinationModel(RDescriptorsComparison &descCmp) final
1699 {
1700 // late model extension for all fExtraSrcFields in Union mode
1701 auto res = ::ExtendDestinationModel(descCmp, *fDstModel, fDestination, *fSrcDescriptor,
1703 /*forSlowMerge=*/true);
1704 // recreate the entry since the model has changed
1705 fDstEntry = fDstModel->CreateEntry();
1706 return res;
1707 }
1708
1709 [[nodiscard]] ROOT::RResult<void> MergeSource(RPageSource &source, RDescriptorsComparison &descCmp) final;
1710
1711 void BeforeCommit() final
1712 {
1713 while (true) {
1714 auto res = fActions->BeforeCommit(*fDstEntry, fMergeContext);
1716 break;
1718 fDstEntry->Append();
1720 break;
1721 }
1722
1723 // TODO: this should happen more often than once at the end and it should depend on the target cluster size...
1726 }
1727 }
1728};
1729} // namespace ROOT::Internal
1730
1732{
1733 const auto &dstDesc = fDestination.GetDescriptor();
1734
1735 // Make sure that types of common fields match
1736 for (const auto &[srcField, dstField] : descCmp.fCommonFields) {
1737 const auto srcType = fSrcDescriptor->GetTypeNameForComparison(*srcField);
1738 const auto dstType = dstDesc.GetTypeNameForComparison(*dstField);
1739 if (srcType != dstType) {
1740 return R__FAIL("Field `" + fSrcDescriptor->GetQualifiedFieldName(srcField->GetId()) +
1741 "` has a type incompatible with a previously-seen field with the same name: (old: " + srcType +
1742 ", new: " + dstType + ")");
1743 }
1744 }
1745
1746 // Impose the destination model on the source. This will ensure we properly schema-evolve the fields if
1747 // necessary (e.g. if this source has fewer/reordered fields than the destination)
1748 auto srcModel = fDstModel->Clone();
1749
1750 // NOTE: this code is almost the same as RNTupleReader::ConnectModel().
1751 // Perhaps it can be made common?
1752 {
1753 auto &fieldZero = ROOT::Internal::GetFieldZeroOfModel(*srcModel);
1755 // We must not use the descriptor guard to prevent recursive locking in field.ConnectPageSource
1756 ROOT::DescriptorId_t fieldZeroId = fSrcDescriptor->GetFieldZeroId();
1757 fieldZero.SetOnDiskId(fieldZeroId);
1758 const auto resetFieldIdAndColumnRepresentatives = [](ROOT::RFieldBase &field, const auto &recursiveCall) -> void {
1760 field.SetColumnRepresentatives({});
1761 for (auto *f : field.GetMutableSubfields()) {
1762 recursiveCall(*f, recursiveCall);
1763 }
1764 };
1765 // Iterate only over fieldZero's direct subfields; their descendants are recursively handled in
1766 // RFieldBase::ConnectPageSource
1767 for (auto *field : fieldZero.GetMutableSubfields()) {
1768 // We need to reset column reprs otherwise ConnectPageSource fails in case we are changing the column reprs
1769 // due to AutoAdjustColumnRepresentatives.
1770 // We also need to reset on disk ids otherwise fields get matched with the wrong ones using the writer's ids
1771 // (which differ from the reader's)
1772 resetFieldIdAndColumnRepresentatives(*field, resetFieldIdAndColumnRepresentatives);
1773
1775 field->SetOnDiskId(fSrcDescriptor->FindFieldId(field->GetFieldName(), fieldZeroId));
1776
1777 // If the OnDiskId is still invalid at this point it means this is an extraDstField
1778 // and we shouldn't/cannot connect it.
1781 else
1783 }
1785 }
1786
1787 auto srcEntry = srcModel->CreateBareEntry();
1788
1789 // Create and connect fields to source and sink and bind them together.
1790 // This causes a read on the src entry to populate the same memory used by the dst entry to write.
1791 for (const auto &[srcFieldDesc, _dstFieldDesc] : descCmp.fCommonFields) {
1792 if (!srcFieldDesc->IsProjectedField()) {
1793 // Reset even common fields to their default value, since they may be Record fields with missing subfields
1794 // in the source (in which case we won't overwrite them with new values and they'd keep the old ones)
1795 fDstEntry->EmplaceNewValue(srcFieldDesc->GetFieldName());
1796 srcEntry->BindValue(srcFieldDesc->GetFieldName(), fDstEntry->GetPtr<void>(srcFieldDesc->GetFieldName()));
1797 }
1798 }
1799 // reset the extraDstfields, to make sure any subfield that's missing from the current source
1800 // is default-initialized (rather than keeping its previous value).
1801 for (const auto *field : descCmp.fExtraDstFields) {
1802 if (!field->IsProjectedField()) {
1803 fDstEntry->EmplaceNewValue(field->GetFieldName());
1804 }
1805 }
1806
1807 // Do the actual data reading/writing
1808 for (auto idx = 0u; idx < fSrcDescriptor->GetNEntries(); ++idx) {
1809 srcEntry->Read(idx);
1811 while (true) {
1812 auto res = fActions->OnFill(*fDstEntry, fMergeContext);
1814 break;
1816 fDstEntry->Append();
1818 break;
1819 }
1820 }
1821
1822 while (true) {
1823 auto res = fActions->OnSourceEnd(*fDstEntry, fMergeContext);
1825 break;
1827 fDstEntry->Append();
1829 break;
1830 }
1831
1833 for (auto &field : ROOT::Internal::GetFieldZeroOfModel(*fDstModel)) {
1835 }
1836 }
1837
1839}
1840
1841// =========================================================================================
1842//
1843// RNTupleMerger
1844//
1845// =========================================================================================
1846RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination,
1847 std::unique_ptr<ROOT::RNTupleModel> model)
1848 : fDestination(std::move(destination)), fModel(std::move(model))
1849{
1851}
1852
1853RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination)
1854 : ROOT::Internal::RNTupleMerger(std::move(destination), nullptr)
1855{
1856}
1857
1858ROOT::RResult<void> RNTupleMerger::Merge(std::span<RPageSource *> sources, const RNTupleMergeOptions &mergeOptsIn)
1859{
1860 RNTupleMergeOptions mergeOpts = mergeOptsIn;
1861
1862 assert(fDestination);
1863
1864 // TODO(gparolini): this is here to allow testing of the slow merging path, it will disappear in the future.
1865 constexpr bool doSlowMerge = false;
1866
1867 // Set compression settings if unset and verify it's compatible with the sink
1868 {
1869 const auto dstCompSettings = fDestination->GetWriteOptions().GetCompression();
1870 if (!mergeOpts.fCompressionSettings) {
1871 mergeOpts.fCompressionSettings = dstCompSettings;
1872 } else if (*mergeOpts.fCompressionSettings != dstCompSettings) {
1873 return R__FAIL(std::string("The compression given to RNTupleMergeOptions is different from that of the "
1874 "sink! (opts: ") +
1875 std::to_string(*mergeOpts.fCompressionSettings) + ", sink: " + std::to_string(dstCompSettings) +
1876 ") This is currently unsupported.");
1877 }
1878 }
1879
1880 // we should have a model if and only if the destination is initialized.
1881 if (!!fModel != fDestination->IsInitialized()) {
1882 return R__FAIL(
1883 "passing an already-initialized destination to RNTupleMerger::Merge (i.e. trying to do incremental "
1884 "merging) can only be done by providing a valid ROOT::RNTupleModel when constructing the RNTupleMerger.");
1885 }
1886
1887 if (doSlowMerge && fModel) {
1888 return R__FAIL("incremental slow merging is currently unsupported.");
1889 }
1890
1891 // NOTE: don't wrap this in a do {} while (0)! It uses continue!
1892#define SKIP_OR_ABORT(errMsg) \
1893 if (mergeOpts.fErrBehavior == ENTupleMergeErrBehavior::kSkip) { \
1894 R__LOG_WARNING(NTupleMergeLog()) << "Skipping RNTuple due to: " << (errMsg); \
1895 continue; \
1896 } else { \
1897 return R__FAIL(errMsg); \
1898 }
1899
1900 auto strat = doSlowMerge ? std::unique_ptr<RNTupleMergeStrategy>(
1901 new RNTupleMergeStrategySlow(std::move(fModel), *fDestination, mergeOpts))
1902 : std::unique_ptr<RNTupleMergeStrategy>(
1903 new RNTupleMergeStrategyDefault(std::move(fModel), *fDestination, mergeOpts));
1904 struct RAttrSetMergeData {
1905 std::unique_ptr<ROOT::Internal::RPagePersistentSink> fSink;
1906 RNTupleMergeStrategySlow fStrategy;
1907
1908 RAttrSetMergeData(std::unique_ptr<ROOT::Internal::RPagePersistentSink> attrSink,
1909 const RNTupleMergeStrategy &parentStrategy, const RNTupleMergeOptions &mergeOpts,
1910 ROOT::NTupleSize_t initialStartOffset)
1911 : fSink(std::move(attrSink)),
1912 fStrategy(nullptr, *fSink, mergeOpts,
1913 std::make_unique<RNTupleAttrMergeActions>(parentStrategy, initialStartOffset))
1914 {
1915 }
1916 };
1917 // Map { attrSetName => mergeData }
1918 std::unordered_map<std::string, RAttrSetMergeData> attrSetsData;
1919
1920 // Merge main loop
1921 for (RPageSource *source : sources) {
1922 strat->InitSource(*source);
1923 {
1924 auto srcDescriptor = source->GetSharedDescriptorGuard();
1925 if (srcDescriptor->GetVersion() > ROOT::RNTuple::GetCurrentVersion()) {
1928 << "RNTuple '" << srcDescriptor->GetName()
1929 << "' has a higher format version than the latest supported by this version "
1930 "of ROOT. Merging will work but some features may be dropped.";
1931 } else {
1932 return R__FAIL("RNTuple '" + srcDescriptor->GetName() +
1933 "' has a higher format version than the latest supported by this version. Refusing to "
1934 "merge, since RNTupleMergeOptions::fVersionBehavior is set to AbortOnHigherVersion.");
1935 }
1936 }
1937
1938 for (const auto &extraTypeInfoDesc : srcDescriptor->GetExtraTypeInfoIterable())
1939 fDestination->UpdateExtraTypeInfo(extraTypeInfoDesc);
1940 }
1941
1942 auto descCmpRes =
1944 if (!descCmpRes) {
1945 SKIP_OR_ABORT(std::string("Source RNTuple has an incompatible schema with the destination:\n") +
1946 descCmpRes.GetError()->GetReport())
1947 }
1948 auto descCmp = descCmpRes.Unwrap();
1949 // TODO: switch to slow merge if we cannot "fast" merge
1950 if (!doSlowMerge && !descCmp.fFastMergeErrors.empty()) {
1951 SKIP_OR_ABORT(std::string("Source RNTuple has an incompatible schema with the destination:\n") +
1952 descCmp.fFastMergeErrors)
1953 }
1954
1955 // If the current source is missing some fields and we're not in Union mode, error
1956 // (if we are in Union mode, MergeSourceClusters will fill the missing fields with default values).
1957 if (mergeOpts.fMergingMode != ENTupleMergingMode::kUnion && !descCmp.fExtraDstFields.empty()) {
1958 std::string msg = "Source RNTuple is missing the following fields:";
1959 for (const auto *field : descCmp.fExtraDstFields) {
1960 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1961 }
1962 SKIP_OR_ABORT(msg)
1963 }
1964
1965 // handle extra src fields
1966 if (!descCmp.fExtraSrcFields.empty()) {
1967 if (mergeOpts.fMergingMode == ENTupleMergingMode::kUnion) {
1968 // late model extension for all fExtraSrcFields in Union mode
1969 auto res = strat->ExtendDestinationModel(descCmp);
1970 if (!res)
1971 return R__FORWARD_ERROR(res);
1972 } else if (mergeOpts.fMergingMode == ENTupleMergingMode::kStrict) {
1973 // If the current source has extra fields and we're in Strict mode, error
1974 std::string msg = "Source RNTuple has extra fields that the destination RNTuple doesn't have:";
1975 for (const auto *field : descCmp.fExtraSrcFields) {
1976 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1977 }
1978 SKIP_OR_ABORT(msg)
1979 }
1980 }
1981
1982 const auto prevNEntries = strat->GetNDstEntries();
1983
1984 auto res = strat->MergeSource(*source, descCmp);
1985 if (!res)
1986 return R__FORWARD_ERROR(res);
1987
1988 // Slow-merge attributes
1989 for (const auto &attrSetDesc : source->GetSharedDescriptorGuard()->GetAttrSetIterable()) {
1990 // Initialize output sink and strategy if it's the first time we encounter this attribute set name
1991 auto attrSetDataIt = attrSetsData.find(attrSetDesc.GetName());
1992 if (attrSetDataIt == attrSetsData.end()) {
1993 auto sinkOpts = RNTupleWriteOptions();
1994 sinkOpts.SetCompression(mergeOpts.fCompressionSettings.value());
1995 auto sink =
1996 std::unique_ptr<ROOT::Internal::RPagePersistentSink>(static_cast<ROOT::Internal::RPagePersistentSink *>(
1997 fDestination->CloneAsHidden(attrSetDesc.GetName(), sinkOpts).release()));
1998 auto [insertedIt, inserted] =
1999 attrSetsData.try_emplace(attrSetDesc.GetName(), std::move(sink), *strat, mergeOpts, prevNEntries);
2000 assert(inserted);
2001 attrSetDataIt = insertedIt;
2002 }
2003 auto &attrSetData = attrSetDataIt->second;
2004
2005 const auto attrLink =
2006 ROOT::Internal::RNTupleLink{attrSetDesc.GetAnchorLocator(), attrSetDesc.GetAnchorLength()};
2007 auto attrSource = source->OpenWithDifferentAnchor(attrLink);
2008 auto &attrStrat = attrSetData.fStrategy;
2009 attrStrat.InitSource(*attrSource);
2010
2011 descCmpRes = CompareDescriptorStructure(attrSetData.fSink->GetDescriptor(),
2012 attrSource->GetSharedDescriptorGuard().GetConstRef(),
2013 /*forAttributeMerging=*/true);
2014 if (!descCmpRes) {
2015 SKIP_OR_ABORT(std::string("Source RNTuple has an incompatible schema with the destination:\n") +
2016 descCmpRes.GetError()->GetReport())
2017 }
2018 descCmp = descCmpRes.Unwrap();
2019
2020 if (!descCmp.fExtraSrcFields.empty() || !descCmp.fExtraSrcSubfields.empty()) {
2021 auto edmRes = attrStrat.ExtendDestinationModel(descCmp);
2022 if (!edmRes)
2023 return R__FORWARD_ERROR(edmRes);
2024 }
2025
2026 auto mergeSourceRes = attrStrat.MergeSource(*attrSource, descCmp);
2027 if (!mergeSourceRes)
2028 return R__FORWARD_ERROR(mergeSourceRes);
2029 }
2030 } // end loop over sources
2031
2032 // Commit the output
2033 for (auto &[attrSetName, attrSetData] : attrSetsData) {
2034 attrSetData.fStrategy.BeforeCommit();
2035 attrSetData.fSink->CommitClusterGroup();
2036 auto anchorLink = attrSetData.fSink->CommitDataset();
2037 fDestination->CommitAttributeSet(attrSetName, anchorLink);
2038 }
2039
2040 strat->BeforeCommit();
2041
2042 // Commit the output
2043 if (fDestination->GetNEntries() == 0)
2044 R__LOG_WARNING(NTupleMergeLog()) << "Output RNTuple '" << fDestination->GetNTupleName() << "' has no entries.";
2045
2046 fDestination->CommitClusterGroup();
2047 fDestination->CommitDataset();
2048
2049 return RResult<void>::Success();
2050}
fBuffer
#define R__FORWARD_ERROR(res)
Short-hand to return an RResult<T> in an error state (i.e. after checking)
Definition RError.hxx:326
#define R__FORWARD_RESULT(res)
Short-hand to return an RResult<T> value from a subroutine to the calling stack frame.
Definition RError.hxx:324
#define R__FAIL(msg)
Short-hand to return an RResult<T> in an error state; the RError is implicitly converted into RResult...
Definition RError.hxx:322
#define R__LOG_WARNING(...)
Definition RLogger.hxx:357
#define R__LOG_ERROR(...)
Definition RLogger.hxx:356
#define R__LOG_INFO(...)
Definition RLogger.hxx:358
static void MatchColumnRepresentations(const ROOT::RNTupleDescriptor &srcDesc, const ROOT::RNTupleDescriptor &dstDesc, const ROOT::RFieldDescriptor &srcField, const ROOT::RFieldDescriptor &dstField, RDescriptorsComparison &result, std::vector< std::string > &errors)
static std::optional< std::type_index > ColumnInMemoryType(std::string_view fieldType, ENTupleColumnType onDiskType)
static void VerifyProjectedFieldsCompatibility(const ROOT::RNTupleDescriptor &dst, const ROOT::RNTupleDescriptor &src, const ROOT::RFieldDescriptor &dstField, const ROOT::RFieldDescriptor &srcField, std::vector< std::string > &errors)
static std::optional< ENTupleMergeErrBehavior > ParseOptionErrBehavior(const TString &opts)
static ROOT::RResult< void > ExtendDestinationModel(RDescriptorsComparison &descCmp, ROOT::RNTupleModel &dstModel, ROOT::Internal::RPageSink &destination, const ROOT::RNTupleDescriptor &srcDesc, ROOT::NTupleSize_t nEntries, bool verbose, bool forSlowMerge)
static ROOT::RResult< RDescriptorsComparison > CompareDescriptorStructure(const ROOT::RNTupleDescriptor &dst, const ROOT::RNTupleDescriptor &src, bool forAttributeMerging=false)
Compares the top level fields of dst and src and determines whether they can be merged or not.
static ROOT::RLogChannel & NTupleMergeLog()
#define SKIP_OR_ABORT(errMsg)
static std::optional< T > ParseStringOption(const TString &opts, const char *pattern, std::initializer_list< std::pair< const char *, T > > validValues)
static ROOT::RResult< RDescriptorsComparison > FillCommonAndExtraFields(const ROOT::RNTupleDescriptor &dst, const ROOT::RNTupleDescriptor &src, bool forAttributeMerging)
static void AddColumnExtensionsInFieldOrder(const ROOT::RFieldDescriptor &field, const ROOT::RNTupleDescriptor &desc, const FieldCollectionMap_t< RColReprExtension > &extensions, std::vector< std::pair< const ROOT::RFieldDescriptor *, std::vector< RColReprExtension > > > &outExtensions, std::unordered_map< ROOT::DescriptorId_t, std::vector< const ROOT::RFieldDescriptor * > > &outProjectionPointees)
static std::optional< ENTupleMergingMode > ParseOptionMergingMode(const TString &opts)
static void PrefillColumnMap(const ROOT::RNTupleDescriptor &desc, const ROOT::RFieldDescriptor &fieldDesc, ColumnIdMap_t &colIdMap, const std::string &prefix="")
static ROOT::RResult< void > RestoreRealFieldParams(ROOT::RFieldBase &field, const ROOT::RFieldDescriptor &fieldDesc, const ROOT::RNTupleDescriptor &srcDesc)
static std::optional< ENTupleMergeVersionBehavior > ParseOptionVersionBehavior(const TString &opts)
static bool BeginsWithDelimitedWord(const TString &str, const char *word)
#define f(i)
Definition RSha256.hxx:104
double * dst
size_t size(const MatrixT &matrix)
retrieve the size of a square matrix
long long Long64_t
Portable signed long integer 8 bytes.
Definition RtypesCore.h:84
#define R__ASSERT(e)
Checks condition e and reports a fatal error if it's false.
Definition TError.h:139
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t Float_t Float_t Int_t Int_t UInt_t UInt_t Rectangle_t result
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t Float_t Float_t Int_t Int_t UInt_t UInt_t Rectangle_t Int_t Int_t Window_t child
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void value
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t src
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t Float_t Float_t Int_t Int_t UInt_t UInt_t Rectangle_t Int_t Int_t Window_t TString Int_t GCValues_t GetPrimarySelectionOwner GetDisplay GetScreen GetColormap GetNativeEvent const char const char dpyName wid window const char font_name cursor keysym reg const char only_if_exist regb h Point_t winding char text const char depth char const char Int_t count const char ColorStruct_t color const char Pixmap_t Pixmap_t PictureAttributes_t attr const char char ret_data h unsigned char height h Atom_t Int_t ULong_t ULong_t unsigned char prop_list Atom_t Atom_t Atom_t Time_t type
char name[80]
Definition TGX11.cxx:142
The available trivial, native content types of a column.
A class to manage the asynchronous execution of work items.
Managed a set of clusters containing compressed and packed pages.
RCluster * GetCluster(ROOT::DescriptorId_t clusterId, const RCluster::ColumnSet_t &physicalColumns)
Returns the requested cluster either from the pool or, in case of a cache miss, lets the I/O thread l...
An in-memory subset of the packed and compressed pages of a cluster.
Definition RCluster.hxx:147
std::unordered_set< ROOT::DescriptorId_t > ColumnSet_t
Definition RCluster.hxx:149
const ROnDiskPage * GetOnDiskPage(const ROnDiskPage::Key &key) const
Definition RCluster.cxx:30
A column element encapsulates the translation between basic C++ types and their column representation...
static const char * GetColumnTypeName(ROOT::ENTupleColumnType type)
static std::unique_ptr< RColumnElementBase > Generate(ENTupleColumnType onDiskType)
std::size_t GetPackedSize(std::size_t nElements=1U) const
The in-memory representation of a 32bit or 64bit on-disk index column.
Holds the index and the tag of a kSwitch column.
static std::size_t Zip(const void *from, std::size_t nbytes, int compression, void *to)
Returns the size of the compressed data, written into the provided output buffer.
static void Unzip(const void *from, size_t nbytes, size_t dataLen, void *to)
The nbytes parameter provides the size ls of the from buffer.
ROOT::RResult< void > ExtendDestinationModel(RDescriptorsComparison &descCmp) final
ROOT::NTupleSize_t GetNDstEntries() const final
RNTupleMergeStrategySlow(std::unique_ptr< RNTupleModel > dstModel, ROOT::Internal::RPagePersistentSink &dst, const RNTupleMergeOptions &mergeOpts, std::unique_ptr< RNTupleMergeActions > actions=std::make_unique< RNTupleMergeActions >())
void InitSource(RPageSource &source) final
std::unique_ptr< RNTupleMergeActions > fActions
std::unique_ptr< ROOT::REntry > fDstEntry
ROOT::RResult< void > MergeSource(RPageSource &source, RDescriptorsComparison &descCmp) final
Given a set of RPageSources merge them into an RPagePersistentSink, optionally changing their compres...
std::unique_ptr< ROOT::RNTupleModel > fModel
RNTupleMerger(std::unique_ptr< ROOT::Internal::RPagePersistentSink > destination, std::unique_ptr< ROOT::RNTupleModel > model)
Creates a RNTupleMerger with the given destination.
std::unique_ptr< ROOT::Internal::RPagePersistentSink > fDestination
RResult< void > Merge(std::span< ROOT::Internal::RPageSource * > sources, const RNTupleMergeOptions &mergeOpts=RNTupleMergeOptions())
Merge a given set of sources into the destination.
A helper class for serializing and deserialization of the RNTuple binary format.
Abstract interface to allocate and release pages.
Base class for a sink with a physical storage backend.
const ROOT::RNTupleDescriptor & GetDescriptor() const final
Return the RNTupleDescriptor being constructed.
Abstract interface to write data into an ntuple.
void Init(RNTupleModel &model)
Physically creates the storage container to hold the ntuple (e.g., a keys a TFile or an S3 bucket) In...
virtual std::uint64_t CommitCluster(ROOT::NTupleSize_t nNewEntries)
Finalize the current cluster and create a new one for the following data.
virtual void UpdateSchema(const ROOT::Internal::RNTupleModelChangeset &changeset, ROOT::NTupleSize_t firstEntry)=0
Incorporate incremental changes to the model into the ntuple descriptor.
RSealedPage SealPage(const ROOT::Internal::RPage &page, const ROOT::Internal::RColumnElementBase &element)
Helper for streaming a page.
virtual const ROOT::RNTupleDescriptor & GetDescriptor() const =0
Return the RNTupleDescriptor being constructed.
Storage provider that reads ntuple pages from a file.
static std::unique_ptr< RPageSourceFile > CreateFromAnchor(const RNTuple &anchor, const ROOT::RNTupleReadOptions &options=ROOT::RNTupleReadOptions())
Used from the RNTuple class to build a datasource if the anchor is already available.
const ROOT::RNTupleDescriptor & GetConstRef() const
Abstract interface to read data from an ntuple.
void Attach(ROOT::Internal::RNTupleSerializer::EDescriptorDeserializeMode mode=ROOT::Internal::RNTupleSerializer::EDescriptorDeserializeMode::kForReading)
Open the physical storage container and deserialize header and footer.
RSharedDescriptorGuard GetSharedDescriptorGuard() const
Takes the read lock for the descriptor.
virtual std::unique_ptr< RPageSource > OpenWithDifferentAnchor(const ROOT::Internal::RNTupleLink &anchorLink, const ROOT::RNTupleReadOptions &options={})=0
Creates a new PageSource using the same underlying file as this but referring to a different RNTuple,...
Common functionality of an ntuple storage for both reading and writing.
static constexpr std::size_t kNBytesPageChecksum
The page checksum is a 64bit xxhash3.
std::deque< RSealedPage > SealedPageSequence_t
RColumnHandle ColumnHandle_t
The column handle identifies a column with the current open page storage.
std::unordered_map< const ROOT::RFieldBase *, const ROOT::RFieldBase * > FieldMap_t
The map keys are the projected target fields, the map values are the backing source fields Note that ...
RResult< void > Add(std::unique_ptr< ROOT::RFieldBase > field, const FieldMap_t &fieldMap)
Adds a new projected field.
Metadata for RNTuple clusters.
ROOT::NTupleSize_t GetNEntries() const
ROOT::DescriptorId_t GetId() const
const RPageRange & GetPageRange(ROOT::DescriptorId_t physicalId) const
bool ContainsColumn(ROOT::DescriptorId_t physicalId) const
const RColumnRange & GetColumnRange(ROOT::DescriptorId_t physicalId) const
ROOT::NTupleSize_t GetFirstEntryIndex() const
ROOT::DescriptorId_t GetPhysicalId() const
ROOT::DescriptorId_t GetLogicalId() const
The REntry is a collection of values in an RNTuple corresponding to a complete row in the data set.
Definition REntry.hxx:55
Base class for all ROOT issued exceptions.
Definition RError.hxx:78
A field translates read and write calls from/to underlying columns to/from tree values.
void SetColumnRepresentatives(const RColumnRepresentations::Selection_t &representatives)
Fixes a column representative.
void SetOnDiskId(ROOT::DescriptorId_t id)
std::vector< RFieldBase * > GetMutableSubfields()
std::string GetQualifiedFieldName() const
Returns the field name and parent field names separated by dots (grandparent.parent....
const std::string & GetFieldName() const
std::vector< ROOT::ENTupleColumnType > ColumnRepresentation_t
ROOT::DescriptorId_t GetOnDiskId() const
Metadata stored for every field of an RNTuple.
const std::vector< ROOT::DescriptorId_t > & GetLogicalColumnIds() const
ROOT::ENTupleStructure GetStructure() const
std::uint32_t GetColumnCardinality() const
const std::vector< ROOT::DescriptorId_t > & GetLinkIds() const
ROOT::DescriptorId_t GetParentId() const
std::uint64_t GetNRepetitions() const
ROOT::DescriptorId_t GetProjectionSourceId() const
const std::string & GetFieldName() const
const std::string & GetTypeName() const
A log configuration for a channel, e.g.
Definition RLogger.hxx:97
The on-storage metadata of an RNTuple.
const RColumnDescriptor & GetColumnDescriptor(ROOT::DescriptorId_t columnId) const
const RFieldDescriptor & GetFieldDescriptor(ROOT::DescriptorId_t fieldId) const
RNTupleAttrSetDescriptorIterable GetAttrSetIterable() const
const std::string & GetName() const
ROOT::NTupleSize_t R__DEPRECATED(6, 46, "This function is ill-conceived in the descriptor " "as not all cluster descriptors may be present. This interface is no longer publicly exposed.") GetNElements(ROOT ROOT::DescriptorId_t GetFieldZeroId() const
Returns the logical parent of all top-level RNTuple data fields.
ROOT::NTupleSize_t GetNEntries() const
We know the number of entries from adding the cluster summaries.
std::unique_ptr< ROOT::RNTupleModel > CreateModel(const RCreateModelOptions &options=RCreateModelOptions()) const
Re-create the C++ model from the stored metadata.
std::string GetTypeNameForComparison(const RFieldDescriptor &fieldDesc) const
Adjust the type name of the passed RFieldDescriptor for comparison with another renormalized type nam...
ROOT::DescriptorId_t FindFieldId(std::string_view fieldName, ROOT::DescriptorId_t parentId) const
ROOT::DescriptorId_t R__DEPRECATED(6, 46, "This function is ill-defined in the descriptor " "as not all cluster descriptors may be present. This interface is no longer exposed.") FindClusterId(ROOT ROOT::DescriptorId_t R__DEPRECATED(6, 46, "This function is ill-defined in the descriptor " "as not all cluster descriptors may be present. This interface is no longer exposed.") FindNextClusterId(ROOT ROOT::DescriptorId_t R__DEPRECATED(6, 46, "This function is ill-defined in the descriptor " "as not all cluster descriptors may be present. This interface is no longer exposed.") FindPrevClusterId(ROOT std::string GetQualifiedFieldName(ROOT::DescriptorId_t fieldId) const
Walks up the parents of the field ID and returns a field name of the form a.b.c.d In case of invalid ...
The RNTupleModel encapulates the schema of an RNTuple.
void Unfreeze()
Transitions an RNTupleModel from the frozen state back to the building state, invalidating all previo...
void Freeze()
Transitions an RNTupleModel from the building state to the frozen state, disabling adding additional ...
const ROOT::RFieldBase & GetConstField(std::string_view fieldName) const
Common user-tunable settings for storing RNTuples.
void SetCompression(std::uint32_t val)
Representation of an RNTuple data set in a ROOT file.
Definition RNTuple.hxx:67
Long64_t Merge(TCollection *input, TFileMergeInfo *mergeInfo)
RNTuple implements the hadd MergeFile interface Merge this NTuple with the input list entries.
static constexpr std::uint64_t GetCurrentVersion()
Returns the RNTuple version in the following form: Epoch: 2 most significant bytes Major: next 2 byte...
Definition RNTuple.hxx:90
void SetQuantized(std::size_t nBits, std::pair< T, T > valueRange)
Sets this field to use a quantized integer representation using nBits per value.
void SetTruncated(std::size_t nBits)
Set the on-disk representation of this field to a single-precision float truncated to nBits.
void ThrowOnError()
Short-hand method to throw an exception in the case of errors.
Definition RError.hxx:312
The class is used as a return type for operations that can fail; wraps a value of type T or an RError...
Definition RError.hxx:222
Collection abstract base class.
Definition TCollection.h:65
virtual Int_t GetEntries() const
TKey * FindKey(const char *keyname) const override
Find key with name keyname in the current directory.
TObject * Get(const char *namecycle) override
Return pointer to object identified by namecycle.
A class to pass information from the TFileMerger to the objects being merged.
TString fOptions
Additional text based option being passed down to customize the merge.
A file, usually with extension .root, that stores data and code in the form of serialized objects in ...
Definition TFile.h:130
Int_t GetCompressionSettings() const
Definition TFile.h:489
Book space in a file, create I/O buffers, to fill them, (un)compress them.
Definition TKey.h:28
T * ReadObject()
To read an object (non deriving from TObject) from the file.
Definition TKey.h:105
const char * GetName() const override
Returns name of object.
Definition TNamed.h:49
Mother of all ROOT objects.
Definition TObject.h:42
virtual const char * ClassName() const
Returns name of class to which the object belongs.
Definition TObject.cxx:226
Basic string class.
Definition TString.h:137
Ssiz_t Length() const
Definition TString.h:426
@ kIgnoreCase
Definition TString.h:284
Bool_t Contains(const char *pat, ECaseCompare cmp=kExact) const
Definition TString.h:642
Ssiz_t Index(const char *pat, Ssiz_t i=0, ECaseCompare cmp=kExact) const
Definition TString.h:661
Double_t ex[n]
Definition legend1.C:17
constexpr const char * kMetaFieldNames[]
void SetAllowFieldSubstitutions(RFieldZero &fieldZero, bool val)
Definition RField.cxx:35
ROOT::RFieldZero & GetFieldZeroOfModel(RNTupleModel &model)
void CallConnectPageSourceOnField(RFieldBase &, ROOT::Internal::RPageSource &)
std::unique_ptr< T[]> MakeUninitArray(std::size_t size)
Make an array of default-initialized elements.
@ kWarnOnHigherVersion
The merger will emit a warning when merging RNTuples with higher version than the latest supported by...
void CallFlushColumnsOnField(RFieldBase &)
@ kStrict
The merger will refuse to merge any 2 RNTuples whose schema doesn't match exactly.
@ kUnion
The merger will update the output model to include all columns from all sources.
void CallSetArtificialOnField(RFieldBase &)
RProjectedFields & GetProjectedFieldsOfModel(RNTupleModel &model)
std::unique_ptr< RColumnElementBase > GenerateColumnElement(std::type_index inMemoryType, ROOT::ENTupleColumnType onDiskType)
std::string GetRenormalizedTypeName(const std::string &metaNormalizedName)
Given a type name normalized by ROOT meta, renormalize it for RNTuple. E.g., insert std::prefix.
Bool_t IsImplicitMTEnabled()
Returns true if the implicit multi-threading in ROOT is enabled.
Definition TROOT.cxx:673
std::uint64_t DescriptorId_t
Distriniguishes elements of the same type within a descriptor, e.g. different fields.
std::uint64_t NTupleSize_t
Integer type long enough to hold the maximum number of entries in a column.
constexpr DescriptorId_t kInvalidDescriptorId
if(fPos !=fText.size()) Fail("unexpected trailing content")
Information regarding the current slow merge process that gets passed to the RNTupleMergeActions.
Set of merging options to pass to RNTupleMerger.
ENTupleMergeVersionBehavior fVersionBehavior
Determines how the Merge function behaves depending on the RNTuple sources' version.
bool fExtraVerbose
If true, the merger will emit further diagnostics and information.
std::optional< std::uint32_t > fCompressionSettings
If fCompressionSettings is empty (the default), the merger will not change the compression of any of ...
ENTupleMergingMode fMergingMode
Determines how the merging treats sources with different models (.
ENTupleMergeErrBehavior fErrBehavior
Determines how the Merge function behaves upon merging errors.
The incremental changes to a RNTupleModel
On-disk pages within a page source are identified by the column and page number.
Definition RCluster.hxx:50
Parameters for the SealPage() method.
bool fWriteChecksum
Adds a 8 byte little-endian xxhash3 checksum to the page payload.
std::uint32_t fCompressionSettings
Compression algorithm and level to apply.
void * fBuffer
Location for sealed output. The memory buffer has to be large enough.
const ROOT::Internal::RPage * fPage
Input page to be sealed.
const ROOT::Internal::RColumnElementBase * fElement
Corresponds to the page's elements, for size calculation etc.
A sealed page contains the bytes of a page as written to storage (packed & compressed).
RResult< void > VerifyChecksumIfEnabled() const
void SetNElements(std::uint32_t nElements)
void SetBufferSize(std::size_t bufferSize)
@ kUseGeneralPurpose
Use the new recommended general-purpose setting; it is a best trade-off between compression ratio/dec...
Definition Compression.h:58