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 auto descGuard = source->GetSharedDescriptorGuard();
198 auto clusterGroupIterable = descGuard->GetClusterGroupIterable();
199 if (clusterGroupIterable.empty()) {
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 firstClusterGroup = clusterGroupIterable.begin();
209 R__ASSERT(firstClusterGroup->HasClusterDetails());
210 const auto &firstCluster = descGuard->GetClusterDescriptor(firstClusterGroup->GetClusterIds()[0]);
211 auto colRangeIter = firstCluster.GetColumnRangeIterable();
212 auto firstColRange = colRangeIter.begin();
213 if (firstColRange == colRangeIter.end()) {
215 << "Asked to use the first source's compression as the output compression, but the "
216 "first source (file '"
217 << inFile->GetName()
218 << "') has an empty RNTuple, therefore the output compression could not be "
219 "determined.";
220 return -1;
221 }
222 compression = (*firstColRange).GetCompressionSettings();
223 R__LOG_INFO(NTupleMergeLog()) << "Using the first RNTuple's compression: " << *compression;
224 }
225 sources.push_back(std::move(source));
226 }
227
228 RNTupleWriteOptions writeOpts;
229 assert(compression);
230 writeOpts.SetCompression(*compression);
231 auto destination = std::make_unique<ROOT::Internal::RPageSinkFile>(ntupleName, *outFile, writeOpts);
232 std::unique_ptr<ROOT::RNTupleModel> model;
233 // If we already have an existing RNTuple, copy over its descriptor to support incremental merging
234 if (outNTuple) {
235 auto outSource = RPageSourceFile::CreateFromAnchor(*outNTuple);
236 outSource->Attach(RNTupleSerializer::EDescriptorDeserializeMode::kForWriting);
237 auto desc = outSource->GetSharedDescriptorGuard();
238 model = destination->InitFromDescriptor(desc.GetRef(), 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
273namespace {
274// Functor used to change the compression of a page to `fCompressionSettings`.
275struct RChangeCompressionFunc {
276 const RColumnElementBase &fSrcColElement;
277 std::uint32_t fCompressionSettings;
278 RPageStorage::RSealedPage &fSealedPage;
280 std::byte *fBuffer;
281 std::size_t fBufSize;
282 const ROOT::RNTupleWriteOptions &fWriteOpts;
283
284 void operator()() const
285 {
287
288 const auto bytesPacked = fSrcColElement.GetPackedSize(fSealedPage.GetNElements());
289 // TODO: this buffer could be kept and reused across pages
290 std::unique_ptr<std::byte[]> unzipBufOwned;
291 std::byte *unzipBuf;
292 if (fCompressionSettings != 0) {
293 unzipBufOwned = MakeUninitArray<std::byte>(bytesPacked);
294 unzipBuf = unzipBufOwned.get();
295 } else {
296 unzipBuf = fBuffer;
297 }
298 ROOT::Internal::RNTupleDecompressor::Unzip(fSealedPage.GetBuffer(), fSealedPage.GetDataSize(), bytesPacked,
299 unzipBuf);
300
301 const auto checksumSize = fWriteOpts.GetEnablePageChecksums() * sizeof(std::uint64_t);
302 std::size_t nBytesZipped;
303 if (fCompressionSettings != 0) {
304 assert(fBuffer != unzipBuf);
305 assert(fBufSize >= bytesPacked + checksumSize);
306 nBytesZipped = ROOT::Internal::RNTupleCompressor::Zip(unzipBuf, bytesPacked, fCompressionSettings, fBuffer);
307 } else {
308 nBytesZipped = bytesPacked;
309 }
310 fSealedPage = {fBuffer, nBytesZipped + checksumSize, fSealedPage.GetNElements(), fSealedPage.GetHasChecksum()};
311 fSealedPage.ChecksumIfEnabled();
312 }
313};
314
315struct RTaskVisitor {
316 std::optional<ROOT::Experimental::TTaskGroup> &fGroup;
317
318 template <typename T>
319 void operator()(T &&f)
320 {
321 if (fGroup)
322 fGroup->Run(f);
323 else
324 f();
325 }
326};
327
328struct RCommonField {
329 const ROOT::RFieldDescriptor *fSrc;
330 const ROOT::RFieldDescriptor *fDst;
331
332 RCommonField(const ROOT::RFieldDescriptor &src, const ROOT::RFieldDescriptor &dst) : fSrc(&src), fDst(&dst) {}
333};
334
335/// Maps a column representation from a source to a destination RNTuple.
336/// fSource and fDest are the first representation indices of a specific column.
337///
338/// When we merge fields from different RNTuples, two compatible fields may use different column
339/// representations. When merging their columns we need to make sure that we keep the output
340/// representation coherent, which is what this mapping is here for.
341struct RColReprMapping {
342 std::uint32_t fSource;
343 std::uint32_t fDest;
344};
345
346/// A column extension that needs to be added to an output field.
347/// Note that this also adds a mapping for each new representation, which is why it inherits RColReprMapping.
348struct RColReprExtension : RColReprMapping {
349 /// The new representations to be added
350 std::vector<ROOT::Internal::RColumnFormat> fSourceRepr;
351 /// The first element index that this column had in its source. When adding this representation to the destination,
352 /// the new column will add this amount to the first element index of the 0th-representation column in the
353 /// destination's current cluster.
354 std::uint32_t fOrigFirstElementIndex = 0;
355};
356
357static std::optional<std::uint32_t>
358FindColumnReprMapping(const std::vector<RColReprMapping> &mappings, std::uint32_t sourceReprIndex)
359{
360 for (const auto [src, dst] : mappings)
361 if (src == sourceReprIndex)
362 return dst;
363 return std::nullopt;
364}
365
366template <typename T>
367using FieldCollectionMap_t = std::unordered_map<const ROOT::RFieldDescriptor *, std::vector<T>>;
368
369struct RDescriptorsComparison {
370 std::vector<const ROOT::RFieldDescriptor *> fExtraDstFields;
371 std::vector<const ROOT::RFieldDescriptor *> fExtraSrcFields;
372 std::vector<RCommonField> fCommonFields;
373 // For each field that has more than 1 column representation in the output model,
374 // maps the column representatives of the source field with those of the destination.
375 // The key is the destination field.
376 FieldCollectionMap_t<RColReprMapping> fColReprMappings;
377 FieldCollectionMap_t<RColReprExtension> fColReprExtensions;
378};
379
380struct RColumnOutInfo {
382};
383
384// { ".fully.qualified.fieldName.colInputIndex.colOutputReprIndex" => colOutputInfo }
385using ColumnIdMap_t = std::unordered_map<std::string, RColumnOutInfo>;
386
387struct RColumnInfoGroup {
388 std::vector<RColumnMergeInfo> fExtraDstColumns;
389 std::vector<RColumnMergeInfo> fCommonColumns;
390};
391
392} // namespace
393
394// These structs cannot be in the anon namespace becase they're used in RNTupleMerger's private interface.
397 // This column name is built as a dot-separated concatenation of the ancestry of
398 // the columns' parent fields' names plus the index of the column itself.
399 // e.g. "Muon.pt.x._0"
400 std::string fColumnName;
401 // The column id in the source RNTuple
403 // The corresponding column id in the destination RNTuple (the mapping happens in AddColumnsFromField())
405 std::uint16_t fOutputReprIndex = 0;
406 // If nullopt, use the default in-memory type
407 std::optional<std::type_index> fInMemoryType;
410};
411
412// Data related to a single call of RNTupleMerger::Merge()
414 std::span<RPageSource *> fSources;
419
420 std::vector<RColumnMergeInfo> fColumns;
421 // Maps input column IDs to output IDs
422 ColumnIdMap_t fColumnIdMap;
423
425
426 RNTupleMergeData(std::span<RPageSource *> sources, RPageSink &destination, const RNTupleMergeOptions &mergeOpts)
427 : fSources{sources}, fDestination{destination}, fMergeOpts{mergeOpts}, fDstDescriptor{destination.GetDescriptor()}
428 {
429 }
430};
431
433 // We use a std::deque so that references to the contained SealedPageSequence_t, and its iterators, are
434 // never invalidated.
435 std::deque<RPageStorage::SealedPageSequence_t> fPagesV;
436 std::vector<RPageStorage::RSealedPageGroup> fGroups;
437 std::vector<std::unique_ptr<std::byte[]>> fBuffers;
438};
439
440} // namespace ROOT::Experimental::Internal
441
442// Subprocedure of CompareDescriptorStructure, extracted for readability.
443// Given two fields, attempts to match their column representations and schedules column extensions if necessary.
445 const ROOT::RFieldDescriptor &srcField, const ROOT::RFieldDescriptor &dstField,
446 RDescriptorsComparison &result, std::vector<std::string> &errors)
447{
448 const auto &srcColumns = srcField.GetLogicalColumnIds();
449 const auto &dstColumns = dstField.GetLogicalColumnIds();
450
451 // Fields must have the same cardinality
452 const std::uint32_t srcColCardinality = srcField.GetColumnCardinality();
453 const std::uint32_t dstColCardinality = dstField.GetColumnCardinality();
454 if (srcColCardinality != dstColCardinality) {
455 std::stringstream ss;
456 ss << "Field `" << srcField.GetFieldName()
457 << "` has a different column cardinality than previously-seen field with the same name (old: "
458 << dstColCardinality << ", new: " << srcColCardinality << ")";
459 errors.push_back(ss.str());
460 return;
461 }
462
463 if (srcColCardinality == 0)
464 return; // no columns to match
465
466 const auto srcNColReprs = srcColumns.size() / srcColCardinality;
467 const auto dstNColReprs = dstColumns.size() / dstColCardinality;
468 std::uint32_t nextDstReprIndex = dstNColReprs;
469
470 // For each column representation of the source, check if it matches one in the descriptor.
471 // If so, and if it doesn't match the destination's repr index, add a mapping for it.
472 // If nothing matches, schedule the column representation to be added later.
473 // NOTE: this has quadratic complexity but the numbers involved are small so it's fine.
474 for (auto srcReprIdx = 0u; srcReprIdx < srcNColReprs; ++srcReprIdx) {
475 std::int64_t matchingRepr = -1;
476 for (auto dstReprIdx = 0u; dstReprIdx < dstNColReprs; ++dstReprIdx) {
477 bool matches = true;
478 for (auto reprColIdx = 0u; reprColIdx < srcColCardinality; ++reprColIdx) {
479 const auto srcColId = srcColumns[srcReprIdx * srcColCardinality + reprColIdx];
480 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
481 const auto dstColId = dstColumns[dstReprIdx * dstColCardinality + reprColIdx];
482 const auto &dstCol = dstDesc.GetColumnDescriptor(dstColId);
483 if (srcCol.GetType() != dstCol.GetType()) {
484 matches = false;
485 break;
486 }
487 }
488
489 if (matches) {
490 // If this column representation matches by column type, we need to make sure that it also has
491 // matching column metadata. Since we currently do not support multiple column representations
492 // that only differ by such metadata, we forbid merging such columns (e.g. we cannot merge two
493 // Real32Trunc columns with different bit widths). This could technically be supported, but it
494 // would require significant effort, so we currently don't.
495 for (auto reprColIdx = 0u; reprColIdx < srcColCardinality; ++reprColIdx) {
496 const auto srcColId = srcColumns[srcReprIdx * srcColCardinality + reprColIdx];
497 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
498 const auto dstColId = dstColumns[dstReprIdx * dstColCardinality + reprColIdx];
499 const auto &dstCol = dstDesc.GetColumnDescriptor(dstColId);
500 if (srcCol.GetType() != dstCol.GetType() || srcCol.GetBitsOnStorage() != dstCol.GetBitsOnStorage() ||
501 srcCol.GetValueRange() != dstCol.GetValueRange()) {
502 matches = false;
503 break;
504 }
505 }
506
507 if (matches) {
508 // We found a valid matching representation.
509 matchingRepr = dstReprIdx;
510 break;
511 }
512 }
513 }
514
515 if (errors.empty()) {
516 if (matchingRepr >= 0 && matchingRepr != srcReprIdx) {
517 // a different matching representation was found
518 assert(matchingRepr < std::numeric_limits<std::uint32_t>::max());
519 result.fColReprMappings[&dstField].push_back(
520 RColReprMapping{srcReprIdx, static_cast<std::uint32_t>(matchingRepr)});
521 } else if (matchingRepr < 0) {
522 // this representation was not found in the destination: add it
523 std::vector<ROOT::Internal::RColumnFormat> newRepr;
524 newRepr.reserve(srcColCardinality);
525 std::uint32_t firstElemIdx = 0;
526 for (auto reprColIdx = 0u; reprColIdx < srcColCardinality; ++reprColIdx) {
527 const auto srcColId = srcColumns[srcReprIdx * srcColCardinality + reprColIdx];
528 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
529 // All added columns are supposed to have the same firstElementIndex
530 assert(firstElemIdx == 0 || firstElemIdx == srcCol.GetFirstElementIndex());
531 firstElemIdx = srcCol.GetFirstElementIndex();
532 auto &reprElement = newRepr.emplace_back();
533 reprElement.fType = srcCol.GetType();
534 reprElement.fBitWidth = srcCol.GetBitsOnStorage();
535 reprElement.fValueRange = srcCol.GetValueRange();
536 }
537 RColReprExtension extension{{srcReprIdx, nextDstReprIndex}, newRepr, firstElemIdx};
538 nextDstReprIndex += newRepr.size();
539 result.fColReprExtensions[&dstField].push_back(extension);
540 result.fColReprMappings[&dstField].push_back(extension);
541 }
542 }
543 }
544}
545
546/// Compares the top level fields of `dst` and `src` and determines whether they can be merged or not.
547/// In addition, returns the differences between `dst` and `src`'s structures
550{
551 // Cases:
552 // 1. dst == src
553 // 2. dst has fields that src hasn't
554 // 3. src has fields that dst hasn't
555 // 4. dst and src have fields that differ (compatible or incompatible)
556
557 std::vector<std::string> errors;
558 RDescriptorsComparison res;
559
560 std::vector<RCommonField> commonFields;
561
562 for (const auto &dstField : dst.GetTopLevelFields()) {
563 const auto srcFieldId = src.FindFieldId(dstField.GetFieldName());
564 if (srcFieldId != ROOT::kInvalidDescriptorId) {
565 const auto &srcField = src.GetFieldDescriptor(srcFieldId);
566 commonFields.push_back({srcField, dstField});
567 } else {
568 res.fExtraDstFields.emplace_back(&dstField);
569 }
570 }
571 for (const auto &srcField : src.GetTopLevelFields()) {
572 const auto dstFieldId = dst.FindFieldId(srcField.GetFieldName());
573 if (dstFieldId == ROOT::kInvalidDescriptorId) {
574 res.fExtraSrcFields.push_back(&srcField);
575 }
576 }
577
578 // Check compatibility of common fields
579 auto fieldsToCheck = commonFields;
580 // NOTE: using index-based for loop because the collection may get extended by the iteration
581 for (std::size_t fieldIdx = 0; fieldIdx < fieldsToCheck.size(); ++fieldIdx) {
582 const auto &field = fieldsToCheck[fieldIdx];
583
584 // NOTE: field.fSrc and field.fDst have the same name by construction
585 const auto &fieldName = field.fSrc->GetFieldName();
586
587 // Require that fields are both projected or both not projected
588 bool projCompatible = field.fSrc->IsProjectedField() == field.fDst->IsProjectedField();
589 if (!projCompatible) {
590 std::stringstream ss;
591 ss << "Field `" << fieldName << "` is incompatible with previously-seen field with that name because the "
592 << (field.fSrc->IsProjectedField() ? "new" : "old") << " one is projected and the other isn't";
593 errors.push_back(ss.str());
594 } else if (field.fSrc->IsProjectedField()) {
595 // if both fields are projected, verify that they point to the same real field
596 const auto srcName = src.GetQualifiedFieldName(field.fSrc->GetProjectionSourceId());
597 const auto dstName = dst.GetQualifiedFieldName(field.fDst->GetProjectionSourceId());
598 if (srcName != dstName) {
599 std::stringstream ss;
600 ss << "Field `" << fieldName
601 << "` is projected to a different field than a previously-seen field with the same name (old: "
602 << dstName << ", new: " << srcName << ")";
603 errors.push_back(ss.str());
604 }
605 }
606
607 // Require that fields types match
608 // TODO(gparolini): allow non-identical but compatible types
609 const auto &srcTyName = ROOT::Internal::GetRenormalizedTypeName(field.fSrc->GetTypeName());
610 // This is already renormalized by construction (see RNTupleDescriptorBuilder::SetSchemaFromExisting)
611 const auto &dstTyName = field.fDst->GetTypeName();
612 if (srcTyName != dstTyName) {
613 std::stringstream ss;
614 ss << "Field `" << fieldName
615 << "` has a type incompatible with a previously-seen field with the same name: (old: " << dstTyName
616 << ", new: " << srcTyName << ")";
617 errors.push_back(ss.str());
618 }
619
620 // Require that type checksums match
621 const auto srcTyChk = field.fSrc->GetTypeChecksum();
622 const auto dstTyChk = field.fDst->GetTypeChecksum();
623 if (srcTyChk && dstTyChk && *srcTyChk != *dstTyChk) {
624 std::stringstream ss;
625 ss << "Field `" << field.fSrc->GetFieldName()
626 << "` has a different type checksum than previously-seen field with the same name";
627 errors.push_back(ss.str());
628 }
629
630 // Require that type versions match
631 const auto srcTyVer = field.fSrc->GetTypeVersion();
632 const auto dstTyVer = field.fDst->GetTypeVersion();
633 if (srcTyVer != dstTyVer) {
634 std::stringstream ss;
635 ss << "Field `" << field.fSrc->GetFieldName()
636 << "` has a different type version than previously-seen field with the same name (old: " << dstTyVer
637 << ", new: " << srcTyVer << ")";
638 errors.push_back(ss.str());
639 }
640
641 // Require that field versions match
642 const auto srcFldVer = field.fSrc->GetFieldVersion();
643 const auto dstFldVer = field.fDst->GetFieldVersion();
644 if (srcFldVer != dstFldVer) {
645 std::stringstream ss;
646 ss << "Field `" << field.fSrc->GetFieldName()
647 << "` has a different field version than previously-seen field with the same name (old: " << dstFldVer
648 << ", new: " << srcFldVer << ")";
649 errors.push_back(ss.str());
650 }
651
652 const auto srcRole = field.fSrc->GetStructure();
653 const auto dstRole = field.fDst->GetStructure();
654 if (srcRole != dstRole) {
655 std::stringstream ss;
656 ss << "Field `" << field.fSrc->GetFieldName()
657 << "` has a different structural role than previously-seen field with the same name (old: " << dstRole
658 << ", new: " << srcRole << ")";
659 errors.push_back(ss.str());
660 }
661
662 // Require that column representations match
663 if (!field.fSrc->IsProjectedField()) {
664 MatchColumnRepresentations(src, dst, *field.fSrc, *field.fDst, res, errors);
665 }
666
667 // Require that subfields are compatible
668 const auto &srcLinks = field.fSrc->GetLinkIds();
669 const auto &dstLinks = field.fDst->GetLinkIds();
670 if (srcLinks.size() != dstLinks.size()) {
671 std::stringstream ss;
672 ss << "Field `" << field.fSrc->GetFieldName()
673 << "` has a different number of children than previously-seen field with the same name (old: "
674 << dstLinks.size() << ", new: " << srcLinks.size() << ")";
675 errors.push_back(ss.str());
676 } else {
677 for (std::size_t linkIdx = 0, linkNum = srcLinks.size(); linkIdx < linkNum; ++linkIdx) {
678 const auto &srcSubfield = src.GetFieldDescriptor(srcLinks[linkIdx]);
679 const auto &dstSubfield = dst.GetFieldDescriptor(dstLinks[linkIdx]);
680 fieldsToCheck.push_back(RCommonField{srcSubfield, dstSubfield});
681 }
682 }
683 }
684
685 std::string errMsg;
686 for (const auto &err : errors)
687 errMsg += std::string("\n * ") + err;
688
689 if (!errMsg.empty())
690 errMsg = errMsg.substr(1); // strip initial newline
691
692 if (errMsg.length())
693 return R__FAIL(errMsg);
694
695 res.fCommonFields = std::move(commonFields);
696
697 return ROOT::RResult(res);
698}
699
700// Restores the truncation/quantization parameters (bit width, value range) of a field recreated
701// from an on-disk descriptor, e.g. by ExtendDestinationModel(). RFieldDescriptor::CreateField only
702// rebuilds the type structure; the parameters live in the in-memory field (RRealField::fBitWidth,
703// fValueMin, fValueMax), so pinning the column representative alone leaves stale defaults behind,
704// which are out of range for kReal32Trunc. Recurses into subfields, matched by on-disk ID, so that
705// truncated/quantized members of late records and collections are restored as well. Nodes whose
706// source column is not truncated/quantized, or which span several representations, are left alone.
708 const ROOT::RNTupleDescriptor &srcDesc)
709{
710 const auto &logicalIds = fieldDesc.GetLogicalColumnIds();
711 if (logicalIds.size() == 1) {
712 const auto &column = srcDesc.GetColumnDescriptor(logicalIds[0]);
713 const auto type = column.GetType();
715 auto *realFloat = dynamic_cast<ROOT::RRealField<float> *>(&field);
716 auto *realDouble = dynamic_cast<ROOT::RRealField<double> *>(&field);
717 if (!realFloat && !realDouble) {
718 return R__FAIL("field `" + field.GetQualifiedFieldName() +
719 "` has a Real32Trunc/Real32Quant column but is not an RRealField");
720 }
721 const auto bits = column.GetBitsOnStorage();
722 try {
724 if (realFloat)
725 realFloat->SetTruncated(bits);
726 else
727 realDouble->SetTruncated(bits);
728 } else {
729 const auto valueRange = column.GetValueRange();
730 if (!valueRange) {
731 return R__FAIL("field `" + field.GetQualifiedFieldName() +
732 "` has a Real32Quant column with no value range");
733 }
734 if (realFloat) {
735 realFloat->SetQuantized(bits,
736 {static_cast<float>(valueRange->fMin), static_cast<float>(valueRange->fMax)});
737 } else {
738 realDouble->SetQuantized(bits, {valueRange->fMin, valueRange->fMax});
739 }
740 }
741 } catch (const ROOT::RException &ex) {
742 return R__FAIL(ex.what());
743 }
744 }
745 }
746 for (auto *subfield : field.GetMutableSubfields()) {
747 // CreateField() may materialize in-memory class members that the source descriptor does not
748 // have (e.g. members added to the class after the file was written); those keep an invalid
749 // on-disk ID and carry no source column, so there is nothing to restore for them.
750 if (subfield->GetOnDiskId() == ROOT::kInvalidDescriptorId)
751 continue;
752 auto restored = RestoreRealFieldParams(*subfield, srcDesc.GetFieldDescriptor(subfield->GetOnDiskId()), srcDesc);
753 if (!restored)
754 return R__FORWARD_ERROR(restored);
755 }
757}
758
759// Applies late model extension to `mergeData.fDestination`, adding all `descCmp.fExtraSrcFields` to it.
760[[nodiscard]]
762ExtendDestinationModel(RDescriptorsComparison &descCmp, ROOT::RNTupleModel &dstModel, RNTupleMergeData &mergeData)
763{
764 const auto &newFields = descCmp.fExtraSrcFields;
765 auto &commonFields = descCmp.fCommonFields;
766
767 dstModel.Unfreeze();
768 ROOT::Internal::RNTupleModelChangeset changeset{dstModel};
769
770 if (mergeData.fMergeOpts.fExtraVerbose) {
771 std::string msg = "destination doesn't contain field";
772 if (newFields.size() > 1)
773 msg += 's';
774 msg += ' ';
775 msg += std::accumulate(newFields.begin(), newFields.end(), std::string{}, [](const auto &acc, const auto *field) {
776 return acc + (acc.length() ? ", " : "") + '`' + field->GetFieldName() + '`';
777 });
778 R__LOG_INFO(NTupleMergeLog()) << msg << ": adding " << (newFields.size() > 1 ? "them" : "it")
779 << " to the destination model (entry #" << mergeData.fNumDstEntries << ").";
780 }
781
782 changeset.fAddedFields.reserve(newFields.size());
783 // First add all non-projected fields...
784 for (const auto *fieldDesc : newFields) {
785 if (fieldDesc->IsProjectedField())
786 continue;
787
788 auto field = fieldDesc->CreateField(*mergeData.fSrcDescriptor);
789 // Explicitly set the field representatives. This prevents UpdateSchema() from changing our column
790 // representations via AutoAdjustColumnTypes.
792 for (const auto &colId : fieldDesc->GetLogicalColumnIds()) {
793 const auto &column = mergeData.fSrcDescriptor->GetColumnDescriptor(colId);
794 representatives.push_back(column.GetType());
795 }
796 field->SetColumnRepresentatives({representatives});
797 // Pinning the representative is not enough for truncated/quantized columns: their parameters
798 // are stored in the in-memory field, which CreateField() leaves at defaults.
799 auto restored = RestoreRealFieldParams(*field, *fieldDesc, *mergeData.fSrcDescriptor);
800 if (!restored)
801 return R__FORWARD_ERROR(restored);
802 changeset.AddField(std::move(field));
803 }
804 // ...then add all projected fields.
805 for (const auto *fieldDesc : newFields) {
806 if (!fieldDesc->IsProjectedField())
807 continue;
808
810 auto field = fieldDesc->CreateField(*mergeData.fSrcDescriptor);
811 const auto sourceId = fieldDesc->GetProjectionSourceId();
812 const auto &sourceField = dstModel.GetConstField(mergeData.fSrcDescriptor->GetQualifiedFieldName(sourceId));
813 fieldMap[field.get()] = &sourceField;
814
815 for (const auto &subfield : *field) {
816 const auto &subFieldDesc = mergeData.fSrcDescriptor->GetFieldDescriptor(subfield.GetOnDiskId());
817 const auto subSourceId = subFieldDesc.GetProjectionSourceId();
818 const auto &subSourceField =
819 dstModel.GetConstField(mergeData.fSrcDescriptor->GetQualifiedFieldName(subSourceId));
820 fieldMap[&subfield] = &subSourceField;
821 }
822 changeset.fAddedProjectedFields.emplace_back(field.get());
823 ROOT::Internal::GetProjectedFieldsOfModel(dstModel).Add(std::move(field), fieldMap);
824 }
825 dstModel.Freeze();
826 try {
827 // FIXME: here we are connecting the new fields/columns to the sink!
828 // We should avoid doing that, as all other non-extended fields never get connected (and we don't
829 // need to connect these either in principle).
830 // NOTE: this calls AutoAdjustColumnTypes, but we have set the column representations of all fields
831 // explicitly, so it will not change it under the hood.
832 mergeData.fDestination.UpdateSchema(changeset, mergeData.fNumDstEntries);
833 } catch (const ROOT::RException &ex) {
834 return R__FAIL(ex.what());
835 }
836
837 commonFields.reserve(commonFields.size() + newFields.size());
838 // NOTE(gparolini): Insert the new fields at the beginning of `commonFields`.
839 // We need to make sure the extended fields appear before all other common fields for the following reason:
840 // in general, when we GatherColumnInfos we (potentially) assign new column output ids in field order; this
841 // assignment happens whenever we find new columns, which happens in 3 cases:
842 // 1. we are in the first source and we're adding the first set of (common) fields;
843 // 2. we are adding a new set of extended common fields (this is done in this function);
844 // 3. we are adding new column representations for fields that we already had before processing this source.
845 //
846 // It's important that the output id assigned to the new columns is coherent with the order of the column descriptors
847 // as they appear in the header and footer of the destination RNTuple.
848 // This is in turn determined by the order by which we append new columns to the dst descriptor during the merging
849 // process.
850 //
851 // Now let's consider the three cases listed above.
852 // Ignoring the trivial case (1), the order of operations for each source is:
853 // - call ExtendDestinationModel (case 2)
854 // (this adds both new fields and column descriptors; see the UpdateSchema call above)
855 // - add new column representations (case 3)
856 // (this only adds column descriptors, see the call to AddColumnRepresentation)
857 //
858 // Since we call ExtendDestinationModel (this function) *before* adding the new column representations,
859 // the dst descriptor always gets updated with the new column descriptors coming from the extended fields before
860 // it gets updated with the extended column representations.
861 //
862 // However, in GatherColumnInfos, the new column output ids are added sequentially in *field* order and the fields
863 // containing the new column representations are already in that list from earlier! So, to make sure the new output
864 // ids are assigned to our extended fields first, we push them in from on the list so they are visited first.
865 for (auto it = newFields.rbegin(); it != newFields.rend(); ++it) {
866 const auto *field = *it;
867 const auto newFieldInDstId = mergeData.fDstDescriptor.FindFieldId(field->GetFieldName());
868 const auto &newFieldInDst = mergeData.fDstDescriptor.GetFieldDescriptor(newFieldInDstId);
869 commonFields.insert(commonFields.begin(), RCommonField{*field, newFieldInDst});
870 }
871
873}
874
875// Generates default (zero) values for the given columns
876[[nodiscard]]
878GenerateZeroPagesForColumns(size_t nEntriesToGenerate, std::span<const RColumnMergeInfo> columns,
879 RSealedPageMergeData &sealedPageData, ROOT::Internal::RPageAllocator &pageAlloc,
880 const ROOT::RNTupleDescriptor &dstDescriptor, const RNTupleMergeData &mergeData)
881{
882 if (!nEntriesToGenerate)
884
885 for (const auto &column : columns) {
886 const ROOT::RFieldDescriptor *field = column.fParentFieldDescriptor;
887
888 // Skip all auxiliary columns
889 assert(!field->GetLogicalColumnIds().empty());
890 if (field->GetLogicalColumnIds()[0] != column.fInputId)
891 continue;
892
893 // Check if this column is a child of a Collection or a Variant. If so, it has no data
894 // and can be skipped.
895 bool skipColumn = false;
896 auto nRepetitions = std::max<std::uint64_t>(field->GetNRepetitions(), 1);
897 for (auto parentId = field->GetParentId(); parentId != ROOT::kInvalidDescriptorId;) {
898 const ROOT::RFieldDescriptor &parent = column.fParentNTupleDescriptor->GetFieldDescriptor(parentId);
901 skipColumn = true;
902 break;
903 }
904 nRepetitions *= std::max<std::uint64_t>(parent.GetNRepetitions(), 1);
905 parentId = parent.GetParentId();
906 }
907 if (skipColumn)
908 continue;
909
910 const auto structure = field->GetStructure();
911
912 if (structure == ROOT::ENTupleStructure::kStreamer) {
913 return R__FAIL("Destination RNTuple contains a streamer field (" + field->GetFieldName() +
914 ") that is not present in one of the sources. "
915 "Creating a default value for a streamer field is ill-defined, therefore the merging "
916 "process will abort.");
917 }
918
919 // NOTE: we cannot have a Record here because it has no associated columns.
921 structure == ROOT::ENTupleStructure::kPlain);
922
923 const auto &columnDesc = dstDescriptor.GetColumnDescriptor(column.fOutputId);
924 const auto colElement = RColumnElementBase::Generate(columnDesc.GetType());
925 const auto nElements = nEntriesToGenerate * nRepetitions;
926 const auto nBytesOnStorage = colElement->GetPackedSize(nElements);
927 // TODO(gparolini): make this configurable
928 constexpr auto kPageSizeLimit = 256 * 1024;
929 // TODO(gparolini): consider coalescing the last page if its size is less than some threshold
930 const size_t nPages = nBytesOnStorage / kPageSizeLimit + !!(nBytesOnStorage % kPageSizeLimit);
931 for (size_t i = 0; i < nPages; ++i) {
932 const auto pageSize = (i < nPages - 1) ? kPageSizeLimit : nBytesOnStorage - kPageSizeLimit * (nPages - 1);
933 const auto checksumSize = RPageStorage::kNBytesPageChecksum;
934 const auto bufSize = pageSize + checksumSize;
935 assert(pageSize % colElement->GetSize() == 0);
936 const auto nElementsPerPage = pageSize / colElement->GetSize();
937 auto page = pageAlloc.NewPage(colElement->GetSize(), nElementsPerPage);
938 page.GrowUnchecked(nElementsPerPage);
939 memset(page.GetBuffer(), 0, page.GetNBytes());
940
941 auto &buffer = sealedPageData.fBuffers.emplace_back(new std::byte[bufSize]);
943 sealConf.fElement = colElement.get();
944 sealConf.fPage = &page;
945 sealConf.fBuffer = buffer.get();
946 sealConf.fCompressionSettings = mergeData.fMergeOpts.fCompressionSettings.value();
948 auto sealedPage = RPageSink::SealPage(sealConf);
949
950 sealedPageData.fPagesV.push_back({sealedPage});
951 sealedPageData.fGroups.emplace_back(column.fOutputId, sealedPageData.fPagesV.back().cbegin(),
952 sealedPageData.fPagesV.back().cend());
953 }
954 }
956}
957
958// Merges all columns appearing both in the source and destination RNTuples, just copying them if their
959// compression matches ("fast merge") or by unsealing and resealing them with the proper compression.
962 const ROOT::RClusterDescriptor &clusterDesc,
963 std::span<RColumnMergeInfo> commonColumns,
964 const RCluster::ColumnSet_t &commonColumnSet, RSealedPageMergeData &sealedPageData,
965 const RNTupleMergeData &mergeData, ROOT::Internal::RPageAllocator &pageAlloc)
966{
967 const auto nCommonColumnsInCluster = commonColumnSet.size();
968 assert(nCommonColumnsInCluster <= commonColumns.size());
969
970 if (nCommonColumnsInCluster == 0)
972
973 const RCluster *cluster = clusterPool.GetCluster(clusterDesc.GetId(), commonColumnSet);
974 // we expect the cluster pool to contain the requested set of columns, since they were
975 // validated by CompareDescriptorStructure() and MergeSourceClusters().
976 assert(cluster);
977
978 const std::uint32_t outCompression = mergeData.fMergeOpts.fCompressionSettings.value();
979
980 for (size_t colIdx = 0; colIdx < nCommonColumnsInCluster; ++colIdx) {
981 const auto &column = commonColumns[colIdx];
982 const auto &columnId = column.fInputId;
983 R__ASSERT(clusterDesc.ContainsColumn(columnId));
984
985 const auto &columnDesc = mergeData.fSrcDescriptor->GetColumnDescriptor(columnId);
986 const auto srcColElement = column.fInMemoryType
987 ? ROOT::Internal::GenerateColumnElement(*column.fInMemoryType, columnDesc.GetType())
988 : RColumnElementBase::Generate(columnDesc.GetType());
989
990 // Now get the pages for this column in this cluster
991 const auto &pages = clusterDesc.GetPageRange(columnId);
992
994 sealedPages.resize(pages.GetPageInfos().size());
995
996 // Each column range potentially has a distinct compression settings
997 const auto &columnRange = clusterDesc.GetColumnRange(columnId);
998 assert(!columnRange.IsSuppressed());
999 const auto colRangeCompressionSettings = columnRange.GetCompressionSettings().value();
1000
1001 // Select "merging level". There are 2 levels, from fastest to slowest, depending on the case:
1002 // L1: compression and encoding of src and dest both match: we can simply copy the page
1003 // L2: compression of dest doesn't match the src we must recompress the page.
1004 // Note that in no case do we need to re-encode the page, as if the encoding differs we simply
1005 // append a new column representation to the field.
1006 const bool needsRecompressing = colRangeCompressionSettings != outCompression;
1007
1008 if (needsRecompressing && mergeData.fMergeOpts.fExtraVerbose) {
1009 R__LOG_INFO(NTupleMergeLog()) << "Recompressing column " << column.fColumnName
1010 << ": { compression: " << colRangeCompressionSettings << " => "
1011 << mergeData.fMergeOpts.fCompressionSettings.value() << ", onDiskType: "
1013 srcColElement->GetIdentifier().fOnDiskType)
1014 << "}";
1015 }
1016
1017 const size_t pageBufferBaseIdx = sealedPageData.fBuffers.size();
1018 // If the column range already has the right compression we don't need to allocate any new buffer, so we don't
1019 // bother reserving memory for them.
1020 if (needsRecompressing)
1021 sealedPageData.fBuffers.resize(sealedPageData.fBuffers.size() + pages.GetPageInfos().size());
1022
1023 // If this column is deferred, we may need to fill "holes" until its real start. We fill any missing entry
1024 // with zeroes, like we do for extraDstColumns.
1025 // As an optimization, we don't do this for the first source (since we can rely on the FirstElementIndex and
1026 // deferred column mechanism in that case).
1027 // TODO: also avoid doing this if we added no real page of this column to the destination yet.
1028 if (columnDesc.GetFirstElementIndex() > clusterDesc.GetFirstEntryIndex() && mergeData.fNumDstEntries > 0) {
1029 const auto nMissingEntries = columnDesc.GetFirstElementIndex() - clusterDesc.GetFirstEntryIndex();
1030 auto res = GenerateZeroPagesForColumns(nMissingEntries, {&column, 1}, sealedPageData, pageAlloc,
1031 mergeData.fDstDescriptor, mergeData);
1032 if (!res)
1033 return R__FORWARD_ERROR(res);
1034 }
1035
1036 // Loop over the pages
1037 std::uint64_t pageIdx = 0;
1038 for (const auto &pageInfo : pages.GetPageInfos()) {
1039 assert(pageIdx < sealedPages.size());
1040 assert(sealedPageData.fBuffers.size() == 0 || pageIdx < sealedPageData.fBuffers.size());
1041 assert(pageInfo.GetLocator().GetType() != RNTupleLocator::kTypePageZero);
1042
1043 ROOT::Internal::ROnDiskPage::Key key{columnId, pageIdx};
1044 auto onDiskPage = cluster->GetOnDiskPage(key);
1045
1046 const auto checksumSize = pageInfo.HasChecksum() * RPageStorage::kNBytesPageChecksum;
1047 RPageStorage::RSealedPage &sealedPage = sealedPages[pageIdx];
1048 sealedPage.SetNElements(pageInfo.GetNElements());
1049 sealedPage.SetHasChecksum(pageInfo.HasChecksum());
1050 sealedPage.SetBufferSize(pageInfo.GetLocator().GetNBytesOnStorage() + checksumSize);
1051 sealedPage.SetBuffer(onDiskPage->GetAddress());
1052 // TODO(gparolini): more graceful error handling (skip the page?)
1054 R__ASSERT(onDiskPage && (onDiskPage->GetSize() == sealedPage.GetBufferSize()));
1055
1056 if (needsRecompressing) {
1057 const auto uncompressedSize = srcColElement->GetSize() * sealedPage.GetNElements();
1058 auto &buffer = sealedPageData.fBuffers[pageBufferBaseIdx + pageIdx];
1059 const auto bufSize = uncompressedSize + checksumSize;
1060 // NOTE: we currently allocate the max possible size for this buffer and don't shrink it afterward.
1061 // We might want to introduce an option that trades speed for memory usage and shrink the buffer to fit
1062 // the actual data size after recompressing.
1063 buffer = MakeUninitArray<std::byte>(bufSize);
1064
1065 // clang-format off
1066 RTaskVisitor{fTaskGroup}(RChangeCompressionFunc{
1067 *srcColElement,
1068 outCompression,
1069 sealedPage,
1070 *fPageAlloc,
1071 buffer.get(),
1072 bufSize,
1073 mergeData.fDestination.GetWriteOptions()
1074 });
1075 // clang-format on
1076 }
1077
1078 ++pageIdx;
1079
1080 } // end of loop over pages
1081
1082 if (fTaskGroup)
1083 fTaskGroup->Wait();
1084
1085 sealedPageData.fPagesV.push_back(std::move(sealedPages));
1086 sealedPageData.fGroups.emplace_back(column.fOutputId, sealedPageData.fPagesV.back().cbegin(),
1087 sealedPageData.fPagesV.back().cend());
1088 } // end loop over common columns
1089
1091}
1092
1093// Iterates over all clusters of `source` and merges their pages into `destination`.
1094// It is assumed that all columns in `commonColumns` are present (and compatible) in both the source and
1095// the destination's schemas.
1096// The pages may be "fast-merged" (i.e. simply copied with no decompression/recompression) if the target
1097// compression is unspecified or matches the original compression settings.
1098ROOT::RResult<void> RNTupleMerger::MergeSourceClusters(RPageSource &source, std::span<RColumnMergeInfo> commonColumns,
1099 std::span<const RColumnMergeInfo> extraDstColumns,
1100 RNTupleMergeData &mergeData)
1101{
1102 ROOT::Internal::RClusterPool clusterPool{source};
1103
1104 std::vector<RColumnMergeInfo> missingColumns{extraDstColumns.begin(), extraDstColumns.end()};
1105
1107 for (const auto &clusterDesc : mergeData.fSrcDescriptor->GetActiveClusterIterable()) {
1108 const auto nClusterEntries = clusterDesc.GetNEntries();
1109 R__ASSERT(nClusterEntries > 0);
1110
1111 // Deduce which columns are suppressed (cluster by cluster) by exclusion, as:
1112 // (columns in the columnIdMap) - (columns in commonColumns which are not suppressed).
1113 // Note that some suppressed columns may not be in commonColumns because they might not appear at all in the
1114 // current source.
1115 FieldCollectionMap_t<ROOT::DescriptorId_t> columnsInCluster;
1116 using ColumnHandle_t = ROOT::Internal::RPageStorage::ColumnHandle_t;
1117
1118 // NOTE: `commonColumns` contains all columns that appear *somewhere* both in the src and in the dst.
1119 // Just because a column is in `commonColumns` it doesn't mean that each cluster in the source contains
1120 // it, as it may be a deferred column that only has real data in a future cluster. We need to figure out which
1121 // columns are actually present in this cluster so we only merge their pages (the missing columns are handled
1122 // by synthesizing zero pages - see below).
1123
1124 // Convert columns to a ColumnSet for the ClusterPool query
1125 RCluster::ColumnSet_t commonColumnSet;
1126 // Collect all common columns appearing in this cluster into commonColumnSet and reorganize commonColumns so
1127 // that those columns are at the start of it (whereas missing columns are at its end).
1128 // NOTE: it's fine if this scrambles the order of columns: the RNTupleSerializer will sort them by physical ID.
1129 int nCommonColumnsInCluster = 0;
1130 std::partition(commonColumns.begin(), commonColumns.end(), [&](const auto &column) {
1131 if (clusterDesc.ContainsColumn(column.fInputId)) {
1132 const auto &colRange = clusterDesc.GetColumnRange(column.fInputId);
1133 ++nCommonColumnsInCluster;
1134 columnsInCluster[column.fParentFieldDescriptor].push_back(column.fOutputId);
1135 if (!colRange.IsSuppressed()) {
1136 commonColumnSet.emplace(column.fInputId);
1137 return true;
1138 }
1139 mergeData.fDestination.CommitSuppressedColumn(ColumnHandle_t{column.fOutputId});
1140 }
1141 return false;
1142 });
1143
1144 // Commit all suppressed columns.
1145 // This is a fairly involved operation, as we need to commit all known columns that:
1146 // a) do not appear in extraDstColumns (those are "missing", not suppressed), and
1147 // b) do not appear in commonColumnSet (those are the active columns).
1148 // Not that these may or may not appear in commonColumns as suppressed columns, since they may or may not be
1149 // present in the current source.
1150 // The only way to find all the columns is to go and get them from fColumnIdMap, which keeps track of every
1151 // column we added to the destination so far. However, since it also contains the extraDstColumns, we need to
1152 // specifically only query those columns that belong to a field that has at least 1 column in commonColumns
1153 // (remember that commonColumns contains all columns associated to the common fields for this source).
1154 for (const auto &[fieldDesc, columnIds] : columnsInCluster) {
1155 const auto &fieldFQName = mergeData.fSrcDescriptor->GetQualifiedFieldName(fieldDesc->GetId());
1156 const auto cardinality = fieldDesc->GetColumnCardinality();
1157 for (auto i = 0u; i < fieldDesc->GetLogicalColumnIds().size(); ++i) {
1158 const auto colIndex = i % cardinality;
1159 const auto reprIndex = i / cardinality;
1160 const auto colName = "." + fieldFQName + '.' + std::to_string(colIndex) + '.' + std::to_string(reprIndex);
1161 const auto colIt = mergeData.fColumnIdMap.find(colName);
1162 assert(colIt != mergeData.fColumnIdMap.end());
1163 const auto colOutId = colIt->second.fColumnId;
1164 if (std::find(columnIds.begin(), columnIds.end(), colOutId) == columnIds.end()) {
1165 mergeData.fDestination.CommitSuppressedColumn(ColumnHandle_t{colOutId});
1166 }
1167 }
1168 }
1169
1170 RSealedPageMergeData sealedPageData;
1171 auto res = MergeCommonColumns(clusterPool, clusterDesc, commonColumns, commonColumnSet, sealedPageData, mergeData,
1172 *fPageAlloc);
1173 if (!res)
1174 return R__FORWARD_ERROR(res);
1175
1176 // Generate zero pages for the missing columns.
1177 // For each cluster, the "missing columns" are the union of the extraDstColumns and the common columns
1178 // that are not present in the cluster.
1179 // Note that this does NOT include suppressed columns, for which no pages are synthesized.
1180 missingColumns.resize(extraDstColumns.size()); // NOTE: this clears all common columns of the previous cluster
1181 for (size_t i = nCommonColumnsInCluster; i < commonColumns.size(); ++i)
1182 missingColumns.push_back(commonColumns[i]);
1183
1184 res = GenerateZeroPagesForColumns(nClusterEntries, missingColumns, sealedPageData, *fPageAlloc,
1185 mergeData.fDstDescriptor, mergeData);
1186 if (!res)
1187 return R__FORWARD_ERROR(res);
1188
1189 // Commit the pages and the clusters
1190 mergeData.fDestination.CommitSealedPageV(sealedPageData.fGroups);
1191 mergeData.fDestination.CommitCluster(nClusterEntries);
1192 mergeData.fNumDstEntries += nClusterEntries;
1193 }
1194
1195 // TODO(gparolini): when we get serious about huge file support (>~ 100GB) we might want to check here
1196 // the size of the running page list and commit a cluster group when it exceeds some threshold,
1197 // which would prevent the page list from getting too large.
1198 // However, as of today, we aren't really handling such huge files, and even relatively big ones
1199 // such as the CMS dataset have a page list size of about only 2 MB.
1200 // So currently we simply merge all cluster groups into one.
1202}
1203
1204static std::optional<std::type_index> ColumnInMemoryType(std::string_view fieldType, ENTupleColumnType onDiskType)
1205{
1206 if (onDiskType == ENTupleColumnType::kIndex32 || onDiskType == ENTupleColumnType::kSplitIndex32 ||
1208 return typeid(ROOT::Internal::RColumnIndex);
1209
1210 if (onDiskType == ENTupleColumnType::kSwitch)
1211 return typeid(ROOT::Internal::RColumnSwitch);
1212
1213 // clang-format off
1214 if (fieldType == "bool") return typeid(bool);
1215 if (fieldType == "std::byte") return typeid(std::byte);
1216 if (fieldType == "char") return typeid(char);
1217 if (fieldType == "std::int8_t") return typeid(std::int8_t);
1218 if (fieldType == "std::uint8_t") return typeid(std::uint8_t);
1219 if (fieldType == "std::int16_t") return typeid(std::int16_t);
1220 if (fieldType == "std::uint16_t") return typeid(std::uint16_t);
1221 if (fieldType == "std::int32_t") return typeid(std::int32_t);
1222 if (fieldType == "std::uint32_t") return typeid(std::uint32_t);
1223 if (fieldType == "std::int64_t") return typeid(std::int64_t);
1224 if (fieldType == "std::uint64_t") return typeid(std::uint64_t);
1225 if (fieldType == "float") return typeid(float);
1226 if (fieldType == "double") return typeid(double);
1227 // clang-format on
1228
1229 // if the type is not one of those above, we use the default in-memory type.
1230 return std::nullopt;
1231}
1232
1233// Given a field, fill `columns` and `mergeData.fColumnIdMap` with information about all columns belonging to it and
1234// its subfields. `mergeData.fColumnIdMap` is used to map matching columns from different sources to the same output
1235// column in the destination. We match columns by their "fully qualified name", which is the concatenation of their
1236// ancestor fields' names and the column index. By this point, since we called `CompareDescriptorStructure()`
1237// earlier, we should be guaranteed that two matching columns will have at least compatible representations.
1238// This function is recursive as it needs to call itself on the entire subfield hierarchy of the source field.
1239// NOTE: srcFieldDesc and dstFieldDesc may alias.
1240static void AddColumnsFromField(std::vector<RColumnMergeInfo> &columns, const ROOT::RNTupleDescriptor &srcDesc,
1241 const FieldCollectionMap_t<RColReprMapping> &colReprMappings,
1242 RNTupleMergeData &mergeData, const ROOT::RFieldDescriptor &srcFieldDesc,
1243 const ROOT::RFieldDescriptor &dstFieldDesc, const std::string &prefix = "")
1244{
1245 std::string name = prefix + '.' + srcFieldDesc.GetFieldName();
1246
1247 // We don't want to try and merge alias columns. Note that subfields of projected fields
1248 // must also be projected, so we don't need to check them.
1249 if (srcFieldDesc.IsProjectedField())
1250 return;
1251
1252 const auto &columnIds = srcFieldDesc.GetLogicalColumnIds();
1253 columns.reserve(columns.size() + columnIds.size());
1254
1255 for (auto i = 0u; i < srcFieldDesc.GetLogicalColumnIds().size(); ++i) {
1256 auto srcColumnId = srcFieldDesc.GetLogicalColumnIds()[i];
1257 const auto &srcColumn = srcDesc.GetColumnDescriptor(srcColumnId);
1258
1259 RColumnMergeInfo info{};
1260 info.fInputId = srcColumn.GetPhysicalId();
1261 // NOTE(gparolini): the parent field is used when synthesizing zero pages, which happens in 2 situations:
1262 // 1. when adding extra dst columns (in which case we need to synthesize zero pages for the incoming src), and
1263 // 2. when merging a deferred column into an existing column (in which case we need to fill the "hole" with
1264 // zeroes). For the first case srcFieldDesc and dstFieldDesc are the same (see the calling site of this
1265 // function), but for the second case they're not, and we need to pick the source field because we will then
1266 // check the column's *input* id inside fParentFieldDescriptor to see if it's a suppressed column (see
1267 // GenerateZeroPagesForColumns()).
1268 info.fParentFieldDescriptor = &srcFieldDesc;
1269 // Save the parent field descriptor since this may be either the source or destination descriptor depending on
1270 // whether this is an extraDstField or a commonField. We will need this in GenerateZeroPagesForColumns() to
1271 // properly walk up the field hierarchy.
1272 info.fParentNTupleDescriptor = &srcDesc;
1273
1274 const auto mappingsIt = colReprMappings.find(&dstFieldDesc);
1275 std::uint16_t reprIndex = srcColumn.GetRepresentationIndex();
1276 if (mappingsIt != colReprMappings.end()) {
1277 if (auto outReprIdx = FindColumnReprMapping(mappingsIt->second, reprIndex); outReprIdx)
1278 reprIndex = *outReprIdx;
1279 }
1280
1281 info.fColumnName = name + '.' + std::to_string(srcColumn.GetIndex()) + '.' + std::to_string(reprIndex);
1282
1284
1285 if (auto it = mergeData.fColumnIdMap.find(info.fColumnName); it != mergeData.fColumnIdMap.end()) {
1286 // We had already added this column to the column id map: just copy its data.
1287 info.fOutputId = it->second.fColumnId;
1288 info.fOutputReprIndex = reprIndex;
1289 } else {
1290 // New column: assign it the next ouput id.
1291 info.fOutputId = mergeData.fColumnIdMap.size();
1292 // NOTE(gparolini): map the representation index of src column to that of dst column.
1293 // This mapping is only relevant for common columns and it's done to ensure we have the correct representation
1294 // index in the output column metadata.
1295 assert(dstFieldDesc.GetColumnCardinality() == srcFieldDesc.GetColumnCardinality());
1296 const auto dstColumnIndex = reprIndex * dstFieldDesc.GetColumnCardinality() + srcColumn.GetIndex();
1297 const auto dstColumnId = dstFieldDesc.GetLogicalColumnIds()[dstColumnIndex];
1298 const auto &dstColumn = mergeData.fDstDescriptor.GetColumnDescriptor(dstColumnId);
1299 columnType = dstColumn.GetType();
1300 info.fOutputReprIndex = reprIndex;
1301 mergeData.fColumnIdMap[info.fColumnName] = RColumnOutInfo{info.fOutputId};
1302 }
1303
1304 if (mergeData.fMergeOpts.fExtraVerbose) {
1305 R__LOG_INFO(NTupleMergeLog()) << "Adding column " << info.fColumnName << " with log.id " << srcColumnId
1306 << ", phys.id " << srcColumn.GetPhysicalId() << ", type "
1307 << RColumnElementBase::GetColumnTypeName(srcColumn.GetType()) << " -> log.id "
1308 << info.fOutputId << ", type "
1310 }
1311
1312 // Since we disallow merging fields of different types, src and dstFieldDesc must have the same type name.
1313 assert(srcDesc.GetTypeNameForComparison(srcFieldDesc) == dstFieldDesc.GetTypeName());
1314 info.fInMemoryType = ColumnInMemoryType(dstFieldDesc.GetTypeName(), columnType);
1315 columns.emplace_back(info);
1316 }
1317
1318 const auto &srcChildrenIds = srcFieldDesc.GetLinkIds();
1319 const auto &dstChildrenIds = dstFieldDesc.GetLinkIds();
1320 assert(srcChildrenIds.size() == dstChildrenIds.size());
1321 for (auto i = 0u; i < srcChildrenIds.size(); ++i) {
1322 const auto &srcChild = srcDesc.GetFieldDescriptor(srcChildrenIds[i]);
1323 const auto &dstChild = mergeData.fDstDescriptor.GetFieldDescriptor(dstChildrenIds[i]);
1324 AddColumnsFromField(columns, srcDesc, colReprMappings, mergeData, srcChild, dstChild, name);
1325 }
1326}
1327
1328// Converts the fields comparison data to the corresponding column information.
1329// While doing so, it collects such information in `mergeData.fColumnIdMap`, which is used by later calls to this
1330// function to map already-seen column names to their chosen outputId, type and so on.
1331static RColumnInfoGroup GatherColumnInfos(const RDescriptorsComparison &descCmp, const ROOT::RNTupleDescriptor &srcDesc,
1332 RNTupleMergeData &mergeData)
1333{
1334 RColumnInfoGroup res;
1335 for (const ROOT::RFieldDescriptor *field : descCmp.fExtraDstFields) {
1336 AddColumnsFromField(res.fExtraDstColumns, mergeData.fDstDescriptor, descCmp.fColReprMappings, mergeData, *field,
1337 *field);
1338 }
1339 for (const auto &[srcField, dstField] : descCmp.fCommonFields) {
1340 AddColumnsFromField(res.fCommonColumns, srcDesc, descCmp.fColReprMappings, mergeData, *srcField, *dstField);
1341 }
1342 return res;
1343}
1344
1345static void PrefillColumnMap(const ROOT::RNTupleDescriptor &desc, const ROOT::RFieldDescriptor &fieldDesc,
1346 ColumnIdMap_t &colIdMap, const std::string &prefix = "")
1347{
1348 std::string name = prefix + '.' + fieldDesc.GetFieldName();
1349 for (const auto &colId : fieldDesc.GetLogicalColumnIds()) {
1350 const auto &colDesc = desc.GetColumnDescriptor(colId);
1351 RColumnOutInfo info{};
1352 info.fColumnId = colDesc.GetLogicalId();
1353 const auto colName =
1354 name + '.' + std::to_string(colDesc.GetIndex()) + '.' + std::to_string(colDesc.GetRepresentationIndex());
1355 colIdMap[colName] = info;
1356 }
1357
1358 for (const auto &subId : fieldDesc.GetLinkIds()) {
1359 const auto &subfield = desc.GetFieldDescriptor(subId);
1360 PrefillColumnMap(desc, subfield, colIdMap, name);
1361 }
1362}
1363
1365 const ROOT::RFieldDescriptor &field, const ROOT::RNTupleDescriptor &desc,
1366 const FieldCollectionMap_t<RColReprExtension> &extensions,
1367 std::vector<std::pair<const ROOT::RFieldDescriptor *, std::vector<RColReprExtension>>> &outExtensions,
1368 std::unordered_map<ROOT::DescriptorId_t, std::vector<const ROOT::RFieldDescriptor *>> &outProjectionPointees)
1369{
1370 const auto it = extensions.find(&field);
1371 if (it != extensions.end())
1372 outExtensions.emplace_back(it->first, it->second);
1373
1374 if (field.IsProjectedField())
1375 outProjectionPointees[field.GetProjectionSourceId()].push_back(&field);
1376
1377 for (auto childId : field.GetLinkIds()) {
1378 const auto &child = desc.GetFieldDescriptor(childId);
1379 AddColumnExtensionsInFieldOrder(child, desc, extensions, outExtensions, outProjectionPointees);
1380 }
1381}
1382
1383RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination,
1384 std::unique_ptr<ROOT::RNTupleModel> model)
1385 // TODO(gparolini): consider using an arena allocator instead, since we know the precise lifetime
1386 // of the RNTuples we are going to handle (e.g. we can reset the arena at every source)
1387 : fDestination(std::move(destination)),
1388 fPageAlloc(std::make_unique<ROOT::Internal::RPageAllocatorHeap>()),
1389 fModel(std::move(model))
1390{
1392
1393#ifdef R__USE_IMT
1396#endif
1397}
1398
1399RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination)
1400 : RNTupleMerger(std::move(destination), nullptr)
1401{
1402}
1403
1404ROOT::RResult<void> RNTupleMerger::Merge(std::span<RPageSource *> sources, const RNTupleMergeOptions &mergeOptsIn)
1405{
1406 RNTupleMergeOptions mergeOpts = mergeOptsIn;
1407
1408 assert(fDestination);
1409
1410 // Set compression settings if unset and verify it's compatible with the sink
1411 {
1412 const auto dstCompSettings = fDestination->GetWriteOptions().GetCompression();
1413 if (!mergeOpts.fCompressionSettings) {
1414 mergeOpts.fCompressionSettings = dstCompSettings;
1415 } else if (*mergeOpts.fCompressionSettings != dstCompSettings) {
1416 return R__FAIL(std::string("The compression given to RNTupleMergeOptions is different from that of the "
1417 "sink! (opts: ") +
1418 std::to_string(*mergeOpts.fCompressionSettings) + ", sink: " + std::to_string(dstCompSettings) +
1419 ") This is currently unsupported.");
1420 }
1421 }
1422
1423 // Maps projection source fields to all their projections.
1424 std::unordered_map<ROOT::DescriptorId_t, std::vector<const ROOT::RFieldDescriptor *>> projectionPointees;
1425
1426 // we should have a model if and only if the destination is initialized.
1427 if (!!fModel != fDestination->IsInitialized()) {
1428 return R__FAIL(
1429 "passing an already-initialized destination to RNTupleMerger::Merge (i.e. trying to do incremental "
1430 "merging) can only be done by providing a valid ROOT::RNTupleModel when constructing the RNTupleMerger.");
1431 }
1432
1433 RNTupleMergeData mergeData{sources, *fDestination, mergeOpts};
1434 mergeData.fNumDstEntries = mergeData.fDestination.GetNEntries();
1435
1436 if (fModel) {
1437 // If this is an incremental merging, pre-fill the column id map with the existing destination ids.
1438 // Otherwise we would generate new output ids that may not match the ones in the destination!
1439 for (const auto &field : mergeData.fDstDescriptor.GetTopLevelFields()) {
1440 PrefillColumnMap(fDestination->GetDescriptor(), field, mergeData.fColumnIdMap);
1441 }
1442 }
1443
1444 // NOTE: don't wrap this in a do {} while (0)! It uses continue!
1445#define SKIP_OR_ABORT(errMsg) \
1446 if (mergeOpts.fErrBehavior == ENTupleMergeErrBehavior::kSkip) { \
1447 R__LOG_WARNING(NTupleMergeLog()) << "Skipping RNTuple due to: " << (errMsg); \
1448 continue; \
1449 } else { \
1450 return R__FAIL(errMsg); \
1451 }
1452
1453 // Merge main loop
1454 for (RPageSource *source : sources) {
1455 source->Attach(RNTupleSerializer::EDescriptorDeserializeMode::kForWriting);
1456 auto srcDescriptor = source->GetSharedDescriptorGuard();
1457 mergeData.fSrcDescriptor = &srcDescriptor.GetRef();
1458
1459 if (mergeData.fSrcDescriptor->GetVersion() > ROOT::RNTuple::GetCurrentVersion()) {
1462 << "RNTuple '" << mergeData.fSrcDescriptor->GetName()
1463 << "' has a higher format version than the latest supported by this version "
1464 "of ROOT. Merging will work but some features may be dropped.";
1465 } else {
1466 return R__FAIL("RNTuple '" + mergeData.fSrcDescriptor->GetName() +
1467 "' has a higher format version than the latest supported by this version. Refusing to "
1468 "merge, since RNTupleMergeOptions::fVersionBehavior is set to AbortOnHigherVersion.");
1469 }
1470 }
1471
1472 // Create sink and model from the input descriptor if not initialized
1473 if (!fModel) {
1474 fModel = fDestination->InitFromDescriptor(srcDescriptor.GetRef(), false /* copyClusters */);
1475 }
1476
1477 for (const auto &extraTypeInfoDesc : srcDescriptor->GetExtraTypeInfoIterable())
1478 fDestination->UpdateExtraTypeInfo(extraTypeInfoDesc);
1479
1480 auto descCmpRes = CompareDescriptorStructure(mergeData.fDstDescriptor, srcDescriptor.GetRef());
1481 if (!descCmpRes) {
1482 SKIP_OR_ABORT(std::string("Source RNTuple has an incompatible schema with the destination:\n") +
1483 descCmpRes.GetError()->GetReport())
1484 }
1485 auto descCmp = descCmpRes.Unwrap();
1486
1487 // If the current source is missing some fields and we're not in Union mode, error
1488 // (if we are in Union mode, MergeSourceClusters will fill the missing fields with default values).
1489 if (mergeOpts.fMergingMode != ENTupleMergingMode::kUnion && !descCmp.fExtraDstFields.empty()) {
1490 std::string msg = "Source RNTuple is missing the following fields:";
1491 for (const auto *field : descCmp.fExtraDstFields) {
1492 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1493 }
1494 SKIP_OR_ABORT(msg)
1495 }
1496
1497 // handle extra src fields
1498 if (!descCmp.fExtraSrcFields.empty()) {
1499 if (mergeOpts.fMergingMode == ENTupleMergingMode::kUnion) {
1500 // late model extension for all fExtraSrcFields in Union mode
1501 auto res = ExtendDestinationModel(descCmp, *fModel, mergeData);
1502 if (!res)
1503 return R__FORWARD_ERROR(res);
1504 } else if (mergeOpts.fMergingMode == ENTupleMergingMode::kStrict) {
1505 // If the current source has extra fields and we're in Strict mode, error
1506 std::string msg = "Source RNTuple has extra fields that the destination RNTuple doesn't have:";
1507 for (const auto *field : descCmp.fExtraSrcFields) {
1508 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1509 }
1510 SKIP_OR_ABORT(msg)
1511 }
1512 }
1513
1514 //// Extend columns if needed
1515 if (!descCmp.fColReprExtensions.empty()) {
1516 for (const auto &field : descCmp.fExtraDstFields) {
1517 if (field->IsProjectedField())
1518 projectionPointees[field->GetProjectionSourceId()].push_back(field);
1519 }
1520
1521 // We need to extend the columns in the proper order, i.e. so that they appear in the same order as
1522 // their first representation. This is to ensure that the pages we write to the cluster are in a consistent
1523 // order as their column descriptors. The page creation order is determined by the order of
1524 // columnInfos.fCommonColumns, which in turn depends on the common fields order (see GatherColumnInfos).
1525 // XXX: do we need this separate sort step? Why not just create this vector directly in
1526 // CompareDescriptorStructure?
1527 std::vector<std::pair<const RFieldDescriptor *, std::vector<RColReprExtension>>> colExtensions;
1528 colExtensions.reserve(descCmp.fColReprExtensions.size());
1529 for (const auto &commonField : descCmp.fCommonFields) {
1530 const auto *field = commonField.fDst;
1531 AddColumnExtensionsInFieldOrder(*field, mergeData.fDstDescriptor, descCmp.fColReprExtensions, colExtensions,
1532 projectionPointees);
1533 }
1534 for (const auto &field : descCmp.fExtraSrcFields) {
1535 if (field->IsProjectedField())
1536 projectionPointees[field->GetProjectionSourceId()].push_back(field);
1537 }
1538
1539 for (const auto &[fieldDesc, extensions] : colExtensions) {
1540 auto &mappings = descCmp.fColReprMappings[fieldDesc];
1541 for (const auto &extension : extensions) {
1542 const auto firstColumnId = fDestination->AddColumnRepresentation(*fieldDesc, extension.fSourceRepr,
1543 extension.fOrigFirstElementIndex);
1544
1545 // When adding new column representations to an existing field which is the source of some projected
1546 // fields, we need to also add new alias columns to those fields so that they can point to the proper
1547 // representation.
1548 if (auto it = projectionPointees.find(fieldDesc->GetId()); it != projectionPointees.end()) {
1549 for (const auto &projection : it->second) {
1550 for (auto colIdx = 0u; colIdx < extension.fSourceRepr.size(); ++colIdx)
1551 fDestination->AddAliasColumn(mergeData.fDstDescriptor, *projection, firstColumnId + colIdx);
1552 }
1553 }
1554 mappings.push_back(extension);
1555 }
1556 }
1557 }
1558
1559 // handle extra dst fields & common fields
1560 auto columnInfos = GatherColumnInfos(descCmp, srcDescriptor.GetRef(), mergeData);
1561 auto res = MergeSourceClusters(*source, columnInfos.fCommonColumns, columnInfos.fExtraDstColumns, mergeData);
1562 if (!res)
1563 return R__FORWARD_ERROR(res);
1564 } // end loop over sources
1565
1566 // Commit the output
1567 if (fDestination->GetNEntries() == 0)
1568 R__LOG_WARNING(NTupleMergeLog()) << "Output RNTuple '" << fDestination->GetNTupleName() << "' has no entries.";
1569 else
1570 fDestination->CommitClusterGroup();
1571
1572 fDestination->CommitDataset();
1573
1574 return RResult<void>::Success();
1575}
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 ROOT::RResult< void > ExtendDestinationModel(RDescriptorsComparison &descCmp, ROOT::RNTupleModel &dstModel, RNTupleMergeData &mergeData)
static ROOT::RResult< void > GenerateZeroPagesForColumns(size_t nEntriesToGenerate, std::span< const RColumnMergeInfo > columns, RSealedPageMergeData &sealedPageData, ROOT::Internal::RPageAllocator &pageAlloc, const ROOT::RNTupleDescriptor &dstDescriptor, const RNTupleMergeData &mergeData)
static void AddColumnsFromField(std::vector< RColumnMergeInfo > &columns, const ROOT::RNTupleDescriptor &srcDesc, const FieldCollectionMap_t< RColReprMapping > &colReprMappings, RNTupleMergeData &mergeData, const ROOT::RFieldDescriptor &srcFieldDesc, const ROOT::RFieldDescriptor &dstFieldDesc, const std::string &prefix="")
static std::optional< ENTupleMergeErrBehavior > ParseOptionErrBehavior(const TString &opts)
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 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 RColumnInfoGroup GatherColumnInfos(const RDescriptorsComparison &descCmp, const ROOT::RNTupleDescriptor &srcDesc, RNTupleMergeData &mergeData)
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...
ROOT::RResult< void > MergeSourceClusters(ROOT::Internal::RPageSource &source, std::span< RColumnMergeInfo > commonColumns, std::span< const RColumnMergeInfo > extraDstColumns, RNTupleMergeData &mergeData)
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
ROOT::RResult< void > MergeCommonColumns(ROOT::Internal::RClusterPool &clusterPool, const ROOT::RClusterDescriptor &clusterDesc, std::span< RColumnMergeInfo > commonColumns, const ROOT::Internal::RCluster::ColumnSet_t &commonColumnSet, RSealedPageMergeData &sealedPageData, const RNTupleMergeData &mergeData, ROOT::Internal::RPageAllocator &pageAlloc)
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.
Uses standard C++ memory allocation for the column data pages.
Abstract interface to allocate and release pages.
virtual RPage NewPage(std::size_t elementSize, std::size_t nElements)=0
Reserves memory large enough to hold nElements of the given size.
Abstract interface to write data into an ntuple.
virtual void CommitSuppressedColumn(ColumnHandle_t columnHandle)=0
Commits a suppressed column for the current cluster.
const ROOT::RNTupleWriteOptions & GetWriteOptions() const
Returns the sink's write options.
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 void CommitSealedPageV(std::span< RPageStorage::RSealedPageGroup > ranges)=0
Write a vector of preprocessed pages to storage. The corresponding columns must have been added befor...
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.
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.
void * GrowUnchecked(std::uint32_t nElements)
Increases the number elements in the page.
Definition RPage.hxx:149
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::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 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
RClusterDescriptorIterable GetActiveClusterIterable() const
std::size_t GetNActiveClusters() const
std::string GetTypeNameForComparison(const RFieldDescriptor &fieldDesc) const
Adjust the type name of the passed RFieldDescriptor for comparison with another renormalized type nam...
std::size_t GetNClusters() const
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.
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")
const ROOT::RFieldDescriptor * fParentFieldDescriptor
std::optional< std::type_index > fInMemoryType
const ROOT::RNTupleDescriptor * fParentNTupleDescriptor
const ROOT::RNTupleDescriptor * fSrcDescriptor
RNTupleMergeData(std::span< RPageSource * > sources, RPageSink &destination, const RNTupleMergeOptions &mergeOpts)
const ROOT::RNTupleDescriptor & fDstDescriptor
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.
std::vector< RPageStorage::RSealedPageGroup > fGroups
std::vector< std::unique_ptr< std::byte[]> > fBuffers
std::deque< RPageStorage::SealedPageSequence_t > fPagesV
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