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{
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
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
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 descriptor = source->GetSharedDescriptorGuard();
198 auto clusterIter = descriptor->GetClusterIterable();
200 if (firstCluster == clusterIter.end()) {
202 << "Asked to use the first source's compression as the output compression, but the "
203 "first source (file '"
204 << inFile->GetName()
205 << "') has an empty RNTuple, therefore the output compression could not be "
206 "determined.";
207 return -1;
208 }
209 auto colRangeIter = (*firstCluster).GetColumnRangeIterable();
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
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) {
234 outSource->Attach(RNTupleSerializer::EDescriptorDeserializeMode::kForWriting);
235 auto desc = outSource->GetSharedDescriptorGuard();
236 model = destination->InitFromDescriptor(desc.GetRef(), true /* copyClusters */);
237 }
238
239 // Interface conversion
240 sourcePtrs.reserve(sources.size());
241 for (const auto &s : sources) {
242 sourcePtrs.push_back(s.get());
243 }
244
245 // Now merge
246 RNTupleMerger merger{std::move(destination), std::move(model)};
248 mergerOpts.fCompressionSettings = compression;
249 mergerOpts.fExtraVerbose = extraVerbose;
250 if (auto mergingMode = ParseOptionMergingMode(mergeInfo->fOptions)) {
251 mergerOpts.fMergingMode = *mergingMode;
252 }
253 if (auto errBehavior = ParseOptionErrBehavior(mergeInfo->fOptions)) {
254 mergerOpts.fErrBehavior = *errBehavior;
255 }
257 mergerOpts.fVersionBehavior = *versionBehavior;
258 }
259 merger.Merge(sourcePtrs, mergerOpts).ThrowOnError();
260
261 // Provide the caller with a merged anchor object (even though we've already
262 // written it).
263 *this = *outFile->Get<ROOT::RNTuple>(ntupleName.c_str());
264
265 return 0;
266} catch (const std::exception &ex) {
267 R__LOG_ERROR(NTupleMergeLog()) << "Exception thrown while merging: " << ex.what();
268 return -1;
269}
270
271namespace {
272// Functor used to change the compression of a page to `fCompressionSettings`.
273struct RChangeCompressionFunc {
274 const RColumnElementBase &fSrcColElement;
275 std::uint32_t fCompressionSettings;
276 RPageStorage::RSealedPage &fSealedPage;
278 std::byte *fBuffer;
279 std::size_t fBufSize;
280 const ROOT::RNTupleWriteOptions &fWriteOpts;
281
282 void operator()() const
283 {
285
286 const auto bytesPacked = fSrcColElement.GetPackedSize(fSealedPage.GetNElements());
287 // TODO: this buffer could be kept and reused across pages
288 std::unique_ptr<std::byte[]> unzipBufOwned;
289 std::byte *unzipBuf;
290 if (fCompressionSettings != 0) {
292 unzipBuf = unzipBufOwned.get();
293 } else {
295 }
297 unzipBuf);
298
299 const auto checksumSize = fWriteOpts.GetEnablePageChecksums() * sizeof(std::uint64_t);
300 std::size_t nBytesZipped;
301 if (fCompressionSettings != 0) {
303 assert(fBufSize >= bytesPacked + checksumSize);
305 } else {
307 }
308 fSealedPage = {fBuffer, nBytesZipped + checksumSize, fSealedPage.GetNElements(), fSealedPage.GetHasChecksum()};
309 fSealedPage.ChecksumIfEnabled();
310 }
311};
312
313struct RTaskVisitor {
314 std::optional<ROOT::Experimental::TTaskGroup> &fGroup;
315
316 template <typename T>
317 void operator()(T &&f)
318 {
319 if (fGroup)
320 fGroup->Run(f);
321 else
322 f();
323 }
324};
325
326struct RCommonField {
327 const ROOT::RFieldDescriptor *fSrc;
328 const ROOT::RFieldDescriptor *fDst;
329
330 RCommonField(const ROOT::RFieldDescriptor &src, const ROOT::RFieldDescriptor &dst) : fSrc(&src), fDst(&dst) {}
331};
332
333/// Maps a column representation from a source to a destination RNTuple.
334/// fSource and fDest are the first representation indices of a specific column.
335///
336/// When we merge fields from different RNTuples, two compatible fields may use different column
337/// representations. When merging their columns we need to make sure that we keep the output
338/// representation coherent, which is what this mapping is here for.
339struct RColReprMapping {
340 std::uint32_t fSource;
341 std::uint32_t fDest;
342};
343
344/// A column extension that needs to be added to an output field.
345/// Note that this also adds a mapping for each new representation, which is why it inherits RColReprMapping.
346struct RColReprExtension : RColReprMapping {
347 /// The new representations to be added
348 std::vector<ROOT::Internal::RColumnFormat> fSourceRepr;
349 /// The first element index that this column had in its source. When adding this representation to the destination,
350 /// the new column will add this amount to the first element index of the 0th-representation column in the
351 /// destination's current cluster.
352 std::uint32_t fOrigFirstElementIndex = 0;
353};
354
355static std::optional<std::uint32_t>
356FindColumnReprMapping(const std::vector<RColReprMapping> &mappings, std::uint32_t sourceReprIndex)
357{
358 for (const auto [src, dst] : mappings)
360 return dst;
361 return std::nullopt;
362}
363
364template <typename T>
365using FieldCollectionMap_t = std::unordered_map<const ROOT::RFieldDescriptor *, std::vector<T>>;
366
367struct RDescriptorsComparison {
368 std::vector<const ROOT::RFieldDescriptor *> fExtraDstFields;
369 std::vector<const ROOT::RFieldDescriptor *> fExtraSrcFields;
370 std::vector<RCommonField> fCommonFields;
371 // For each field that has more than 1 column representation in the output model,
372 // maps the column representatives of the source field with those of the destination.
373 // The key is the destination field.
376};
377
378struct RColumnOutInfo {
380};
381
382// { ".fully.qualified.fieldName.colInputIndex.colOutputReprIndex" => colOutputInfo }
383using ColumnIdMap_t = std::unordered_map<std::string, RColumnOutInfo>;
384
385struct RColumnInfoGroup {
386 std::vector<RColumnMergeInfo> fExtraDstColumns;
387 std::vector<RColumnMergeInfo> fCommonColumns;
388};
389
390} // namespace
391
392// These structs cannot be in the anon namespace becase they're used in RNTupleMerger's private interface.
395 // This column name is built as a dot-separated concatenation of the ancestry of
396 // the columns' parent fields' names plus the index of the column itself.
397 // e.g. "Muon.pt.x._0"
398 std::string fColumnName;
399 // The column id in the source RNTuple
401 // The corresponding column id in the destination RNTuple (the mapping happens in AddColumnsFromField())
403 std::uint16_t fOutputReprIndex = 0;
404 // If nullopt, use the default in-memory type
405 std::optional<std::type_index> fInMemoryType;
408};
409
410// Data related to a single call of RNTupleMerger::Merge()
412 std::span<RPageSource *> fSources;
417
418 std::vector<RColumnMergeInfo> fColumns;
419 // Maps input column IDs to output IDs
420 ColumnIdMap_t fColumnIdMap;
421
423
428};
429
431 // We use a std::deque so that references to the contained SealedPageSequence_t, and its iterators, are
432 // never invalidated.
433 std::deque<RPageStorage::SealedPageSequence_t> fPagesV;
434 std::vector<RPageStorage::RSealedPageGroup> fGroups;
435 std::vector<std::unique_ptr<std::byte[]>> fBuffers;
436};
437
438} // namespace ROOT::Experimental::Internal
439
440// Subprocedure of CompareDescriptorStructure, extracted for readability.
441// Given two fields, attempts to match their column representations and schedules column extensions if necessary.
444 RDescriptorsComparison &result, std::vector<std::string> &errors)
445{
446 const auto &srcColumns = srcField.GetLogicalColumnIds();
447 const auto &dstColumns = dstField.GetLogicalColumnIds();
448
449 // Fields must have the same cardinality
450 const std::uint32_t srcColCardinality = srcField.GetColumnCardinality();
451 const std::uint32_t dstColCardinality = dstField.GetColumnCardinality();
453 std::stringstream ss;
454 ss << "Field `" << srcField.GetFieldName()
455 << "` has a different column cardinality than previously-seen field with the same name (old: "
456 << dstColCardinality << ", new: " << srcColCardinality << ")";
457 errors.push_back(ss.str());
458 return;
459 }
460
461 if (srcColCardinality == 0)
462 return; // no columns to match
463
464 const auto srcNColReprs = srcColumns.size() / srcColCardinality;
465 const auto dstNColReprs = dstColumns.size() / dstColCardinality;
466 std::uint32_t nextDstReprIndex = dstNColReprs;
467
468 // For each column representation of the source, check if it matches one in the descriptor.
469 // If so, and if it doesn't match the destination's repr index, add a mapping for it.
470 // If nothing matches, schedule the column representation to be added later.
471 // NOTE: this has quadratic complexity but the numbers involved are small so it's fine.
472 for (auto srcReprIdx = 0u; srcReprIdx < srcNColReprs; ++srcReprIdx) {
473 std::int64_t matchingRepr = -1;
474 for (auto dstReprIdx = 0u; dstReprIdx < dstNColReprs; ++dstReprIdx) {
475 bool matches = true;
478 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
480 const auto &dstCol = dstDesc.GetColumnDescriptor(dstColId);
481 if (srcCol.GetType() != dstCol.GetType()) {
482 matches = false;
483 break;
484 }
485 }
486
487 if (matches) {
488 // If this column representation matches by column type, we need to make sure that it also has
489 // matching column metadata. Since we currently do not support multiple column representations
490 // that only differ by such metadata, we forbid merging such columns (e.g. we cannot merge two
491 // Real32Trunc columns with different bit widths). This could technically be supported, but it
492 // would require significant effort, so we currently don't.
495 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
497 const auto &dstCol = dstDesc.GetColumnDescriptor(dstColId);
498 if (srcCol.GetType() != dstCol.GetType() || srcCol.GetBitsOnStorage() != dstCol.GetBitsOnStorage() ||
499 srcCol.GetValueRange() != dstCol.GetValueRange()) {
500 matches = false;
501 break;
502 }
503 }
504
505 if (matches) {
506 // We found a valid matching representation.
508 break;
509 }
510 }
511 }
512
513 if (errors.empty()) {
514 if (matchingRepr >= 0 && matchingRepr != srcReprIdx) {
515 // a different matching representation was found
516 assert(matchingRepr < std::numeric_limits<std::uint32_t>::max());
517 result.fColReprMappings[&dstField].push_back(
518 RColReprMapping{srcReprIdx, static_cast<std::uint32_t>(matchingRepr)});
519 } else if (matchingRepr < 0) {
520 // this representation was not found in the destination: add it
521 std::vector<ROOT::Internal::RColumnFormat> newRepr;
522 newRepr.reserve(srcColCardinality);
523 std::uint32_t firstElemIdx = 0;
526 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
527 // All added columns are supposed to have the same firstElementIndex
528 assert(firstElemIdx == 0 || firstElemIdx == srcCol.GetFirstElementIndex());
529 firstElemIdx = srcCol.GetFirstElementIndex();
530 auto &reprElement = newRepr.emplace_back();
531 reprElement.fType = srcCol.GetType();
532 reprElement.fBitWidth = srcCol.GetBitsOnStorage();
533 reprElement.fValueRange = srcCol.GetValueRange();
534 }
536 nextDstReprIndex += newRepr.size();
537 result.fColReprExtensions[&dstField].push_back(extension);
538 result.fColReprMappings[&dstField].push_back(extension);
539 }
540 }
541 }
542}
543
544/// Compares the top level fields of `dst` and `src` and determines whether they can be merged or not.
545/// In addition, returns the differences between `dst` and `src`'s structures
548{
549 // Cases:
550 // 1. dst == src
551 // 2. dst has fields that src hasn't
552 // 3. src has fields that dst hasn't
553 // 4. dst and src have fields that differ (compatible or incompatible)
554
555 std::vector<std::string> errors;
556 RDescriptorsComparison res;
557
558 std::vector<RCommonField> commonFields;
559
560 for (const auto &dstField : dst.GetTopLevelFields()) {
561 const auto srcFieldId = src.FindFieldId(dstField.GetFieldName());
563 const auto &srcField = src.GetFieldDescriptor(srcFieldId);
564 commonFields.push_back({srcField, dstField});
565 } else {
566 res.fExtraDstFields.emplace_back(&dstField);
567 }
568 }
569 for (const auto &srcField : src.GetTopLevelFields()) {
570 const auto dstFieldId = dst.FindFieldId(srcField.GetFieldName());
572 res.fExtraSrcFields.push_back(&srcField);
573 }
574 }
575
576 // Check compatibility of common fields
578 // NOTE: using index-based for loop because the collection may get extended by the iteration
579 for (std::size_t fieldIdx = 0; fieldIdx < fieldsToCheck.size(); ++fieldIdx) {
580 const auto &field = fieldsToCheck[fieldIdx];
581
582 // NOTE: field.fSrc and field.fDst have the same name by construction
583 const auto &fieldName = field.fSrc->GetFieldName();
584
585 // Require that fields are both projected or both not projected
586 bool projCompatible = field.fSrc->IsProjectedField() == field.fDst->IsProjectedField();
587 if (!projCompatible) {
588 std::stringstream ss;
589 ss << "Field `" << fieldName << "` is incompatible with previously-seen field with that name because the "
590 << (field.fSrc->IsProjectedField() ? "new" : "old") << " one is projected and the other isn't";
591 errors.push_back(ss.str());
592 } else if (field.fSrc->IsProjectedField()) {
593 // if both fields are projected, verify that they point to the same real field
594 const auto srcName = src.GetQualifiedFieldName(field.fSrc->GetProjectionSourceId());
595 const auto dstName = dst.GetQualifiedFieldName(field.fDst->GetProjectionSourceId());
596 if (srcName != dstName) {
597 std::stringstream ss;
598 ss << "Field `" << fieldName
599 << "` is projected to a different field than a previously-seen field with the same name (old: "
600 << dstName << ", new: " << srcName << ")";
601 errors.push_back(ss.str());
602 }
603 }
604
605 // Require that fields types match
606 // TODO(gparolini): allow non-identical but compatible types
607 const auto &srcTyName = field.fSrc->GetTypeName();
608 const auto &dstTyName = field.fDst->GetTypeName();
609 if (srcTyName != dstTyName) {
610 std::stringstream ss;
611 ss << "Field `" << fieldName
612 << "` has a type incompatible with a previously-seen field with the same name: (old: " << dstTyName
613 << ", new: " << srcTyName << ")";
614 errors.push_back(ss.str());
615 }
616
617 // Require that type checksums match
618 const auto srcTyChk = field.fSrc->GetTypeChecksum();
619 const auto dstTyChk = field.fDst->GetTypeChecksum();
620 if (srcTyChk && dstTyChk && *srcTyChk != *dstTyChk) {
621 std::stringstream ss;
622 ss << "Field `" << field.fSrc->GetFieldName()
623 << "` has a different type checksum than previously-seen field with the same name";
624 errors.push_back(ss.str());
625 }
626
627 // Require that type versions match
628 const auto srcTyVer = field.fSrc->GetTypeVersion();
629 const auto dstTyVer = field.fDst->GetTypeVersion();
630 if (srcTyVer != dstTyVer) {
631 std::stringstream ss;
632 ss << "Field `" << field.fSrc->GetFieldName()
633 << "` has a different type version than previously-seen field with the same name (old: " << dstTyVer
634 << ", new: " << srcTyVer << ")";
635 errors.push_back(ss.str());
636 }
637
638 // Require that field versions match
639 const auto srcFldVer = field.fSrc->GetFieldVersion();
640 const auto dstFldVer = field.fDst->GetFieldVersion();
641 if (srcFldVer != dstFldVer) {
642 std::stringstream ss;
643 ss << "Field `" << field.fSrc->GetFieldName()
644 << "` has a different field version than previously-seen field with the same name (old: " << dstFldVer
645 << ", new: " << srcFldVer << ")";
646 errors.push_back(ss.str());
647 }
648
649 const auto srcRole = field.fSrc->GetStructure();
650 const auto dstRole = field.fDst->GetStructure();
651 if (srcRole != dstRole) {
652 std::stringstream ss;
653 ss << "Field `" << field.fSrc->GetFieldName()
654 << "` has a different structural role than previously-seen field with the same name (old: " << dstRole
655 << ", new: " << srcRole << ")";
656 errors.push_back(ss.str());
657 }
658
659 // Require that column representations match
660 if (!field.fSrc->IsProjectedField()) {
661 MatchColumnRepresentations(src, dst, *field.fSrc, *field.fDst, res, errors);
662 }
663
664 // Require that subfields are compatible
665 const auto &srcLinks = field.fSrc->GetLinkIds();
666 const auto &dstLinks = field.fDst->GetLinkIds();
667 if (srcLinks.size() != dstLinks.size()) {
668 std::stringstream ss;
669 ss << "Field `" << field.fSrc->GetFieldName()
670 << "` has a different number of children than previously-seen field with the same name (old: "
671 << dstLinks.size() << ", new: " << srcLinks.size() << ")";
672 errors.push_back(ss.str());
673 } else {
674 for (std::size_t linkIdx = 0, linkNum = srcLinks.size(); linkIdx < linkNum; ++linkIdx) {
675 const auto &srcSubfield = src.GetFieldDescriptor(srcLinks[linkIdx]);
676 const auto &dstSubfield = dst.GetFieldDescriptor(dstLinks[linkIdx]);
677 fieldsToCheck.push_back(RCommonField{srcSubfield, dstSubfield});
678 }
679 }
680 }
681
682 std::string errMsg;
683 for (const auto &err : errors)
684 errMsg += std::string("\n * ") + err;
685
686 if (!errMsg.empty())
687 errMsg = errMsg.substr(1); // strip initial newline
688
689 if (errMsg.length())
690 return R__FAIL(errMsg);
691
692 res.fCommonFields = std::move(commonFields);
693
694 return ROOT::RResult(res);
695}
696
697// Applies late model extension to `mergeData.fDestination`, adding all `descCmp.fExtraSrcFields` to it.
698[[nodiscard]]
701{
702 const auto &newFields = descCmp.fExtraSrcFields;
703 auto &commonFields = descCmp.fCommonFields;
704
705 dstModel.Unfreeze();
707
708 if (mergeData.fMergeOpts.fExtraVerbose) {
709 std::string msg = "destination doesn't contain field";
710 if (newFields.size() > 1)
711 msg += 's';
712 msg += ' ';
713 msg += std::accumulate(newFields.begin(), newFields.end(), std::string{}, [](const auto &acc, const auto *field) {
714 return acc + (acc.length() ? ", " : "") + '`' + field->GetFieldName() + '`';
715 });
716 R__LOG_INFO(NTupleMergeLog()) << msg << ": adding " << (newFields.size() > 1 ? "them" : "it")
717 << " to the destination model (entry #" << mergeData.fNumDstEntries << ").";
718 }
719
720 changeset.fAddedFields.reserve(newFields.size());
721 // First add all non-projected fields...
722 for (const auto *fieldDesc : newFields) {
723 if (fieldDesc->IsProjectedField())
724 continue;
725
726 auto field = fieldDesc->CreateField(*mergeData.fSrcDescriptor);
727 // Explicitly set the field representatives. This prevents UpdateSchema() from changing our column
728 // representations via AutoAdjustColumnTypes.
730 for (const auto &colId : fieldDesc->GetLogicalColumnIds()) {
731 const auto &column = mergeData.fSrcDescriptor->GetColumnDescriptor(colId);
732 representatives.push_back(column.GetType());
733 }
734 field->SetColumnRepresentatives({representatives});
735 changeset.AddField(std::move(field));
736 }
737 // ...then add all projected fields.
738 for (const auto *fieldDesc : newFields) {
739 if (!fieldDesc->IsProjectedField())
740 continue;
741
743 auto field = fieldDesc->CreateField(*mergeData.fSrcDescriptor);
744 const auto sourceId = fieldDesc->GetProjectionSourceId();
745 const auto &sourceField = dstModel.GetConstField(mergeData.fSrcDescriptor->GetQualifiedFieldName(sourceId));
746 fieldMap[field.get()] = &sourceField;
747
748 for (const auto &subfield : *field) {
749 const auto &subFieldDesc = mergeData.fSrcDescriptor->GetFieldDescriptor(subfield.GetOnDiskId());
750 const auto subSourceId = subFieldDesc.GetProjectionSourceId();
751 const auto &subSourceField =
752 dstModel.GetConstField(mergeData.fSrcDescriptor->GetQualifiedFieldName(subSourceId));
754 }
755 changeset.fAddedProjectedFields.emplace_back(field.get());
757 }
758 dstModel.Freeze();
759 try {
760 // FIXME: here we are connecting the new fields/columns to the sink!
761 // We should avoid doing that, as all other non-extended fields never get connected (and we don't
762 // need to connect these either in principle).
763 // NOTE: this calls AutoAdjustColumnTypes, but we have set the column representations of all fields
764 // explicitly, so it will not change it under the hood.
765 mergeData.fDestination.UpdateSchema(changeset, mergeData.fNumDstEntries);
766 } catch (const ROOT::RException &ex) {
767 return R__FAIL(ex.what());
768 }
769
770 commonFields.reserve(commonFields.size() + newFields.size());
771 // NOTE(gparolini): Insert the new fields at the beginning of `commonFields`.
772 // We need to make sure the extended fields appear before all other common fields for the following reason:
773 // in general, when we GatherColumnInfos we (potentially) assign new column output ids in field order; this
774 // assignment happens whenever we find new columns, which happens in 3 cases:
775 // 1. we are in the first source and we're adding the first set of (common) fields;
776 // 2. we are adding a new set of extended common fields (this is done in this function);
777 // 3. we are adding new column representations for fields that we already had before processing this source.
778 //
779 // It's important that the output id assigned to the new columns is coherent with the order of the column descriptors
780 // as they appear in the header and footer of the destination RNTuple.
781 // This is in turn determined by the order by which we append new columns to the dst descriptor during the merging
782 // process.
783 //
784 // Now let's consider the three cases listed above.
785 // Ignoring the trivial case (1), the order of operations for each source is:
786 // - call ExtendDestinationModel (case 2)
787 // (this adds both new fields and column descriptors; see the UpdateSchema call above)
788 // - add new column representations (case 3)
789 // (this only adds column descriptors, see the call to AddColumnRepresentation)
790 //
791 // Since we call ExtendDestinationModel (this function) *before* adding the new column representations,
792 // the dst descriptor always gets updated with the new column descriptors coming from the extended fields before
793 // it gets updated with the extended column representations.
794 //
795 // However, in GatherColumnInfos, the new column output ids are added sequentially in *field* order and the fields
796 // containing the new column representations are already in that list from earlier! So, to make sure the new output
797 // ids are assigned to our extended fields first, we push them in from on the list so they are visited first.
798 for (auto it = newFields.rbegin(); it != newFields.rend(); ++it) {
799 const auto *field = *it;
800 const auto newFieldInDstId = mergeData.fDstDescriptor.FindFieldId(field->GetFieldName());
801 const auto &newFieldInDst = mergeData.fDstDescriptor.GetFieldDescriptor(newFieldInDstId);
802 commonFields.insert(commonFields.begin(), RCommonField{*field, newFieldInDst});
803 }
804
806}
807
808// Generates default (zero) values for the given columns
809[[nodiscard]]
811GenerateZeroPagesForColumns(size_t nEntriesToGenerate, std::span<const RColumnMergeInfo> columns,
814{
817
818 for (const auto &column : columns) {
819 const ROOT::RFieldDescriptor *field = column.fParentFieldDescriptor;
820
821 // Skip all auxiliary columns
822 assert(!field->GetLogicalColumnIds().empty());
823 if (field->GetLogicalColumnIds()[0] != column.fInputId)
824 continue;
825
826 // Check if this column is a child of a Collection or a Variant. If so, it has no data
827 // and can be skipped.
828 bool skipColumn = false;
829 auto nRepetitions = std::max<std::uint64_t>(field->GetNRepetitions(), 1);
830 for (auto parentId = field->GetParentId(); parentId != ROOT::kInvalidDescriptorId;) {
831 const ROOT::RFieldDescriptor &parent = column.fParentNTupleDescriptor->GetFieldDescriptor(parentId);
834 skipColumn = true;
835 break;
836 }
837 nRepetitions *= std::max<std::uint64_t>(parent.GetNRepetitions(), 1);
838 parentId = parent.GetParentId();
839 }
840 if (skipColumn)
841 continue;
842
843 const auto structure = field->GetStructure();
844
845 if (structure == ROOT::ENTupleStructure::kStreamer) {
846 return R__FAIL("Destination RNTuple contains a streamer field (" + field->GetFieldName() +
847 ") that is not present in one of the sources. "
848 "Creating a default value for a streamer field is ill-defined, therefore the merging "
849 "process will abort.");
850 }
851
852 // NOTE: we cannot have a Record here because it has no associated columns.
854 structure == ROOT::ENTupleStructure::kPlain);
855
856 const auto &columnDesc = dstDescriptor.GetColumnDescriptor(column.fOutputId);
857 const auto colElement = RColumnElementBase::Generate(columnDesc.GetType());
859 const auto nBytesOnStorage = colElement->GetPackedSize(nElements);
860 // TODO(gparolini): make this configurable
861 constexpr auto kPageSizeLimit = 256 * 1024;
862 // TODO(gparolini): consider coalescing the last page if its size is less than some threshold
864 for (size_t i = 0; i < nPages; ++i) {
865 const auto pageSize = (i < nPages - 1) ? kPageSizeLimit : nBytesOnStorage - kPageSizeLimit * (nPages - 1);
867 const auto bufSize = pageSize + checksumSize;
868 assert(pageSize % colElement->GetSize() == 0);
869 const auto nElementsPerPage = pageSize / colElement->GetSize();
870 auto page = pageAlloc.NewPage(colElement->GetSize(), nElementsPerPage);
871 page.GrowUnchecked(nElementsPerPage);
872 memset(page.GetBuffer(), 0, page.GetNBytes());
873
874 auto &buffer = sealedPageData.fBuffers.emplace_back(new std::byte[bufSize]);
876 sealConf.fElement = colElement.get();
877 sealConf.fPage = &page;
878 sealConf.fBuffer = buffer.get();
879 sealConf.fCompressionSettings = mergeData.fMergeOpts.fCompressionSettings.value();
880 sealConf.fWriteChecksum = mergeData.fDestination.GetWriteOptions().GetEnablePageChecksums();
882
883 sealedPageData.fPagesV.push_back({sealedPage});
884 sealedPageData.fGroups.emplace_back(column.fOutputId, sealedPageData.fPagesV.back().cbegin(),
885 sealedPageData.fPagesV.back().cend());
886 }
887 }
889}
890
891// Merges all columns appearing both in the source and destination RNTuples, just copying them if their
892// compression matches ("fast merge") or by unsealing and resealing them with the proper compression.
896 std::span<RColumnMergeInfo> commonColumns,
899{
900 const auto nCommonColumnsInCluster = commonColumnSet.size();
902
905
906 const RCluster *cluster = clusterPool.GetCluster(clusterDesc.GetId(), commonColumnSet);
907 // we expect the cluster pool to contain the requested set of columns, since they were
908 // validated by CompareDescriptorStructure() and MergeSourceClusters().
910
911 const std::uint32_t outCompression = mergeData.fMergeOpts.fCompressionSettings.value();
912
913 for (size_t colIdx = 0; colIdx < nCommonColumnsInCluster; ++colIdx) {
914 const auto &column = commonColumns[colIdx];
915 const auto &columnId = column.fInputId;
916 R__ASSERT(clusterDesc.ContainsColumn(columnId));
917
918 const auto &columnDesc = mergeData.fSrcDescriptor->GetColumnDescriptor(columnId);
919 const auto srcColElement = column.fInMemoryType
920 ? ROOT::Internal::GenerateColumnElement(*column.fInMemoryType, columnDesc.GetType())
922
923 // Now get the pages for this column in this cluster
924 const auto &pages = clusterDesc.GetPageRange(columnId);
925
927 sealedPages.resize(pages.GetPageInfos().size());
928
929 // Each column range potentially has a distinct compression settings
930 const auto &columnRange = clusterDesc.GetColumnRange(columnId);
931 assert(!columnRange.IsSuppressed());
932 const auto colRangeCompressionSettings = columnRange.GetCompressionSettings().value();
933
934 // Select "merging level". There are 2 levels, from fastest to slowest, depending on the case:
935 // L1: compression and encoding of src and dest both match: we can simply copy the page
936 // L2: compression of dest doesn't match the src we must recompress the page.
937 // Note that in no case do we need to re-encode the page, as if the encoding differs we simply
938 // append a new column representation to the field.
940
941 if (needsRecompressing && mergeData.fMergeOpts.fExtraVerbose) {
942 R__LOG_INFO(NTupleMergeLog()) << "Recompressing column " << column.fColumnName
943 << ": { compression: " << colRangeCompressionSettings << " => "
944 << mergeData.fMergeOpts.fCompressionSettings.value() << ", onDiskType: "
946 srcColElement->GetIdentifier().fOnDiskType)
947 << "}";
948 }
949
950 const size_t pageBufferBaseIdx = sealedPageData.fBuffers.size();
951 // If the column range already has the right compression we don't need to allocate any new buffer, so we don't
952 // bother reserving memory for them.
954 sealedPageData.fBuffers.resize(sealedPageData.fBuffers.size() + pages.GetPageInfos().size());
955
956 // If this column is deferred, we may need to fill "holes" until its real start. We fill any missing entry
957 // with zeroes, like we do for extraDstColumns.
958 // As an optimization, we don't do this for the first source (since we can rely on the FirstElementIndex and
959 // deferred column mechanism in that case).
960 // TODO: also avoid doing this if we added no real page of this column to the destination yet.
961 if (columnDesc.GetFirstElementIndex() > clusterDesc.GetFirstEntryIndex() && mergeData.fNumDstEntries > 0) {
962 const auto nMissingEntries = columnDesc.GetFirstElementIndex() - clusterDesc.GetFirstEntryIndex();
964 mergeData.fDstDescriptor, mergeData);
965 if (!res)
966 return R__FORWARD_ERROR(res);
967 }
968
969 // Loop over the pages
970 std::uint64_t pageIdx = 0;
971 for (const auto &pageInfo : pages.GetPageInfos()) {
972 assert(pageIdx < sealedPages.size());
973 assert(sealedPageData.fBuffers.size() == 0 || pageIdx < sealedPageData.fBuffers.size());
974 assert(pageInfo.GetLocator().GetType() != RNTupleLocator::kTypePageZero);
975
977 auto onDiskPage = cluster->GetOnDiskPage(key);
978
979 const auto checksumSize = pageInfo.HasChecksum() * RPageStorage::kNBytesPageChecksum;
981 sealedPage.SetNElements(pageInfo.GetNElements());
982 sealedPage.SetHasChecksum(pageInfo.HasChecksum());
983 sealedPage.SetBufferSize(pageInfo.GetLocator().GetNBytesOnStorage() + checksumSize);
984 sealedPage.SetBuffer(onDiskPage->GetAddress());
985 // TODO(gparolini): more graceful error handling (skip the page?)
986 sealedPage.VerifyChecksumIfEnabled().ThrowOnError();
987 R__ASSERT(onDiskPage && (onDiskPage->GetSize() == sealedPage.GetBufferSize()));
988
989 if (needsRecompressing) {
990 const auto uncompressedSize = srcColElement->GetSize() * sealedPage.GetNElements();
991 auto &buffer = sealedPageData.fBuffers[pageBufferBaseIdx + pageIdx];
993 // NOTE: we currently allocate the max possible size for this buffer and don't shrink it afterward.
994 // We might want to introduce an option that trades speed for memory usage and shrink the buffer to fit
995 // the actual data size after recompressing.
997
998 // clang-format off
999 RTaskVisitor{fTaskGroup}(RChangeCompressionFunc{
1002 sealedPage,
1003 *fPageAlloc,
1004 buffer.get(),
1005 bufSize,
1006 mergeData.fDestination.GetWriteOptions()
1007 });
1008 // clang-format on
1009 }
1010
1011 ++pageIdx;
1012
1013 } // end of loop over pages
1014
1015 if (fTaskGroup)
1016 fTaskGroup->Wait();
1017
1018 sealedPageData.fPagesV.push_back(std::move(sealedPages));
1019 sealedPageData.fGroups.emplace_back(column.fOutputId, sealedPageData.fPagesV.back().cbegin(),
1020 sealedPageData.fPagesV.back().cend());
1021 } // end loop over common columns
1022
1024}
1025
1026// Iterates over all clusters of `source` and merges their pages into `destination`.
1027// It is assumed that all columns in `commonColumns` are present (and compatible) in both the source and
1028// the destination's schemas.
1029// The pages may be "fast-merged" (i.e. simply copied with no decompression/recompression) if the target
1030// compression is unspecified or matches the original compression settings.
1032 std::span<const RColumnMergeInfo> extraDstColumns,
1034{
1036
1037 std::vector<RColumnMergeInfo> missingColumns{extraDstColumns.begin(), extraDstColumns.end()};
1038
1039 // Loop over all clusters in this file.
1040 // descriptor->GetClusterIterable() doesn't guarantee any specific order, so we explicitly
1041 // request the first cluster.
1042 ROOT::DescriptorId_t clusterId = mergeData.fSrcDescriptor->FindClusterId(0, 0);
1044 const auto &clusterDesc = mergeData.fSrcDescriptor->GetClusterDescriptor(clusterId);
1045 const auto nClusterEntries = clusterDesc.GetNEntries();
1047
1048 // Deduce which columns are suppressed (cluster by cluster) by exclusion, as:
1049 // (columns in the columnIdMap) - (columns in commonColumns which are not suppressed).
1050 // Note that some suppressed columns may not be in commonColumns because they might not appear at all in the
1051 // current source.
1053 using ColumnHandle_t = ROOT::Internal::RPageStorage::ColumnHandle_t;
1054
1055 // NOTE: `commonColumns` contains all columns that appear *somewhere* both in the src and in the dst.
1056 // Just because a column is in `commonColumns` it doesn't mean that each cluster in the source contains
1057 // it, as it may be a deferred column that only has real data in a future cluster. We need to figure out which
1058 // columns are actually present in this cluster so we only merge their pages (the missing columns are handled
1059 // by synthesizing zero pages - see below).
1060
1061 // Convert columns to a ColumnSet for the ClusterPool query
1063 // Collect all common columns appearing in this cluster into commonColumnSet and reorganize commonColumns so
1064 // that those columns are at the start of it (whereas missing columns are at its end).
1065 // NOTE: it's fine if this scrambles the order of columns: the RNTupleSerializer will sort them by physical ID.
1067 std::partition(commonColumns.begin(), commonColumns.end(), [&](const auto &column) {
1068 if (clusterDesc.ContainsColumn(column.fInputId)) {
1069 const auto &colRange = clusterDesc.GetColumnRange(column.fInputId);
1070 ++nCommonColumnsInCluster;
1071 columnsInCluster[column.fParentFieldDescriptor].push_back(column.fOutputId);
1072 if (!colRange.IsSuppressed()) {
1073 commonColumnSet.emplace(column.fInputId);
1074 return true;
1075 }
1076 mergeData.fDestination.CommitSuppressedColumn(ColumnHandle_t{column.fOutputId});
1077 }
1078 return false;
1079 });
1080
1081 // Commit all suppressed columns.
1082 // This is a fairly involved operation, as we need to commit all known columns that:
1083 // a) do not appear in extraDstColumns (those are "missing", not suppressed), and
1084 // b) do not appear in commonColumnSet (those are the active columns).
1085 // Not that these may or may not appear in commonColumns as suppressed columns, since they may or may not be
1086 // present in the current source.
1087 // The only way to find all the columns is to go and get them from fColumnIdMap, which keeps track of every
1088 // column we added to the destination so far. However, since it also contains the extraDstColumns, we need to
1089 // specifically only query those columns that belong to a field that has at least 1 column in commonColumns
1090 // (remember that commonColumns contains all columns associated to the common fields for this source).
1091 for (const auto &[fieldDesc, columnIds] : columnsInCluster) {
1092 const auto &fieldFQName = mergeData.fSrcDescriptor->GetQualifiedFieldName(fieldDesc->GetId());
1093 const auto cardinality = fieldDesc->GetColumnCardinality();
1094 for (auto i = 0u; i < fieldDesc->GetLogicalColumnIds().size(); ++i) {
1095 const auto colIndex = i % cardinality;
1096 const auto reprIndex = i / cardinality;
1097 const auto colName = "." + fieldFQName + '.' + std::to_string(colIndex) + '.' + std::to_string(reprIndex);
1098 const auto colIt = mergeData.fColumnIdMap.find(colName);
1099 assert(colIt != mergeData.fColumnIdMap.end());
1100 const auto colOutId = colIt->second.fColumnId;
1101 if (std::find(columnIds.begin(), columnIds.end(), colOutId) == columnIds.end()) {
1102 mergeData.fDestination.CommitSuppressedColumn(ColumnHandle_t{colOutId});
1103 }
1104 }
1105 }
1106
1109 *fPageAlloc);
1110 if (!res)
1111 return R__FORWARD_ERROR(res);
1112
1113 // Generate zero pages for the missing columns.
1114 // For each cluster, the "missing columns" are the union of the extraDstColumns and the common columns
1115 // that are not present in the cluster.
1116 // Note that this does NOT include suppressed columns, for which no pages are synthesized.
1117 missingColumns.resize(extraDstColumns.size()); // NOTE: this clears all common columns of the previous cluster
1118 for (size_t i = nCommonColumnsInCluster; i < commonColumns.size(); ++i)
1119 missingColumns.push_back(commonColumns[i]);
1120
1122 mergeData.fDstDescriptor, mergeData);
1123 if (!res)
1124 return R__FORWARD_ERROR(res);
1125
1126 // Commit the pages and the clusters
1127 mergeData.fDestination.CommitSealedPageV(sealedPageData.fGroups);
1128 mergeData.fDestination.CommitCluster(nClusterEntries);
1129 mergeData.fNumDstEntries += nClusterEntries;
1130
1131 // Go to the next cluster
1132 clusterId = mergeData.fSrcDescriptor->FindNextClusterId(clusterId);
1133 }
1134
1135 // TODO(gparolini): when we get serious about huge file support (>~ 100GB) we might want to check here
1136 // the size of the running page list and commit a cluster group when it exceeds some threshold,
1137 // which would prevent the page list from getting too large.
1138 // However, as of today, we aren't really handling such huge files, and even relatively big ones
1139 // such as the CMS dataset have a page list size of about only 2 MB.
1140 // So currently we simply merge all cluster groups into one.
1142}
1143
1144static std::optional<std::type_index> ColumnInMemoryType(std::string_view fieldType, ENTupleColumnType onDiskType)
1145{
1148 return typeid(ROOT::Internal::RColumnIndex);
1149
1151 return typeid(ROOT::Internal::RColumnSwitch);
1152
1153 // clang-format off
1154 if (fieldType == "bool") return typeid(bool);
1155 if (fieldType == "std::byte") return typeid(std::byte);
1156 if (fieldType == "char") return typeid(char);
1157 if (fieldType == "std::int8_t") return typeid(std::int8_t);
1158 if (fieldType == "std::uint8_t") return typeid(std::uint8_t);
1159 if (fieldType == "std::int16_t") return typeid(std::int16_t);
1160 if (fieldType == "std::uint16_t") return typeid(std::uint16_t);
1161 if (fieldType == "std::int32_t") return typeid(std::int32_t);
1162 if (fieldType == "std::uint32_t") return typeid(std::uint32_t);
1163 if (fieldType == "std::int64_t") return typeid(std::int64_t);
1164 if (fieldType == "std::uint64_t") return typeid(std::uint64_t);
1165 if (fieldType == "float") return typeid(float);
1166 if (fieldType == "double") return typeid(double);
1167 // clang-format on
1168
1169 // if the type is not one of those above, we use the default in-memory type.
1170 return std::nullopt;
1171}
1172
1173// Given a field, fill `columns` and `mergeData.fColumnIdMap` with information about all columns belonging to it and
1174// its subfields. `mergeData.fColumnIdMap` is used to map matching columns from different sources to the same output
1175// column in the destination. We match columns by their "fully qualified name", which is the concatenation of their
1176// ancestor fields' names and the column index. By this point, since we called `CompareDescriptorStructure()`
1177// earlier, we should be guaranteed that two matching columns will have at least compatible representations.
1178// This function is recursive as it needs to call itself on the entire subfield hierarchy of the source field.
1179// NOTE: srcFieldDesc and dstFieldDesc may alias.
1180static void AddColumnsFromField(std::vector<RColumnMergeInfo> &columns, const ROOT::RNTupleDescriptor &srcDesc,
1183 const ROOT::RFieldDescriptor &dstFieldDesc, const std::string &prefix = "")
1184{
1185 std::string name = prefix + '.' + srcFieldDesc.GetFieldName();
1186
1187 // We don't want to try and merge alias columns. Note that subfields of projected fields
1188 // must also be projected, so we don't need to check them.
1189 if (srcFieldDesc.IsProjectedField())
1190 return;
1191
1192 const auto &columnIds = srcFieldDesc.GetLogicalColumnIds();
1193 columns.reserve(columns.size() + columnIds.size());
1194
1195 for (auto i = 0u; i < srcFieldDesc.GetLogicalColumnIds().size(); ++i) {
1196 auto srcColumnId = srcFieldDesc.GetLogicalColumnIds()[i];
1197 const auto &srcColumn = srcDesc.GetColumnDescriptor(srcColumnId);
1198
1200 info.fInputId = srcColumn.GetPhysicalId();
1201 // NOTE(gparolini): the parent field is used when synthesizing zero pages, which happens in 2 situations:
1202 // 1. when adding extra dst columns (in which case we need to synthesize zero pages for the incoming src), and
1203 // 2. when merging a deferred column into an existing column (in which case we need to fill the "hole" with
1204 // zeroes). For the first case srcFieldDesc and dstFieldDesc are the same (see the calling site of this
1205 // function), but for the second case they're not, and we need to pick the source field because we will then
1206 // check the column's *input* id inside fParentFieldDescriptor to see if it's a suppressed column (see
1207 // GenerateZeroPagesForColumns()).
1208 info.fParentFieldDescriptor = &srcFieldDesc;
1209 // Save the parent field descriptor since this may be either the source or destination descriptor depending on
1210 // whether this is an extraDstField or a commonField. We will need this in GenerateZeroPagesForColumns() to
1211 // properly walk up the field hierarchy.
1212 info.fParentNTupleDescriptor = &srcDesc;
1213
1214 const auto mappingsIt = colReprMappings.find(&dstFieldDesc);
1215 std::uint16_t reprIndex = srcColumn.GetRepresentationIndex();
1216 if (mappingsIt != colReprMappings.end()) {
1219 }
1220
1221 info.fColumnName = name + '.' + std::to_string(srcColumn.GetIndex()) + '.' + std::to_string(reprIndex);
1222
1224
1225 if (auto it = mergeData.fColumnIdMap.find(info.fColumnName); it != mergeData.fColumnIdMap.end()) {
1226 // We had already added this column to the column id map: just copy its data.
1227 info.fOutputId = it->second.fColumnId;
1228 info.fOutputReprIndex = reprIndex;
1229 } else {
1230 // New column: assign it the next ouput id.
1231 info.fOutputId = mergeData.fColumnIdMap.size();
1232 // NOTE(gparolini): map the representation index of src column to that of dst column.
1233 // This mapping is only relevant for common columns and it's done to ensure we have the correct representation
1234 // index in the output column metadata.
1235 assert(dstFieldDesc.GetColumnCardinality() == srcFieldDesc.GetColumnCardinality());
1236 const auto dstColumnIndex = reprIndex * dstFieldDesc.GetColumnCardinality() + srcColumn.GetIndex();
1237 const auto dstColumnId = dstFieldDesc.GetLogicalColumnIds()[dstColumnIndex];
1238 const auto &dstColumn = mergeData.fDstDescriptor.GetColumnDescriptor(dstColumnId);
1239 columnType = dstColumn.GetType();
1240 info.fOutputReprIndex = reprIndex;
1241 mergeData.fColumnIdMap[info.fColumnName] = RColumnOutInfo{info.fOutputId};
1242 }
1243
1244 if (mergeData.fMergeOpts.fExtraVerbose) {
1245 R__LOG_INFO(NTupleMergeLog()) << "Adding column " << info.fColumnName << " with log.id " << srcColumnId
1246 << ", phys.id " << srcColumn.GetPhysicalId() << ", type "
1247 << RColumnElementBase::GetColumnTypeName(srcColumn.GetType()) << " -> log.id "
1248 << info.fOutputId << ", type "
1250 }
1251
1252 // Since we disallow merging fields of different types, src and dstFieldDesc must have the same type name.
1253 assert(srcFieldDesc.GetTypeName() == dstFieldDesc.GetTypeName());
1254 info.fInMemoryType = ColumnInMemoryType(srcFieldDesc.GetTypeName(), columnType);
1255 columns.emplace_back(info);
1256 }
1257
1258 const auto &srcChildrenIds = srcFieldDesc.GetLinkIds();
1259 const auto &dstChildrenIds = dstFieldDesc.GetLinkIds();
1260 assert(srcChildrenIds.size() == dstChildrenIds.size());
1261 for (auto i = 0u; i < srcChildrenIds.size(); ++i) {
1262 const auto &srcChild = srcDesc.GetFieldDescriptor(srcChildrenIds[i]);
1263 const auto &dstChild = mergeData.fDstDescriptor.GetFieldDescriptor(dstChildrenIds[i]);
1265 }
1266}
1267
1268// Converts the fields comparison data to the corresponding column information.
1269// While doing so, it collects such information in `mergeData.fColumnIdMap`, which is used by later calls to this
1270// function to map already-seen column names to their chosen outputId, type and so on.
1271static RColumnInfoGroup GatherColumnInfos(const RDescriptorsComparison &descCmp, const ROOT::RNTupleDescriptor &srcDesc,
1273{
1274 RColumnInfoGroup res;
1275 for (const ROOT::RFieldDescriptor *field : descCmp.fExtraDstFields) {
1276 AddColumnsFromField(res.fExtraDstColumns, mergeData.fDstDescriptor, descCmp.fColReprMappings, mergeData, *field,
1277 *field);
1278 }
1279 for (const auto &[srcField, dstField] : descCmp.fCommonFields) {
1280 AddColumnsFromField(res.fCommonColumns, srcDesc, descCmp.fColReprMappings, mergeData, *srcField, *dstField);
1281 }
1282 return res;
1283}
1284
1286 ColumnIdMap_t &colIdMap, const std::string &prefix = "")
1287{
1288 std::string name = prefix + '.' + fieldDesc.GetFieldName();
1289 for (const auto &colId : fieldDesc.GetLogicalColumnIds()) {
1290 const auto &colDesc = desc.GetColumnDescriptor(colId);
1291 RColumnOutInfo info{};
1292 info.fColumnId = colDesc.GetLogicalId();
1293 const auto colName =
1294 name + '.' + std::to_string(colDesc.GetIndex()) + '.' + std::to_string(colDesc.GetRepresentationIndex());
1296 }
1297
1298 for (const auto &subId : fieldDesc.GetLinkIds()) {
1299 const auto &subfield = desc.GetFieldDescriptor(subId);
1301 }
1302}
1303
1307 std::vector<std::pair<const ROOT::RFieldDescriptor *, std::vector<RColReprExtension>>> &outExtensions,
1308 std::unordered_map<ROOT::DescriptorId_t, std::vector<const ROOT::RFieldDescriptor *>> &outProjectionPointees)
1309{
1310 const auto it = extensions.find(&field);
1311 if (it != extensions.end())
1312 outExtensions.emplace_back(it->first, it->second);
1313
1314 if (field.IsProjectedField())
1315 outProjectionPointees[field.GetProjectionSourceId()].push_back(&field);
1316
1317 for (auto childId : field.GetLinkIds()) {
1318 const auto &child = desc.GetFieldDescriptor(childId);
1320 }
1321}
1322
1323RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination,
1324 std::unique_ptr<ROOT::RNTupleModel> model)
1325 // TODO(gparolini): consider using an arena allocator instead, since we know the precise lifetime
1326 // of the RNTuples we are going to handle (e.g. we can reset the arena at every source)
1327 : fDestination(std::move(destination)),
1328 fPageAlloc(std::make_unique<ROOT::Internal::RPageAllocatorHeap>()),
1329 fModel(std::move(model))
1330{
1332
1333#ifdef R__USE_IMT
1336#endif
1337}
1338
1339RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination)
1340 : RNTupleMerger(std::move(destination), nullptr)
1341{
1342}
1343
1345{
1347
1349
1350 // Set compression settings if unset and verify it's compatible with the sink
1351 {
1352 const auto dstCompSettings = fDestination->GetWriteOptions().GetCompression();
1353 if (!mergeOpts.fCompressionSettings) {
1354 mergeOpts.fCompressionSettings = dstCompSettings;
1355 } else if (*mergeOpts.fCompressionSettings != dstCompSettings) {
1356 return R__FAIL(std::string("The compression given to RNTupleMergeOptions is different from that of the "
1357 "sink! (opts: ") +
1358 std::to_string(*mergeOpts.fCompressionSettings) + ", sink: " + std::to_string(dstCompSettings) +
1359 ") This is currently unsupported.");
1360 }
1361 }
1362
1363 // Maps projection source fields to all their projections.
1364 std::unordered_map<ROOT::DescriptorId_t, std::vector<const ROOT::RFieldDescriptor *>> projectionPointees;
1365
1366 // we should have a model if and only if the destination is initialized.
1367 if (!!fModel != fDestination->IsInitialized()) {
1368 return R__FAIL(
1369 "passing an already-initialized destination to RNTupleMerger::Merge (i.e. trying to do incremental "
1370 "merging) can only be done by providing a valid ROOT::RNTupleModel when constructing the RNTupleMerger.");
1371 }
1372
1374 mergeData.fNumDstEntries = mergeData.fDestination.GetNEntries();
1375
1376 if (fModel) {
1377 // If this is an incremental merging, pre-fill the column id map with the existing destination ids.
1378 // Otherwise we would generate new output ids that may not match the ones in the destination!
1379 for (const auto &field : mergeData.fDstDescriptor.GetTopLevelFields()) {
1380 PrefillColumnMap(fDestination->GetDescriptor(), field, mergeData.fColumnIdMap);
1381 }
1382 }
1383
1384#define SKIP_OR_ABORT(errMsg) \
1385 do { \
1386 if (mergeOpts.fErrBehavior == ENTupleMergeErrBehavior::kSkip) { \
1387 R__LOG_WARNING(NTupleMergeLog()) << "Skipping RNTuple due to: " << (errMsg); \
1388 continue; \
1389 } else { \
1390 return R__FAIL(errMsg); \
1391 } \
1392 } while (0)
1393
1394 // Merge main loop
1395 for (RPageSource *source : sources) {
1396 // We need to make sure the streamer info from the source files is loaded otherwise we may not be able
1397 // to build the streamer info of user-defined types unless we have their dictionaries available.
1398 source->LoadStreamerInfo();
1399
1400 source->Attach(RNTupleSerializer::EDescriptorDeserializeMode::kForWriting);
1401 auto srcDescriptor = source->GetSharedDescriptorGuard();
1402 mergeData.fSrcDescriptor = &srcDescriptor.GetRef();
1403
1404 if (mergeData.fSrcDescriptor->GetVersion() > ROOT::RNTuple::GetCurrentVersion()) {
1407 << "RNTuple '" << mergeData.fSrcDescriptor->GetName()
1408 << "' has a higher format version than the latest supported by this version "
1409 "of ROOT. Merging will work but some features may be dropped.";
1410 } else {
1411 return R__FAIL("RNTuple '" + mergeData.fSrcDescriptor->GetName() +
1412 "' has a higher format version than the latest supported by this version. Refusing to "
1413 "merge, since RNTupleMergeOptions::fVersionBehavior is set to AbortOnHigherVersion.");
1414 }
1415 }
1416
1417 // Create sink and model from the input descriptor if not initialized
1418 if (!fModel) {
1419 fModel = fDestination->InitFromDescriptor(srcDescriptor.GetRef(), false /* copyClusters */);
1420 }
1421
1422 for (const auto &extraTypeInfoDesc : srcDescriptor->GetExtraTypeInfoIterable())
1423 fDestination->UpdateExtraTypeInfo(extraTypeInfoDesc);
1424
1425 auto descCmpRes = CompareDescriptorStructure(mergeData.fDstDescriptor, srcDescriptor.GetRef());
1426 if (!descCmpRes) {
1427 SKIP_OR_ABORT(std::string("Source RNTuple has an incompatible schema with the destination:\n") +
1428 descCmpRes.GetError()->GetReport());
1429 }
1430 auto descCmp = descCmpRes.Unwrap();
1431
1432 // If the current source is missing some fields and we're not in Union mode, error
1433 // (if we are in Union mode, MergeSourceClusters will fill the missing fields with default values).
1434 if (mergeOpts.fMergingMode != ENTupleMergingMode::kUnion && !descCmp.fExtraDstFields.empty()) {
1435 std::string msg = "Source RNTuple is missing the following fields:";
1436 for (const auto *field : descCmp.fExtraDstFields) {
1437 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1438 }
1440 }
1441
1442 // handle extra src fields
1443 if (!descCmp.fExtraSrcFields.empty()) {
1444 if (mergeOpts.fMergingMode == ENTupleMergingMode::kUnion) {
1445 // late model extension for all fExtraSrcFields in Union mode
1447 if (!res)
1448 return R__FORWARD_ERROR(res);
1449 } else if (mergeOpts.fMergingMode == ENTupleMergingMode::kStrict) {
1450 // If the current source has extra fields and we're in Strict mode, error
1451 std::string msg = "Source RNTuple has extra fields that the destination RNTuple doesn't have:";
1452 for (const auto *field : descCmp.fExtraSrcFields) {
1453 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1454 }
1456 }
1457 }
1458
1459 //// Extend columns if needed
1460 if (!descCmp.fColReprExtensions.empty()) {
1461 for (const auto &field : descCmp.fExtraDstFields) {
1462 if (field->IsProjectedField())
1463 projectionPointees[field->GetProjectionSourceId()].push_back(field);
1464 }
1465
1466 // We need to extend the columns in the proper order, i.e. so that they appear in the same order as
1467 // their first representation. This is to ensure that the pages we write to the cluster are in a consistent
1468 // order as their column descriptors. The page creation order is determined by the order of
1469 // columnInfos.fCommonColumns, which in turn depends on the common fields order (see GatherColumnInfos).
1470 // XXX: do we need this separate sort step? Why not just create this vector directly in
1471 // CompareDescriptorStructure?
1472 std::vector<std::pair<const RFieldDescriptor *, std::vector<RColReprExtension>>> colExtensions;
1473 colExtensions.reserve(descCmp.fColReprExtensions.size());
1474 for (const auto &commonField : descCmp.fCommonFields) {
1475 const auto *field = commonField.fDst;
1476 AddColumnExtensionsInFieldOrder(*field, mergeData.fDstDescriptor, descCmp.fColReprExtensions, colExtensions,
1478 }
1479 for (const auto &field : descCmp.fExtraSrcFields) {
1480 if (field->IsProjectedField())
1481 projectionPointees[field->GetProjectionSourceId()].push_back(field);
1482 }
1483
1484 for (const auto &[fieldDesc, extensions] : colExtensions) {
1485 auto &mappings = descCmp.fColReprMappings[fieldDesc];
1486 for (const auto &extension : extensions) {
1487 const auto firstColumnId = fDestination->AddColumnRepresentation(*fieldDesc, extension.fSourceRepr,
1488 extension.fOrigFirstElementIndex);
1489
1490 // When adding new column representations to an existing field which is the source of some projected
1491 // fields, we need to also add new alias columns to those fields so that they can point to the proper
1492 // representation.
1493 if (auto it = projectionPointees.find(fieldDesc->GetId()); it != projectionPointees.end()) {
1494 for (const auto &projection : it->second) {
1495 for (auto colIdx = 0u; colIdx < extension.fSourceRepr.size(); ++colIdx)
1496 fDestination->AddAliasColumn(mergeData.fDstDescriptor, *projection, firstColumnId + colIdx);
1497 }
1498 }
1499 mappings.push_back(extension);
1500 }
1501 }
1502 }
1503
1504 // handle extra dst fields & common fields
1506 auto res = MergeSourceClusters(*source, columnInfos.fCommonColumns, columnInfos.fExtraDstColumns, mergeData);
1507 if (!res)
1508 return R__FORWARD_ERROR(res);
1509 } // end loop over sources
1510
1511 if (fDestination->GetNEntries() == 0)
1512 R__LOG_WARNING(NTupleMergeLog()) << "Output RNTuple '" << fDestination->GetNTupleName() << "' has no entries.";
1513
1514 // Commit the output
1515 fDestination->CommitClusterGroup();
1516 fDestination->CommitDataset();
1517
1518 return RResult<void>::Success();
1519}
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 std::optional< ENTupleMergeVersionBehavior > ParseOptionVersionBehavior(const TString &opts)
static bool BeginsWithDelimitedWord(const TString &str, const char *word)
#define f(i)
Definition RSha256.hxx:104
size_t size(const MatrixT &matrix)
retrieve the size of a square matrix
ROOT::Detail::TRangeCast< T, true > TRangeDynCast
TRangeDynCast is an adapter class that allows the typed iteration through a TCollection.
#define R__ASSERT(e)
Checks condition e and reports a fatal error if it's false.
Definition TError.h:125
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
char name[80]
Definition TGX11.cxx:148
const char * extension
Definition civetweb.c:8515
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.
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
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.
Abstract interface to write data into an ntuple.
RSealedPage SealPage(const ROOT::Internal::RPage &page, const ROOT::Internal::RColumnElementBase &element)
Helper for streaming a page.
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.
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.
Base class for all ROOT issued exceptions.
Definition RError.hxx:78
std::vector< ROOT::ENTupleColumnType > ColumnRepresentation_t
Metadata stored for every field of an RNTuple.
ROOT::ENTupleStructure GetStructure() const
ROOT::DescriptorId_t GetParentId() const
std::uint64_t GetNRepetitions() 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
The RNTupleModel encapulates the schema of an RNTuple.
Common user-tunable settings for storing RNTuples.
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:89
const_iterator begin() const
const_iterator end() const
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
A class to pass information from the TFileMerger to the objects being merged.
A file, usually with extension .root, that stores data and code in the form of serialized objects in ...
Definition TFile.h:130
Book space in a file, create I/O buffers, to fill them, (un)compress them.
Definition TKey.h:28
Mother of all ROOT objects.
Definition TObject.h:42
Basic string class.
Definition TString.h:138
@ kIgnoreCase
Definition TString.h:285
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)
Bool_t IsImplicitMTEnabled()
Returns true if the implicit multi-threading in ROOT is enabled.
Definition TROOT.cxx:669
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
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.
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.
A sealed page contains the bytes of a page as written to storage (packed & compressed).
RResult< void > VerifyChecksumIfEnabled() const
@ kUseGeneralPurpose
Use the new recommended general-purpose setting; it is a best trade-off between compression ratio/dec...
Definition Compression.h:58