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) {
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 {
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 const auto checksumSize = pageInfo.HasChecksum() * RPageStorage::kNBytesPageChecksum;
1440 RPageStorage::RSealedPage &sealedPage = sealedPages[pageIdx];
1441 sealedPage.SetNElements(pageInfo.GetNElements());
1442 sealedPage.SetHasChecksum(pageInfo.HasChecksum());
1443 sealedPage.SetBufferSize(pageInfo.GetLocator().GetNBytesOnStorage() + checksumSize);
1444 sealedPage.SetBuffer(onDiskPage->GetAddress());
1445 // TODO(gparolini): more graceful error handling (skip the page?)
1447 R__ASSERT(onDiskPage && (onDiskPage->GetSize() == sealedPage.GetBufferSize()));
1448
1449 if (needsRecompressing) {
1450 const auto uncompressedSize = srcColElement->GetSize() * sealedPage.GetNElements();
1451 auto &buffer = sealedPageData.fBuffers[pageBufferBaseIdx + pageIdx];
1452 const auto bufSize = uncompressedSize + checksumSize;
1453 // NOTE: we currently allocate the max possible size for this buffer and don't shrink it afterward.
1454 // We might want to introduce an option that trades speed for memory usage and shrink the buffer to fit
1455 // the actual data size after recompressing.
1456 buffer = MakeUninitArray<std::byte>(bufSize);
1457
1458 // clang-format off
1459 RTaskVisitor{fTaskGroup}(RChangeCompressionFunc{
1460 *srcColElement,
1461 outCompression,
1462 sealedPage,
1463 *fPageAlloc,
1464 buffer.get(),
1465 bufSize,
1466 fDestination.GetWriteOptions()
1467 });
1468 // clang-format on
1469 }
1470
1471 ++pageIdx;
1472
1473 } // end of loop over pages
1474
1475 if (fTaskGroup)
1476 fTaskGroup->Wait();
1477
1478 sealedPageData.fPagesV.push_back(std::move(sealedPages));
1479 sealedPageData.fGroups.emplace_back(column.fOutputId, sealedPageData.fPagesV.back().cbegin(),
1480 sealedPageData.fPagesV.back().cend());
1481 } // end loop over common columns
1482
1484}
1485
1486// Iterates over all clusters of `source` and merges their pages into `destination`.
1487// It is assumed that all columns in `commonColumns` are present (and compatible) in both the source and
1488// the destination's schemas.
1489// The pages may be "fast-merged" (i.e. simply copied with no decompression/recompression) if the target
1490// compression is unspecified or matches the original compression settings.
1491ROOT::RResult<void> RNTupleMergeStrategyDefault::MergeSourceClusters(RPageSource &source,
1492 std::span<RColumnMergeInfo> commonColumns,
1493 std::span<const RColumnMergeInfo> extraDstColumns)
1494{
1495 ROOT::Internal::RClusterPool clusterPool{source};
1496
1497 std::vector<RColumnMergeInfo> missingColumns{extraDstColumns.begin(), extraDstColumns.end()};
1498
1499 R__ASSERT(fSrcDescriptor->GetNClusters() == fSrcDescriptor->GetNActiveClusters());
1500 for (const auto &clusterDesc : fSrcDescriptor->GetActiveClusterIterable()) {
1501 const auto nClusterEntries = clusterDesc.GetNEntries();
1502 R__ASSERT(nClusterEntries > 0);
1503
1504 // Deduce which columns are suppressed (cluster by cluster) by exclusion, as:
1505 // (columns in the columnIdMap) - (columns in commonColumns which are not suppressed).
1506 // Note that some suppressed columns may not be in commonColumns because they might not appear at all in the
1507 // current source.
1508 FieldCollectionMap_t<ROOT::DescriptorId_t> columnsInCluster;
1509 using ColumnHandle_t = ROOT::Internal::RPageStorage::ColumnHandle_t;
1510
1511 // NOTE: `commonColumns` contains all columns that appear *somewhere* both in the src and in the dst.
1512 // Just because a column is in `commonColumns` it doesn't mean that each cluster in the source contains
1513 // it, as it may be a deferred column that only has real data in a future cluster. We need to figure out which
1514 // columns are actually present in this cluster so we only merge their pages (the missing columns are handled
1515 // by synthesizing zero pages - see below).
1516
1517 // Convert columns to a ColumnSet for the ClusterPool query
1518 RCluster::ColumnSet_t commonColumnSet;
1519 // Collect all common columns appearing in this cluster into commonColumnSet and reorganize commonColumns so
1520 // that those columns are at the start of it (whereas missing columns are at its end).
1521 // NOTE: it's fine if this scrambles the order of columns: the RNTupleSerializer will sort them by physical ID.
1522 int nCommonColumnsInCluster = 0;
1523 std::partition(commonColumns.begin(), commonColumns.end(), [&](const auto &column) {
1524 if (clusterDesc.ContainsColumn(column.fInputId)) {
1525 const auto &colRange = clusterDesc.GetColumnRange(column.fInputId);
1526 ++nCommonColumnsInCluster;
1527 columnsInCluster[column.fParentFieldDescriptor].push_back(column.fOutputId);
1528 if (!colRange.IsSuppressed()) {
1529 commonColumnSet.emplace(column.fInputId);
1530 return true;
1531 }
1532 fDestination.CommitSuppressedColumn(ColumnHandle_t{column.fOutputId});
1533 }
1534 return false;
1535 });
1536
1537 // Commit all suppressed columns.
1538 // This is a fairly involved operation, as we need to commit all known columns that:
1539 // a) do not appear in extraDstColumns (those are "missing", not suppressed), and
1540 // b) do not appear in commonColumnSet (those are the active columns).
1541 // Not that these may or may not appear in commonColumns as suppressed columns, since they may or may not be
1542 // present in the current source.
1543 // The only way to find all the columns is to go and get them from fColumnIdMap, which keeps track of every
1544 // column we added to the destination so far. However, since it also contains the extraDstColumns, we need to
1545 // specifically only query those columns that belong to a field that has at least 1 column in commonColumns
1546 // (remember that commonColumns contains all columns associated to the common fields for this source).
1547 for (const auto &[fieldDesc, columnIds] : columnsInCluster) {
1548 const auto &fieldFQName = fSrcDescriptor->GetQualifiedFieldName(fieldDesc->GetId());
1549 const auto cardinality = fieldDesc->GetColumnCardinality();
1550 for (auto i = 0u; i < fieldDesc->GetLogicalColumnIds().size(); ++i) {
1551 const auto colIndex = i % cardinality;
1552 const auto reprIndex = i / cardinality;
1553 const auto colName = "." + fieldFQName + '.' + std::to_string(colIndex) + '.' + std::to_string(reprIndex);
1554 const auto colIt = fColumnIdMap.find(colName);
1555 assert(colIt != fColumnIdMap.end());
1556 const auto colOutId = colIt->second.fColumnId;
1557 if (std::find(columnIds.begin(), columnIds.end(), colOutId) == columnIds.end()) {
1558 fDestination.CommitSuppressedColumn(ColumnHandle_t{colOutId});
1559 }
1560 }
1561 }
1562
1563 RSealedPageMergeData sealedPageData;
1564 auto res = MergeCommonColumns(clusterPool, clusterDesc, commonColumns, commonColumnSet, sealedPageData);
1565 if (!res)
1566 return R__FORWARD_ERROR(res);
1567
1568 // Generate zero pages for the missing columns.
1569 // For each cluster, the "missing columns" are the union of the extraDstColumns and the common columns
1570 // that are not present in the cluster.
1571 // Note that this does NOT include suppressed columns, for which no pages are synthesized.
1572 missingColumns.resize(extraDstColumns.size()); // NOTE: this clears all common columns of the previous cluster
1573 for (size_t i = nCommonColumnsInCluster; i < commonColumns.size(); ++i)
1574 missingColumns.push_back(commonColumns[i]);
1575
1576 res = GenerateZeroPagesForColumns(nClusterEntries, missingColumns, sealedPageData);
1577 if (!res)
1578 return R__FORWARD_ERROR(res);
1579
1580 // Commit the pages and the clusters
1581 fDestination.CommitSealedPageV(sealedPageData.fGroups);
1582 fDestination.CommitCluster(nClusterEntries);
1583 }
1584
1585 // TODO(gparolini): when we get serious about huge file support (>~ 100GB) we might want to check here
1586 // the size of the running page list and commit a cluster group when it exceeds some threshold,
1587 // which would prevent the page list from getting too large.
1588 // However, as of today, we aren't really handling such huge files, and even relatively big ones
1589 // such as the CMS dataset have a page list size of about only 2 MB.
1590 // So currently we simply merge all cluster groups into one.
1592}
1593
1594ROOT::RResult<void> RNTupleMergeStrategyDefault::MergeSource(RPageSource &source, RDescriptorsComparison &descCmp)
1595{
1596 //// Extend columns if needed
1597 if (!descCmp.fColReprExtensions.empty()) {
1598 for (const auto &field : descCmp.fExtraDstFields) {
1599 if (field->IsProjectedField())
1600 fProjectionPointees[field->GetProjectionSourceId()].push_back(field);
1601 }
1602
1603 // We need to extend the columns in the proper order, i.e. so that they appear in the same order as
1604 // their first representation. This is to ensure that the pages we write to the cluster are in a consistent
1605 // order as their column descriptors. The page creation order is determined by the order of
1606 // columnInfos.fCommonColumns, which in turn depends on the common fields order (see GatherColumnInfos).
1607 // XXX: do we need this separate sort step? Why not just create this vector directly in
1608 // CompareDescriptorStructure?
1609 std::vector<std::pair<const ROOT::RFieldDescriptor *, std::vector<RColReprExtension>>> colExtensions;
1610 colExtensions.reserve(descCmp.fColReprExtensions.size());
1611 for (const auto &commonField : descCmp.fCommonFields) {
1612 const auto *field = commonField.fDst;
1613 AddColumnExtensionsInFieldOrder(*field, fDstDescriptor, descCmp.fColReprExtensions, colExtensions,
1614 fProjectionPointees);
1615 }
1616 for (const auto &field : descCmp.fExtraSrcFields) {
1617 if (field->IsProjectedField())
1618 fProjectionPointees[field->GetProjectionSourceId()].push_back(field);
1619 }
1620
1621 for (const auto &[fieldDesc, extensions] : colExtensions) {
1622 auto &mappings = descCmp.fColReprMappings[fieldDesc];
1623 for (const auto &extension : extensions) {
1624 const auto firstColumnId = fDestination.AddColumnRepresentation(*fieldDesc, extension.fSourceRepr,
1625 extension.fOrigFirstElementIndex);
1626
1627 // When adding new column representations to an existing field which is the source of some projected
1628 // fields, we need to also add new alias columns to those fields so that they can point to the proper
1629 // representation.
1630 if (auto it = fProjectionPointees.find(fieldDesc->GetId()); it != fProjectionPointees.end()) {
1631 for (const auto &projection : it->second) {
1632 for (auto colIdx = 0u; colIdx < extension.fSourceRepr.size(); ++colIdx)
1633 fDestination.AddAliasColumn(fDstDescriptor, *projection, firstColumnId + colIdx);
1634 }
1635 }
1636 mappings.push_back(extension);
1637 }
1638 }
1639 }
1640
1641 // handle extra dst fields & common fields
1642 auto columnInfos = GatherColumnInfos(descCmp, *fSrcDescriptor);
1643 auto res = MergeSourceClusters(source, columnInfos.fCommonColumns, columnInfos.fExtraDstColumns);
1644 return res;
1645}
1646
1647// =========================================================================================
1648//
1649// MergeStrategySlow
1650//
1651// =========================================================================================
1652namespace ROOT::Internal {
1653// Strategy used for L4 merging (https://github.com/root-project/root/blob/master/tree/ntuple/doc/Merging.md#glossary).
1654// This class must be in ROOT::Experimental::Internal because it befriends ROOT::REntry
1655class RNTupleMergeStrategySlow final : public RNTupleMergeStrategy {
1656 std::unique_ptr<ROOT::REntry> fDstEntry;
1657 std::unique_ptr<RNTupleMergeActions> fActions = std::make_unique<RNTupleMergeActions>();
1659
1660public:
1661 RNTupleMergeStrategySlow(std::unique_ptr<RNTupleModel> dstModel, ROOT::Internal::RPagePersistentSink &dst,
1662 const RNTupleMergeOptions &mergeOpts,
1663 std::unique_ptr<RNTupleMergeActions> actions = std::make_unique<RNTupleMergeActions>())
1664 : RNTupleMergeStrategy(std::move(dstModel), dst, mergeOpts), fActions(std::move(actions))
1665 {
1666 // dstModel is non null iff we are doing an incremental merge. Currently we don't support it.
1667 assert(!dstModel);
1668 }
1669
1671 {
1672 // During slow merging clusters are not necessarily committed at source boundary, so we need to
1673 // manually track this number.
1675 }
1676
1677 void InitSource(RPageSource &source) final
1678 {
1679 source.Attach();
1680 fSrcDescriptor = &source.GetSharedDescriptorGuard().GetConstRef();
1681
1682 if (!fDstModel) {
1684 cmOpts.SetCreateBare(true);
1685 cmOpts.SetReconstructProjections(true);
1686 fDstModel = source.GetSharedDescriptorGuard()->CreateModel(cmOpts);
1687 fDestination.Init(*fDstModel);
1688 fDstEntry = fDstModel->CreateEntry();
1689 }
1690 }
1691
1692 [[nodiscard]] ROOT::RResult<void> ExtendDestinationModel(RDescriptorsComparison &descCmp) final
1693 {
1694 // late model extension for all fExtraSrcFields in Union mode
1695 auto res = ::ExtendDestinationModel(descCmp, *fDstModel, fDestination, *fSrcDescriptor,
1697 /*forSlowMerge=*/true);
1698 // recreate the entry since the model has changed
1699 fDstEntry = fDstModel->CreateEntry();
1700 return res;
1701 }
1702
1703 [[nodiscard]] ROOT::RResult<void> MergeSource(RPageSource &source, RDescriptorsComparison &descCmp) final;
1704
1705 void BeforeCommit() final
1706 {
1707 while (true) {
1708 auto res = fActions->BeforeCommit(*fDstEntry, fMergeContext);
1710 break;
1712 fDstEntry->Append();
1714 break;
1715 }
1716
1717 // TODO: this should happen more often than once at the end and it should depend on the target cluster size...
1720 }
1721 }
1722};
1723} // namespace ROOT::Internal
1724
1726{
1727 const auto &dstDesc = fDestination.GetDescriptor();
1728
1729 // Make sure that types of common fields match
1730 for (const auto &[srcField, dstField] : descCmp.fCommonFields) {
1731 const auto srcType = fSrcDescriptor->GetTypeNameForComparison(*srcField);
1732 const auto dstType = dstDesc.GetTypeNameForComparison(*dstField);
1733 if (srcType != dstType) {
1734 return R__FAIL("Field `" + fSrcDescriptor->GetQualifiedFieldName(srcField->GetId()) +
1735 "` has a type incompatible with a previously-seen field with the same name: (old: " + srcType +
1736 ", new: " + dstType + ")");
1737 }
1738 }
1739
1740 // Impose the destination model on the source. This will ensure we properly schema-evolve the fields if
1741 // necessary (e.g. if this source has fewer/reordered fields than the destination)
1742 auto srcModel = fDstModel->Clone();
1743
1744 // NOTE: this code is almost the same as RNTupleReader::ConnectModel().
1745 // Perhaps it can be made common?
1746 {
1747 auto &fieldZero = ROOT::Internal::GetFieldZeroOfModel(*srcModel);
1749 // We must not use the descriptor guard to prevent recursive locking in field.ConnectPageSource
1750 ROOT::DescriptorId_t fieldZeroId = fSrcDescriptor->GetFieldZeroId();
1751 fieldZero.SetOnDiskId(fieldZeroId);
1752 const auto resetFieldIdAndColumnRepresentatives = [](ROOT::RFieldBase &field, const auto &recursiveCall) -> void {
1754 field.SetColumnRepresentatives({});
1755 for (auto *f : field.GetMutableSubfields()) {
1756 recursiveCall(*f, recursiveCall);
1757 }
1758 };
1759 // Iterate only over fieldZero's direct subfields; their descendants are recursively handled in
1760 // RFieldBase::ConnectPageSource
1761 for (auto *field : fieldZero.GetMutableSubfields()) {
1762 // We need to reset column reprs otherwise ConnectPageSource fails in case we are changing the column reprs
1763 // due to AutoAdjustColumnRepresentatives.
1764 // We also need to reset on disk ids otherwise fields get matched with the wrong ones using the writer's ids
1765 // (which differ from the reader's)
1766 resetFieldIdAndColumnRepresentatives(*field, resetFieldIdAndColumnRepresentatives);
1767
1769 field->SetOnDiskId(fSrcDescriptor->FindFieldId(field->GetFieldName(), fieldZeroId));
1770
1771 // If the OnDiskId is still invalid at this point it means this is an extraDstField
1772 // and we shouldn't/cannot connect it.
1775 else
1777 }
1779 }
1780
1781 auto srcEntry = srcModel->CreateBareEntry();
1782
1783 // Create and connect fields to source and sink and bind them together.
1784 // This causes a read on the src entry to populate the same memory used by the dst entry to write.
1785 for (const auto &[srcFieldDesc, _dstFieldDesc] : descCmp.fCommonFields) {
1786 if (!srcFieldDesc->IsProjectedField()) {
1787 // Reset even common fields to their default value, since they may be Record fields with missing subfields
1788 // in the source (in which case we won't overwrite them with new values and they'd keep the old ones)
1789 fDstEntry->EmplaceNewValue(srcFieldDesc->GetFieldName());
1790 srcEntry->BindValue(srcFieldDesc->GetFieldName(), fDstEntry->GetPtr<void>(srcFieldDesc->GetFieldName()));
1791 }
1792 }
1793 // reset the extraDstfields, to make sure any subfield that's missing from the current source
1794 // is default-initialized (rather than keeping its previous value).
1795 for (const auto *field : descCmp.fExtraDstFields) {
1796 if (!field->IsProjectedField()) {
1797 fDstEntry->EmplaceNewValue(field->GetFieldName());
1798 }
1799 }
1800
1801 // Do the actual data reading/writing
1802 for (auto idx = 0u; idx < fSrcDescriptor->GetNEntries(); ++idx) {
1803 srcEntry->Read(idx);
1805 while (true) {
1806 auto res = fActions->OnFill(*fDstEntry, fMergeContext);
1808 break;
1810 fDstEntry->Append();
1812 break;
1813 }
1814 }
1815
1816 while (true) {
1817 auto res = fActions->OnSourceEnd(*fDstEntry, fMergeContext);
1819 break;
1821 fDstEntry->Append();
1823 break;
1824 }
1825
1827 for (auto &field : ROOT::Internal::GetFieldZeroOfModel(*fDstModel)) {
1829 }
1830 }
1831
1833}
1834
1835// =========================================================================================
1836//
1837// RNTupleMerger
1838//
1839// =========================================================================================
1840RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination,
1841 std::unique_ptr<ROOT::RNTupleModel> model)
1842 : fDestination(std::move(destination)), fModel(std::move(model))
1843{
1845}
1846
1847RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination)
1848 : ROOT::Internal::RNTupleMerger(std::move(destination), nullptr)
1849{
1850}
1851
1852ROOT::RResult<void> RNTupleMerger::Merge(std::span<RPageSource *> sources, const RNTupleMergeOptions &mergeOptsIn)
1853{
1854 RNTupleMergeOptions mergeOpts = mergeOptsIn;
1855
1856 assert(fDestination);
1857
1858 // TODO(gparolini): this is here to allow testing of the slow merging path, it will disappear in the future.
1859 constexpr bool doSlowMerge = false;
1860
1861 // Set compression settings if unset and verify it's compatible with the sink
1862 {
1863 const auto dstCompSettings = fDestination->GetWriteOptions().GetCompression();
1864 if (!mergeOpts.fCompressionSettings) {
1865 mergeOpts.fCompressionSettings = dstCompSettings;
1866 } else if (*mergeOpts.fCompressionSettings != dstCompSettings) {
1867 return R__FAIL(std::string("The compression given to RNTupleMergeOptions is different from that of the "
1868 "sink! (opts: ") +
1869 std::to_string(*mergeOpts.fCompressionSettings) + ", sink: " + std::to_string(dstCompSettings) +
1870 ") This is currently unsupported.");
1871 }
1872 }
1873
1874 // we should have a model if and only if the destination is initialized.
1875 if (!!fModel != fDestination->IsInitialized()) {
1876 return R__FAIL(
1877 "passing an already-initialized destination to RNTupleMerger::Merge (i.e. trying to do incremental "
1878 "merging) can only be done by providing a valid ROOT::RNTupleModel when constructing the RNTupleMerger.");
1879 }
1880
1881 if (doSlowMerge && fModel) {
1882 return R__FAIL("incremental slow merging is currently unsupported.");
1883 }
1884
1885 // NOTE: don't wrap this in a do {} while (0)! It uses continue!
1886#define SKIP_OR_ABORT(errMsg) \
1887 if (mergeOpts.fErrBehavior == ENTupleMergeErrBehavior::kSkip) { \
1888 R__LOG_WARNING(NTupleMergeLog()) << "Skipping RNTuple due to: " << (errMsg); \
1889 continue; \
1890 } else { \
1891 return R__FAIL(errMsg); \
1892 }
1893
1894 auto strat = doSlowMerge ? std::unique_ptr<RNTupleMergeStrategy>(
1895 new RNTupleMergeStrategySlow(std::move(fModel), *fDestination, mergeOpts))
1896 : std::unique_ptr<RNTupleMergeStrategy>(
1897 new RNTupleMergeStrategyDefault(std::move(fModel), *fDestination, mergeOpts));
1898 struct RAttrSetMergeData {
1899 std::unique_ptr<ROOT::Internal::RPagePersistentSink> fSink;
1900 RNTupleMergeStrategySlow fStrategy;
1901
1902 RAttrSetMergeData(std::unique_ptr<ROOT::Internal::RPagePersistentSink> attrSink,
1903 const RNTupleMergeStrategy &parentStrategy, const RNTupleMergeOptions &mergeOpts,
1904 ROOT::NTupleSize_t initialStartOffset)
1905 : fSink(std::move(attrSink)),
1906 fStrategy(nullptr, *fSink, mergeOpts,
1907 std::make_unique<RNTupleAttrMergeActions>(parentStrategy, initialStartOffset))
1908 {
1909 }
1910 };
1911 // Map { attrSetName => mergeData }
1912 std::unordered_map<std::string, RAttrSetMergeData> attrSetsData;
1913
1914 // Merge main loop
1915 for (RPageSource *source : sources) {
1916 strat->InitSource(*source);
1917 {
1918 auto srcDescriptor = source->GetSharedDescriptorGuard();
1919 if (srcDescriptor->GetVersion() > ROOT::RNTuple::GetCurrentVersion()) {
1922 << "RNTuple '" << srcDescriptor->GetName()
1923 << "' has a higher format version than the latest supported by this version "
1924 "of ROOT. Merging will work but some features may be dropped.";
1925 } else {
1926 return R__FAIL("RNTuple '" + srcDescriptor->GetName() +
1927 "' has a higher format version than the latest supported by this version. Refusing to "
1928 "merge, since RNTupleMergeOptions::fVersionBehavior is set to AbortOnHigherVersion.");
1929 }
1930 }
1931
1932 for (const auto &extraTypeInfoDesc : srcDescriptor->GetExtraTypeInfoIterable())
1933 fDestination->UpdateExtraTypeInfo(extraTypeInfoDesc);
1934 }
1935
1936 auto descCmpRes =
1938 if (!descCmpRes) {
1939 SKIP_OR_ABORT(std::string("Source RNTuple has an incompatible schema with the destination:\n") +
1940 descCmpRes.GetError()->GetReport())
1941 }
1942 auto descCmp = descCmpRes.Unwrap();
1943 // TODO: switch to slow merge if we cannot "fast" merge
1944 if (!doSlowMerge && !descCmp.fFastMergeErrors.empty()) {
1945 SKIP_OR_ABORT(std::string("Source RNTuple has an incompatible schema with the destination:\n") +
1946 descCmp.fFastMergeErrors)
1947 }
1948
1949 // If the current source is missing some fields and we're not in Union mode, error
1950 // (if we are in Union mode, MergeSourceClusters will fill the missing fields with default values).
1951 if (mergeOpts.fMergingMode != ENTupleMergingMode::kUnion && !descCmp.fExtraDstFields.empty()) {
1952 std::string msg = "Source RNTuple is missing the following fields:";
1953 for (const auto *field : descCmp.fExtraDstFields) {
1954 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1955 }
1956 SKIP_OR_ABORT(msg)
1957 }
1958
1959 // handle extra src fields
1960 if (!descCmp.fExtraSrcFields.empty()) {
1961 if (mergeOpts.fMergingMode == ENTupleMergingMode::kUnion) {
1962 // late model extension for all fExtraSrcFields in Union mode
1963 auto res = strat->ExtendDestinationModel(descCmp);
1964 if (!res)
1965 return R__FORWARD_ERROR(res);
1966 } else if (mergeOpts.fMergingMode == ENTupleMergingMode::kStrict) {
1967 // If the current source has extra fields and we're in Strict mode, error
1968 std::string msg = "Source RNTuple has extra fields that the destination RNTuple doesn't have:";
1969 for (const auto *field : descCmp.fExtraSrcFields) {
1970 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1971 }
1972 SKIP_OR_ABORT(msg)
1973 }
1974 }
1975
1976 const auto prevNEntries = strat->GetNDstEntries();
1977
1978 auto res = strat->MergeSource(*source, descCmp);
1979 if (!res)
1980 return R__FORWARD_ERROR(res);
1981
1982 // Slow-merge attributes
1983 for (const auto &attrSetDesc : source->GetSharedDescriptorGuard()->GetAttrSetIterable()) {
1984 // Initialize output sink and strategy if it's the first time we encounter this attribute set name
1985 auto attrSetDataIt = attrSetsData.find(attrSetDesc.GetName());
1986 if (attrSetDataIt == attrSetsData.end()) {
1987 auto sinkOpts = RNTupleWriteOptions();
1988 sinkOpts.SetCompression(mergeOpts.fCompressionSettings.value());
1989 auto sink =
1990 std::unique_ptr<ROOT::Internal::RPagePersistentSink>(static_cast<ROOT::Internal::RPagePersistentSink *>(
1991 fDestination->CloneAsHidden(attrSetDesc.GetName(), sinkOpts).release()));
1992 auto [insertedIt, inserted] =
1993 attrSetsData.try_emplace(attrSetDesc.GetName(), std::move(sink), *strat, mergeOpts, prevNEntries);
1994 assert(inserted);
1995 attrSetDataIt = insertedIt;
1996 }
1997 auto &attrSetData = attrSetDataIt->second;
1998
1999 const auto attrLink =
2000 ROOT::Internal::RNTupleLink{attrSetDesc.GetAnchorLocator(), attrSetDesc.GetAnchorLength()};
2001 auto attrSource = source->OpenWithDifferentAnchor(attrLink);
2002 auto &attrStrat = attrSetData.fStrategy;
2003 attrStrat.InitSource(*attrSource);
2004
2005 descCmpRes = CompareDescriptorStructure(attrSetData.fSink->GetDescriptor(),
2006 attrSource->GetSharedDescriptorGuard().GetConstRef(),
2007 /*forAttributeMerging=*/true);
2008 if (!descCmpRes) {
2009 SKIP_OR_ABORT(std::string("Source RNTuple has an incompatible schema with the destination:\n") +
2010 descCmpRes.GetError()->GetReport())
2011 }
2012 descCmp = descCmpRes.Unwrap();
2013
2014 if (!descCmp.fExtraSrcFields.empty() || !descCmp.fExtraSrcSubfields.empty()) {
2015 auto edmRes = attrStrat.ExtendDestinationModel(descCmp);
2016 if (!edmRes)
2017 return R__FORWARD_ERROR(edmRes);
2018 }
2019
2020 auto mergeSourceRes = attrStrat.MergeSource(*attrSource, descCmp);
2021 if (!mergeSourceRes)
2022 return R__FORWARD_ERROR(mergeSourceRes);
2023 }
2024 } // end loop over sources
2025
2026 // Commit the output
2027 for (auto &[attrSetName, attrSetData] : attrSetsData) {
2028 attrSetData.fStrategy.BeforeCommit();
2029 if (attrSetData.fSink->GetNEntries() > 0) {
2030 attrSetData.fSink->CommitClusterGroup();
2031 }
2032 auto anchorLink = attrSetData.fSink->CommitDataset();
2033 fDestination->CommitAttributeSet(attrSetName, anchorLink);
2034 }
2035
2036 strat->BeforeCommit();
2037
2038 // Commit the output
2039 if (fDestination->GetNEntries() == 0)
2040 R__LOG_WARNING(NTupleMergeLog()) << "Output RNTuple '" << fDestination->GetNTupleName() << "' has no entries.";
2041 else
2042 fDestination->CommitClusterGroup();
2043
2044 fDestination->CommitDataset();
2045
2046 return RResult<void>::Success();
2047}
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:58
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
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::Experimental::RNTupleAttrSetDescriptorIterable GetAttrSetIterable() 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
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