diff --git a/ydb/core/viewer/json_handlers_viewer.cpp b/ydb/core/viewer/json_handlers_viewer.cpp index 5f238798756c..e40505208209 100644 --- a/ydb/core/viewer/json_handlers_viewer.cpp +++ b/ydb/core/viewer/json_handlers_viewer.cpp @@ -259,7 +259,7 @@ void InitViewerRenderJsonHandler(TJsonHandlers& handlers) { } void InitViewerAutocompleteJsonHandler(TJsonHandlers& jsonHandlers) { - jsonHandlers.AddHandler("/viewer/autocomplete", new TJsonHandler(TJsonAutocomplete::GetSwagger())); + jsonHandlers.AddHandler("/viewer/autocomplete", new TJsonHandler(TJsonAutocomplete::GetSwagger()), 2); } void InitViewerCheckAccessJsonHandler(TJsonHandlers& jsonHandlers) { diff --git a/ydb/core/viewer/json_pipe_req.cpp b/ydb/core/viewer/json_pipe_req.cpp index fd37faf2f4de..804e7f37c0e7 100644 --- a/ydb/core/viewer/json_pipe_req.cpp +++ b/ydb/core/viewer/json_pipe_req.cpp @@ -122,37 +122,45 @@ TString TViewerPipeClient::GetPath(TEvTxProxySchemeCache::TEvNavigateKeySetResul } bool TViewerPipeClient::IsSuccess(const std::unique_ptr& ev) { - return (ev->Request->ResultSet.size() == 1) && (ev->Request->ResultSet.begin()->Status == NSchemeCache::TSchemeCacheNavigate::EStatus::Ok); + return (ev->Request->ResultSet.size() > 0) && (std::find_if(ev->Request->ResultSet.begin(), ev->Request->ResultSet.end(), + [](const auto& entry) { + return entry.Status == NSchemeCache::TSchemeCacheNavigate::EStatus::Ok; + }) != ev->Request->ResultSet.end()); } TString TViewerPipeClient::GetError(const std::unique_ptr& ev) { if (ev->Request->ResultSet.size() == 0) { return "empty response"; } - switch (ev->Request->ResultSet.begin()->Status) { - case NSchemeCache::TSchemeCacheNavigate::EStatus::Ok: - return "Ok"; - case NSchemeCache::TSchemeCacheNavigate::EStatus::Unknown: - return "Unknown"; - case NSchemeCache::TSchemeCacheNavigate::EStatus::RootUnknown: - return "RootUnknown"; - case NSchemeCache::TSchemeCacheNavigate::EStatus::PathErrorUnknown: - return "PathErrorUnknown"; - case NSchemeCache::TSchemeCacheNavigate::EStatus::PathNotTable: - return "PathNotTable"; - case NSchemeCache::TSchemeCacheNavigate::EStatus::PathNotPath: - return "PathNotPath"; - case NSchemeCache::TSchemeCacheNavigate::EStatus::TableCreationNotComplete: - return "TableCreationNotComplete"; - case NSchemeCache::TSchemeCacheNavigate::EStatus::LookupError: - return "LookupError"; - case NSchemeCache::TSchemeCacheNavigate::EStatus::RedirectLookupError: - return "RedirectLookupError"; - case NSchemeCache::TSchemeCacheNavigate::EStatus::AccessDenied: - return "AccessDenied"; - default: - return ::ToString(static_cast(ev->Request->ResultSet.begin()->Status)); + for (const auto& entry : ev->Request->ResultSet) { + if (entry.Status != NSchemeCache::TSchemeCacheNavigate::EStatus::Ok) { + switch (entry.Status) { + case NSchemeCache::TSchemeCacheNavigate::EStatus::Ok: + return "Ok"; + case NSchemeCache::TSchemeCacheNavigate::EStatus::Unknown: + return "Unknown"; + case NSchemeCache::TSchemeCacheNavigate::EStatus::RootUnknown: + return "RootUnknown"; + case NSchemeCache::TSchemeCacheNavigate::EStatus::PathErrorUnknown: + return "PathErrorUnknown"; + case NSchemeCache::TSchemeCacheNavigate::EStatus::PathNotTable: + return "PathNotTable"; + case NSchemeCache::TSchemeCacheNavigate::EStatus::PathNotPath: + return "PathNotPath"; + case NSchemeCache::TSchemeCacheNavigate::EStatus::TableCreationNotComplete: + return "TableCreationNotComplete"; + case NSchemeCache::TSchemeCacheNavigate::EStatus::LookupError: + return "LookupError"; + case NSchemeCache::TSchemeCacheNavigate::EStatus::RedirectLookupError: + return "RedirectLookupError"; + case NSchemeCache::TSchemeCacheNavigate::EStatus::AccessDenied: + return "AccessDenied"; + default: + return ::ToString(static_cast(ev->Request->ResultSet.begin()->Status)); + } + } } + return "no error"; } bool TViewerPipeClient::IsSuccess(const std::unique_ptr& ev) { diff --git a/ydb/core/viewer/protos/viewer.proto b/ydb/core/viewer/protos/viewer.proto index 15ee7d118e83..7cc76fd779ff 100644 --- a/ydb/core/viewer/protos/viewer.proto +++ b/ydb/core/viewer/protos/viewer.proto @@ -729,13 +729,14 @@ message TQueryAutocomplete { EAutocompleteType Type = 2; string Parent = 3; } - uint32 Total = 1; + optional uint32 Total = 1; repeated TEntity Entities = 2; } bool Success = 1; TResult Result = 2; repeated string Error = 3; + uint32 Version = 4; } message TPDiskInfoWhiteboard { diff --git a/ydb/core/viewer/query_autocomplete_helper.h b/ydb/core/viewer/query_autocomplete_helper.h index 9096a0f4261e..b9da60290800 100644 --- a/ydb/core/viewer/query_autocomplete_helper.h +++ b/ydb/core/viewer/query_autocomplete_helper.h @@ -33,89 +33,35 @@ inline ui32 LevenshteinDistance(TString word1, TString word2) { return dist[size1][size2]; } -template class FuzzySearcher { - struct WordHit { - bool Contains; - ui32 LengthDifference; - ui32 LevenshteinDistance; - Type Data; - - WordHit(bool contains, ui32 lengthDifference, ui32 levenshteinDistance, Type data) - : Contains(contains) - , LengthDifference(lengthDifference) - , LevenshteinDistance(levenshteinDistance) - , Data(data) - {} - - bool operator<(const WordHit& other) const { - if (this->Contains && !other.Contains) { - return true; - } - if (this->Contains && other.Contains) { - return this->LengthDifference < other.LengthDifference; - } - return this->LevenshteinDistance < other.LevenshteinDistance; - } - - bool operator>(const WordHit& other) const { - if (!this->Contains && other.Contains) { - return true; - } - if (this->Contains && other.Contains) { - return this->LengthDifference > other.LengthDifference; - } - return this->LevenshteinDistance > other.LevenshteinDistance; - } - }; - - static WordHit CalculateWordHit(TString searchWord, TString testWord, Type testData) { - searchWord = to_lower(searchWord); - testWord = to_lower(testWord); - if (testWord.Contains(searchWord)) { - return {1, static_cast(testWord.length() - searchWord.length()), 0, testData}; + static size_t CalculateWordHit(const TString& searchWord, const TString& testWord) { + size_t findPos = testWord.find(searchWord); + if (findPos != TString::npos) { + return testWord.size() - searchWord.size() + findPos; } else { - ui32 levenshteinDistance = LevenshteinDistance(searchWord, testWord); - return {0, 0, levenshteinDistance, testData}; + return 1000 * LevenshteinDistance(searchWord, testWord); } } public: - THashMap Dictionary; - - FuzzySearcher(const THashMap& dictionary) - : Dictionary(dictionary) {} - - FuzzySearcher(const TVector& words) { - for (const auto& word : words) { - Dictionary[word] = word; + template + static std::vector Search(const std::vector& dictionary, const TString& searchWord, ui32 limit = 10) { + TString search = to_lower(searchWord); + std::vector> hits; // {distance, index} + hits.reserve(dictionary.size()); + for (size_t index = 0; index < dictionary.size(); ++index) { + hits.emplace_back(CalculateWordHit(search, to_lower(TString(dictionary[index]))), index); } - } - - TVector Search(const TString& searchWord, ui32 limit = 10) { - auto cmp = [](const WordHit& left, const WordHit& right) { - return left < right; - }; - std::priority_queue, decltype(cmp)> queue(cmp); - - for (const auto& [word, data]: Dictionary) { - auto wordHit = CalculateWordHit(searchWord, word, data); - if (queue.size() < limit) { - queue.emplace(wordHit); - } else if (queue.size() > 0 && wordHit < queue.top()) { - queue.pop(); - queue.emplace(wordHit); - } + std::sort(hits.begin(), hits.end()); + if (hits.size() > limit) { + hits.resize(limit); } - - TVector results; - while (!queue.empty()) { - results.emplace_back(queue.top().Data); - queue.pop(); + std::vector result; + result.reserve(hits.size()); + for (const auto& hit : hits) { + result.emplace_back(&dictionary[hit.second]); } - - std::reverse(results.begin(), results.end()); - return results; + return result; } }; diff --git a/ydb/core/viewer/viewer_autocomplete.h b/ydb/core/viewer/viewer_autocomplete.h index 79fd9f4fdcc7..4d7c8a63295a 100644 --- a/ydb/core/viewer/viewer_autocomplete.h +++ b/ydb/core/viewer/viewer_autocomplete.h @@ -17,22 +17,27 @@ class TJsonAutocomplete : public TViewerPipeClient { TJsonSettings JsonSettings; ui32 Timeout = 0; - TAutoPtr ProxyResult; - TAutoPtr ConsoleResult; - TAutoPtr CacheResult; + std::optional> ConsoleResult; + std::optional> CacheResult; struct TSchemaWordData { TString Name; NKikimrViewer::EAutocompleteType Type; - TString Table; - TSchemaWordData() {} - TSchemaWordData(const TString& name, const NKikimrViewer::EAutocompleteType type, const TString& table = "") + TString Parent; + + TSchemaWordData(const TString& name, const NKikimrViewer::EAutocompleteType type, const TString& parent = {}) : Name(name) , Type(type) - , Table(table) + , Parent(parent) {} + + operator TString() const { + return Name; + } }; - THashMap Dictionary; + + TVector DatabasePath; + std::vector Dictionary; TVector Tables; TVector Paths; TString Prefix; @@ -40,8 +45,6 @@ class TJsonAutocomplete : public TViewerPipeClient { ui32 Limit = 10; NKikimrViewer::TQueryAutocomplete Result; - std::optional SubscribedNodeId; - std::vector TenantDynamicNodes; public: TJsonAutocomplete(IViewer* viewer, NMon::TEvHttpInfo::TPtr& ev) : TBase(viewer, ev) @@ -76,54 +79,48 @@ class TJsonAutocomplete : public TViewerPipeClient { void PrepareParameters() { if (Database) { - TString prefixUpToLastSlash = ""; - auto splitPos = Prefix.find_last_of('/'); - if (splitPos != std::string::npos) { - prefixUpToLastSlash += Prefix.substr(0, splitPos); - SearchWord = Prefix.substr(splitPos + 1); - } else { - SearchWord = Prefix; + DatabasePath = SplitPath(Database); + auto prefixPaths = SplitPath(Prefix); + if (Prefix.EndsWith('/')) { + prefixPaths.emplace_back(); } - - if (Tables.size() == 0) { - Paths.emplace_back(Database); - } else { - for (TString& table: Tables) { - TString path = table; - if (!table.StartsWith(Database)) { - path = Database + "/" + path; - } - path += "/" + prefixUpToLastSlash; - Paths.emplace_back(path); - } + if (!prefixPaths.empty()) { + SearchWord = prefixPaths.back(); + prefixPaths.pop_back(); + } + if (!prefixPaths.empty()) { + Paths.emplace_back(JoinPath(prefixPaths)); + } + for (const TString& table : Tables) { + Paths.emplace_back(table); + } + if (Paths.empty()) { + Paths.emplace_back(); } } else { SearchWord = Prefix; } if (Limit == 0) { - Limit = std::numeric_limits::max(); + Limit = 1000; } } void ParseCgiParameters(const TCgiParameters& params) { JsonSettings.EnumAsNumbers = !FromStringWithDefault(params.Get("enums"), true); JsonSettings.UI64AsString = !FromStringWithDefault(params.Get("ui64"), false); - Database = params.Get("database"); StringSplitter(params.Get("table")).Split(',').SkipEmpty().Collect(&Tables); Prefix = params.Get("prefix"); Limit = FromStringWithDefault(params.Get("limit"), Limit); - Direct = FromStringWithDefault(params.Get("direct"), Direct); Timeout = FromStringWithDefault(params.Get("timeout"), 10000); } void ParsePostContent(const TStringBuf& content) { - static NJson::TJsonReaderConfig JsonConfig; NJson::TJsonValue requestData; - bool success = NJson::ReadJsonTree(content, &JsonConfig, &requestData); + bool success = NJson::ReadJsonTree(content, &requestData); if (success) { Database = Database.empty() ? requestData["database"].GetStringSafe({}) : Database; if (requestData["table"].IsArray()) { - for (auto& table: requestData["table"].GetArraySafe()) { + for (const auto& table : requestData["table"].GetArraySafe()) { Tables.emplace_back(table.GetStringSafe()); } } @@ -138,50 +135,41 @@ class TJsonAutocomplete : public TViewerPipeClient { return NViewer::IsPostContent(Event); } - TAutoPtr MakeSchemeCacheRequest() { - TAutoPtr request(new NSchemeCache::TSchemeCacheNavigate()); - - for (TString& path: Paths) { + TRequestResponse MakeRequestSchemeCacheNavigate() { + auto request = std::make_unique(); + for (const TString& path : Paths) { + Cerr << "Looking into " << path << Endl; NSchemeCache::TSchemeCacheNavigate::TEntry entry; entry.Operation = NSchemeCache::TSchemeCacheNavigate::OpList; entry.SyncVersion = false; - entry.Path = SplitPath(path); + auto splittedPath = SplitPath(path); + entry.Path = DatabasePath; + entry.Path.insert(entry.Path.end(), splittedPath.begin(), splittedPath.end()); request->ResultSet.emplace_back(entry); } - - return request; - } - - void SendSchemeCacheRequest() { - SendRequest(MakeSchemeCacheID(), new TEvTxProxySchemeCache::TEvNavigateKeySet(MakeSchemeCacheRequest())); + return MakeRequest(MakeSchemeCacheID(), + new TEvTxProxySchemeCache::TEvNavigateKeySet(request.release())); } void Bootstrap() override { if (ViewerRequest) { // handle proxied request - SendSchemeCacheRequest(); + CacheResult = MakeRequestSchemeCacheNavigate(); } else { if (NeedToRedirect()) { return; } - if (!Database) { + if (Database) { + CacheResult = MakeRequestSchemeCacheNavigate(); + } else { // autocomplete database list via console request - RequestConsoleListTenants(); + ConsoleResult = MakeRequestConsoleListTenants(); } - SendSchemeCacheRequest(); } Become(&TThis::StateRequestedDescribe, TDuration::MilliSeconds(Timeout), new TEvents::TEvWakeup()); } - void PassAway() override { - if (SubscribedNodeId.has_value()) { - Send(TActivationContext::InterconnectProxy(SubscribedNodeId.value()), new TEvents::TEvUnsubscribe()); - } - TBase::PassAway(); - BLOG_TRACE("PassAway()"); - } - STATEFN(StateRequestedDescribe) { switch (ev->GetTypeRewrite()) { hFunc(NConsole::TEvConsole::TEvListTenantsResponse, Handle); @@ -190,33 +178,15 @@ class TJsonAutocomplete : public TViewerPipeClient { } } - void ParseProxyResult() { - if (ProxyResult == nullptr) { - Result.add_error("Failed to collect information from ProxyResult"); - return; - } - if (ProxyResult->Record.HasAutocompleteResponse()) { - Result = ProxyResult->Record.GetAutocompleteResponse(); - } else { - Result.add_error("Proxying return empty response"); - } - - } - void ParseConsoleResult() { - if (ConsoleResult == nullptr) { - Result.add_error("Failed to collect information from ConsoleResult"); - return; - } - Ydb::Cms::ListDatabasesResult listTenantsResult; - ConsoleResult->Record.GetResponse().operation().result().UnpackTo(&listTenantsResult); + ConsoleResult->Get()->Record.GetResponse().operation().result().UnpackTo(&listTenantsResult); for (const TString& path : listTenantsResult.paths()) { - Dictionary[path] = TSchemaWordData(path, NKikimrViewer::ext_sub_domain); + Dictionary.emplace_back(path, NKikimrViewer::ext_sub_domain); } } - NKikimrViewer::EAutocompleteType ConvertType(TNavigate::EKind navigate) { + static NKikimrViewer::EAutocompleteType ConvertType(TNavigate::EKind navigate) { switch (navigate) { case TNavigate::KindSubdomain: return NKikimrViewer::sub_domain; @@ -262,88 +232,86 @@ class TJsonAutocomplete : public TViewerPipeClient { } void ParseCacheResult() { - if (CacheResult == nullptr) { - Result.add_error("Failed to collect information from CacheResult"); - return; - } - NSchemeCache::TSchemeCacheNavigate *navigate = CacheResult->Request.Get(); - if (navigate->ErrorCount > 0) { - for (auto& entry: CacheResult->Request.Get()->ResultSet) { - if (entry.Status != TSchemeCacheNavigate::EStatus::Ok) { - Result.add_error(TStringBuilder() << "Error receiving Navigate response: `" << CanonizePath(entry.Path) << "` has <" << ToString(entry.Status) << "> status"); + NSchemeCache::TSchemeCacheNavigate& navigate = *CacheResult->Get()->Request; + for (auto& entry : navigate.ResultSet) { + if (entry.Status == TSchemeCacheNavigate::EStatus::Ok) { + if (entry.Path.size() >= DatabasePath.size()) { + entry.Path.erase(entry.Path.begin(), entry.Path.begin() + DatabasePath.size()); } - } - return; - } - for (auto& entry: CacheResult->Request.Get()->ResultSet) { - TString path = CanonizePath(entry.Path); - if (entry.ListNodeEntry) { - for (const auto& child : entry.ListNodeEntry->Children) { - Dictionary[child.Name] = TSchemaWordData(child.Name, ConvertType(child.Kind), path); + TString path = JoinPath(entry.Path); + for (const auto& [id, column] : entry.Columns) { + Dictionary.emplace_back(column.Name, NKikimrViewer::column, path); } - }; - for (const auto& [id, column] : entry.Columns) { - Dictionary[column.Name] = TSchemaWordData(column.Name, NKikimrViewer::column, path); - } - for (const auto& index : entry.Indexes) { - Dictionary[index.GetName()] = TSchemaWordData(index.GetName(), NKikimrViewer::index, path); - } - for (const auto& cdcStream : entry.CdcStreams) { - Dictionary[cdcStream.GetName()] = TSchemaWordData(cdcStream.GetName(), NKikimrViewer::cdc_stream, path); + for (const auto& index : entry.Indexes) { + Dictionary.emplace_back(index.GetName(), NKikimrViewer::index, path); + } + for (const auto& cdcStream : entry.CdcStreams) { + Dictionary.emplace_back(cdcStream.GetName(), NKikimrViewer::cdc_stream, path); + } + if (entry.ListNodeEntry) { + for (const auto& child : entry.ListNodeEntry->Children) { + Dictionary.emplace_back(child.Name, ConvertType(child.Kind), path); + } + }; + } else { + Result.add_error(TStringBuilder() << "Error receiving Navigate response: `" << CanonizePath(entry.Path) << "` has <" << ToString(entry.Status) << "> status"); } } } - void Handle(TEvTxProxySchemeCache::TEvNavigateKeySetResult::TPtr &ev) { - CacheResult = ev->Release(); + void Handle(TEvTxProxySchemeCache::TEvNavigateKeySetResult::TPtr& ev) { + CacheResult->Set(std::move(ev)); RequestDone(); } void Handle(NConsole::TEvConsole::TEvListTenantsResponse::TPtr& ev) { - ConsoleResult = ev->Release(); + ConsoleResult->Set(std::move(ev)); RequestDone(); } - void SendAutocompleteResponse() { - if (ViewerRequest) { - TEvViewer::TEvViewerResponse* viewerResponse = new TEvViewer::TEvViewerResponse(); - viewerResponse->Record.MutableAutocompleteResponse()->CopyFrom(Result); - Send(ViewerRequest->Sender, viewerResponse); - } else { - TStringStream json; - TProtoToJson::ProtoToJson(json, Result, JsonSettings); - Send(Event->Sender, new NMon::TEvHttpInfoRes(Viewer->GetHTTPOKJSON(Event->Get(), json.Str()), 0, NMon::IEvHttpInfoRes::EContentType::Custom)); + void ReplyAndPassAway() override { + Result.SetVersion(Viewer->GetCapabilityVersion("/viewer/autocomplete")); + + if (CacheResult) { + if (CacheResult->IsOk()) { + ParseCacheResult(); + } else { + Result.add_error("Failed to collect information from CacheResult"); + } } - } - void ReplyAndPassAway() override { - if (ProxyResult) { - ParseProxyResult(); - } else if (Database) { - ParseCacheResult(); - } else { - ParseConsoleResult(); + if (ConsoleResult) { + if (ConsoleResult->IsOk()) { + ParseConsoleResult(); + } else { + Result.add_error("Failed to collect information from ConsoleResult"); + } } - if (!ProxyResult) { - Result.set_success(Result.error_size() == 0); - if (Result.error_size() == 0) { - auto fuzzy = FuzzySearcher(Dictionary); - auto autocomplete = fuzzy.Search(SearchWord, Limit); - Result.MutableResult()->SetTotal(autocomplete.size()); - for (TSchemaWordData& wordData: autocomplete) { - auto entity = Result.MutableResult()->AddEntities(); - entity->SetName(wordData.Name); - entity->SetType(wordData.Type); - if (wordData.Table) { - entity->SetParent(wordData.Table); - } + Result.set_success(Result.error_size() == 0); + if (Result.error_size() == 0) { + auto autocomplete = FuzzySearcher::Search(Dictionary, SearchWord, Limit); + Result.MutableResult()->SetTotal(autocomplete.size()); + for (const TSchemaWordData* wordData : autocomplete) { + auto entity = Result.MutableResult()->AddEntities(); + entity->SetName(wordData->Name); + entity->SetType(wordData->Type); + if (wordData->Parent) { + entity->SetParent(wordData->Parent); } } } - SendAutocompleteResponse(); - PassAway(); + if (ViewerRequest) { + TEvViewer::TEvViewerResponse* viewerResponse = new TEvViewer::TEvViewerResponse(); + viewerResponse->Record.MutableAutocompleteResponse()->CopyFrom(Result); + Send(ViewerRequest->Sender, viewerResponse); + PassAway(); + } else { + TStringStream json; + TProtoToJson::ProtoToJson(json, Result, JsonSettings); + TBase::ReplyAndPassAway(GetHTTPOKJSON(json.Str())); + } } void HandleTimeout() { @@ -351,8 +319,7 @@ class TJsonAutocomplete : public TViewerPipeClient { Result.add_error("Request timed out"); ReplyAndPassAway(); } else { - Send(Event->Sender, new NMon::TEvHttpInfoRes(Viewer->GetHTTPGATEWAYTIMEOUT(Event->Get()), 0, NMon::IEvHttpInfoRes::EContentType::Custom)); - PassAway(); + TBase::ReplyAndPassAway(GetHTTPGATEWAYTIMEOUT()); } } diff --git a/ydb/core/viewer/viewer_ut.cpp b/ydb/core/viewer/viewer_ut.cpp index 4482a04dda7f..318484dcdee7 100644 --- a/ydb/core/viewer/viewer_ut.cpp +++ b/ydb/core/viewer/viewer_ut.cpp @@ -1039,12 +1039,11 @@ Y_UNIT_TEST_SUITE(Viewer) { TVector DifferentWordsDictionary = { "/orders", "/peoples", "/OrdinaryScheduleTables" }; void FuzzySearcherTest(TVector& dictionary, TString search, ui32 limit, TVector expectations) { - auto fuzzy = FuzzySearcher(dictionary); - auto result = fuzzy.Search(search, limit); + auto result = FuzzySearcher::Search(dictionary, search, limit); UNIT_ASSERT_VALUES_EQUAL(expectations.size(), result.size()); for (ui32 i = 0; i < expectations.size(); i++) { - UNIT_ASSERT_VALUES_EQUAL(expectations[i], result[i]); + UNIT_ASSERT_VALUES_EQUAL(expectations[i], *result[i]); } } @@ -1308,8 +1307,11 @@ Y_UNIT_TEST_SUITE(Viewer) { JsonAutocompleteTest(HTTP_METHOD_GET, value, "nam", "/Root/Database", {"orders", "products"}); VerifyJsonAutocompleteSuccess(value, { "name", + "name", + "id", "id", "description", + "description", }); } @@ -1318,8 +1320,11 @@ Y_UNIT_TEST_SUITE(Viewer) { JsonAutocompleteTest(HTTP_METHOD_POST, value, "nam", "/Root/Database", {"orders", "products"}); VerifyJsonAutocompleteSuccess(value, { "name", + "name", + "id", "id", "description", + "description", }); }