diff --git a/ydb/core/sys_view/auth/auth_scan_base.h b/ydb/core/sys_view/auth/auth_scan_base.h index 4a1ef6b25bf4..7b8c2d0b2146 100644 --- a/ydb/core/sys_view/auth/auth_scan_base.h +++ b/ydb/core/sys_view/auth/auth_scan_base.h @@ -14,9 +14,15 @@ using namespace NSchemeShard; using namespace NActors; using namespace NSchemeCache; using TNavigate = NSchemeCache::TSchemeCacheNavigate; +using TPath = TVector; template class TAuthScanBase : public TScanActorBase { + struct TTraversingChildren { + TNavigate::TEntry Entry; + size_t Index = 0; + }; + public: using TBase = TScanActorBase; @@ -47,16 +53,7 @@ class TAuthScanBase : public TScanActorBase { protected: void ProceedToScan() override { TBase::Become(&TAuthScanBase::StateScan); - if (TBase::AckReceived) { - StartScan(); - } - } - void Handle(NKqp::TEvKqpCompute::TEvScanDataAck::TPtr&) { - StartScan(); - } - - void StartScan() { // TODO: support TableRange filter if (auto cellsFrom = TBase::TableRange.From.GetCells(); cellsFrom.size() > 0 && !cellsFrom[0].IsNull()) { TBase::ReplyErrorAndDie(Ydb::StatusIds::INTERNAL_ERROR, TStringBuilder() << "TableRange.From filter is not supported"); @@ -67,18 +64,58 @@ class TAuthScanBase : public TScanActorBase { return; } - NavigatePath(TBase::TenantName); + auto& last = DeepFirstSearchStack.emplace_back(); + last.Index = Max(); // tenant root + + if (TBase::AckReceived) { + ContinueScan(); + } + } + + void Handle(NKqp::TEvKqpCompute::TEvScanDataAck::TPtr&) { + ContinueScan(); + } + + void ContinueScan() { + while (DeepFirstSearchStack) { + auto& last = DeepFirstSearchStack.back(); + + if (last.Index == Max()) { // tenant root + NavigatePath(SplitPath(TBase::TenantName)); + DeepFirstSearchStack.pop_back(); + return; + } + + auto& children = last.Entry.ListNodeEntry->Children; + if (last.Index < children.size()) { + auto& child = children.at(last.Index++); + + if (child.Kind == TSchemeCacheNavigate::KindExtSubdomain || child.Kind == TSchemeCacheNavigate::KindSubdomain) { + continue; + } + + last.Entry.Path.push_back(child.Name); + NavigatePath(last.Entry.Path); + last.Entry.Path.pop_back(); + return; + } else { + DeepFirstSearchStack.pop_back(); + } + } + + TBase::ReplyEmptyAndDie(); } void Handle(TEvTxProxySchemeCache::TEvNavigateKeySetResult::TPtr& ev, const TActorContext& ctx) { THolder request(ev->Get()->Request.Release()); + + Y_ABORT_UNLESS(request->ResultSet.size() == 1); + auto& entry = request->ResultSet.back(); - for (const auto& entry : request->ResultSet) { - if (entry.Status != TNavigate::EStatus::Ok) { - TBase::ReplyErrorAndDie(Ydb::StatusIds::INTERNAL_ERROR, TStringBuilder() << - "Failed to navigate " << CanonizePath(entry.Path) << ": " << entry.Status); - return; - } + if (entry.Status != TNavigate::EStatus::Ok) { + TBase::ReplyErrorAndDie(Ydb::StatusIds::INTERNAL_ERROR, TStringBuilder() << + "Failed to navigate " << CanonizePath(entry.Path) << ": " << entry.Status); + return; } LOG_TRACE_S(ctx, NKikimrServices::SYSTEM_VIEWS, @@ -86,7 +123,13 @@ class TAuthScanBase : public TScanActorBase { auto batch = MakeHolder(TBase::ScanId); - FillBatch(*batch, request->ResultSet); + FillBatch(*batch, entry); + + if (!batch->Finished && entry.ListNodeEntry) { + DeepFirstSearchStack.emplace_back(std::move(entry)); + } + + batch->Finished = DeepFirstSearchStack.empty(); TBase::SendBatch(std::move(batch)); } @@ -99,23 +142,25 @@ class TAuthScanBase : public TScanActorBase { TBase::PassAway(); } - void NavigatePath(TString path) { + void NavigatePath(TPath path) { auto request = MakeHolder(); auto& entry = request->ResultSet.emplace_back(); entry.RequestType = TSchemeCacheNavigate::TEntry::ERequestType::ByPath; - entry.Path = SplitPath(path); - entry.Operation = TSchemeCacheNavigate::OpPath; + entry.Path = std::move(path); + entry.Operation = TSchemeCacheNavigate::OpList; entry.RedirectRequired = false; LOG_TRACE_S(TlsActivationContext->AsActorContext(), NKikimrServices::SYSTEM_VIEWS, - "Navigate " << path << ": " << request->ToString(*AppData()->TypeRegistry)); + "Navigate " << request->ToString(*AppData()->TypeRegistry)); TBase::Send(MakeSchemeCacheID(), new TEvTxProxySchemeCache::TEvNavigateKeySet(request.Release())); } - virtual void FillBatch(NKqp::TEvKqpCompute::TEvScanData& batch, const TNavigate::TResultSet& resultSet) = 0; + virtual void FillBatch(NKqp::TEvKqpCompute::TEvScanData& batch, const TNavigate::TEntry& entry) = 0; +private: + TVector DeepFirstSearchStack; }; } diff --git a/ydb/core/sys_view/auth/group_members.cpp b/ydb/core/sys_view/auth/group_members.cpp index 0d9a8cdf29eb..2096944cf48b 100644 --- a/ydb/core/sys_view/auth/group_members.cpp +++ b/ydb/core/sys_view/auth/group_members.cpp @@ -26,9 +26,7 @@ class TGroupMembersScan : public TAuthScanBase { } protected: - void FillBatch(NKqp::TEvKqpCompute::TEvScanData& batch, const TNavigate::TResultSet& resultSet) override { - Y_ABORT_UNLESS(resultSet.size() == 1); - auto& entry = resultSet.back(); + void FillBatch(NKqp::TEvKqpCompute::TEvScanData& batch, const TNavigate::TEntry& entry) override { Y_ABORT_UNLESS(entry.Status == TNavigate::EStatus::Ok); Y_ABORT_UNLESS(CanonizePath(entry.Path) == TBase::TenantName); diff --git a/ydb/core/sys_view/auth/groups.cpp b/ydb/core/sys_view/auth/groups.cpp index 805318a41b6b..34b5a938f9f8 100644 --- a/ydb/core/sys_view/auth/groups.cpp +++ b/ydb/core/sys_view/auth/groups.cpp @@ -26,9 +26,7 @@ class TGroupsScan : public TAuthScanBase { } protected: - void FillBatch(NKqp::TEvKqpCompute::TEvScanData& batch, const TNavigate::TResultSet& resultSet) override { - Y_ABORT_UNLESS(resultSet.size() == 1); - auto& entry = resultSet.back(); + void FillBatch(NKqp::TEvKqpCompute::TEvScanData& batch, const TNavigate::TEntry& entry) override { Y_ABORT_UNLESS(entry.Status == TNavigate::EStatus::Ok); Y_ABORT_UNLESS(CanonizePath(entry.Path) == TBase::TenantName); diff --git a/ydb/core/sys_view/auth/owners.cpp b/ydb/core/sys_view/auth/owners.cpp new file mode 100644 index 000000000000..f58343a9f55f --- /dev/null +++ b/ydb/core/sys_view/auth/owners.cpp @@ -0,0 +1,66 @@ +#include "auth_scan_base.h" +#include "owners.h" + +#include +#include +#include +#include +#include + +#include + +namespace NKikimr::NSysView::NAuth { + +using namespace NSchemeShard; +using namespace NActors; + +class TOwnersScan : public TAuthScanBase { +public: + using TScanBase = TScanActorBase; + using TAuthBase = TAuthScanBase; + + TOwnersScan(const NActors::TActorId& ownerId, ui32 scanId, const TTableId& tableId, + const TTableRange& tableRange, const TArrayRef& columns) + : TAuthBase(ownerId, scanId, tableId, tableRange, columns) + { + } + +protected: + void FillBatch(NKqp::TEvKqpCompute::TEvScanData& batch, const TNavigate::TEntry& entry) override { + Y_ABORT_UNLESS(entry.Status == TNavigate::EStatus::Ok); + + TVector cells(::Reserve(Columns.size())); + + // TODO: add rows according to request's sender user rights + + auto entryPath = CanonizePath(entry.Path); + auto entryOwner = entry.Self->Info.GetOwner(); + + for (auto& column : Columns) { + switch (column.Tag) { + case Schema::AuthOwners::Path::ColumnId: + cells.push_back(TCell(entryPath.data(), entryPath.size())); + break; + case Schema::AuthOwners::Sid::ColumnId: + cells.push_back(TCell(entryOwner.data(), entryOwner.size())); + break; + default: + cells.emplace_back(); + } + } + + TArrayRef ref(cells); + batch.Rows.emplace_back(TOwnedCellVec::Make(ref)); + cells.clear(); + + batch.Finished = false; + } +}; + +THolder CreateOwnersScan(const NActors::TActorId& ownerId, ui32 scanId, const TTableId& tableId, + const TTableRange& tableRange, const TArrayRef& columns) +{ + return MakeHolder(ownerId, scanId, tableId, tableRange, columns); +} + +} diff --git a/ydb/core/sys_view/auth/owners.h b/ydb/core/sys_view/auth/owners.h new file mode 100644 index 000000000000..b33ed96f843c --- /dev/null +++ b/ydb/core/sys_view/auth/owners.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#include +#include + +namespace NKikimr::NSysView::NAuth { + +THolder CreateOwnersScan(const NActors::TActorId& ownerId, ui32 scanId, const TTableId& tableId, + const TTableRange& tableRange, const TArrayRef& columns); + +} diff --git a/ydb/core/sys_view/auth/users.cpp b/ydb/core/sys_view/auth/users.cpp index 339b07da7239..f76e19386762 100644 --- a/ydb/core/sys_view/auth/users.cpp +++ b/ydb/core/sys_view/auth/users.cpp @@ -26,9 +26,7 @@ class TUsersScan : public TAuthScanBase { } protected: - void FillBatch(NKqp::TEvKqpCompute::TEvScanData& batch, const TNavigate::TResultSet& resultSet) override { - Y_ABORT_UNLESS(resultSet.size() == 1); - auto& entry = resultSet.back(); + void FillBatch(NKqp::TEvKqpCompute::TEvScanData& batch, const TNavigate::TEntry& entry) override { Y_ABORT_UNLESS(entry.Status == TNavigate::EStatus::Ok); Y_ABORT_UNLESS(CanonizePath(entry.Path) == TBase::TenantName); diff --git a/ydb/core/sys_view/auth/ya.make b/ydb/core/sys_view/auth/ya.make index 72ef7d11f20e..add0ed7c38d4 100644 --- a/ydb/core/sys_view/auth/ya.make +++ b/ydb/core/sys_view/auth/ya.make @@ -1,12 +1,14 @@ LIBRARY() SRCS( - group_members.h group_members.cpp - users.h - users.cpp - groups.h + group_members.h groups.cpp + groups.h + owners.cpp + owners.h + users.cpp + users.h ) PEERDIR( diff --git a/ydb/core/sys_view/common/schema.cpp b/ydb/core/sys_view/common/schema.cpp index 6355ec528e5b..e73cbbcb55bf 100644 --- a/ydb/core/sys_view/common/schema.cpp +++ b/ydb/core/sys_view/common/schema.cpp @@ -292,6 +292,7 @@ class TSystemViewResolver : public ISystemViewResolver { RegisterSystemView(UsersName); RegisterSystemView(NAuth::GroupsName); RegisterSystemView(GroupMembersName); + RegisterSystemView(OwnersName); } } diff --git a/ydb/core/sys_view/common/schema.h b/ydb/core/sys_view/common/schema.h index e6592f66be0d..8f63993e2ba2 100644 --- a/ydb/core/sys_view/common/schema.h +++ b/ydb/core/sys_view/common/schema.h @@ -53,6 +53,7 @@ namespace NAuth { constexpr TStringBuf UsersName = "auth_users"; constexpr TStringBuf GroupsName = "auth_groups"; constexpr TStringBuf GroupMembersName = "auth_group_members"; + constexpr TStringBuf OwnersName = "auth_owners"; } @@ -646,6 +647,17 @@ struct Schema : NIceDb::Schema { >; }; + struct AuthOwners : Table<18> { + struct Path: Column<1, NScheme::NTypeIds::Utf8> {}; + struct Sid: Column<2, NScheme::NTypeIds::Utf8> {}; + + using TKey = TableKey; + using TColumns = TableColumns< + Path, + Sid + >; + }; + struct PgColumn { NIceDb::TColumnId _ColumnId; NScheme::TTypeInfo _ColumnTypeInfo; diff --git a/ydb/core/sys_view/scan.cpp b/ydb/core/sys_view/scan.cpp index 763689b4c98c..72db35ad0618 100644 --- a/ydb/core/sys_view/scan.cpp +++ b/ydb/core/sys_view/scan.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -252,6 +253,9 @@ THolder CreateSystemViewScan( if (tableId.SysViewInfo == GroupMembersName) { return NAuth::CreateGroupMembersScan(ownerId, scanId, tableId, tableRange, columns); } + if (tableId.SysViewInfo == OwnersName) { + return NAuth::CreateOwnersScan(ownerId, scanId, tableId, tableRange, columns); + } } return {}; diff --git a/ydb/core/sys_view/ut_kqp.cpp b/ydb/core/sys_view/ut_kqp.cpp index aa5ba9ff0e87..e5df06a795f3 100644 --- a/ydb/core/sys_view/ut_kqp.cpp +++ b/ydb/core/sys_view/ut_kqp.cpp @@ -1730,7 +1730,7 @@ Y_UNIT_TEST_SUITE(SystemView) { UNIT_ASSERT_VALUES_EQUAL(entry.Type, ESchemeEntryType::Directory); auto children = result.GetChildren(); - UNIT_ASSERT_VALUES_EQUAL(children.size(), 26); + UNIT_ASSERT_VALUES_EQUAL(children.size(), 27); THashSet names; for (const auto& child : children) { @@ -1748,7 +1748,7 @@ Y_UNIT_TEST_SUITE(SystemView) { UNIT_ASSERT_VALUES_EQUAL(entry.Type, ESchemeEntryType::Directory); auto children = result.GetChildren(); - UNIT_ASSERT_VALUES_EQUAL(children.size(), 20); + UNIT_ASSERT_VALUES_EQUAL(children.size(), 21); THashSet names; for (const auto& child : children) { @@ -2326,6 +2326,96 @@ Y_UNIT_TEST_SUITE(SystemView) { NKqp::CompareYson(expected, NKqp::StreamResultToYson(it)); } } + + Y_UNIT_TEST(AuthOwners) { + TTestEnv env; + env.GetServer().GetRuntime()->SetLogPriority(NKikimrServices::FLAT_TX_SCHEMESHARD, NLog::PRI_DEBUG); + env.GetServer().GetRuntime()->SetLogPriority(NKikimrServices::SYSTEM_VIEWS, NLog::PRI_TRACE); + CreateTenantsAndTables(env, true); + TTableClient client(env.GetDriver()); + + env.GetClient().CreateUser("/Root", "user1", "password1"); + env.GetClient().CreateUser("/Root/Tenant1", "user2", "password2"); + env.GetClient().CreateUser("/Root/Tenant2", "user3", "password3"); + env.GetClient().CreateUser("/Root/Tenant2", "user4", "password4"); + env.GetClient().CreateGroup("/Root/Tenant2", "group1"); + + env.GetClient().MkDir("/Root", "Dir1/SubDir1"); + env.GetClient().ModifyOwner("/Root", "Dir1", "user1"); + env.GetClient().ModifyOwner("/Root/Dir1", "SubDir1", "user1"); + + env.GetClient().MkDir("/Root/Tenant1", "Dir2/SubDir2"); + env.GetClient().ModifyOwner("/Root/Tenant1", "Dir2", "user2"); + env.GetClient().ModifyOwner("/Root/Tenant1/Dir2", "SubDir2", "user2"); + + env.GetClient().MkDir("/Root/Tenant2", "Dir3/SubDir33"); + env.GetClient().MkDir("/Root/Tenant2", "Dir3/SubDir34"); + env.GetClient().MkDir("/Root/Tenant2", "Dir4/SubDir45"); + env.GetClient().MkDir("/Root/Tenant2", "Dir4/SubDir46"); + env.GetClient().ModifyOwner("/Root/Tenant2", "Dir3", "user3"); + env.GetClient().ModifyOwner("/Root/Tenant2", "Dir4", "user4"); + env.GetClient().ModifyOwner("/Root/Tenant2/Dir3", "SubDir33", "group1"); + env.GetClient().ModifyOwner("/Root/Tenant2/Dir4", "SubDir46", "user4"); + + // Cerr << env.GetClient().Describe(env.GetServer().GetRuntime(), "/Root").DebugString() << Endl; + // Cerr << env.GetClient().Describe(env.GetServer().GetRuntime(), "/Root/Tenant2").DebugString() << Endl; + + { + auto it = client.StreamExecuteScanQuery(R"( + SELECT * + FROM `Root/.sys/auth_owners` + )").GetValueSync(); + + auto expected = R"([ + [["/Root"];["root@builtin"]]; + [["/Root/.metadata"];["metadata@system"]]; + [["/Root/.metadata/workload_manager"];["metadata@system"]]; + [["/Root/.metadata/workload_manager/pools"];["metadata@system"]]; + [["/Root/.metadata/workload_manager/pools/default"];["metadata@system"]]; + [["/Root/Dir1"];["user1"]]; + [["/Root/Dir1/SubDir1"];["user1"]]; + [["/Root/Table0"];["root@builtin"]]; + ])"; + + NKqp::CompareYson(expected, NKqp::StreamResultToYson(it)); + } + + { + auto it = client.StreamExecuteScanQuery(R"( + SELECT * + FROM `Root/Tenant1/.sys/auth_owners` + )").GetValueSync(); + + auto expected = R"([ + [["/Root/Tenant1"];["root@builtin"]]; + [["/Root/Tenant1/Dir2"];["user2"]]; + [["/Root/Tenant1/Dir2/SubDir2"];["user2"]]; + [["/Root/Tenant1/Table1"];["root@builtin"]]; + ])"; + + NKqp::CompareYson(expected, NKqp::StreamResultToYson(it)); + } + + { + auto it = client.StreamExecuteScanQuery(R"( + SELECT * + FROM `Root/Tenant2/.sys/auth_owners` + )").GetValueSync(); + + auto expected = R"([ + [["/Root/Tenant2"];["root@builtin"]]; + [["/Root/Tenant2/Dir3"];["user3"]]; + [["/Root/Tenant2/Dir3/SubDir33"];["group1"]]; + [["/Root/Tenant2/Dir3/SubDir34"];["root@builtin"]]; + [["/Root/Tenant2/Dir4"];["user4"]]; + [["/Root/Tenant2/Dir4/SubDir45"];["root@builtin"]]; + [["/Root/Tenant2/Dir4/SubDir46"];["user4"]]; + [["/Root/Tenant2/Table2"];["root@builtin"]]; + ])"; + + NKqp::CompareYson(expected, NKqp::StreamResultToYson(it)); + } + } } } // NSysView diff --git a/ydb/core/sys_view/ut_large.cpp b/ydb/core/sys_view/ut_large.cpp new file mode 100644 index 000000000000..f6391dafdafa --- /dev/null +++ b/ydb/core/sys_view/ut_large.cpp @@ -0,0 +1,91 @@ +#include "ut_common.h" + +#include + +#include +#include +#include + +#include + +#include + +namespace NKikimr { +namespace NSysView { + +using namespace NYdb; +using namespace NYdb::NTable; +using namespace NYdb::NScheme; + +struct TLogStopwatch { + TLogStopwatch(TString message) + : Message(std::move(message)) + , Started(TAppData::TimeProvider->Now()) + {} + + ~TLogStopwatch() { + Cerr << "[STOPWATCH] " << Message << " in " << (TAppData::TimeProvider->Now() - Started).MilliSeconds() << "ms" << Endl; + } + +private: + TString Message; + TInstant Started; +}; + +Y_UNIT_TEST_SUITE(SystemViewLarge) { + + Y_UNIT_TEST(AuthOwners) { + TTestEnv env; + env.GetServer().GetRuntime()->SetLogPriority(NKikimrServices::FLAT_TX_SCHEMESHARD, NLog::PRI_DEBUG); + env.GetServer().GetRuntime()->SetLogPriority(NKikimrServices::SYSTEM_VIEWS, NLog::PRI_TRACE); + env.GetServer().GetRuntime()->SetDispatchedEventsLimit(100'000'000'000); + + TTableClient client(env.GetDriver()); + + const size_t pathsToCreate = 10'000 - 100; + + { + TLogStopwatch stopwatch(TStringBuilder() << "Created " << pathsToCreate << " paths"); + + THashSet paths; + paths.emplace("Root"); + + // creating a random directories tree: + while (paths.size() < pathsToCreate) { + TString path = "/Root"; + ui32 index = RandomNumber(); + for (ui32 depth : xrange(15)) { + Y_UNUSED(depth); + TString dir = "Dir" + std::to_string(index % 3); + index /= 3; + if (paths.size() < pathsToCreate && paths.emplace(path + "/" + dir).second) { + env.GetClient().MkDir(path, dir); + } + path += "/" + dir; + } + } + } + + Cerr << env.GetClient().Describe(env.GetServer().GetRuntime(), "/Root").DebugString() << Endl; + + { + const size_t expectedCount = pathsToCreate + 4; + + TLogStopwatch stopwatch(TStringBuilder() << "Selected " << expectedCount << " rows from .sys/auth_owners"); + + auto it = client.StreamExecuteScanQuery(R"( + SELECT COUNT (*) + FROM `Root/.sys/auth_owners` + )").GetValueSync(); + + auto expected = Sprintf(R"([ + [%du]; + ])", expectedCount); + + NKqp::CompareYson(expected, NKqp::StreamResultToYson(it)); + } + } +} + +} // NSysView +} // NKikimr diff --git a/ydb/core/sys_view/ut_large/ya.make b/ydb/core/sys_view/ut_large/ya.make new file mode 100644 index 000000000000..72fcdc91e289 --- /dev/null +++ b/ydb/core/sys_view/ut_large/ya.make @@ -0,0 +1,22 @@ +UNITTEST_FOR(ydb/core/sys_view) + +FORK_SUBTESTS() + +SIZE(LARGE) +TAG(ya:fat) + +PEERDIR( + library/cpp/testing/unittest + ydb/core/kqp/ut/common + ydb/core/persqueue/ut/common + ydb/core/testlib/pg +) + +YQL_LAST_ABI_VERSION() + +SRCS( + ut_large.cpp + ut_common.cpp +) + +END() diff --git a/ydb/core/sys_view/ya.make b/ydb/core/sys_view/ya.make index 74bfd9e155c4..11257ba7a4c6 100644 --- a/ydb/core/sys_view/ya.make +++ b/ydb/core/sys_view/ya.make @@ -39,4 +39,5 @@ RECURSE( RECURSE_FOR_TESTS( ut + ut_large ) diff --git a/ydb/core/tx/scheme_cache/scheme_cache.cpp b/ydb/core/tx/scheme_cache/scheme_cache.cpp index 8e5a3a367cea..9722dc953266 100644 --- a/ydb/core/tx/scheme_cache/scheme_cache.cpp +++ b/ydb/core/tx/scheme_cache/scheme_cache.cpp @@ -75,7 +75,7 @@ TString TDomainInfo::TGroup::ToString() const { } TString TSchemeCacheNavigate::TEntry::ToString() const { - return TStringBuilder() << "{" + auto out = TStringBuilder() << "{" << " Path: " << JoinPath(Path) << " TableId: " << TableId << " RequestType: " << RequestType @@ -85,8 +85,22 @@ TString TSchemeCacheNavigate::TEntry::ToString() const { << " SyncVersion: " << (SyncVersion ? "true" : "false") << " Status: " << Status << " Kind: " << Kind - << " DomainInfo " << (DomainInfo ? DomainInfo->ToString() : "") - << " }"; + << " DomainInfo " << (DomainInfo ? DomainInfo->ToString() : ""); + + if (ListNodeEntry) { + out << " Children ["; + for (ui32 i = 0; i < ListNodeEntry->Children.size(); ++i) { + if (i) { + out << ","; + } + + out << ListNodeEntry->Children.at(i).Name; + } + out << "]"; + } + + out << " }"; + return out; } TString TSchemeCacheNavigate::TEntry::ToString(const NScheme::TTypeRegistry& typeRegistry) const {