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 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 }
209 R__ASSERT(firstClusterGroup->HasClusterDetails());
210 const auto &firstCluster = descGuard->GetClusterDescriptor(firstClusterGroup->GetClusterIds()[0]);
211 auto colRangeIter = firstCluster.GetColumnRangeIterable();
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
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) {
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)};
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 }
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) {
294 unzipBuf = unzipBufOwned.get();
295 } else {
297 }
299 unzipBuf);
300
301 const auto checksumSize = fWriteOpts.GetEnablePageChecksums() * sizeof(std::uint64_t);
302 std::size_t nBytesZipped;
303 if (fCompressionSettings != 0) {
305 assert(fBufSize >= bytesPacked + checksumSize);
307 } else {
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)
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.
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
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.
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();
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;
480 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
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.
497 const auto &srcCol = srcDesc.GetColumnDescriptor(srcColId);
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.
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;
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 }
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());
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());
574 res.fExtraSrcFields.push_back(&srcField);
575 }
576 }
577
578 // Check compatibility of common fields
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// Applies late model extension to `mergeData.fDestination`, adding all `descCmp.fExtraSrcFields` to it.
701[[nodiscard]]
704{
705 const auto &newFields = descCmp.fExtraSrcFields;
706 auto &commonFields = descCmp.fCommonFields;
707
708 dstModel.Unfreeze();
710
711 if (mergeData.fMergeOpts.fExtraVerbose) {
712 std::string msg = "destination doesn't contain field";
713 if (newFields.size() > 1)
714 msg += 's';
715 msg += ' ';
716 msg += std::accumulate(newFields.begin(), newFields.end(), std::string{}, [](const auto &acc, const auto *field) {
717 return acc + (acc.length() ? ", " : "") + '`' + field->GetFieldName() + '`';
718 });
719 R__LOG_INFO(NTupleMergeLog()) << msg << ": adding " << (newFields.size() > 1 ? "them" : "it")
720 << " to the destination model (entry #" << mergeData.fNumDstEntries << ").";
721 }
722
723 changeset.fAddedFields.reserve(newFields.size());
724 // First add all non-projected fields...
725 for (const auto *fieldDesc : newFields) {
726 if (fieldDesc->IsProjectedField())
727 continue;
728
729 auto field = fieldDesc->CreateField(*mergeData.fSrcDescriptor);
730 // Explicitly set the field representatives. This prevents UpdateSchema() from changing our column
731 // representations via AutoAdjustColumnTypes.
733 for (const auto &colId : fieldDesc->GetLogicalColumnIds()) {
734 const auto &column = mergeData.fSrcDescriptor->GetColumnDescriptor(colId);
735 representatives.push_back(column.GetType());
736 }
737 field->SetColumnRepresentatives({representatives});
738 changeset.AddField(std::move(field));
739 }
740 // ...then add all projected fields.
741 for (const auto *fieldDesc : newFields) {
742 if (!fieldDesc->IsProjectedField())
743 continue;
744
746 auto field = fieldDesc->CreateField(*mergeData.fSrcDescriptor);
747 const auto sourceId = fieldDesc->GetProjectionSourceId();
748 const auto &sourceField = dstModel.GetConstField(mergeData.fSrcDescriptor->GetQualifiedFieldName(sourceId));
749 fieldMap[field.get()] = &sourceField;
750
751 for (const auto &subfield : *field) {
752 const auto &subFieldDesc = mergeData.fSrcDescriptor->GetFieldDescriptor(subfield.GetOnDiskId());
753 const auto subSourceId = subFieldDesc.GetProjectionSourceId();
754 const auto &subSourceField =
755 dstModel.GetConstField(mergeData.fSrcDescriptor->GetQualifiedFieldName(subSourceId));
757 }
758 changeset.fAddedProjectedFields.emplace_back(field.get());
760 }
761 dstModel.Freeze();
762 try {
763 // FIXME: here we are connecting the new fields/columns to the sink!
764 // We should avoid doing that, as all other non-extended fields never get connected (and we don't
765 // need to connect these either in principle).
766 // NOTE: this calls AutoAdjustColumnTypes, but we have set the column representations of all fields
767 // explicitly, so it will not change it under the hood.
768 mergeData.fDestination.UpdateSchema(changeset, mergeData.fNumDstEntries);
769 } catch (const ROOT::RException &ex) {
770 return R__FAIL(ex.what());
771 }
772
773 commonFields.reserve(commonFields.size() + newFields.size());
774 // NOTE(gparolini): Insert the new fields at the beginning of `commonFields`.
775 // We need to make sure the extended fields appear before all other common fields for the following reason:
776 // in general, when we GatherColumnInfos we (potentially) assign new column output ids in field order; this
777 // assignment happens whenever we find new columns, which happens in 3 cases:
778 // 1. we are in the first source and we're adding the first set of (common) fields;
779 // 2. we are adding a new set of extended common fields (this is done in this function);
780 // 3. we are adding new column representations for fields that we already had before processing this source.
781 //
782 // It's important that the output id assigned to the new columns is coherent with the order of the column descriptors
783 // as they appear in the header and footer of the destination RNTuple.
784 // This is in turn determined by the order by which we append new columns to the dst descriptor during the merging
785 // process.
786 //
787 // Now let's consider the three cases listed above.
788 // Ignoring the trivial case (1), the order of operations for each source is:
789 // - call ExtendDestinationModel (case 2)
790 // (this adds both new fields and column descriptors; see the UpdateSchema call above)
791 // - add new column representations (case 3)
792 // (this only adds column descriptors, see the call to AddColumnRepresentation)
793 //
794 // Since we call ExtendDestinationModel (this function) *before* adding the new column representations,
795 // the dst descriptor always gets updated with the new column descriptors coming from the extended fields before
796 // it gets updated with the extended column representations.
797 //
798 // However, in GatherColumnInfos, the new column output ids are added sequentially in *field* order and the fields
799 // containing the new column representations are already in that list from earlier! So, to make sure the new output
800 // ids are assigned to our extended fields first, we push them in from on the list so they are visited first.
801 for (auto it = newFields.rbegin(); it != newFields.rend(); ++it) {
802 const auto *field = *it;
803 const auto newFieldInDstId = mergeData.fDstDescriptor.FindFieldId(field->GetFieldName());
804 const auto &newFieldInDst = mergeData.fDstDescriptor.GetFieldDescriptor(newFieldInDstId);
805 commonFields.insert(commonFields.begin(), RCommonField{*field, newFieldInDst});
806 }
807
809}
810
811// Generates default (zero) values for the given columns
812[[nodiscard]]
814GenerateZeroPagesForColumns(size_t nEntriesToGenerate, std::span<const RColumnMergeInfo> columns,
817{
820
821 for (const auto &column : columns) {
822 const ROOT::RFieldDescriptor *field = column.fParentFieldDescriptor;
823
824 // Skip all auxiliary columns
825 assert(!field->GetLogicalColumnIds().empty());
826 if (field->GetLogicalColumnIds()[0] != column.fInputId)
827 continue;
828
829 // Check if this column is a child of a Collection or a Variant. If so, it has no data
830 // and can be skipped.
831 bool skipColumn = false;
832 auto nRepetitions = std::max<std::uint64_t>(field->GetNRepetitions(), 1);
833 for (auto parentId = field->GetParentId(); parentId != ROOT::kInvalidDescriptorId;) {
834 const ROOT::RFieldDescriptor &parent = column.fParentNTupleDescriptor->GetFieldDescriptor(parentId);
837 skipColumn = true;
838 break;
839 }
840 nRepetitions *= std::max<std::uint64_t>(parent.GetNRepetitions(), 1);
841 parentId = parent.GetParentId();
842 }
843 if (skipColumn)
844 continue;
845
846 const auto structure = field->GetStructure();
847
848 if (structure == ROOT::ENTupleStructure::kStreamer) {
849 return R__FAIL("Destination RNTuple contains a streamer field (" + field->GetFieldName() +
850 ") that is not present in one of the sources. "
851 "Creating a default value for a streamer field is ill-defined, therefore the merging "
852 "process will abort.");
853 }
854
855 // NOTE: we cannot have a Record here because it has no associated columns.
857 structure == ROOT::ENTupleStructure::kPlain);
858
859 const auto &columnDesc = dstDescriptor.GetColumnDescriptor(column.fOutputId);
860 const auto colElement = RColumnElementBase::Generate(columnDesc.GetType());
862 const auto nBytesOnStorage = colElement->GetPackedSize(nElements);
863 // TODO(gparolini): make this configurable
864 constexpr auto kPageSizeLimit = 256 * 1024;
865 // TODO(gparolini): consider coalescing the last page if its size is less than some threshold
867 for (size_t i = 0; i < nPages; ++i) {
868 const auto pageSize = (i < nPages - 1) ? kPageSizeLimit : nBytesOnStorage - kPageSizeLimit * (nPages - 1);
870 const auto bufSize = pageSize + checksumSize;
871 assert(pageSize % colElement->GetSize() == 0);
872 const auto nElementsPerPage = pageSize / colElement->GetSize();
873 auto page = pageAlloc.NewPage(colElement->GetSize(), nElementsPerPage);
874 page.GrowUnchecked(nElementsPerPage);
875 memset(page.GetBuffer(), 0, page.GetNBytes());
876
877 auto &buffer = sealedPageData.fBuffers.emplace_back(new std::byte[bufSize]);
879 sealConf.fElement = colElement.get();
880 sealConf.fPage = &page;
881 sealConf.fBuffer = buffer.get();
882 sealConf.fCompressionSettings = mergeData.fMergeOpts.fCompressionSettings.value();
883 sealConf.fWriteChecksum = mergeData.fDestination.GetWriteOptions().GetEnablePageChecksums();
885
886 sealedPageData.fPagesV.push_back({sealedPage});
887 sealedPageData.fGroups.emplace_back(column.fOutputId, sealedPageData.fPagesV.back().cbegin(),
888 sealedPageData.fPagesV.back().cend());
889 }
890 }
892}
893
894// Merges all columns appearing both in the source and destination RNTuples, just copying them if their
895// compression matches ("fast merge") or by unsealing and resealing them with the proper compression.
899 std::span<RColumnMergeInfo> commonColumns,
902{
903 const auto nCommonColumnsInCluster = commonColumnSet.size();
905
908
909 const RCluster *cluster = clusterPool.GetCluster(clusterDesc.GetId(), commonColumnSet);
910 // we expect the cluster pool to contain the requested set of columns, since they were
911 // validated by CompareDescriptorStructure() and MergeSourceClusters().
913
914 const std::uint32_t outCompression = mergeData.fMergeOpts.fCompressionSettings.value();
915
916 for (size_t colIdx = 0; colIdx < nCommonColumnsInCluster; ++colIdx) {
917 const auto &column = commonColumns[colIdx];
918 const auto &columnId = column.fInputId;
919 R__ASSERT(clusterDesc.ContainsColumn(columnId));
920
921 const auto &columnDesc = mergeData.fSrcDescriptor->GetColumnDescriptor(columnId);
922 const auto srcColElement = column.fInMemoryType
923 ? ROOT::Internal::GenerateColumnElement(*column.fInMemoryType, columnDesc.GetType())
925
926 // Now get the pages for this column in this cluster
927 const auto &pages = clusterDesc.GetPageRange(columnId);
928
930 sealedPages.resize(pages.GetPageInfos().size());
931
932 // Each column range potentially has a distinct compression settings
933 const auto &columnRange = clusterDesc.GetColumnRange(columnId);
934 assert(!columnRange.IsSuppressed());
935 const auto colRangeCompressionSettings = columnRange.GetCompressionSettings().value();
936
937 // Select "merging level". There are 2 levels, from fastest to slowest, depending on the case:
938 // L1: compression and encoding of src and dest both match: we can simply copy the page
939 // L2: compression of dest doesn't match the src we must recompress the page.
940 // Note that in no case do we need to re-encode the page, as if the encoding differs we simply
941 // append a new column representation to the field.
943
944 if (needsRecompressing && mergeData.fMergeOpts.fExtraVerbose) {
945 R__LOG_INFO(NTupleMergeLog()) << "Recompressing column " << column.fColumnName
946 << ": { compression: " << colRangeCompressionSettings << " => "
947 << mergeData.fMergeOpts.fCompressionSettings.value() << ", onDiskType: "
949 srcColElement->GetIdentifier().fOnDiskType)
950 << "}";
951 }
952
953 const size_t pageBufferBaseIdx = sealedPageData.fBuffers.size();
954 // If the column range already has the right compression we don't need to allocate any new buffer, so we don't
955 // bother reserving memory for them.
957 sealedPageData.fBuffers.resize(sealedPageData.fBuffers.size() + pages.GetPageInfos().size());
958
959 // If this column is deferred, we may need to fill "holes" until its real start. We fill any missing entry
960 // with zeroes, like we do for extraDstColumns.
961 // As an optimization, we don't do this for the first source (since we can rely on the FirstElementIndex and
962 // deferred column mechanism in that case).
963 // TODO: also avoid doing this if we added no real page of this column to the destination yet.
964 if (columnDesc.GetFirstElementIndex() > clusterDesc.GetFirstEntryIndex() && mergeData.fNumDstEntries > 0) {
965 const auto nMissingEntries = columnDesc.GetFirstElementIndex() - clusterDesc.GetFirstEntryIndex();
967 mergeData.fDstDescriptor, mergeData);
968 if (!res)
969 return R__FORWARD_ERROR(res);
970 }
971
972 // Loop over the pages
973 std::uint64_t pageIdx = 0;
974 for (const auto &pageInfo : pages.GetPageInfos()) {
975 assert(pageIdx < sealedPages.size());
976 assert(sealedPageData.fBuffers.size() == 0 || pageIdx < sealedPageData.fBuffers.size());
977 assert(pageInfo.GetLocator().GetType() != RNTupleLocator::kTypePageZero);
978
980 auto onDiskPage = cluster->GetOnDiskPage(key);
981
982 const auto checksumSize = pageInfo.HasChecksum() * RPageStorage::kNBytesPageChecksum;
984 sealedPage.SetNElements(pageInfo.GetNElements());
985 sealedPage.SetHasChecksum(pageInfo.HasChecksum());
986 sealedPage.SetBufferSize(pageInfo.GetLocator().GetNBytesOnStorage() + checksumSize);
987 sealedPage.SetBuffer(onDiskPage->GetAddress());
988 // TODO(gparolini): more graceful error handling (skip the page?)
989 sealedPage.VerifyChecksumIfEnabled().ThrowOnError();
990 R__ASSERT(onDiskPage && (onDiskPage->GetSize() == sealedPage.GetBufferSize()));
991
992 if (needsRecompressing) {
993 const auto uncompressedSize = srcColElement->GetSize() * sealedPage.GetNElements();
994 auto &buffer = sealedPageData.fBuffers[pageBufferBaseIdx + pageIdx];
996 // NOTE: we currently allocate the max possible size for this buffer and don't shrink it afterward.
997 // We might want to introduce an option that trades speed for memory usage and shrink the buffer to fit
998 // the actual data size after recompressing.
1000
1001 // clang-format off
1002 RTaskVisitor{fTaskGroup}(RChangeCompressionFunc{
1005 sealedPage,
1006 *fPageAlloc,
1007 buffer.get(),
1008 bufSize,
1009 mergeData.fDestination.GetWriteOptions()
1010 });
1011 // clang-format on
1012 }
1013
1014 ++pageIdx;
1015
1016 } // end of loop over pages
1017
1018 if (fTaskGroup)
1019 fTaskGroup->Wait();
1020
1021 sealedPageData.fPagesV.push_back(std::move(sealedPages));
1022 sealedPageData.fGroups.emplace_back(column.fOutputId, sealedPageData.fPagesV.back().cbegin(),
1023 sealedPageData.fPagesV.back().cend());
1024 } // end loop over common columns
1025
1027}
1028
1029// Iterates over all clusters of `source` and merges their pages into `destination`.
1030// It is assumed that all columns in `commonColumns` are present (and compatible) in both the source and
1031// the destination's schemas.
1032// The pages may be "fast-merged" (i.e. simply copied with no decompression/recompression) if the target
1033// compression is unspecified or matches the original compression settings.
1035 std::span<const RColumnMergeInfo> extraDstColumns,
1037{
1039
1040 std::vector<RColumnMergeInfo> missingColumns{extraDstColumns.begin(), extraDstColumns.end()};
1041
1042 R__ASSERT(mergeData.fSrcDescriptor->GetNClusters() == mergeData.fSrcDescriptor->GetNActiveClusters());
1043 for (const auto &clusterDesc : mergeData.fSrcDescriptor->GetActiveClusterIterable()) {
1044 const auto nClusterEntries = clusterDesc.GetNEntries();
1046
1047 // Deduce which columns are suppressed (cluster by cluster) by exclusion, as:
1048 // (columns in the columnIdMap) - (columns in commonColumns which are not suppressed).
1049 // Note that some suppressed columns may not be in commonColumns because they might not appear at all in the
1050 // current source.
1052 using ColumnHandle_t = ROOT::Internal::RPageStorage::ColumnHandle_t;
1053
1054 // NOTE: `commonColumns` contains all columns that appear *somewhere* both in the src and in the dst.
1055 // Just because a column is in `commonColumns` it doesn't mean that each cluster in the source contains
1056 // it, as it may be a deferred column that only has real data in a future cluster. We need to figure out which
1057 // columns are actually present in this cluster so we only merge their pages (the missing columns are handled
1058 // by synthesizing zero pages - see below).
1059
1060 // Convert columns to a ColumnSet for the ClusterPool query
1062 // Collect all common columns appearing in this cluster into commonColumnSet and reorganize commonColumns so
1063 // that those columns are at the start of it (whereas missing columns are at its end).
1064 // NOTE: it's fine if this scrambles the order of columns: the RNTupleSerializer will sort them by physical ID.
1066 std::partition(commonColumns.begin(), commonColumns.end(), [&](const auto &column) {
1067 if (clusterDesc.ContainsColumn(column.fInputId)) {
1068 const auto &colRange = clusterDesc.GetColumnRange(column.fInputId);
1069 ++nCommonColumnsInCluster;
1070 columnsInCluster[column.fParentFieldDescriptor].push_back(column.fOutputId);
1071 if (!colRange.IsSuppressed()) {
1072 commonColumnSet.emplace(column.fInputId);
1073 return true;
1074 }
1075 mergeData.fDestination.CommitSuppressedColumn(ColumnHandle_t{column.fOutputId});
1076 }
1077 return false;
1078 });
1079
1080 // Commit all suppressed columns.
1081 // This is a fairly involved operation, as we need to commit all known columns that:
1082 // a) do not appear in extraDstColumns (those are "missing", not suppressed), and
1083 // b) do not appear in commonColumnSet (those are the active columns).
1084 // Not that these may or may not appear in commonColumns as suppressed columns, since they may or may not be
1085 // present in the current source.
1086 // The only way to find all the columns is to go and get them from fColumnIdMap, which keeps track of every
1087 // column we added to the destination so far. However, since it also contains the extraDstColumns, we need to
1088 // specifically only query those columns that belong to a field that has at least 1 column in commonColumns
1089 // (remember that commonColumns contains all columns associated to the common fields for this source).
1090 for (const auto &[fieldDesc, columnIds] : columnsInCluster) {
1091 const auto &fieldFQName = mergeData.fSrcDescriptor->GetQualifiedFieldName(fieldDesc->GetId());
1092 const auto cardinality = fieldDesc->GetColumnCardinality();
1093 for (auto i = 0u; i < fieldDesc->GetLogicalColumnIds().size(); ++i) {
1094 const auto colIndex = i % cardinality;
1095 const auto reprIndex = i / cardinality;
1096 const auto colName = "." + fieldFQName + '.' + std::to_string(colIndex) + '.' + std::to_string(reprIndex);
1097 const auto colIt = mergeData.fColumnIdMap.find(colName);
1098 assert(colIt != mergeData.fColumnIdMap.end());
1099 const auto colOutId = colIt->second.fColumnId;
1100 if (std::find(columnIds.begin(), columnIds.end(), colOutId) == columnIds.end()) {
1101 mergeData.fDestination.CommitSuppressedColumn(ColumnHandle_t{colOutId});
1102 }
1103 }
1104 }
1105
1108 *fPageAlloc);
1109 if (!res)
1110 return R__FORWARD_ERROR(res);
1111
1112 // Generate zero pages for the missing columns.
1113 // For each cluster, the "missing columns" are the union of the extraDstColumns and the common columns
1114 // that are not present in the cluster.
1115 // Note that this does NOT include suppressed columns, for which no pages are synthesized.
1116 missingColumns.resize(extraDstColumns.size()); // NOTE: this clears all common columns of the previous cluster
1117 for (size_t i = nCommonColumnsInCluster; i < commonColumns.size(); ++i)
1118 missingColumns.push_back(commonColumns[i]);
1119
1121 mergeData.fDstDescriptor, mergeData);
1122 if (!res)
1123 return R__FORWARD_ERROR(res);
1124
1125 // Commit the pages and the clusters
1126 mergeData.fDestination.CommitSealedPageV(sealedPageData.fGroups);
1127 mergeData.fDestination.CommitCluster(nClusterEntries);
1128 mergeData.fNumDstEntries += nClusterEntries;
1129 }
1130
1131 // TODO(gparolini): when we get serious about huge file support (>~ 100GB) we might want to check here
1132 // the size of the running page list and commit a cluster group when it exceeds some threshold,
1133 // which would prevent the page list from getting too large.
1134 // However, as of today, we aren't really handling such huge files, and even relatively big ones
1135 // such as the CMS dataset have a page list size of about only 2 MB.
1136 // So currently we simply merge all cluster groups into one.
1138}
1139
1140static std::optional<std::type_index> ColumnInMemoryType(std::string_view fieldType, ENTupleColumnType onDiskType)
1141{
1144 return typeid(ROOT::Internal::RColumnIndex);
1145
1147 return typeid(ROOT::Internal::RColumnSwitch);
1148
1149 // clang-format off
1150 if (fieldType == "bool") return typeid(bool);
1151 if (fieldType == "std::byte") return typeid(std::byte);
1152 if (fieldType == "char") return typeid(char);
1153 if (fieldType == "std::int8_t") return typeid(std::int8_t);
1154 if (fieldType == "std::uint8_t") return typeid(std::uint8_t);
1155 if (fieldType == "std::int16_t") return typeid(std::int16_t);
1156 if (fieldType == "std::uint16_t") return typeid(std::uint16_t);
1157 if (fieldType == "std::int32_t") return typeid(std::int32_t);
1158 if (fieldType == "std::uint32_t") return typeid(std::uint32_t);
1159 if (fieldType == "std::int64_t") return typeid(std::int64_t);
1160 if (fieldType == "std::uint64_t") return typeid(std::uint64_t);
1161 if (fieldType == "float") return typeid(float);
1162 if (fieldType == "double") return typeid(double);
1163 // clang-format on
1164
1165 // if the type is not one of those above, we use the default in-memory type.
1166 return std::nullopt;
1167}
1168
1169// Given a field, fill `columns` and `mergeData.fColumnIdMap` with information about all columns belonging to it and
1170// its subfields. `mergeData.fColumnIdMap` is used to map matching columns from different sources to the same output
1171// column in the destination. We match columns by their "fully qualified name", which is the concatenation of their
1172// ancestor fields' names and the column index. By this point, since we called `CompareDescriptorStructure()`
1173// earlier, we should be guaranteed that two matching columns will have at least compatible representations.
1174// This function is recursive as it needs to call itself on the entire subfield hierarchy of the source field.
1175// NOTE: srcFieldDesc and dstFieldDesc may alias.
1176static void AddColumnsFromField(std::vector<RColumnMergeInfo> &columns, const ROOT::RNTupleDescriptor &srcDesc,
1179 const ROOT::RFieldDescriptor &dstFieldDesc, const std::string &prefix = "")
1180{
1181 std::string name = prefix + '.' + srcFieldDesc.GetFieldName();
1182
1183 // We don't want to try and merge alias columns. Note that subfields of projected fields
1184 // must also be projected, so we don't need to check them.
1185 if (srcFieldDesc.IsProjectedField())
1186 return;
1187
1188 const auto &columnIds = srcFieldDesc.GetLogicalColumnIds();
1189 columns.reserve(columns.size() + columnIds.size());
1190
1191 for (auto i = 0u; i < srcFieldDesc.GetLogicalColumnIds().size(); ++i) {
1192 auto srcColumnId = srcFieldDesc.GetLogicalColumnIds()[i];
1193 const auto &srcColumn = srcDesc.GetColumnDescriptor(srcColumnId);
1194
1196 info.fInputId = srcColumn.GetPhysicalId();
1197 // NOTE(gparolini): the parent field is used when synthesizing zero pages, which happens in 2 situations:
1198 // 1. when adding extra dst columns (in which case we need to synthesize zero pages for the incoming src), and
1199 // 2. when merging a deferred column into an existing column (in which case we need to fill the "hole" with
1200 // zeroes). For the first case srcFieldDesc and dstFieldDesc are the same (see the calling site of this
1201 // function), but for the second case they're not, and we need to pick the source field because we will then
1202 // check the column's *input* id inside fParentFieldDescriptor to see if it's a suppressed column (see
1203 // GenerateZeroPagesForColumns()).
1204 info.fParentFieldDescriptor = &srcFieldDesc;
1205 // Save the parent field descriptor since this may be either the source or destination descriptor depending on
1206 // whether this is an extraDstField or a commonField. We will need this in GenerateZeroPagesForColumns() to
1207 // properly walk up the field hierarchy.
1208 info.fParentNTupleDescriptor = &srcDesc;
1209
1210 const auto mappingsIt = colReprMappings.find(&dstFieldDesc);
1211 std::uint16_t reprIndex = srcColumn.GetRepresentationIndex();
1212 if (mappingsIt != colReprMappings.end()) {
1215 }
1216
1217 info.fColumnName = name + '.' + std::to_string(srcColumn.GetIndex()) + '.' + std::to_string(reprIndex);
1218
1220
1221 if (auto it = mergeData.fColumnIdMap.find(info.fColumnName); it != mergeData.fColumnIdMap.end()) {
1222 // We had already added this column to the column id map: just copy its data.
1223 info.fOutputId = it->second.fColumnId;
1224 info.fOutputReprIndex = reprIndex;
1225 } else {
1226 // New column: assign it the next ouput id.
1227 info.fOutputId = mergeData.fColumnIdMap.size();
1228 // NOTE(gparolini): map the representation index of src column to that of dst column.
1229 // This mapping is only relevant for common columns and it's done to ensure we have the correct representation
1230 // index in the output column metadata.
1231 assert(dstFieldDesc.GetColumnCardinality() == srcFieldDesc.GetColumnCardinality());
1232 const auto dstColumnIndex = reprIndex * dstFieldDesc.GetColumnCardinality() + srcColumn.GetIndex();
1233 const auto dstColumnId = dstFieldDesc.GetLogicalColumnIds()[dstColumnIndex];
1234 const auto &dstColumn = mergeData.fDstDescriptor.GetColumnDescriptor(dstColumnId);
1235 columnType = dstColumn.GetType();
1236 info.fOutputReprIndex = reprIndex;
1237 mergeData.fColumnIdMap[info.fColumnName] = RColumnOutInfo{info.fOutputId};
1238 }
1239
1240 if (mergeData.fMergeOpts.fExtraVerbose) {
1241 R__LOG_INFO(NTupleMergeLog()) << "Adding column " << info.fColumnName << " with log.id " << srcColumnId
1242 << ", phys.id " << srcColumn.GetPhysicalId() << ", type "
1243 << RColumnElementBase::GetColumnTypeName(srcColumn.GetType()) << " -> log.id "
1244 << info.fOutputId << ", type "
1246 }
1247
1248 // Since we disallow merging fields of different types, src and dstFieldDesc must have the same type name.
1249 assert(srcDesc.GetTypeNameForComparison(srcFieldDesc) == dstFieldDesc.GetTypeName());
1250 info.fInMemoryType = ColumnInMemoryType(dstFieldDesc.GetTypeName(), columnType);
1251 columns.emplace_back(info);
1252 }
1253
1254 const auto &srcChildrenIds = srcFieldDesc.GetLinkIds();
1255 const auto &dstChildrenIds = dstFieldDesc.GetLinkIds();
1256 assert(srcChildrenIds.size() == dstChildrenIds.size());
1257 for (auto i = 0u; i < srcChildrenIds.size(); ++i) {
1258 const auto &srcChild = srcDesc.GetFieldDescriptor(srcChildrenIds[i]);
1259 const auto &dstChild = mergeData.fDstDescriptor.GetFieldDescriptor(dstChildrenIds[i]);
1261 }
1262}
1263
1264// Converts the fields comparison data to the corresponding column information.
1265// While doing so, it collects such information in `mergeData.fColumnIdMap`, which is used by later calls to this
1266// function to map already-seen column names to their chosen outputId, type and so on.
1267static RColumnInfoGroup GatherColumnInfos(const RDescriptorsComparison &descCmp, const ROOT::RNTupleDescriptor &srcDesc,
1269{
1270 RColumnInfoGroup res;
1271 for (const ROOT::RFieldDescriptor *field : descCmp.fExtraDstFields) {
1272 AddColumnsFromField(res.fExtraDstColumns, mergeData.fDstDescriptor, descCmp.fColReprMappings, mergeData, *field,
1273 *field);
1274 }
1275 for (const auto &[srcField, dstField] : descCmp.fCommonFields) {
1276 AddColumnsFromField(res.fCommonColumns, srcDesc, descCmp.fColReprMappings, mergeData, *srcField, *dstField);
1277 }
1278 return res;
1279}
1280
1282 ColumnIdMap_t &colIdMap, const std::string &prefix = "")
1283{
1284 std::string name = prefix + '.' + fieldDesc.GetFieldName();
1285 for (const auto &colId : fieldDesc.GetLogicalColumnIds()) {
1286 const auto &colDesc = desc.GetColumnDescriptor(colId);
1287 RColumnOutInfo info{};
1288 info.fColumnId = colDesc.GetLogicalId();
1289 const auto colName =
1290 name + '.' + std::to_string(colDesc.GetIndex()) + '.' + std::to_string(colDesc.GetRepresentationIndex());
1292 }
1293
1294 for (const auto &subId : fieldDesc.GetLinkIds()) {
1295 const auto &subfield = desc.GetFieldDescriptor(subId);
1297 }
1298}
1299
1303 std::vector<std::pair<const ROOT::RFieldDescriptor *, std::vector<RColReprExtension>>> &outExtensions,
1304 std::unordered_map<ROOT::DescriptorId_t, std::vector<const ROOT::RFieldDescriptor *>> &outProjectionPointees)
1305{
1306 const auto it = extensions.find(&field);
1307 if (it != extensions.end())
1308 outExtensions.emplace_back(it->first, it->second);
1309
1310 if (field.IsProjectedField())
1311 outProjectionPointees[field.GetProjectionSourceId()].push_back(&field);
1312
1313 for (auto childId : field.GetLinkIds()) {
1314 const auto &child = desc.GetFieldDescriptor(childId);
1316 }
1317}
1318
1319RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination,
1320 std::unique_ptr<ROOT::RNTupleModel> model)
1321 // TODO(gparolini): consider using an arena allocator instead, since we know the precise lifetime
1322 // of the RNTuples we are going to handle (e.g. we can reset the arena at every source)
1323 : fDestination(std::move(destination)),
1324 fPageAlloc(std::make_unique<ROOT::Internal::RPageAllocatorHeap>()),
1325 fModel(std::move(model))
1326{
1328
1329#ifdef R__USE_IMT
1332#endif
1333}
1334
1335RNTupleMerger::RNTupleMerger(std::unique_ptr<ROOT::Internal::RPagePersistentSink> destination)
1336 : RNTupleMerger(std::move(destination), nullptr)
1337{
1338}
1339
1341{
1343
1345
1346 // Set compression settings if unset and verify it's compatible with the sink
1347 {
1348 const auto dstCompSettings = fDestination->GetWriteOptions().GetCompression();
1349 if (!mergeOpts.fCompressionSettings) {
1350 mergeOpts.fCompressionSettings = dstCompSettings;
1351 } else if (*mergeOpts.fCompressionSettings != dstCompSettings) {
1352 return R__FAIL(std::string("The compression given to RNTupleMergeOptions is different from that of the "
1353 "sink! (opts: ") +
1354 std::to_string(*mergeOpts.fCompressionSettings) + ", sink: " + std::to_string(dstCompSettings) +
1355 ") This is currently unsupported.");
1356 }
1357 }
1358
1359 // Maps projection source fields to all their projections.
1360 std::unordered_map<ROOT::DescriptorId_t, std::vector<const ROOT::RFieldDescriptor *>> projectionPointees;
1361
1362 // we should have a model if and only if the destination is initialized.
1363 if (!!fModel != fDestination->IsInitialized()) {
1364 return R__FAIL(
1365 "passing an already-initialized destination to RNTupleMerger::Merge (i.e. trying to do incremental "
1366 "merging) can only be done by providing a valid ROOT::RNTupleModel when constructing the RNTupleMerger.");
1367 }
1368
1370 mergeData.fNumDstEntries = mergeData.fDestination.GetNEntries();
1371
1372 if (fModel) {
1373 // If this is an incremental merging, pre-fill the column id map with the existing destination ids.
1374 // Otherwise we would generate new output ids that may not match the ones in the destination!
1375 for (const auto &field : mergeData.fDstDescriptor.GetTopLevelFields()) {
1376 PrefillColumnMap(fDestination->GetDescriptor(), field, mergeData.fColumnIdMap);
1377 }
1378 }
1379
1380 // NOTE: don't wrap this in a do {} while (0)! It uses continue!
1381#define SKIP_OR_ABORT(errMsg) \
1382 if (mergeOpts.fErrBehavior == ENTupleMergeErrBehavior::kSkip) { \
1383 R__LOG_WARNING(NTupleMergeLog()) << "Skipping RNTuple due to: " << (errMsg); \
1384 continue; \
1385 } else { \
1386 return R__FAIL(errMsg); \
1387 }
1388
1389 // Merge main loop
1390 for (RPageSource *source : sources) {
1391 source->Attach(RNTupleSerializer::EDescriptorDeserializeMode::kForWriting);
1392 auto srcDescriptor = source->GetSharedDescriptorGuard();
1393 mergeData.fSrcDescriptor = &srcDescriptor.GetRef();
1394
1395 if (mergeData.fSrcDescriptor->GetVersion() > ROOT::RNTuple::GetCurrentVersion()) {
1398 << "RNTuple '" << mergeData.fSrcDescriptor->GetName()
1399 << "' has a higher format version than the latest supported by this version "
1400 "of ROOT. Merging will work but some features may be dropped.";
1401 } else {
1402 return R__FAIL("RNTuple '" + mergeData.fSrcDescriptor->GetName() +
1403 "' has a higher format version than the latest supported by this version. Refusing to "
1404 "merge, since RNTupleMergeOptions::fVersionBehavior is set to AbortOnHigherVersion.");
1405 }
1406 }
1407
1408 // Create sink and model from the input descriptor if not initialized
1409 if (!fModel) {
1410 fModel = fDestination->InitFromDescriptor(srcDescriptor.GetRef(), false /* copyClusters */);
1411 }
1412
1413 for (const auto &extraTypeInfoDesc : srcDescriptor->GetExtraTypeInfoIterable())
1414 fDestination->UpdateExtraTypeInfo(extraTypeInfoDesc);
1415
1416 auto descCmpRes = CompareDescriptorStructure(mergeData.fDstDescriptor, srcDescriptor.GetRef());
1417 if (!descCmpRes) {
1418 SKIP_OR_ABORT(std::string("Source RNTuple has an incompatible schema with the destination:\n") +
1419 descCmpRes.GetError()->GetReport())
1420 }
1421 auto descCmp = descCmpRes.Unwrap();
1422
1423 // If the current source is missing some fields and we're not in Union mode, error
1424 // (if we are in Union mode, MergeSourceClusters will fill the missing fields with default values).
1425 if (mergeOpts.fMergingMode != ENTupleMergingMode::kUnion && !descCmp.fExtraDstFields.empty()) {
1426 std::string msg = "Source RNTuple is missing the following fields:";
1427 for (const auto *field : descCmp.fExtraDstFields) {
1428 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1429 }
1431 }
1432
1433 // handle extra src fields
1434 if (!descCmp.fExtraSrcFields.empty()) {
1435 if (mergeOpts.fMergingMode == ENTupleMergingMode::kUnion) {
1436 // late model extension for all fExtraSrcFields in Union mode
1438 if (!res)
1439 return R__FORWARD_ERROR(res);
1440 } else if (mergeOpts.fMergingMode == ENTupleMergingMode::kStrict) {
1441 // If the current source has extra fields and we're in Strict mode, error
1442 std::string msg = "Source RNTuple has extra fields that the destination RNTuple doesn't have:";
1443 for (const auto *field : descCmp.fExtraSrcFields) {
1444 msg += "\n " + field->GetFieldName() + " : " + field->GetTypeName();
1445 }
1447 }
1448 }
1449
1450 //// Extend columns if needed
1451 if (!descCmp.fColReprExtensions.empty()) {
1452 for (const auto &field : descCmp.fExtraDstFields) {
1453 if (field->IsProjectedField())
1454 projectionPointees[field->GetProjectionSourceId()].push_back(field);
1455 }
1456
1457 // We need to extend the columns in the proper order, i.e. so that they appear in the same order as
1458 // their first representation. This is to ensure that the pages we write to the cluster are in a consistent
1459 // order as their column descriptors. The page creation order is determined by the order of
1460 // columnInfos.fCommonColumns, which in turn depends on the common fields order (see GatherColumnInfos).
1461 // XXX: do we need this separate sort step? Why not just create this vector directly in
1462 // CompareDescriptorStructure?
1463 std::vector<std::pair<const RFieldDescriptor *, std::vector<RColReprExtension>>> colExtensions;
1464 colExtensions.reserve(descCmp.fColReprExtensions.size());
1465 for (const auto &commonField : descCmp.fCommonFields) {
1466 const auto *field = commonField.fDst;
1467 AddColumnExtensionsInFieldOrder(*field, mergeData.fDstDescriptor, descCmp.fColReprExtensions, colExtensions,
1469 }
1470 for (const auto &field : descCmp.fExtraSrcFields) {
1471 if (field->IsProjectedField())
1472 projectionPointees[field->GetProjectionSourceId()].push_back(field);
1473 }
1474
1475 for (const auto &[fieldDesc, extensions] : colExtensions) {
1476 auto &mappings = descCmp.fColReprMappings[fieldDesc];
1477 for (const auto &extension : extensions) {
1478 const auto firstColumnId = fDestination->AddColumnRepresentation(*fieldDesc, extension.fSourceRepr,
1479 extension.fOrigFirstElementIndex);
1480
1481 // When adding new column representations to an existing field which is the source of some projected
1482 // fields, we need to also add new alias columns to those fields so that they can point to the proper
1483 // representation.
1484 if (auto it = projectionPointees.find(fieldDesc->GetId()); it != projectionPointees.end()) {
1485 for (const auto &projection : it->second) {
1486 for (auto colIdx = 0u; colIdx < extension.fSourceRepr.size(); ++colIdx)
1487 fDestination->AddAliasColumn(mergeData.fDstDescriptor, *projection, firstColumnId + colIdx);
1488 }
1489 }
1490 mappings.push_back(extension);
1491 }
1492 }
1493 }
1494
1495 // handle extra dst fields & common fields
1497 auto res = MergeSourceClusters(*source, columnInfos.fCommonColumns, columnInfos.fExtraDstColumns, mergeData);
1498 if (!res)
1499 return R__FORWARD_ERROR(res);
1500 } // end loop over sources
1501
1502 if (fDestination->GetNEntries() == 0)
1503 R__LOG_WARNING(NTupleMergeLog()) << "Output RNTuple '" << fDestination->GetNTupleName() << "' has no entries.";
1504
1505 // Commit the output
1506 fDestination->CommitClusterGroup();
1507 fDestination->CommitDataset();
1508
1509 return RResult<void>::Success();
1510}
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
double * dst
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:130
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: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.
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.
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:90
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:137
@ kIgnoreCase
Definition TString.h:284
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
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