Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions tree/ntuple/inc/ROOT/RFieldBase.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -530,13 +530,14 @@ protected:
/// Returns a combination of kDiff... flags, indicating peroperties that are different between the field at hand
/// and the given on-disk field
std::uint32_t CompareOnDiskField(const RFieldDescriptor &fieldDesc, std::uint32_t ignoreBits) const;
/// Compares the field to the provieded on-disk field descriptor. Throws an exception if the fields don't match.
/// Compares the field to the corresponding on-disk field information in the provided descriptor.
/// Throws an exception if the fields don't match.
/// Optionally, a set of bits can be provided that should be ignored in the comparison.
RResult<void> EnsureMatchingOnDiskField(const RFieldDescriptor &fieldDesc, std::uint32_t ignoreBits = 0) const;
RResult<void> EnsureMatchingOnDiskField(const RNTupleDescriptor &desc, std::uint32_t ignoreBits = 0) const;
/// Many fields accept a range of type prefixes for schema evolution,
/// e.g. std::unique_ptr< and std::optional< for nullable fields
RResult<void>
EnsureMatchingTypePrefix(const RFieldDescriptor &fieldDesc, const std::vector<std::string> &prefixes) const;
EnsureMatchingTypePrefix(const RNTupleDescriptor &desc, const std::vector<std::string> &prefixes) const;

/// Factory method to resurrect a field from the stored on-disk type information. This overload takes an already
/// normalized type name and type alias.
Expand Down
9 changes: 9 additions & 0 deletions tree/ntuple/inc/ROOT/RFieldUtils.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
class TClass;

namespace ROOT {

class RFieldBase;
class RNTupleDescriptor;

namespace Internal {

/// Applies RNTuple specific type name normalization rules (see specs) that help the string parsing in
Expand Down Expand Up @@ -62,6 +66,11 @@ std::vector<std::string> TokenizeTypeList(std::string_view templateType, std::si
/// however, needs to additionally check for ROOT-specific special cases.
bool IsMatchingFieldType(std::string_view actualTypeName, std::string_view expectedTypeName, const std::type_info &ti);

/// Prints the hierarchy of types with their field names and field IDs for the given in-memory field and the
/// on-disk hierarchy, matching the fields on-disk ID with the information of the descriptor.
/// Useful information when the in-memory field cannot be matched to the the on-disk information.
std::string GetTypeTraceReport(const RFieldBase &field, const RNTupleDescriptor &desc);

} // namespace Internal
} // namespace ROOT

Expand Down
35 changes: 18 additions & 17 deletions tree/ntuple/src/RField.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,18 @@ void ROOT::RCardinalityField::GenerateColumns(const ROOT::RNTupleDescriptor &des

void ROOT::RCardinalityField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
EnsureMatchingOnDiskField(desc, kDiffTypeVersion | kDiffStructure | kDiffTypeName).ThrowOnError();

const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
EnsureMatchingOnDiskField(fieldDesc, kDiffTypeVersion | kDiffStructure | kDiffTypeName).ThrowOnError();
if (fieldDesc.GetStructure() == ENTupleStructure::kPlain) {
if (fieldDesc.GetTypeName().rfind("ROOT::RNTupleCardinality<", 0) != 0) {
throw RException(R__FAIL("RCardinalityField " + GetQualifiedFieldName() +
" expects an on-disk leaf field of the same type"));
" expects an on-disk leaf field of the same type\n" +
Internal::GetTypeTraceReport(*this, desc)));
}
} else if (fieldDesc.GetStructure() != ENTupleStructure::kCollection) {
throw RException(R__FAIL("invalid on-disk structural role for RCardinalityField " + GetQualifiedFieldName()));
throw RException(R__FAIL("invalid on-disk structural role for RCardinalityField " + GetQualifiedFieldName() +
"\n" + Internal::GetTypeTraceReport(*this, desc)));
}
}

Expand All @@ -109,9 +112,9 @@ const ROOT::RField<ROOT::RNTupleCardinality<std::uint64_t>> *ROOT::RCardinalityF
template <typename T>
void ROOT::RSimpleField<T>::ReconcileIntegralField(const RNTupleDescriptor &desc)
{
const RFieldDescriptor &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName);
EnsureMatchingOnDiskField(desc, kDiffTypeName);

const RFieldDescriptor &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
if (fieldDesc.IsCustomEnum(desc)) {
SetOnDiskId(desc.FindFieldId("_0", GetOnDiskId()));
return;
Expand All @@ -123,19 +126,19 @@ void ROOT::RSimpleField<T>::ReconcileIntegralField(const RNTupleDescriptor &desc
if (std::find(std::begin(gIntegralTypeNames), std::end(gIntegralTypeNames), fieldDesc.GetTypeName()) ==
std::end(gIntegralTypeNames)) {
throw RException(R__FAIL("unexpected on-disk type name '" + fieldDesc.GetTypeName() + "' for field of type '" +
GetTypeName() + "'"));
GetTypeName() + "'\n" + Internal::GetTypeTraceReport(*this, desc)));
}
}

template <typename T>
void ROOT::RSimpleField<T>::ReconcileFloatingPointField(const RNTupleDescriptor &desc)
{
const RFieldDescriptor &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName);
EnsureMatchingOnDiskField(desc, kDiffTypeName);

const RFieldDescriptor &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
if (!(fieldDesc.GetTypeName() == "float" || fieldDesc.GetTypeName() == "double")) {
throw RException(R__FAIL("unexpected on-disk type name '" + fieldDesc.GetTypeName() + "' for field of type '" +
GetTypeName() + "'"));
GetTypeName() + "'\n" + Internal::GetTypeTraceReport(*this, desc)));
}
}

Expand Down Expand Up @@ -663,12 +666,12 @@ void ROOT::RRecordField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
// Note that the RPairField and RTupleField descendants have their own reconcilation logic
R__ASSERT(GetTypeName().empty());

const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName | kDiffTypeVersion).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName | kDiffTypeVersion).ThrowOnError();

// The on-disk ID of subfields is matched by field name. So we inherently support reordering of fields
// and we will ignore extra on-disk fields.
// It remains to mark the extra in-memory fields as artificial.
const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
std::unordered_set<std::string_view> onDiskSubfields;
for (const auto &subField : desc.GetFieldIterable(fieldDesc)) {
onDiskSubfields.insert(subField.GetFieldName());
Expand Down Expand Up @@ -878,9 +881,8 @@ void ROOT::RNullableField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
static const std::vector<std::string> prefixes = {"std::optional<", "std::unique_ptr<"};

const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError();
}

ROOT::RNTupleLocalIndex ROOT::RNullableField::GetItemIndex(ROOT::NTupleSize_t globalIndex)
Expand Down Expand Up @@ -1098,9 +1100,8 @@ void ROOT::RAtomicField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
static const std::vector<std::string> prefixes = {"std::atomic<"};

const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError();
}

std::vector<ROOT::RFieldBase::RValue> ROOT::RAtomicField::SplitValue(const RValue &value) const
Expand Down
18 changes: 10 additions & 8 deletions tree/ntuple/src/RFieldBase.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -1015,15 +1015,15 @@ void ROOT::RFieldBase::ConnectPageSource(ROOT::Internal::RPageSource &pageSource

void ROOT::RFieldBase::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
// The default implementation throws an exception if the on-disk ID is set and there are any meaningful differences
// to the on-disk field. Derived classes may overwrite this and relax the checks to support automatic schema
// evolution.
EnsureMatchingOnDiskField(desc.GetFieldDescriptor(fOnDiskId)).ThrowOnError();
// The default implementation throws an exception if there are any meaningful differences to the on-disk field.
// Derived classes may overwrite this and relax the checks to support automatic schema evolution.
EnsureMatchingOnDiskField(desc).ThrowOnError();
}

ROOT::RResult<void>
ROOT::RFieldBase::EnsureMatchingOnDiskField(const RFieldDescriptor &fieldDesc, std::uint32_t ignoreBits) const
ROOT::RFieldBase::EnsureMatchingOnDiskField(const RNTupleDescriptor &desc, std::uint32_t ignoreBits) const
{
const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
const std::uint32_t diffBits = CompareOnDiskField(fieldDesc, ignoreBits);
if (diffBits == 0)
return RResult<void>::Success();
Expand All @@ -1046,17 +1046,19 @@ ROOT::RFieldBase::EnsureMatchingOnDiskField(const RFieldDescriptor &fieldDesc, s
if (diffBits & kDiffNRepetitions) {
errMsg << " repetition count " << GetNRepetitions() << " vs. " << fieldDesc.GetNRepetitions() << ";";
}
return R__FAIL(errMsg.str());
return R__FAIL(errMsg.str() + "\n" + Internal::GetTypeTraceReport(*this, desc));
}

ROOT::RResult<void> ROOT::RFieldBase::EnsureMatchingTypePrefix(const RFieldDescriptor &fieldDesc,
ROOT::RResult<void> ROOT::RFieldBase::EnsureMatchingTypePrefix(const RNTupleDescriptor &desc,
const std::vector<std::string> &prefixes) const
{
const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
for (const auto &p : prefixes) {
if (fieldDesc.GetTypeName().rfind(p, 0) == 0)
return RResult<void>::Success();
}
return R__FAIL("incompatible type " + fieldDesc.GetTypeName() + " for field " + GetQualifiedFieldName());
return R__FAIL("incompatible type " + fieldDesc.GetTypeName() + " for field " + GetQualifiedFieldName() + "\n" +
Internal::GetTypeTraceReport(*this, desc));
}

std::uint32_t ROOT::RFieldBase::CompareOnDiskField(const RFieldDescriptor &fieldDesc, std::uint32_t ignoreBits) const
Expand Down
37 changes: 20 additions & 17 deletions tree/ntuple/src/RFieldMeta.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ std::unique_ptr<ROOT::RFieldBase> ROOT::RClassField::BeforeConnectPageSource(ROO

void ROOT::RClassField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeVersion | kDiffTypeName).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeVersion | kDiffTypeName).ThrowOnError();
}

void ROOT::RClassField::ConstructValue(void *where) const
Expand Down Expand Up @@ -619,7 +619,7 @@ std::unique_ptr<ROOT::RFieldBase> ROOT::REnumField::CloneImpl(std::string_view n
void ROOT::REnumField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
// TODO(jblomer): allow enum to enum conversion only by rename rule
EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName | kDiffTypeVersion).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName | kDiffTypeVersion).ThrowOnError();
}

std::vector<ROOT::RFieldBase::RValue> ROOT::REnumField::SplitValue(const RValue &value) const
Expand Down Expand Up @@ -684,14 +684,15 @@ void ROOT::RPairField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
static const std::vector<std::string> prefixes = {"std::pair<", "std::tuple<"};

const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError();

const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
const auto nOnDiskSubfields = fieldDesc.GetLinkIds().size();
if (nOnDiskSubfields != 2) {
throw ROOT::RException(
R__FAIL("invalid number of on-disk subfields for std::pair " + std::to_string(nOnDiskSubfields)));
throw ROOT::RException(R__FAIL("invalid number of on-disk subfields for std::pair " +
std::to_string(nOnDiskSubfields) + "\n" +
Internal::GetTypeTraceReport(*this, desc)));
}
}

Expand Down Expand Up @@ -830,7 +831,7 @@ void ROOT::RProxiedCollectionField::GenerateColumns(const ROOT::RNTupleDescripto

void ROOT::RProxiedCollectionField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError();
}

void ROOT::RProxiedCollectionField::ConstructValue(void *where) const
Expand Down Expand Up @@ -997,7 +998,7 @@ std::unique_ptr<ROOT::RFieldBase> ROOT::RStreamerField::BeforeConnectPageSource(

void ROOT::RStreamerField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName | kDiffTypeVersion).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName | kDiffTypeVersion).ThrowOnError();
}

void ROOT::RStreamerField::ConstructValue(void *where) const
Expand Down Expand Up @@ -1231,15 +1232,16 @@ void ROOT::RTupleField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
static const std::vector<std::string> prefixes = {"std::pair<", "std::tuple<"};

const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError();

const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
const auto nOnDiskSubfields = fieldDesc.GetLinkIds().size();
const auto nSubfields = fSubfields.size();
if (nOnDiskSubfields != nSubfields) {
throw ROOT::RException(R__FAIL("invalid number of on-disk subfields for std::tuple " +
std::to_string(nOnDiskSubfields) + " vs. " + std::to_string(nSubfields)));
std::to_string(nOnDiskSubfields) + " vs. " + std::to_string(nSubfields) + "\n" +
Internal::GetTypeTraceReport(*this, desc)));
}
}

Expand Down Expand Up @@ -1393,12 +1395,13 @@ void ROOT::RVariantField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
static const std::vector<std::string> prefixes = {"std::variant<"};

const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError();

const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
if (fSubfields.size() != fieldDesc.GetLinkIds().size()) {
throw RException(R__FAIL("number of variants on-disk do not match for " + GetQualifiedFieldName()));
throw RException(R__FAIL("number of variants on-disk do not match for " + GetQualifiedFieldName() + "\n" +
Internal::GetTypeTraceReport(*this, desc)));
}
}

Expand Down
18 changes: 9 additions & 9 deletions tree/ntuple/src/RFieldSequenceContainer.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,8 @@ void ROOT::RArrayField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
static const std::vector<std::string> prefixes = {"std::array<"};

const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(fieldDesc, prefixes).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError();
EnsureMatchingTypePrefix(desc, prefixes).ThrowOnError();
}

void ROOT::RArrayField::ConstructValue(void *where) const
Expand Down Expand Up @@ -462,7 +461,7 @@ std::unique_ptr<ROOT::RFieldBase> ROOT::RRVecField::BeforeConnectPageSource(Inte

void ROOT::RRVecField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError();
}

void ROOT::RRVecField::ConstructValue(void *where) const
Expand Down Expand Up @@ -644,7 +643,7 @@ void ROOT::RVectorField::GenerateColumns(const ROOT::RNTupleDescriptor &desc)

void ROOT::RVectorField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError();
}

void ROOT::RVectorField::RVectorDeleter::operator()(void *objPtr, bool dtorOnly)
Expand Down Expand Up @@ -748,7 +747,7 @@ void ROOT::RField<std::vector<bool>>::GenerateColumns(const ROOT::RNTupleDescrip

void ROOT::RField<std::vector<bool>>::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
EnsureMatchingOnDiskField(desc.GetFieldDescriptor(GetOnDiskId()), kDiffTypeName).ThrowOnError();
EnsureMatchingOnDiskField(desc, kDiffTypeName).ThrowOnError();
}

std::vector<ROOT::RFieldBase::RValue> ROOT::RField<std::vector<bool>>::SplitValue(const RValue &value) const
Expand Down Expand Up @@ -842,11 +841,12 @@ void ROOT::RArrayAsRVecField::ReadInClusterImpl(RNTupleLocalIndex localIndex, vo

void ROOT::RArrayAsRVecField::ReconcileOnDiskField(const RNTupleDescriptor &desc)
{
const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
EnsureMatchingOnDiskField(fieldDesc, kDiffTypeName | kDiffTypeVersion | kDiffStructure | kDiffNRepetitions)
EnsureMatchingOnDiskField(desc, kDiffTypeName | kDiffTypeVersion | kDiffStructure | kDiffNRepetitions)
.ThrowOnError();
const auto &fieldDesc = desc.GetFieldDescriptor(GetOnDiskId());
if (fieldDesc.GetTypeName().rfind("std::array<", 0) != 0) {
throw RException(R__FAIL("RArrayAsRVecField " + GetQualifiedFieldName() + " expects an on-disk array field"));
throw RException(R__FAIL("RArrayAsRVecField " + GetQualifiedFieldName() + " expects an on-disk array field\n" +
Internal::GetTypeTraceReport(*this, desc)));
}
}

Expand Down
39 changes: 39 additions & 0 deletions tree/ntuple/src/RFieldUtils.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <ROOT/RField.hxx>
#include <ROOT/RLogger.hxx>
#include <ROOT/RNTupleDescriptor.hxx>
#include <ROOT/RNTupleTypes.hxx>
#include <ROOT/RNTupleUtils.hxx>

Expand Down Expand Up @@ -722,3 +723,41 @@ bool ROOT::Internal::IsMatchingFieldType(std::string_view actualTypeName, std::s
// Thus, we check again using first ROOT Meta normalization followed by RNTuple re-normalization.
return (actualTypeName == ROOT::Internal::GetRenormalizedTypeName(ROOT::Internal::GetDemangledTypeName(ti)));
}

std::string ROOT::Internal::GetTypeTraceReport(const RFieldBase &field, const RNTupleDescriptor &desc)
{
std::vector<const RFieldBase *> inMemoryStack;
std::vector<const RFieldDescriptor *> onDiskStack;

auto fnGetLine = [](const std::string &fieldName, const std::string &fieldType, DescriptorId_t fieldId,
int level) -> std::string {
return std::string(2 * level, ' ') + fieldName + " [" + fieldType + "] (id: " + std::to_string(fieldId) + ")\n";
};

const RFieldBase *fieldPtr = &field;
while (fieldPtr->GetParent()) {
inMemoryStack.push_back(fieldPtr);
fieldPtr = fieldPtr->GetParent();
}

auto fieldId = field.GetOnDiskId();
while (fieldId != kInvalidDescriptorId && fieldId != desc.GetFieldZeroId()) {
const auto &fieldDesc = desc.GetFieldDescriptor(fieldId);
onDiskStack.push_back(&fieldDesc);
fieldId = fieldDesc.GetParentId();
}

std::string report = "In-memory field/type hierarchy:\n";
int indentLevel = 0;
for (auto itr = inMemoryStack.rbegin(); itr != inMemoryStack.rend(); ++itr, ++indentLevel) {
report += fnGetLine((*itr)->GetFieldName(), (*itr)->GetTypeName(), (*itr)->GetOnDiskId(), indentLevel);
}

report += "On-disk field/type hierarchy:\n";
indentLevel = 0;
for (auto itr = onDiskStack.rbegin(); itr != onDiskStack.rend(); ++itr, ++indentLevel) {
report += fnGetLine((*itr)->GetFieldName(), (*itr)->GetTypeName(), (*itr)->GetId(), indentLevel);
}

return report;
}
Loading
Loading