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>
30#include <TROOT.h>
31#include <TFileMergeInfo.h>
32#include <TFile.h>
33#include <TKey.h>
34
35#include <algorithm>
36#include <deque>
37#include <initializer_list>
38#include <unordered_map>
39#include <vector>
40
51
52using namespace ROOT::Experimental::Internal;
53
55{
56 static ROOT::RLogChannel sLog("ROOT.NTuple.Merge");
57 return sLog;
58}
59
60// TFile options parsing
61// -------------------------------------------------------------------------------------
62static bool BeginsWithDelimitedWord(const TString &str, const char *word)
63{
64 const Ssiz_t wordLen = strlen(word);
65 if (str.Length() < wordLen)
66 return false;
67 if (!str.BeginsWith(word, TString::ECaseCompare::kIgnoreCase))
68 return false;
69 return str.Length() == wordLen || str(wordLen) == ' ';
70}
71
72template <typename T>
73static std::optional<T> ParseStringOption(const TString &opts, const char *pattern,
74 std::initializer_list<std::pair<const char *, T>> validValues)
75{
76 const Ssiz_t patternLen = strlen(pattern);
77 assert(pattern[patternLen - 1] == '='); // we want to parse options with the format `option=Value`
78 if (auto idx = opts.Index(pattern, 0, TString::ECaseCompare::kIgnoreCase);
79 idx >= 0 && opts.Length() > idx + patternLen) {
80 auto sub = TString(opts(idx + patternLen, opts.Length() - idx - patternLen));
81 for (const auto &[name, value] : validValues) {
82 if (BeginsWithDelimitedWord(sub, name)) {
83 return value;
84 }
85 }
86 }
87 return std::nullopt;
88}
89
90static std::optional<ENTupleMergingMode> ParseOptionMergingMode(const TString &opts)
91{
92 return ParseStringOption<ENTupleMergingMode>(opts, "rntuple.MergingMode=",
93 {
94 {"Filter", ENTupleMergingMode::kFilter},
95 {"Union", ENTupleMergingMode::kUnion},
96 {"Strict", ENTupleMergingMode::kStrict},
97 });
98}
99
100static std::optional<ENTupleMergeErrBehavior> ParseOptionErrBehavior(const TString &opts)
101{
102 return ParseStringOption<ENTupleMergeErrBehavior>(opts, "rntuple.ErrBehavior=",
103 {
104 {"Abort", ENTupleMergeErrBehavior::kAbort},
105 {"Skip", ENTupleMergeErrBehavior::kSkip},
106 });
107}
108
109static std::optional<ENTupleMergeVersionBehavior> ParseOptionVersionBehavior(const TString &opts)
110{
111 return ParseStringOption<ENTupleMergeVersionBehavior>(
112 opts, "rntuple.VersionBehavior=",
113 {
114 {"WarnOnHigherVersion", ENTupleMergeVersionBehavior::kWarnOnHigherVersion},
115 {"AbortOnHigherVersion", ENTupleMergeVersionBehavior::kAbortOnHigherVersion},
116 });
117}
118// -------------------------------------------------------------------------------------
119
120// Entry point for TFileMerger. Internally calls RNTupleMerger::Merge().
122// IMPORTANT: this function must not throw, as it is used in exception-unsafe code (TFileMerger).
123try {
124 // Check the inputs
125 if (!inputs || inputs->GetEntries() < 3 || !mergeInfo) {
126 R__LOG_ERROR(NTupleMergeLog()) << "Invalid inputs.";
127 return -1;
128 }
129
130 // Parse the input parameters
131 TIter itr(inputs);
132
133 // First entry is the RNTuple name
134 std::string ntupleName = std::string(itr()->GetName());
135
136 // Second entry is the output file
137 TObject *secondArg = itr();
138 TFile *outFile = dynamic_cast<TFile *>(secondArg);
139 if (!outFile) {
140 R__LOG_ERROR(NTupleMergeLog()) << "Second input parameter should be a TFile, but it's a "
141 << secondArg->ClassName() << ".";
142 return -1;
143 }
144
145 // Check if the output file already has a key with that name
146 TKey *outKey = outFile->FindKey(ntupleName.c_str());
147 ROOT::RNTuple *outNTuple = nullptr;
148 if (outKey) {
149 outNTuple = outKey->ReadObject<ROOT::RNTuple>();
150 if (!outNTuple) {
151 R__LOG_ERROR(NTupleMergeLog()) << "Output file already has key, but not of type RNTuple!";
152 return -1;
153 }
154 // In principle, we should already be working on the RNTuple object from the output file, but just continue with
155 // pointer we just got.
156 }
157
158 const bool defaultComp = mergeInfo->fOptions.Contains("DefaultCompression");
159 const bool firstSrcComp = mergeInfo->fOptions.Contains("FirstSrcCompression");
160 const bool extraVerbose = mergeInfo->fOptions.Contains("rntuple.ExtraVerbose");
161 if (defaultComp && firstSrcComp) {
162 // this should never happen through hadd, but a user may call RNTuple::Merge() from custom code.
163 R__LOG_WARNING(NTupleMergeLog()) << "Passed both options \"DefaultCompression\" and \"FirstSrcCompression\": "
164 "only the latter will apply.";
165 }
166 std::optional<std::uint32_t> compression;
167 if (firstSrcComp) {
168 // user passed -ff or -fk: use the same compression as the first RNTuple we find in the sources.
169 // (do nothing here, the compression will be fetched below)
170 } else if (!defaultComp) {
171 // compression was explicitly passed by the user: use it.
172 compression = outFile->GetCompressionSettings();
173 } else {
174 // user passed no compression-related options: use default
176 R__LOG_INFO(NTupleMergeLog()) << "Using the default compression: " << *compression;
177 }
178
179 // The remaining entries are the input files
180 std::vector<std::unique_ptr<RPageSourceFile>> sources;
181 std::vector<RPageSource *> sourcePtrs;
182
183 while (const auto &pitr = itr()) {
184 TFile *inFile = dynamic_cast<TFile *>(pitr);
185 ROOT::RNTuple *anchor = inFile ? inFile->Get<ROOT::RNTuple>(ntupleName.c_str()) : nullptr;
186 if (!anchor) {
187 R__LOG_INFO(NTupleMergeLog()) << "No RNTuple anchor named '" << ntupleName << "' from file '"
188 << inFile->GetName() << "'";
189 continue;
190 }
191
192 auto source = RPageSourceFile::CreateFromAnchor(*anchor);
193 if (!compression) {
194 // Get the compression of this RNTuple and use it as the output compression.
195 // We currently assume all column ranges have the same compression, so we just peek at the first one.
196 source->Attach(RNTupleSerializer::EDescriptorDeserializeMode::kRaw);
197 DescriptorId_t firstClusterId = kInvalidDescriptorId;
198 auto descGuard = source->FindClusterId(0, firstClusterId);
199 if (firstClusterId == kInvalidDescriptorId) {
201 << "Asked to use the first source's compression as the output compression, but the "
202 "first source (file '"
203 << inFile->GetName()
204 << "') has an empty RNTuple, therefore the output compression could not be "
205 "determined.";
206 return -1;
207 }
208 const auto &firstCluster = descGuard->GetClusterDescriptor(firstClusterId);
209 auto colRangeIter = firstCluster.GetColumnRangeIterable();
210 auto firstColRange = colRangeIter.begin();
211 if (firstColRange == colRangeIter.end()) {
213 << "Asked to use the first source's compression as the output compression, but the "
214 "first source (file '"
215 << inFile->GetName()
216 << "') has an empty RNTuple, therefore the output compression could not be "
217 "determined.";
218 return -1;
219 }
220 compression = (*firstColRange).GetCompressionSettings();
221 R__LOG_INFO(NTupleMergeLog()) << "Using the first RNTuple's compression: " << *compression;
222 }
223 sources.push_back(std::move(source));
224 }
225
226 RNTupleWriteOptions writeOpts;
227 assert(compression);
228 writeOpts.SetCompression(*compression);
229 auto destination = std::make_unique<ROOT::Internal::RPageSinkFile>(ntupleName, *outFile, writeOpts);
230 std::unique_ptr<ROOT::RNTupleModel> model;
231 // If we already have an existing RNTuple, copy over its descriptor to support incremental merging
232 if (outNTuple) {
233 auto outSource = RPageSourceFile::CreateFromAnchor(*outNTuple);
234 outSource->Attach(RNTupleSerializer::EDescriptorDeserializeMode::kForWriting);
235 outSource->LoadAllPageLists();
236 auto desc = outSource->GetSharedDescriptorGuard();
237 model = destination->InitFromDescriptor(desc.GetConstRef(), true /* copyClusters */);
238 }
239
240 // Interface conversion
241 sourcePtrs.reserve(sources.size());
242 for (const auto &s : sources) {
243 sourcePtrs.push_back(s.get());
244 }
245
246 // Now merge
247 RNTupleMerger merger{std::move(destination), std::move(model)};
248 RNTupleMergeOptions mergerOpts;
249 mergerOpts.fCompressionSettings = compression;
250 mergerOpts.fExtraVerbose = extraVerbose;
251 if (auto mergingMode = ParseOptionMergingMode(mergeInfo->fOptions)) {
252 mergerOpts.fMergingMode = *mergingMode;
253 }
254 if (auto errBehavior = ParseOptionErrBehavior(mergeInfo->fOptions)) {
255 mergerOpts.fErrBehavior = *errBehavior;
256 }
257 if (auto versionBehavior = ParseOptionVersionBehavior(mergeInfo->fOptions)) {
258 mergerOpts.fVersionBehavior = *versionBehavior;
259 }
260 merger.Merge(sourcePtrs, mergerOpts).ThrowOnError();
261
262 // Provide the caller with a merged anchor object (even though we've already
263 // written it).
264 *this = *outFile->Get<ROOT::RNTuple>(ntupleName.c_str());
265
266 return 0;
267} catch (const std::exception &ex) {
268 R__LOG_ERROR(NTupleMergeLog()) << "Exception thrown while merging: " << ex.what();
269 return -1;
270}
271
272namespace {
273// Functor used to change the compression of a page to `fCompressionSettings`.
274struct RChangeCompressionFunc {
275 const RColumnElementBase &fSrcColElement;
276 std::uint32_t fCompressionSettings;
277 RPageStorage::RSealedPage &fSealedPage;
279 std::byte *fBuffer;
280 std::size_t fBufSize;
281 const ROOT::RNTupleWriteOptions &fWriteOpts;
282
283 void operator()() const
284 {
286
287 const auto bytesPacked = fSrcColElement.GetPackedSize(fSealedPage.GetNElements());
288 // TODO: this buffer could be kept and reused across pages
289 std::unique_ptr<std::byte[]> unzipBufOwned;
290 std::byte *unzipBuf;
291 if (fCompressionSettings != 0) {
292 unzipBufOwned = MakeUninitArray<std::byte>(bytesPacked);
293 unzipBuf = unzipBufOwned.get();
294 } else {
295 unzipBuf = fBuffer;
296 }
297 ROOT::Internal::RNTupleDecompressor::Unzip(fSealedPage.GetBuffer(), fSealedPage.GetDataSize(), bytesPacked,
298 unzipBuf);
299
300 const auto checksumSize = fWriteOpts.GetEnablePageChecksums() * sizeof(std::uint64_t);
301 std::size_t nBytesZipped;
302 if (fCompressionSettings != 0) {
303 assert(fBuffer != unzipBuf);
304 assert(fBufSize >= bytesPacked + checksumSize);
305 nBytesZipped = ROOT::Internal::RNTupleCompressor::Zip(unzipBuf, bytesPacked, fCompressionSettings, fBuffer);
306 } else {
307 nBytesZipped = bytesPacked;
308 }
309 fSealedPage = {fBuffer, nBytesZipped + checksumSize, fSealedPage.GetNElements(), fSealedPage.GetHasChecksum()};
310 fSealedPage.ChecksumIfEnabled();
311 }
312};
313
314struct RTaskVisitor {
315 std::optional<ROOT::Experimental::TTaskGroup> &fGroup;
316
317 template <typename T>
318 void operator()(T &&f)
319 {
320 if (fGroup)
321 fGroup->Run(f);
322 else
323 f();
324 }
325};
326
327struct RCommonField {
328 const ROOT::RFieldDescriptor *fSrc;
329 const ROOT::RFieldDescriptor *fDst;
330
331 RCommonField(const ROOT::RFieldDescriptor &src, const ROOT::RFieldDescriptor &dst) : fSrc(&src), fDst(&dst) {}
332};
333
334/// Maps a column representation from a source to a destination RNTuple.
335/// fSource and fDest are the first representation indices of a specific column.
336///
337/// When we merge fields from different RNTuples, two compatible fields may use different column
338/// representations. When merging their columns we need to make sure that we keep the output
339/// representation coherent, which is what this mapping is here for.
340struct RColReprMapping {
341 std::uint32_t fSource;
342 std::uint32_t fDest;
343};
344
345/// A column extension that needs to be added to an output field.
346/// Note that this also adds a mapping for each new representation, which is why it inherits RColReprMapping.
347struct RColReprExtension : RColReprMapping {
348 /// The new representations to be added
349 std::vector<ROOT::Internal::RColumnFormat> fSourceRepr;
350 /// The first element index that this column had in its source. When adding this representation to the destination,
351 /// the new column will add this amount to the first element index of the 0th-representation column in the
352 /// destination's current cluster.
353 std::uint32_t fOrigFirstElementIndex = 0;
354};
355
356static std::optional<std::uint32_t>
357FindColumnReprMapping(const std::vector<RColReprMapping> &mappings, std::uint32_t sourceReprIndex)
358{
359 for (const auto [src, dst] : mappings)
360 if (src == sourceReprIndex)
361 return dst;
362 return std::nullopt;
363}
364
365struct RColumnOutInfo {
367};
368
369struct RColumnMergeInfo {
370 // This column name is built as a dot-separated concatenation of the ancestry of
371 // the columns' parent fields' names plus the index of the column itself.
372 // e.g. "Muon.pt.x._0"
373 std::string fColumnName;
374 // The column id in the source RNTuple
376 // The corresponding column id in the destination RNTuple (the mapping happens in AddColumnsFromField())
378 std::uint16_t fOutputReprIndex = 0;
379 // If nullopt, use the default in-memory type
380 std::optional<std::type_index> fInMemoryType;
381 const ROOT::RFieldDescriptor *fParentFieldDescriptor = nullptr;
382 const ROOT::RNTupleDescriptor *fParentNTupleDescriptor = nullptr;
383};
384
385// { ".fully.qualified.fieldName.colInputIndex.colOutputReprIndex" => colOutputInfo }
386using ColumnIdMap_t = std::unordered_map<std::string, RColumnOutInfo>;
387
388struct RColumnInfoGroup {
389 std::vector<RColumnMergeInfo> fExtraDstColumns;
390 std::vector<RColumnMergeInfo> fCommonColumns;
391};
392
393struct RSealedPageMergeData {
394 // We use a std::deque so that references to the contained SealedPageSequence_t, and its iterators, are
395 // never invalidated.
396 std::deque<RPageStorage::SealedPageSequence_t> fPagesV;
397 std::vector<RPageStorage::RSealedPageGroup> fGroups;
398 std::vector<std::unique_ptr<std::byte[]>> fBuffers;
399};
400
401template <typename T>
402using FieldCollectionMap_t = std::unordered_map<const ROOT::RFieldDescriptor *, std::vector<T>>;
403
404struct RDescriptorsComparison {
405 std::vector<const ROOT::RFieldDescriptor *> fExtraDstFields;
406 std::vector<const ROOT::RFieldDescriptor *> fExtraSrcFields;
407 std::vector<RCommonField> fCommonFields;
408 // For each field that has more than 1 column representation in the output model,
409 // maps the column representatives of the source field with those of the destination.
410 // The key is the destination field.
411 FieldCollectionMap_t<RColReprMapping> fColReprMappings;
412 FieldCollectionMap_t<RColReprExtension> fColReprExtensions;
413};
414
415} // namespace
416
417// Subprocedure of CompareDescriptorStructure, extracted for readability.
418// Given two fields, attempts to match their column representations and schedules column extensions if necessary.
420 const ROOT::RFieldDescriptor &srcField, const ROOT::RFieldDescriptor &dstField,
421 RDescriptorsComparison &result, std::vector<std::string> &errors)
422{
423 const auto &srcColumns = srcField.GetLogicalColumnIds();
424 const auto &dstColumns = dstField.GetLogicalColumnIds();
425
426 // Fields must have the same cardinality
427 const std::uint32_t srcColCardinality = srcField.GetColumnCardinality();
428 const std::uint32_t dstColCardinality = dstField.GetColumnCardinality();
429 if (srcColCardinality != dstColCardinality) {
430 std::stringstream ss;
431 ss << "Field `" << srcField.GetFieldName()
432 << "` has a different column cardinality than previously-seen field with the same name (old: "
433 << dstColCardinality << ", new: " << srcColCardinality << ")";
434 errors.push_back(ss.str());
435 return;
436 }
437
438 if (srcColCardinality == 0)
439 return; // no columns to match
440
441 const auto srcNColReprs = srcColumns.size() / srcColCardinality;
442 const auto dstNColReprs = dstColumns.size() / dstColCardinality;
443 std::uint32_t nextDstReprIndex = dstNColReprs;
444
445 // For each column representation of the source, check if it matches one in the descriptor.
446 // If so, and if it doesn't match the destination's repr index, add a mapping for it.
447 // If nothing matches, schedule the column representation to be added later.
448 // NOTE: this has quadratic complexity but the numbers involved are small so it's fine.
449 for (auto srcReprIdx = 0u; srcReprIdx < srcNColReprs; ++srcReprIdx) {
450 std::int64_t matchingRepr = -1;
451 for (auto dstReprIdx = 0u; dstReprIdx < dstNColReprs; ++dstReprIdx) {
452 bool matches = true;
453 for (auto reprColIdx = 0u; reprColIdx < srcColCardinality; ++reprColIdx) {
454 const auto srcColId = srcColumns[srcReprIdx * srcColCardinality + reprColIdx];
455 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
456 const auto dstColId = dstColumns[dstReprIdx * dstColCardinality + reprColIdx];
457 const auto &dstCol = dstDesc.GetColumnDescriptor(dstColId);
458 if (srcCol.GetType() != dstCol.GetType()) {
459 matches = false;
460 break;
461 }
462 }
463
464 if (matches) {
465 // If this column representation matches by column type, we need to make sure that it also has
466 // matching column metadata. Since we currently do not support multiple column representations
467 // that only differ by such metadata, we forbid merging such columns (e.g. we cannot merge two
468 // Real32Trunc columns with different bit widths). This could technically be supported, but it
469 // would require significant effort, so we currently don't.
470 for (auto reprColIdx = 0u; reprColIdx < srcColCardinality; ++reprColIdx) {
471 const auto srcColId = srcColumns[srcReprIdx * srcColCardinality + reprColIdx];
472 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
473 const auto dstColId = dstColumns[dstReprIdx * dstColCardinality + reprColIdx];
474 const auto &dstCol = dstDesc.GetColumnDescriptor(dstColId);
475 if (srcCol.GetType() != dstCol.GetType() || srcCol.GetBitsOnStorage() != dstCol.GetBitsOnStorage() ||
476 srcCol.GetValueRange() != dstCol.GetValueRange()) {
477 matches = false;
478 break;
479 }
480 }
481
482 if (matches) {
483 // We found a valid matching representation.
484 matchingRepr = dstReprIdx;
485 break;
486 }
487 }
488 }
489
490 if (errors.empty()) {
491 if (matchingRepr >= 0 && matchingRepr != srcReprIdx) {
492 // a different matching representation was found
493 assert(matchingRepr < std::numeric_limits<std::uint32_t>::max());
494 result.fColReprMappings[&dstField].push_back(
495 RColReprMapping{srcReprIdx, static_cast<std::uint32_t>(matchingRepr)});
496 } else if (matchingRepr < 0) {
497 // this representation was not found in the destination: add it
498 std::vector<ROOT::Internal::RColumnFormat> newRepr;
499 newRepr.reserve(srcColCardinality);
500 std::uint32_t firstElemIdx = 0;
501 for (auto reprColIdx = 0u; reprColIdx < srcColCardinality; ++reprColIdx) {
502 const auto srcColId = srcColumns[srcReprIdx * srcColCardinality + reprColIdx];
503 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
504 // All added columns are supposed to have the same firstElementIndex
505 assert(firstElemIdx == 0 || firstElemIdx == srcCol.GetFirstElementIndex());
506 firstElemIdx = srcCol.GetFirstElementIndex();
507 auto &reprElement = newRepr.emplace_back();
508 reprElement.fType = srcCol.GetType();
509 reprElement.fBitWidth = srcCol.GetBitsOnStorage();
510 reprElement.fValueRange = srcCol.GetValueRange();
511 }
512 RColReprExtension extension{{srcReprIdx, nextDstReprIndex}, newRepr, firstElemIdx};
513 nextDstReprIndex += newRepr.size();
514 result.fColReprExtensions[&dstField].push_back(extension);
515 result.fColReprMappings[&dstField].push_back(extension);
516 }
517 }
518 }
519}
520
521static RDescriptorsComparison
523{
524 RDescriptorsComparison res;
525
526 for (const auto &dstField : dst.GetTopLevelFields()) {
527 const auto srcFieldId = src.FindFieldId(dstField.GetFieldName());
528 if (srcFieldId != ROOT::kInvalidDescriptorId) {
529 const auto &srcField = src.GetFieldDescriptor(srcFieldId);
530 res.fCommonFields.push_back({srcField, dstField});
531 } else {
532 res.fExtraDstFields.emplace_back(&dstField);
533 }
534 }
535 for (const auto &srcField : src.GetTopLevelFields()) {
536 const auto dstFieldId = dst.FindFieldId(srcField.GetFieldName());
537 if (dstFieldId == ROOT::kInvalidDescriptorId) {
538 res.fExtraSrcFields.push_back(&srcField);
539 }
540 }
541
542 return res;
543}
544
545/// Compares the top level fields of `dst` and `src` and determines whether they can be merged or not.
546/// In addition, returns the differences between `dst` and `src`'s structures
549{
550 // Cases:
551 // 1. dst == src
552 // 2. dst has fields that src hasn't
553 // 3. src has fields that dst hasn't
554 // 4. dst and src have fields that differ (compatible or incompatible)
555
556 RDescriptorsComparison res = FillCommonAndExtraFields(dst, src);
557 std::vector<std::string> errors;
558
559 // Check compatibility of common fields
560 auto fieldsToCheck = res.fCommonFields;
561 // NOTE: using index-based for loop because the collection may get extended by the iteration
562 for (std::size_t fieldIdx = 0; fieldIdx < fieldsToCheck.size(); ++fieldIdx) {
563 const auto &field = fieldsToCheck[fieldIdx];
564
565 // NOTE: field.fSrc and field.fDst have the same name by construction
566 const auto &fieldName = field.fSrc->GetFieldName();
567
568 // Require that fields are both projected or both not projected
569 bool projCompatible = field.fSrc->IsProjectedField() == field.fDst->IsProjectedField();
570 if (!projCompatible) {
571 std::stringstream ss;
572 ss << "Field `" << fieldName << "` is incompatible with previously-seen field with that name because the "
573 << (field.fSrc->IsProjectedField() ? "new" : "old") << " one is projected and the other isn't";
574 errors.push_back(ss.str());
575 } else if (field.fSrc->IsProjectedField()) {
576 // if both fields are projected, verify that they point to the same real field
577 const auto srcName = src.GetQualifiedFieldName(field.fSrc->GetProjectionSourceId());
578 const auto dstName = dst.GetQualifiedFieldName(field.fDst->GetProjectionSourceId());
579 if (srcName != dstName) {
580 std::stringstream ss;
581 ss << "Field `" << fieldName
582 << "` is projected to a different field than a previously-seen field with the same name (old: "
583 << dstName << ", new: " << srcName << ")";
584 errors.push_back(ss.str());
585 }
586 }
587
588 // Require that fields types match
589 // TODO(gparolini): allow non-identical but compatible types
590 const auto &srcTyName = ROOT::Internal::GetRenormalizedTypeName(field.fSrc->GetTypeName());
591 // This is already renormalized by construction (see RNTupleDescriptorBuilder::SetSchemaFromExisting)
592 const auto &dstTyName = field.fDst->GetTypeName();
593 if (srcTyName != dstTyName) {
594 std::stringstream ss;
595 ss << "Field `" << fieldName
596 << "` has a type incompatible with a previously-seen field with the same name: (old: " << dstTyName
597 << ", new: " << srcTyName << ")";
598 errors.push_back(ss.str());
599 }
600
601 // Require that type checksums match
602 const auto srcTyChk = field.fSrc->GetTypeChecksum();
603 const auto dstTyChk = field.fDst->GetTypeChecksum();
604 if (srcTyChk && dstTyChk && *srcTyChk != *dstTyChk) {
605 std::stringstream ss;
606 ss << "Field `" << field.fSrc->GetFieldName()
607 << "` has a different type checksum than previously-seen field with the same name";
608 errors.push_back(ss.str());
609 }
610
611 // Require that type versions match
612 const auto srcTyVer = field.fSrc->GetTypeVersion();
613 const auto dstTyVer = field.fDst->GetTypeVersion();
614 if (srcTyVer != dstTyVer) {
615 std::stringstream ss;
616 ss << "Field `" << field.fSrc->GetFieldName()
617 << "` has a different type version than previously-seen field with the same name (old: " << dstTyVer
618 << ", new: " << srcTyVer << ")";
619 errors.push_back(ss.str());
620 }
621
622 // Require that field versions match
623 const auto srcFldVer = field.fSrc->GetFieldVersion();
624 const auto dstFldVer = field.fDst->GetFieldVersion();
625 if (srcFldVer != dstFldVer) {
626 std::stringstream ss;
627 ss << "Field `" << field.fSrc->GetFieldName()
628 << "` has a different field version than previously-seen field with the same name (old: " << dstFldVer
629 << ", new: " << srcFldVer << ")";
630 errors.push_back(ss.str());
631 }
632
633 const auto srcRole = field.fSrc->GetStructure();
634 const auto dstRole = field.fDst->GetStructure();
635 if (srcRole != dstRole) {
636 std::stringstream ss;
637 ss << "Field `" << field.fSrc->GetFieldName()
638 << "` has a different structural role than previously-seen field with the same name (old: " << dstRole
639 << ", new: " << srcRole << ")";
640 errors.push_back(ss.str());
641 }
642
643 // Require that column representations match
644 if (!field.fSrc->IsProjectedField()) {
645 MatchColumnRepresentations(src, dst, *field.fSrc, *field.fDst, res, errors);
646 }
647
648 // Require that subfields are compatible
649 const auto &srcLinks = field.fSrc->GetLinkIds();
650 const auto &dstLinks = field.fDst->GetLinkIds();
651 if (srcLinks.size() != dstLinks.size()) {
652 std::stringstream ss;
653 ss << "Field `" << field.fSrc->GetFieldName()
654 << "` has a different number of children than previously-seen field with the same name (old: "
655 << dstLinks.size() << ", new: " << srcLinks.size() << ")";
656 errors.push_back(ss.str());
657 } else {
658 for (std::size_t linkIdx = 0, linkNum = srcLinks.size(); linkIdx < linkNum; ++linkIdx) {
659 const auto &srcSubfield = src.GetFieldDescriptor(srcLinks[linkIdx]);
660 const auto &dstSubfield = dst.GetFieldDescriptor(dstLinks[linkIdx]);
661 fieldsToCheck.push_back(RCommonField{srcSubfield, dstSubfield});
662 }
663 }
664 }
665
666 std::string errMsg;
667 for (const auto &err : errors)
668 errMsg += std::string("\n * ") + err;
669
670 if (!errMsg.empty())
671 errMsg = errMsg.substr(1); // strip initial newline
672
673 if (errMsg.length())
674 return R__FAIL(errMsg);
675
676 return ROOT::RResult(res);
677}
678
679// Restores the truncation/quantization parameters (bit width, value range) of a field recreated
680// from an on-disk descriptor, e.g. by ExtendDestinationModel(). RFieldDescriptor::CreateField only
681// rebuilds the type structure; the parameters live in the in-memory field (RRealField::fBitWidth,
682// fValueMin, fValueMax), so pinning the column representative alone leaves stale defaults behind,
683// which are out of range for kReal32Trunc. Recurses into subfields, matched by on-disk ID, so that
684// truncated/quantized members of late records and collections are restored as well. Nodes whose
685// source column is not truncated/quantized, or which span several representations, are left alone.
687 const ROOT::RNTupleDescriptor &srcDesc)
688{
689 const auto &logicalIds = fieldDesc.GetLogicalColumnIds();
690 if (logicalIds.size() == 1) {
691 const auto &column = srcDesc.GetColumnDescriptor(logicalIds[0]);
692 const auto type = column.GetType();
694 auto *realFloat = dynamic_cast<ROOT::RRealField<float> *>(&field);
695 auto *realDouble = dynamic_cast<ROOT::RRealField<double> *>(&field);
696 if (!realFloat && !realDouble) {
697 return R__FAIL("field `" + field.GetQualifiedFieldName() +
698 "` has a Real32Trunc/Real32Quant column but is not an RRealField");
699 }
700 const auto bits = column.GetBitsOnStorage();
701 try {
703 if (realFloat)
704 realFloat->SetTruncated(bits);
705 else
706 realDouble->SetTruncated(bits);
707 } else {
708 const auto valueRange = column.GetValueRange();
709 if (!valueRange) {
710 return R__FAIL("field `" + field.GetQualifiedFieldName() +
711 "` has a Real32Quant column with no value range");
712 }
713 if (realFloat) {
714 realFloat->SetQuantized(bits,
715 {static_cast<float>(valueRange->fMin), static_cast<float>(valueRange->fMax)});
716 } else {
717 realDouble->SetQuantized(bits, {valueRange->fMin, valueRange->fMax});
718 }
719 }
720 } catch (const ROOT::RException &ex) {
721 return R__FAIL(ex.what());
722 }
723 }
724 }
725 for (auto *subfield : field.GetMutableSubfields()) {
726 // CreateField() may materialize in-memory class members that the source descriptor does not
727 // have (e.g. members added to the class after the file was written); those keep an invalid
728 // on-disk ID and carry no source column, so there is nothing to restore for them.
729 if (subfield->GetOnDiskId() == ROOT::kInvalidDescriptorId)
730 continue;
731 auto restored = RestoreRealFieldParams(*subfield, srcDesc.GetFieldDescriptor(subfield->GetOnDiskId()), srcDesc);
732 if (!restored)
733 return R__FORWARD_ERROR(restored);
734 }
736}
737
738// Applies late model extension to `mergeData.fDestination`, adding all `descCmp.fExtraSrcFields` to it.
739[[nodiscard]]
741ExtendDestinationModel(RDescriptorsComparison &descCmp, ROOT::RNTupleModel &dstModel,
742 ROOT::Internal::RPageSink &destination, const ROOT::RNTupleDescriptor &srcDesc, bool verbose)
743{
744 const auto &newFields = descCmp.fExtraSrcFields;
745 auto &commonFields = descCmp.fCommonFields;
746
747 dstModel.Unfreeze();
748 ROOT::Internal::RNTupleModelChangeset changeset{dstModel};
749
750 if (verbose) {
751 std::string msg = "destination doesn't contain field";
752 if (newFields.size() > 1)
753 msg += 's';
754 msg += ' ';
755 msg += std::accumulate(newFields.begin(), newFields.end(), std::string{}, [](const auto &acc, const auto *field) {
756 return acc + (acc.length() ? ", " : "") + '`' + field->GetFieldName() + '`';
757 });
758 R__LOG_INFO(NTupleMergeLog()) << msg << ": adding " << (newFields.size() > 1 ? "them" : "it")
759 << " to the destination model (entry #" << destination.GetNEntries() << ").";
760 }
761
762 changeset.fAddedFields.reserve(newFields.size());
763 // First add all non-projected fields...
764 for (const auto *fieldDesc : newFields) {
765 if (fieldDesc->IsProjectedField())
766 continue;
767
768 auto field = fieldDesc->CreateField(srcDesc);
769 // Explicitly set the field representatives. This prevents UpdateSchema() from changing our column
770 // representations via AutoAdjustColumnTypes.
772 for (const auto &colId : fieldDesc->GetLogicalColumnIds()) {
773 const auto &column = srcDesc.GetColumnDescriptor(colId);
774 representatives.push_back(column.GetType());
775 }
776 field->SetColumnRepresentatives({representatives});
777 // Pinning the representative is not enough for truncated/quantized columns: their parameters
778 // are stored in the in-memory field, which CreateField() leaves at defaults.
779 auto restored = RestoreRealFieldParams(*field, *fieldDesc, srcDesc);
780 if (!restored)
781 return R__FORWARD_ERROR(restored);
782 changeset.AddField(std::move(field));
783 }
784 // ...then add all projected fields.
785 for (const auto *fieldDesc : newFields) {
786 if (!fieldDesc->IsProjectedField())
787 continue;
788
790 auto field = fieldDesc->CreateField(srcDesc);
791 const auto sourceId = fieldDesc->GetProjectionSourceId();
792 const auto &sourceField = dstModel.GetConstField(srcDesc.GetQualifiedFieldName(sourceId));
793 fieldMap[field.get()] = &sourceField;
794
795 for (const auto &subfield : *field) {
796 const auto &subFieldDesc = srcDesc.GetFieldDescriptor(subfield.GetOnDiskId());
797 const auto subSourceId = subFieldDesc.GetProjectionSourceId();
798 const auto &subSourceField = dstModel.GetConstField(srcDesc.GetQualifiedFieldName(subSourceId));
799 fieldMap[&subfield] = &subSourceField;
800 }
801 changeset.fAddedProjectedFields.emplace_back(field.get());
802 ROOT::Internal::GetProjectedFieldsOfModel(dstModel).Add(std::move(field), fieldMap);
803 }
804 dstModel.Freeze();
805 try {
806 // FIXME: here we are connecting the new fields/columns to the sink!
807 // We should avoid doing that, as all other non-extended fields never get connected (and we don't
808 // need to connect these either in principle).
809 // NOTE: this calls AutoAdjustColumnTypes, but we have set the column representations of all fields
810 // explicitly, so it will not change it under the hood.
811 destination.UpdateSchema(changeset, destination.GetNEntries());
812 } catch (const ROOT::RException &ex) {
813 return R__FAIL(ex.what());
814 }
815
816 commonFields.reserve(commonFields.size() + newFields.size());
817 // NOTE(gparolini): Insert the new fields at the beginning of `commonFields`.
818 // We need to make sure the extended fields appear before all other common fields for the following reason:
819 // in general, when we GatherColumnInfos we (potentially) assign new column output ids in field order; this
820 // assignment happens whenever we find new columns, which happens in 3 cases:
821 // 1. we are in the first source and we're adding the first set of (common) fields;
822 // 2. we are adding a new set of extended common fields (this is done in this function);
823 // 3. we are adding new column representations for fields that we already had before processing this source.
824 //
825 // It's important that the output id assigned to the new columns is coherent with the order of the column descriptors
826 // as they appear in the header and footer of the destination RNTuple.
827 // This is in turn determined by the order by which we append new columns to the dst descriptor during the merging
828 // process.
829 //
830 // Now let's consider the three cases listed above.
831 // Ignoring the trivial case (1), the order of operations for each source is:
832 // - call ExtendDestinationModel (case 2)
833 // (this adds both new fields and column descriptors; see the UpdateSchema call above)
834 // - add new column representations (case 3)
835 // (this only adds column descriptors, see the call to AddColumnRepresentation)
836 //
837 // Since we call ExtendDestinationModel (this function) *before* adding the new column representations,
838 // the dst descriptor always gets updated with the new column descriptors coming from the extended fields before
839 // it gets updated with the extended column representations.
840 //
841 // However, in GatherColumnInfos, the new column output ids are added sequentially in *field* order and the fields
842 // containing the new column representations are already in that list from earlier! So, to make sure the new output
843 // ids are assigned to our extended fields first, we push them in from on the list so they are visited first.
844 const auto &dstDesc = destination.GetDescriptor();
845 for (auto it = newFields.rbegin(); it != newFields.rend(); ++it) {
846 const auto *field = *it;
847 const auto newFieldInDstId = dstDesc.FindFieldId(field->GetFieldName());
848 const auto &newFieldInDst = dstDesc.GetFieldDescriptor(newFieldInDstId);
849 commonFields.insert(commonFields.begin(), RCommonField{*field, newFieldInDst});
850 }
851
853}
854
855static std::optional<std::type_index> ColumnInMemoryType(std::string_view fieldType, ENTupleColumnType onDiskType)
856{
857 if (onDiskType == ENTupleColumnType::kIndex32 || onDiskType == ENTupleColumnType::kSplitIndex32 ||
859 return typeid(ROOT::Internal::RColumnIndex);
860
861 if (onDiskType == ENTupleColumnType::kSwitch)
862 return typeid(ROOT::Internal::RColumnSwitch);
863
864 // clang-format off
865 if (fieldType == "bool") return typeid(bool);
866 if (fieldType == "std::byte") return typeid(std::byte);
867 if (fieldType == "char") return typeid(char);
868 if (fieldType == "std::int8_t") return typeid(std::int8_t);
869 if (fieldType == "std::uint8_t") return typeid(std::uint8_t);
870 if (fieldType == "std::int16_t") return typeid(std::int16_t);
871 if (fieldType == "std::uint16_t") return typeid(std::uint16_t);
872 if (fieldType == "std::int32_t") return typeid(std::int32_t);
873 if (fieldType == "std::uint32_t") return typeid(std::uint32_t);
874 if (fieldType == "std::int64_t") return typeid(std::int64_t);
875 if (fieldType == "std::uint64_t") return typeid(std::uint64_t);
876 if (fieldType == "float") return typeid(float);
877 if (fieldType == "double") return typeid(double);
878 // clang-format on
879
880 // if the type is not one of those above, we use the default in-memory type.
881 return std::nullopt;
882}
883
884static void PrefillColumnMap(const ROOT::RNTupleDescriptor &desc, const ROOT::RFieldDescriptor &fieldDesc,
885 ColumnIdMap_t &colIdMap, const std::string &prefix = "")
886{
887 std::string name = prefix + '.' + fieldDesc.GetFieldName();
888 for (const auto &colId : fieldDesc.GetLogicalColumnIds()) {
889 const auto &colDesc = desc.GetColumnDescriptor(colId);
890 RColumnOutInfo info{};
891 info.fColumnId = colDesc.GetLogicalId();
892 const auto colName =
893 name + '.' + std::to_string(colDesc.GetIndex()) + '.' + std::to_string(colDesc.GetRepresentationIndex());
894 colIdMap[colName] = info;
895 }
896
897 for (const auto &subId : fieldDesc.GetLinkIds()) {
898 const auto &subfield = desc.GetFieldDescriptor(subId);
899 PrefillColumnMap(desc, subfield, colIdMap, name);
900 }
901}
902
904 const ROOT::RFieldDescriptor &field, const ROOT::RNTupleDescriptor &desc,
905 const FieldCollectionMap_t<RColReprExtension> &extensions,
906 std::vector<std::pair<const ROOT::RFieldDescriptor *, std::vector<RColReprExtension>>> &outExtensions,
907 std::unordered_map<ROOT::DescriptorId_t, std::vector<const ROOT::RFieldDescriptor *>> &outProjectionPointees)
908{
909 const auto it = extensions.find(&field);
910 if (it != extensions.end())
911 outExtensions.emplace_back(it->first, it->second);
912
913 if (field.IsProjectedField())
914 outProjectionPointees[field.GetProjectionSourceId()].push_back(&field);
915
916 for (auto childId : field.GetLinkIds()) {
917 const auto &child = desc.GetFieldDescriptor(childId);
918 AddColumnExtensionsInFieldOrder(child, desc, extensions, outExtensions, outProjectionPointees);
919 }
920}
921
922namespace {
923class RNTupleMergeStrategy {
924protected:
925 // Fixed during the whole merging
926 std::unique_ptr<ROOT::RNTupleModel> &fDstModel;
928 const ROOT::RNTupleDescriptor &fDstDescriptor;
929 RNTupleMergeOptions fMergeOpts;
930
931 // Changes at every source
932 const ROOT::RNTupleDescriptor *fSrcDescriptor = nullptr;
933
934public:
935 RNTupleMergeStrategy(std::unique_ptr<RNTupleModel> &dstModel, ROOT::Internal::RPagePersistentSink &dst,
936 const RNTupleMergeOptions &mergeOpts)
937 : fDstModel(dstModel), fDestination(dst), fDstDescriptor(dst.GetDescriptor()), fMergeOpts(mergeOpts)
938 {
939 }
940
941 virtual ~RNTupleMergeStrategy() = default;
942
943 virtual void InitSource(RPageSource &source) = 0;
944 [[nodiscard]] virtual ROOT::RResult<RDescriptorsComparison> CompareSrcAndDstDescriptors() = 0;
945 [[nodiscard]] virtual ROOT::RResult<void> ExtendDestinationModel(RDescriptorsComparison &descCmp) = 0;
946 [[nodiscard]] virtual ROOT::RResult<void> MergeSource(RPageSource &source, RDescriptorsComparison &descCmp) = 0;
947 virtual void BeforeCommit() = 0;
948};
949
950class RNTupleMergeStrategyDefault final : public RNTupleMergeStrategy {
951 std::unique_ptr<ROOT::Internal::RPageAllocator> fPageAlloc = std::make_unique<ROOT::Internal::RPageAllocatorHeap>();
952 std::optional<ROOT::Experimental::TTaskGroup> fTaskGroup;
953 std::unordered_map<ROOT::DescriptorId_t, std::vector<const ROOT::RFieldDescriptor *>> fProjectionPointees;
954
955 std::vector<RColumnMergeInfo> fColumns;
956 // Maps input column IDs to output IDs
957 ColumnIdMap_t fColumnIdMap;
958
959 [[nodiscard]] ROOT::RResult<void>
960 MergeCommonColumns(ROOT::Internal::RClusterPool &clusterPool, const ROOT::RClusterDescriptor &clusterDesc,
961 std::span<RColumnMergeInfo> commonColumns, const RCluster::ColumnSet_t &commonColumnSet,
962 RSealedPageMergeData &sealedPageData);
963 [[nodiscard]]
964 ROOT::RResult<void> GenerateZeroPagesForColumns(size_t nEntriesToGenerate, std::span<const RColumnMergeInfo> columns,
965 RSealedPageMergeData &sealedPageData);
966
967 void AddColumnsFromField(std::vector<RColumnMergeInfo> &columns, const ROOT::RNTupleDescriptor &srcDesc,
968 const FieldCollectionMap_t<RColReprMapping> &colReprMappings,
969 const ROOT::RFieldDescriptor &srcFieldDesc, const ROOT::RFieldDescriptor &dstFieldDesc,
970 const std::string &prefix = "");
971
972 RColumnInfoGroup GatherColumnInfos(const RDescriptorsComparison &descCmp, const ROOT::RNTupleDescriptor &srcDesc);
973
974 [[nodiscard]] ROOT::RResult<void> MergeSourceClusters(RPageSource &source, std::span<RColumnMergeInfo> commonColumns,
975 std::span<const RColumnMergeInfo> extraDstColumns);
976
977public:
978 RNTupleMergeStrategyDefault(std::unique_ptr<RNTupleModel> &dstModel, ROOT::Internal::RPagePersistentSink &dst,
979 const RNTupleMergeOptions &mergeOpts)
980 : RNTupleMergeStrategy(dstModel, dst, mergeOpts)
981 {
982#ifdef R__USE_IMT
984 fTaskGroup = ROOT::Experimental::TTaskGroup();
985#endif
986
987 if (dstModel) {
988 // If this is an incremental merging, pre-fill the column id map with the existing destination ids.
989 // Otherwise we would generate new output ids that may not match the ones in the destination!
990 for (const auto &field : dst.GetDescriptor().GetTopLevelFields()) {
991 PrefillColumnMap(dst.GetDescriptor(), field, fColumnIdMap);
992 }
993 }
994 }
995
996 void InitSource(RPageSource &source) final
997 {
998 source.Attach(RNTupleSerializer::EDescriptorDeserializeMode::kForWriting);
999 source.LoadAllPageLists();
1000 fSrcDescriptor = &source.GetSharedDescriptorGuard().GetConstRef();
1001
1002 if (!fDstModel) {
1003 constexpr bool copyClusters = false;
1004 fDstModel = fDestination.InitFromDescriptor(*fSrcDescriptor, copyClusters);
1005 }
1006 }
1007
1008 ROOT::RResult<RDescriptorsComparison> CompareSrcAndDstDescriptors() final
1009 {
1010 return CompareDescriptorStructure(fDstDescriptor, *fSrcDescriptor);
1011 }
1012
1013 [[nodiscard]] ROOT::RResult<void> ExtendDestinationModel(RDescriptorsComparison &descCmp) final
1014 {
1015 // late model extension for all fExtraSrcFields in Union mode
1016 auto res = ::ExtendDestinationModel(descCmp, *fDstModel, fDestination, *fSrcDescriptor, fMergeOpts.fExtraVerbose);
1017 return res;
1018 }
1019
1020 void BeforeCommit() final {}
1021
1022 [[nodiscard]] ROOT::RResult<void> MergeSource(RPageSource &source, RDescriptorsComparison &descCmp) final;
1023};
1024
1025} // namespace
1026
1027// Given a field, fill `columns` and `mergeData.fColumnIdMap` with information about all columns belonging to it and
1028// its subfields. `fColumnIdMap` is used to map matching columns from different sources to the same output
1029// column in the destination. We match columns by their "fully qualified name", which is the concatenation of their
1030// ancestor fields' names and the column index. By this point, since we called `CompareDescriptorStructure()`
1031// earlier, we should be guaranteed that two matching columns will have at least compatible representations.
1032// This function is recursive as it needs to call itself on the entire subfield hierarchy of the source field.
1033// NOTE: srcFieldDesc and dstFieldDesc may alias.
1034void RNTupleMergeStrategyDefault::AddColumnsFromField(std::vector<RColumnMergeInfo> &columns,
1035 const ROOT::RNTupleDescriptor &srcDesc,
1036 const FieldCollectionMap_t<RColReprMapping> &colReprMappings,
1037 const ROOT::RFieldDescriptor &srcFieldDesc,
1038 const ROOT::RFieldDescriptor &dstFieldDesc,
1039 const std::string &prefix)
1040{
1041 std::string name = prefix + '.' + srcFieldDesc.GetFieldName();
1042
1043 // We don't want to try and merge alias columns. Note that subfields of projected fields
1044 // must also be projected, so we don't need to check them.
1045 if (srcFieldDesc.IsProjectedField())
1046 return;
1047
1048 const auto &columnIds = srcFieldDesc.GetLogicalColumnIds();
1049 columns.reserve(columns.size() + columnIds.size());
1050
1051 for (auto i = 0u; i < srcFieldDesc.GetLogicalColumnIds().size(); ++i) {
1052 auto srcColumnId = srcFieldDesc.GetLogicalColumnIds()[i];
1053 const auto &srcColumn = srcDesc.GetColumnDescriptor(srcColumnId);
1054
1055 RColumnMergeInfo info{};
1056 info.fInputId = srcColumn.GetPhysicalId();
1057 // NOTE(gparolini): the parent field is used when synthesizing zero pages, which happens in 2 situations:
1058 // 1. when adding extra dst columns (in which case we need to synthesize zero pages for the incoming src), and
1059 // 2. when merging a deferred column into an existing column (in which case we need to fill the "hole" with
1060 // zeroes). For the first case srcFieldDesc and dstFieldDesc are the same (see the calling site of this
1061 // function), but for the second case they're not, and we need to pick the source field because we will then
1062 // check the column's *input* id inside fParentFieldDescriptor to see if it's a suppressed column (see
1063 // GenerateZeroPagesForColumns()).
1064 info.fParentFieldDescriptor = &srcFieldDesc;
1065 // Save the parent field descriptor since this may be either the source or destination descriptor depending on
1066 // whether this is an extraDstField or a commonField. We will need this in GenerateZeroPagesForColumns() to
1067 // properly walk up the field hierarchy.
1068 info.fParentNTupleDescriptor = &srcDesc;
1069
1070 const auto mappingsIt = colReprMappings.find(&dstFieldDesc);
1071 std::uint16_t reprIndex = srcColumn.GetRepresentationIndex();
1072 if (mappingsIt != colReprMappings.end()) {
1073 if (auto outReprIdx = FindColumnReprMapping(mappingsIt->second, reprIndex); outReprIdx)
1074 reprIndex = *outReprIdx;
1075 }
1076
1077 info.fColumnName = name + '.' + std::to_string(srcColumn.GetIndex()) + '.' + std::to_string(reprIndex);
1078
1080
1081 if (auto it = fColumnIdMap.find(info.fColumnName); it != fColumnIdMap.end()) {
1082 // We had already added this column to the column id map: just copy its data.
1083 info.fOutputId = it->second.fColumnId;
1084 info.fOutputReprIndex = reprIndex;
1085 } else {
1086 // New column: assign it the next ouput id.
1087 info.fOutputId = fColumnIdMap.size();
1088 // NOTE(gparolini): map the representation index of src column to that of dst column.
1089 // This mapping is only relevant for common columns and it's done to ensure we have the correct representation
1090 // index in the output column metadata.
1091 assert(dstFieldDesc.GetColumnCardinality() == srcFieldDesc.GetColumnCardinality());
1092 const auto dstColumnIndex = reprIndex * dstFieldDesc.GetColumnCardinality() + srcColumn.GetIndex();
1093 const auto dstColumnId = dstFieldDesc.GetLogicalColumnIds()[dstColumnIndex];
1094 const auto &dstColumn = fDstDescriptor.GetColumnDescriptor(dstColumnId);
1095 columnType = dstColumn.GetType();
1096 info.fOutputReprIndex = reprIndex;
1097 fColumnIdMap[info.fColumnName] = RColumnOutInfo{info.fOutputId};
1098 }
1099
1100 if (fMergeOpts.fExtraVerbose) {
1101 R__LOG_INFO(NTupleMergeLog()) << "Adding column " << info.fColumnName << " with log.id " << srcColumnId
1102 << ", phys.id " << srcColumn.GetPhysicalId() << ", type "
1103 << RColumnElementBase::GetColumnTypeName(srcColumn.GetType()) << " -> log.id "
1104 << info.fOutputId << ", type "
1106 }
1107
1108 // Since we disallow merging fields of different types, src and dstFieldDesc must have the same type name.
1109 assert(srcDesc.GetTypeNameForComparison(srcFieldDesc) == dstFieldDesc.GetTypeName());
1110 info.fInMemoryType = ColumnInMemoryType(dstFieldDesc.GetTypeName(), columnType);
1111 columns.emplace_back(info);
1112 }
1113
1114 const auto &srcChildrenIds = srcFieldDesc.GetLinkIds();
1115 const auto &dstChildrenIds = dstFieldDesc.GetLinkIds();
1116 assert(srcChildrenIds.size() == dstChildrenIds.size());
1117 for (auto i = 0u; i < srcChildrenIds.size(); ++i) {
1118 const auto &srcChild = srcDesc.GetFieldDescriptor(srcChildrenIds[i]);
1119 const auto &dstChild = fDstDescriptor.GetFieldDescriptor(dstChildrenIds[i]);
1120 AddColumnsFromField(columns, srcDesc, colReprMappings, srcChild, dstChild, name);
1121 }
1122}
1123
1124// Converts the fields comparison data to the corresponding column information.
1125// While doing so, it collects such information in `fColumnIdMap`, which is used by later calls to this
1126// function to map already-seen column names to their chosen outputId, type and so on.
1127RColumnInfoGroup RNTupleMergeStrategyDefault::GatherColumnInfos(const RDescriptorsComparison &descCmp,
1128 const ROOT::RNTupleDescriptor &srcDesc)
1129{
1130 RColumnInfoGroup res;
1131 for (const ROOT::RFieldDescriptor *field : descCmp.fExtraDstFields) {
1132 AddColumnsFromField(res.fExtraDstColumns, fDstDescriptor, descCmp.fColReprMappings, *field, *field);
1133 }
1134 for (const auto &[srcField, dstField] : descCmp.fCommonFields) {
1135 AddColumnsFromField(res.fCommonColumns, srcDesc, descCmp.fColReprMappings, *srcField, *dstField);
1136 }
1137 return res;
1138}
1139
1140// Generates default (zero) values for the given columns
1141ROOT::RResult<void> RNTupleMergeStrategyDefault::GenerateZeroPagesForColumns(size_t nEntriesToGenerate,
1142 std::span<const RColumnMergeInfo> columns,
1143 RSealedPageMergeData &sealedPageData)
1144{
1145 if (!nEntriesToGenerate)
1147
1148 for (const auto &column : columns) {
1149 const ROOT::RFieldDescriptor *field = column.fParentFieldDescriptor;
1150
1151 // Skip all auxiliary columns
1152 assert(!field->GetLogicalColumnIds().empty());
1153 if (field->GetLogicalColumnIds()[0] != column.fInputId)
1154 continue;
1155
1156 // Check if this column is a child of a Collection or a Variant. If so, it has no data
1157 // and can be skipped.
1158 bool skipColumn = false;
1159 auto nRepetitions = std::max<std::uint64_t>(field->GetNRepetitions(), 1);
1160 for (auto parentId = field->GetParentId(); parentId != ROOT::kInvalidDescriptorId;) {
1161 const ROOT::RFieldDescriptor &parent = column.fParentNTupleDescriptor->GetFieldDescriptor(parentId);
1164 skipColumn = true;
1165 break;
1166 }
1167 nRepetitions *= std::max<std::uint64_t>(parent.GetNRepetitions(), 1);
1168 parentId = parent.GetParentId();
1169 }
1170 if (skipColumn)
1171 continue;
1172
1173 const auto structure = field->GetStructure();
1174
1175 if (structure == ROOT::ENTupleStructure::kStreamer) {
1176 return R__FAIL("Destination RNTuple contains a streamer field (" + field->GetFieldName() +
1177 ") that is not present in one of the sources. "
1178 "Creating a default value for a streamer field is ill-defined, therefore the merging "
1179 "process will abort.");
1180 }
1181
1182 // NOTE: we cannot have a Record here because it has no associated columns.
1184 structure == ROOT::ENTupleStructure::kPlain);
1185
1186 const auto &columnDesc = fDstDescriptor.GetColumnDescriptor(column.fOutputId);
1187 const auto colElement = RColumnElementBase::Generate(columnDesc.GetType());
1188 const auto nElements = nEntriesToGenerate * nRepetitions;
1189 const auto nBytesOnStorage = colElement->GetPackedSize(nElements);
1190 // TODO(gparolini): make this configurable
1191 constexpr auto kPageSizeLimit = 256 * 1024;
1192 // TODO(gparolini): consider coalescing the last page if its size is less than some threshold
1193 const size_t nPages = nBytesOnStorage / kPageSizeLimit + !!(nBytesOnStorage % kPageSizeLimit);
1194 for (size_t i = 0; i < nPages; ++i) {
1195 const auto pageSize = (i < nPages - 1) ? kPageSizeLimit : nBytesOnStorage - kPageSizeLimit * (nPages - 1);
1196 const auto checksumSize = RPageStorage::kNBytesPageChecksum;
1197 const auto bufSize = pageSize + checksumSize;
1198 assert(pageSize % colElement->GetSize() == 0);
1199 const auto nElementsPerPage = pageSize / colElement->GetSize();
1200 auto page = fPageAlloc->NewPage(colElement->GetSize(), nElementsPerPage);
1201 page.GrowUnchecked(nElementsPerPage);
1202 memset(page.GetBuffer(), 0, page.GetNBytes());
1203
1204 auto &buffer = sealedPageData.fBuffers.emplace_back(new std::byte[bufSize]);
1206 sealConf.fElement = colElement.get();
1207 sealConf.fPage = &page;
1208 sealConf.fBuffer = buffer.get();
1209 sealConf.fCompressionSettings = fMergeOpts.fCompressionSettings.value();
1210 sealConf.fWriteChecksum = fDestination.GetWriteOptions().GetEnablePageChecksums();
1211 auto sealedPage = RPageSink::SealPage(sealConf);
1212
1213 sealedPageData.fPagesV.push_back({sealedPage});
1214 sealedPageData.fGroups.emplace_back(column.fOutputId, sealedPageData.fPagesV.back().cbegin(),
1215 sealedPageData.fPagesV.back().cend());
1216 }
1217 }
1219}
1220
1221// Merges all columns appearing both in the source and destination RNTuples, just copying them if their
1222// compression matches ("fast merge") or by unsealing and resealing them with the proper compression.
1223ROOT::RResult<void> RNTupleMergeStrategyDefault::MergeCommonColumns(ROOT::Internal::RClusterPool &clusterPool,
1224 const ROOT::RClusterDescriptor &clusterDesc,
1225 std::span<RColumnMergeInfo> commonColumns,
1226 const RCluster::ColumnSet_t &commonColumnSet,
1227 RSealedPageMergeData &sealedPageData)
1228{
1229 const auto nCommonColumnsInCluster = commonColumnSet.size();
1230 assert(nCommonColumnsInCluster <= commonColumns.size());
1231
1232 if (nCommonColumnsInCluster == 0)
1234
1235 const RCluster *cluster = clusterPool.GetCluster(clusterDesc.GetId(), commonColumnSet);
1236 // we expect the cluster pool to contain the requested set of columns, since they were
1237 // validated by CompareDescriptorStructure() and MergeSourceClusters().
1238 assert(cluster);
1239
1240 const std::uint32_t outCompression = fMergeOpts.fCompressionSettings.value();
1241
1242 for (size_t colIdx = 0; colIdx < nCommonColumnsInCluster; ++colIdx) {
1243 const auto &column = commonColumns[colIdx];
1244 const auto &columnId = column.fInputId;
1245 R__ASSERT(clusterDesc.ContainsColumn(columnId));
1246
1247 const auto &columnDesc = fSrcDescriptor->GetColumnDescriptor(columnId);
1248 const auto srcColElement = column.fInMemoryType
1249 ? ROOT::Internal::GenerateColumnElement(*column.fInMemoryType, columnDesc.GetType())
1250 : RColumnElementBase::Generate(columnDesc.GetType());
1251
1252 // Now get the pages for this column in this cluster
1253 const auto &pages = clusterDesc.GetPageRange(columnId);
1254
1256 sealedPages.resize(pages.GetPageInfos().size());
1257
1258 // Each column range potentially has a distinct compression settings
1259 const auto &columnRange = clusterDesc.GetColumnRange(columnId);
1260 assert(!columnRange.IsSuppressed());
1261 const auto colRangeCompressionSettings = columnRange.GetCompressionSettings().value();
1262
1263 // Select "merging level". There are 2 levels, from fastest to slowest, depending on the case:
1264 // L1: compression and encoding of src and dest both match: we can simply copy the page
1265 // L2: compression of dest doesn't match the src we must recompress the page.
1266 // Note that in no case do we need to re-encode the page, as if the encoding differs we simply
1267 // append a new column representation to the field.
1268 const bool needsRecompressing = colRangeCompressionSettings != outCompression;
1269
1270 if (needsRecompressing && fMergeOpts.fExtraVerbose) {
1271 R__LOG_INFO(NTupleMergeLog()) << "Recompressing column " << column.fColumnName
1272 << ": { compression: " << colRangeCompressionSettings << " => "
1273 << fMergeOpts.fCompressionSettings.value() << ", onDiskType: "
1275 srcColElement->GetIdentifier().fOnDiskType)
1276 << "}";
1277 }
1278
1279 const size_t pageBufferBaseIdx = sealedPageData.fBuffers.size();
1280 // If the column range already has the right compression we don't need to allocate any new buffer, so we don't
1281 // bother reserving memory for them.
1282 if (needsRecompressing)
1283 sealedPageData.fBuffers.resize(sealedPageData.fBuffers.size() + pages.GetPageInfos().size());
1284
1285 // If this column is deferred, we may need to fill "holes" until its real start. We fill any missing entry
1286 // with zeroes, like we do for extraDstColumns.
1287 // As an optimization, we don't do this for the first source (since we can rely on the FirstElementIndex and
1288 // deferred column mechanism in that case).
1289 // TODO: also avoid doing this if we added no real page of this column to the destination yet.
1290 if (columnDesc.GetFirstElementIndex() > clusterDesc.GetFirstEntryIndex() && fDestination.GetNEntries() > 0) {
1291 const auto nMissingEntries = columnDesc.GetFirstElementIndex() - clusterDesc.GetFirstEntryIndex();
1292 auto res = GenerateZeroPagesForColumns(nMissingEntries, {&column, 1}, sealedPageData);
1293 if (!res)
1294 return R__FORWARD_ERROR(res);
1295 }
1296
1297 // Loop over the pages
1298 std::uint64_t pageIdx = 0;
1299 for (const auto &pageInfo : pages.GetPageInfos()) {
1300 assert(pageIdx < sealedPages.size());
1301 assert(sealedPageData.fBuffers.size() == 0 || pageIdx < sealedPageData.fBuffers.size());
1302 assert(pageInfo.GetLocator().GetType() != ROOT::RNTupleLocator::kTypePageZero);
1303
1304 ROOT::Internal::ROnDiskPage::Key key{columnId, pageIdx};
1305 auto onDiskPage = cluster->GetOnDiskPage(key);
1306
1307 const auto checksumSize = pageInfo.HasChecksum() * RPageStorage::kNBytesPageChecksum;
1308 RPageStorage::RSealedPage &sealedPage = sealedPages[pageIdx];
1309 sealedPage.SetNElements(pageInfo.GetNElements());
1310 sealedPage.SetHasChecksum(pageInfo.HasChecksum());
1311 sealedPage.SetBufferSize(pageInfo.GetLocator().GetNBytesOnStorage() + checksumSize);
1312 sealedPage.SetBuffer(onDiskPage->GetAddress());
1313 // TODO(gparolini): more graceful error handling (skip the page?)
1315 R__ASSERT(onDiskPage && (onDiskPage->GetSize() == sealedPage.GetBufferSize()));
1316
1317 if (needsRecompressing) {
1318 const auto uncompressedSize = srcColElement->GetSize() * sealedPage.GetNElements();
1319 auto &buffer = sealedPageData.fBuffers[pageBufferBaseIdx + pageIdx];
1320 const auto bufSize = uncompressedSize + checksumSize;
1321 // NOTE: we currently allocate the max possible size for this buffer and don't shrink it afterward.
1322 // We might want to introduce an option that trades speed for memory usage and shrink the buffer to fit
1323 // the actual data size after recompressing.
1324 buffer = MakeUninitArray<std::byte>(bufSize);
1325
1326 // clang-format off
1327 RTaskVisitor{fTaskGroup}(RChangeCompressionFunc{
1328 *srcColElement,
1329 outCompression,
1330 sealedPage,
1331 *fPageAlloc,
1332 buffer.get(),
1333 bufSize,
1334 fDestination.GetWriteOptions()
1335 });
1336 // clang-format on
1337 }
1338
1339 ++pageIdx;
1340
1341 } // end of loop over pages
1342
1343 if (fTaskGroup)
1344 fTaskGroup->Wait();
1345
1346 sealedPageData.fPagesV.push_back(std::move(sealedPages));
1347 sealedPageData.fGroups.emplace_back(column.fOutputId, sealedPageData.fPagesV.back().cbegin(),
1348 sealedPageData.fPagesV.back().cend());
1349 } // end loop over common columns
1350
1352}
1353
1354// Iterates over all clusters of `source` and merges their pages into `destination`.
1355// It is assumed that all columns in `commonColumns` are present (and compatible) in both the source and
1356// the destination's schemas.
1357// The pages may be "fast-merged" (i.e. simply copied with no decompression/recompression) if the target
1358// compression is unspecified or matches the original compression settings.
1359ROOT::RResult<void> RNTupleMergeStrategyDefault::MergeSourceClusters(RPageSource &source,
1360 std::span<RColumnMergeInfo> commonColumns,
1361 std::span<const RColumnMergeInfo> extraDstColumns)
1362{
1363 ROOT::Internal::RClusterPool clusterPool{source};
1364
1365 std::vector<RColumnMergeInfo> missingColumns{extraDstColumns.begin(), extraDstColumns.end()};
1366
1367 R__ASSERT(fSrcDescriptor->GetNClusters() == fSrcDescriptor->GetNActiveClusters());
1368 for (const auto &clusterDesc : fSrcDescriptor->GetActiveClusterIterable()) {
1369 const auto nClusterEntries = clusterDesc.GetNEntries();
1370 R__ASSERT(nClusterEntries > 0);
1371
1372 // Deduce which columns are suppressed (cluster by cluster) by exclusion, as:
1373 // (columns in the columnIdMap) - (columns in commonColumns which are not suppressed).
1374 // Note that some suppressed columns may not be in commonColumns because they might not appear at all in the
1375 // current source.
1376 FieldCollectionMap_t<ROOT::DescriptorId_t> columnsInCluster;
1377 using ColumnHandle_t = ROOT::Internal::RPageStorage::ColumnHandle_t;
1378
1379 // NOTE: `commonColumns` contains all columns that appear *somewhere* both in the src and in the dst.
1380 // Just because a column is in `commonColumns` it doesn't mean that each cluster in the source contains
1381 // it, as it may be a deferred column that only has real data in a future cluster. We need to figure out which
1382 // columns are actually present in this cluster so we only merge their pages (the missing columns are handled
1383 // by synthesizing zero pages - see below).
1384
1385 // Convert columns to a ColumnSet for the ClusterPool query
1386 RCluster::ColumnSet_t commonColumnSet;
1387 // Collect all common columns appearing in this cluster into commonColumnSet and reorganize commonColumns so
1388 // that those columns are at the start of it (whereas missing columns are at its end).
1389 // NOTE: it's fine if this scrambles the order of columns: the RNTupleSerializer will sort them by physical ID.
1390 int nCommonColumnsInCluster = 0;
1391 std::partition(commonColumns.begin(), commonColumns.end(), [&](const auto &column) {
1392 if (clusterDesc.ContainsColumn(column.fInputId)) {
1393 const auto &colRange = clusterDesc.GetColumnRange(column.fInputId);
1394 ++nCommonColumnsInCluster;
1395 columnsInCluster[column.fParentFieldDescriptor].push_back(column.fOutputId);
1396 if (!colRange.IsSuppressed()) {
1397 commonColumnSet.emplace(column.fInputId);
1398 return true;
1399 }
1400 fDestination.CommitSuppressedColumn(ColumnHandle_t{column.fOutputId});
1401 }
1402 return false;
1403 });
1404
1405 // Commit all suppressed columns.
1406 // This is a fairly involved operation, as we need to commit all known columns that:
1407 // a) do not appear in extraDstColumns (those are "missing", not suppressed), and
1408 // b) do not appear in commonColumnSet (those are the active columns).
1409 // Not that these may or may not appear in commonColumns as suppressed columns, since they may or may not be
1410 // present in the current source.
1411 // The only way to find all the columns is to go and get them from fColumnIdMap, which keeps track of every
1412 // column we added to the destination so far. However, since it also contains the extraDstColumns, we need to
1413 // specifically only query those columns that belong to a field that has at least 1 column in commonColumns
1414 // (remember that commonColumns contains all columns associated to the common fields for this source).
1415 for (const auto &[fieldDesc, columnIds] : columnsInCluster) {
1416 const auto &fieldFQName = fSrcDescriptor->GetQualifiedFieldName(fieldDesc->GetId());
1417 const auto cardinality = fieldDesc->GetColumnCardinality();
1418 for (auto i = 0u; i < fieldDesc->GetLogicalColumnIds().size(); ++i) {
1419 const auto colIndex = i % cardinality;
1420 const auto reprIndex = i / cardinality;
1421 const auto colName = "." + fieldFQName + '.' + std::to_string(colIndex) + '.' + std::to_string(reprIndex);
1422 const auto colIt = fColumnIdMap.find(colName);
1423 assert(colIt != fColumnIdMap.end());
1424 const auto colOutId = colIt->second.fColumnId;
1425 if (std::find(columnIds.begin(), columnIds.end(), colOutId) == columnIds.end()) {
1426 fDestination.CommitSuppressedColumn(ColumnHandle_t{colOutId});
1427 }
1428 }
1429 }
1430
1431 RSealedPageMergeData sealedPageData;
1432 auto res = MergeCommonColumns(clusterPool, clusterDesc, commonColumns, commonColumnSet, sealedPageData);
1433 if (!res)
1434 return R__FORWARD_ERROR(res);
1435
1436 // Generate zero pages for the missing columns.
1437 // For each cluster, the "missing columns" are the union of the extraDstColumns and the common columns
1438 // that are not present in the cluster.
1439 // Note that this does NOT include suppressed columns, for which no pages are synthesized.
1440 missingColumns.resize(extraDstColumns.size()); // NOTE: this clears all common columns of the previous cluster
1441 for (size_t i = nCommonColumnsInCluster; i < commonColumns.size(); ++i)
1442 missingColumns.push_back(commonColumns[i]);
1443
1444 res = GenerateZeroPagesForColumns(nClusterEntries, missingColumns, sealedPageData);
1445 if (!res)
1446 return R__FORWARD_ERROR(res);
1447
1448 // Commit the pages and the clusters
1449 fDestination.CommitSealedPageV(sealedPageData.fGroups);
1450 fDestination.CommitCluster(nClusterEntries);
1451 }
1452
1453 // TODO(gparolini): when we get serious about huge file support (>~ 100GB) we might want to check here
1454 // the size of the running page list and commit a cluster group when it exceeds some threshold,
1455 // which would prevent the page list from getting too large.
1456 // However, as of today, we aren't really handling such huge files, and even relatively big ones
1457 // such as the CMS dataset have a page list size of about only 2 MB.
1458 // So currently we simply merge all cluster groups into one.
1460}
1461
1462ROOT::RResult<void> RNTupleMergeStrategyDefault::MergeSource(RPageSource &source, RDescriptorsComparison &descCmp)
1463{
1464 //// Extend columns if needed
1465 if (!descCmp.fColReprExtensions.empty()) {
1466 for (const auto &field : descCmp.fExtraDstFields) {
1467 if (field->IsProjectedField())
1468 fProjectionPointees[field->GetProjectionSourceId()].push_back(field);
1469 }
1470
1471 // We need to extend the columns in the proper order, i.e. so that they appear in the same order as
1472 // their first representation. This is to ensure that the pages we write to the cluster are in a consistent
1473 // order as their column descriptors. The page creation order is determined by the order of
1474 // columnInfos.fCommonColumns, which in turn depends on the common fields order (see GatherColumnInfos).
1475 // XXX: do we need this separate sort step? Why not just create this vector directly in
1476 // CompareDescriptorStructure?
1477 std::vector<std::pair<const ROOT::RFieldDescriptor *, std::vector<RColReprExtension>>> colExtensions;
1478 colExtensions.reserve(descCmp.fColReprExtensions.size());
1479 for (const auto &commonField : descCmp.fCommonFields) {
1480 const auto *field = commonField.fDst;
1481 AddColumnExtensionsInFieldOrder(*field, fDstDescriptor, descCmp.fColReprExtensions, colExtensions,
1482 fProjectionPointees);
1483 }
1484 for (const auto &field : descCmp.fExtraSrcFields) {
1485 if (field->IsProjectedField())
1486 fProjectionPointees[field->GetProjectionSourceId()].push_back(field);
1487 }
1488
1489 for (const auto &[fieldDesc, extensions] : colExtensions) {
1490 auto &mappings = descCmp.fColReprMappings[fieldDesc];
1491 for (const auto &extension : extensions) {
1492 const auto firstColumnId = fDestination.AddColumnRepresentation(*fieldDesc, extension.fSourceRepr,
1493 extension.fOrigFirstElementIndex);
1494
1495 // When adding new column representations to an existing field which is the source of some projected
1496 // fields, we need to also add new alias columns to those fields so that they can point to the proper
1497 // representation.
1498 if (auto it = fProjectionPointees.find(fieldDesc->GetId()); it != fProjectionPointees.end()) {
1499 for (const auto &projection : it->second) {
1500 for (auto colIdx = 0u; colIdx < extension.fSourceRepr.size(); ++colIdx)
1501 fDestination.AddAliasColumn(fDstDescriptor, *projection, firstColumnId + colIdx);
1502 }
1503 }
1504 mappings.push_back(extension);
1505 }
1506 }
1507 }
1508
1509 // handle extra dst fields & common fields
1510 auto columnInfos = GatherColumnInfos(descCmp, *fSrcDescriptor);
1511 auto res = MergeSourceClusters(source, columnInfos.fCommonColumns, columnInfos.fExtraDstColumns);
1512 return res;
1513}
1514
1515RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination,
1516 std::unique_ptr<ROOT::RNTupleModel> model)
1517 : fDestination(std::move(destination)), fModel(std::move(model))
1518{
1520}
1521
1522RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination)
1523 : ROOT::Experimental::Internal::RNTupleMerger(std::move(destination), nullptr)
1524{
1525}
1526
1527ROOT::RResult<void> RNTupleMerger::Merge(std::span<RPageSource *> sources, const RNTupleMergeOptions &mergeOptsIn)
1528{
1529 RNTupleMergeOptions mergeOpts = mergeOptsIn;
1530
1531 assert(fDestination);
1532
1533 // Set compression settings if unset and verify it's compatible with the sink
1534 {
1535 const auto dstCompSettings = fDestination->GetWriteOptions().GetCompression();
1536 if (!mergeOpts.fCompressionSettings) {
1537 mergeOpts.fCompressionSettings = dstCompSettings;
1538 } else if (*mergeOpts.fCompressionSettings != dstCompSettings) {
1539 return R__FAIL(std::string("The compression given to RNTupleMergeOptions is different from that of the "
1540 "sink! (opts: ") +
1541 std::to_string(*mergeOpts.fCompressionSettings) + ", sink: " + std::to_string(dstCompSettings) +
1542 ") This is currently unsupported.");
1543 }
1544 }
1545
1546 // we should have a model if and only if the destination is initialized.
1547 if (!!fModel != fDestination->IsInitialized()) {
1548 return R__FAIL(
1549 "passing an already-initialized destination to RNTupleMerger::Merge (i.e. trying to do incremental "
1550 "merging) can only be done by providing a valid ROOT::RNTupleModel when constructing the RNTupleMerger.");
1551 }
1552
1553 // NOTE: don't wrap this in a do {} while (0)! It uses continue!
1554#define SKIP_OR_ABORT(errMsg) \
1555 if (mergeOpts.fErrBehavior == ENTupleMergeErrBehavior::kSkip) { \
1556 R__LOG_WARNING(NTupleMergeLog()) << "Skipping RNTuple due to: " << (errMsg); \
1557 continue; \
1558 } else { \
1559 return R__FAIL(errMsg); \
1560 }
1561
1562 auto strat =
1563 std::unique_ptr<RNTupleMergeStrategy>(new RNTupleMergeStrategyDefault(fModel, *fDestination, mergeOpts));
1564
1565 // Merge main loop
1566 for (RPageSource *source : sources) {
1567 strat->InitSource(*source);
1568
1569 {
1570 auto srcDescriptor = source->GetSharedDescriptorGuard();
1571 if (srcDescriptor->GetVersion() > ROOT::RNTuple::GetCurrentVersion()) {
1574 << "RNTuple '" << srcDescriptor->GetName()
1575 << "' has a higher format version than the latest supported by this version "
1576 "of ROOT. Merging will work but some features may be dropped.";
1577 } else {
1578 return R__FAIL("RNTuple '" + srcDescriptor->GetName() +
1579 "' has a higher format version than the latest supported by this version. Refusing to "
1580 "merge, since RNTupleMergeOptions::fVersionBehavior is set to AbortOnHigherVersion.");
1581 }
1582 }
1583
1584 for (const auto &extraTypeInfoDesc : srcDescriptor->GetExtraTypeInfoIterable())
1585 fDestination->UpdateExtraTypeInfo(extraTypeInfoDesc);
1586 }
1587
1588 auto descCmpRes = strat->CompareSrcAndDstDescriptors();
1589 if (!descCmpRes) {
1590 SKIP_OR_ABORT(std::string("Source RNTuple has an incompatible schema with the destination:\n") +
1591 descCmpRes.GetError()->GetReport())
1592 }
1593 auto descCmp = descCmpRes.Unwrap();
1594
1595 // If the current source is missing some fields and we're not in Union mode, error
1596 // (if we are in Union mode, MergeSourceClusters will fill the missing fields with default values).
1597 if (mergeOpts.fMergingMode != ENTupleMergingMode::kUnion && !descCmp.fExtraDstFields.empty()) {
1598 std::string msg = "Source RNTuple is missing the following fields:";
1599 for (const auto *field : descCmp.fExtraDstFields) {
1600 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1601 }
1602 SKIP_OR_ABORT(msg)
1603 }
1604
1605 // handle extra src fields
1606 if (!descCmp.fExtraSrcFields.empty()) {
1607 if (mergeOpts.fMergingMode == ENTupleMergingMode::kUnion) {
1608 // late model extension for all fExtraSrcFields in Union mode
1609 auto res = strat->ExtendDestinationModel(descCmp);
1610 if (!res)
1611 return R__FORWARD_ERROR(res);
1612 } else if (mergeOpts.fMergingMode == ENTupleMergingMode::kStrict) {
1613 // If the current source has extra fields and we're in Strict mode, error
1614 std::string msg = "Source RNTuple has extra fields that the destination RNTuple doesn't have:";
1615 for (const auto *field : descCmp.fExtraSrcFields) {
1616 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1617 }
1618 SKIP_OR_ABORT(msg)
1619 }
1620 }
1621
1622 auto res = strat->MergeSource(*source, descCmp);
1623 if (!res)
1624 return R__FORWARD_ERROR(res);
1625 } // end loop over sources
1626
1627 // Commit the output
1628 strat->BeforeCommit();
1629
1630 if (fDestination->GetNEntries() == 0)
1631 R__LOG_WARNING(NTupleMergeLog()) << "Output RNTuple '" << fDestination->GetNTupleName() << "' has no entries.";
1632 else
1633 fDestination->CommitClusterGroup();
1634
1635 fDestination->CommitDataset();
1636
1637 return RResult<void>::Success();
1638}
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__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 ROOT::RResult< RDescriptorsComparison > CompareDescriptorStructure(const ROOT::RNTupleDescriptor &dst, const ROOT::RNTupleDescriptor &src)
Compares the top level fields of dst and src and determines whether they can be merged or not.
static RDescriptorsComparison FillCommonAndExtraFields(const ROOT::RNTupleDescriptor &dst, const ROOT::RNTupleDescriptor &src)
static std::optional< ENTupleMergeErrBehavior > ParseOptionErrBehavior(const TString &opts)
static ROOT::RLogChannel & NTupleMergeLog()
static ROOT::RResult< void > ExtendDestinationModel(RDescriptorsComparison &descCmp, ROOT::RNTupleModel &dstModel, ROOT::Internal::RPageSink &destination, const ROOT::RNTupleDescriptor &srcDesc, bool verbose)
#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 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.
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 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(ROOT::ENTupleColumnType type)
If CppT == void, use the default C++ type for the given column type.
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.
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.
Abstract interface to write data into an ntuple.
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.
virtual ROOT::NTupleSize_t GetNEntries() const =0
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.
Abstract interface to read data from an ntuple.
RSharedDescriptorGuard GetSharedDescriptorGuard() const
Takes the read lock for the descriptor.
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
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.
std::vector< RFieldBase * > GetMutableSubfields()
std::string GetQualifiedFieldName() const
Returns the field name and parent field names separated by dots (grandparent.parent....
std::vector< ROOT::ENTupleColumnType > ColumnRepresentation_t
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
std::string GetTypeNameForComparison(const RFieldDescriptor &fieldDesc) const
Adjust the type name of the passed RFieldDescriptor for comparison with another renormalized type nam...
ROOT::DescriptorId_t FindFieldId(std::string_view fieldName, ROOT::DescriptorId_t parentId) const
ROOT::DescriptorId_t R__DEPRECATED(6, 46, "This function is ill-defined in the descriptor " "as not all cluster descriptors may be present. This interface is no longer exposed.") FindClusterId(ROOT ROOT::DescriptorId_t R__DEPRECATED(6, 46, "This function is ill-defined in the descriptor " "as not all cluster descriptors may be present. This interface is no longer exposed.") FindNextClusterId(ROOT ROOT::DescriptorId_t R__DEPRECATED(6, 46, "This function is ill-defined in the descriptor " "as not all cluster descriptors may be present. This interface is no longer exposed.") FindPrevClusterId(ROOT std::string GetQualifiedFieldName(ROOT::DescriptorId_t fieldId) const
Walks up the parents of the field ID and returns a field name of the form a.b.c.d In case of invalid ...
The RNTupleModel encapulates the schema of an RNTuple.
void Unfreeze()
Transitions an RNTupleModel from the frozen state back to the building state, invalidating all previo...
void Freeze()
Transitions an RNTupleModel from the building state to the frozen state, disabling adding additional ...
const ROOT::RFieldBase & GetConstField(std::string_view fieldName) const
Common user-tunable settings for storing RNTuples.
void SetCompression(std::uint32_t val)
Representation of an RNTuple data set in a ROOT file.
Definition RNTuple.hxx:67
Long64_t Merge(TCollection *input, TFileMergeInfo *mergeInfo)
RNTuple implements the hadd MergeFile interface Merge this NTuple with the input list entries.
static constexpr std::uint64_t GetCurrentVersion()
Returns the RNTuple version in the following form: Epoch: 2 most significant bytes Major: next 2 byte...
Definition RNTuple.hxx:90
void SetQuantized(std::size_t nBits, std::pair< T, T > valueRange)
Sets this field to use a quantized integer representation using nBits per value.
void SetTruncated(std::size_t nBits)
Set the on-disk representation of this field to a single-precision float truncated to nBits.
void ThrowOnError()
Short-hand method to throw an exception in the case of errors.
Definition RError.hxx:312
The class is used as a return type for operations that can fail; wraps a value of type T or an RError...
Definition RError.hxx:222
Collection abstract base class.
Definition TCollection.h:65
virtual Int_t GetEntries() const
TKey * FindKey(const char *keyname) const override
Find key with name keyname in the current directory.
TObject * Get(const char *namecycle) override
Return pointer to object identified by namecycle.
A class to pass information from the TFileMerger to the objects being merged.
TString fOptions
Additional text based option being passed down to customize the merge.
A file, usually with extension .root, that stores data and code in the form of serialized objects in ...
Definition TFile.h:130
Int_t GetCompressionSettings() const
Definition TFile.h:489
Book space in a file, create I/O buffers, to fill them, (un)compress them.
Definition TKey.h:28
T * ReadObject()
To read an object (non deriving from TObject) from the file.
Definition TKey.h:105
const char * GetName() const override
Returns name of object.
Definition TNamed.h:49
Mother of all ROOT objects.
Definition TObject.h:42
virtual const char * ClassName() const
Returns name of class to which the object belongs.
Definition TObject.cxx:226
Basic string class.
Definition TString.h:137
Ssiz_t Length() const
Definition TString.h:426
@ kIgnoreCase
Definition TString.h:284
Bool_t Contains(const char *pat, ECaseCompare cmp=kExact) const
Definition TString.h:642
Ssiz_t Index(const char *pat, Ssiz_t i=0, ECaseCompare cmp=kExact) const
Definition TString.h:661
Double_t ex[n]
Definition legend1.C:17
@ 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.
@ kWarnOnHigherVersion
The merger will emit a warning when merging RNTuples with higher version than the latest supported by...
std::unique_ptr< T[]> MakeUninitArray(std::size_t size)
Make an array of default-initialized elements.
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.
constexpr DescriptorId_t kInvalidDescriptorId
Type GetType(const std::string &Name)
if(fPos !=fText.size()) Fail("unexpected trailing content")
Set of merging options to pass to RNTupleMerger.
ENTupleMergingMode fMergingMode
Determines how the merging treats sources with different models (.
ENTupleMergeVersionBehavior fVersionBehavior
Determines how the Merge function behaves depending on the RNTuple sources' version.
ENTupleMergeErrBehavior fErrBehavior
Determines how the Merge function behaves upon merging errors.
std::optional< std::uint32_t > fCompressionSettings
If fCompressionSettings is empty (the default), the merger will not change the compression of any of ...
bool fExtraVerbose
If true, the merger will emit further diagnostics and information.
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