diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 21fc7b7e78..4a85175a39 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -189,6 +189,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 1a6a0ce9a5..7cb5b11151 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -96,6 +96,9 @@ Commands + + Header Files + diff --git a/src/AppInstallerCLICore/TableOutput.h b/src/AppInstallerCLICore/TableOutput.h new file mode 100644 index 0000000000..738c0c3bef --- /dev/null +++ b/src/AppInstallerCLICore/TableOutput.h @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionReporter.h" + +#include +#include +#include +#include + + +namespace AppInstaller::CLI::Execution +{ + namespace details + { + // Gets the column width of the console. + inline size_t GetConsoleWidth() + { + CONSOLE_SCREEN_BUFFER_INFO consoleInfo{}; + if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &consoleInfo)) + { + return static_cast(consoleInfo.dwSize.X); + } + else + { + return 120; + } + } + } + + // Enables output data in a table format. + template + struct TableOutput + { + using line_t = std::array; + + TableOutput(Reporter& reporter, line_t&& header, size_t sizingBuffer = 50) : + m_reporter(reporter), m_sizingBuffer(sizingBuffer) + { + for (size_t i = 0; i < FieldCount; ++i) + { + m_columns[i].Name = std::move(header[i]); + m_columns[i].MinLength = m_columns[i].Name.length(); + m_columns[i].MaxLength = 0; + } + } + + void OutputLine(line_t&& line) + { + if (m_buffer.size() < m_sizingBuffer) + { + m_buffer.emplace_back(std::move(line)); + } + else + { + EvaulateAndFlushBuffer(); + OutputLineToStream(line); + } + } + + void Complete() + { + EvaulateAndFlushBuffer(); + } + + private: + // A column in the table. + struct Column + { + std::string Name; + size_t MinLength = 0; + size_t MaxLength = 0; + bool SpaceAfter = true; + }; + + Reporter& m_reporter; + std::array m_columns; + size_t m_sizingBuffer; + std::vector m_buffer; + bool m_bufferEvaluated = false; + + void EvaulateAndFlushBuffer() + { + if (m_bufferEvaluated) + { + return; + } + + // Determine the maximum length for all columns + for (const auto& line : m_buffer) + { + for (size_t i = 0; i < FieldCount; ++i) + { + m_columns[i].MaxLength = std::max(m_columns[i].MaxLength, line[i].length()); + } + } + + // Only output the extra space if: + // 1. Not the last field + m_columns[FieldCount - 1].SpaceAfter = false; + + // 2. Not empty (taken care of by not doing anything if empty) + // 3. There are non-empty fields after + for (size_t i = FieldCount - 1; i > 0; --i) + { + if (m_columns[i].MaxLength) + { + break; + } + else + { + m_columns[i - 1].SpaceAfter = false; + } + } + + // Determine the total width required to not truncate any columns + size_t totalRequired = 0; + + for (size_t i = 0; i < FieldCount; ++i) + { + totalRequired += m_columns[i].MaxLength + (m_columns[i].SpaceAfter ? 1 : 0); + } + + size_t consoleWidth = details::GetConsoleWidth(); + + // If the total space would be too big, shrink them. + // We don't want to use the last column, lest we auto-wrap + if (totalRequired >= consoleWidth) + { + size_t extra = (totalRequired - consoleWidth) + 1; + + while (extra) + { + size_t targetIndex = 0; + size_t targetVal = m_columns[0].MaxLength; + for (size_t j = 1; j < FieldCount; ++j) + { + if (m_columns[j].MaxLength > targetVal) + { + targetIndex = j; + targetVal = m_columns[j].MaxLength; + } + } + m_columns[targetIndex].MaxLength -= 1; + extra -= 1; + } + + totalRequired = consoleWidth - 1; + } + + // Header line + line_t headerLine; + + for (size_t i = 0; i < FieldCount; ++i) + { + headerLine[i] = m_columns[i].Name; + } + + OutputLineToStream(headerLine); + + m_reporter.Info() << std::string(totalRequired, '-') << std::endl; + + for (const auto& line : m_buffer) + { + OutputLineToStream(line); + } + + m_bufferEvaluated = true; + } + + void OutputLineToStream(const line_t& line) + { + auto out = m_reporter.Info(); + + for (size_t i = 0; i < FieldCount; ++i) + { + const auto& col = m_columns[i]; + + if (col.MaxLength) + { + if (line[i].length() > col.MaxLength) + { + size_t replaceChars = std::min(col.MaxLength, static_cast(3)); + std::string replacement = line[i].substr(0, col.MaxLength - replaceChars); + replacement.append(replaceChars, '.'); + out << replacement; + + if (col.SpaceAfter) + { + out << ' '; + } + } + else + { + out << line[i]; + + if (col.SpaceAfter) + { + out << std::string(col.MaxLength - line[i].length() + 1, ' '); + } + } + } + } + + out << std::endl; + } + }; +} diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 59a41cc425..562cd54cec 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -4,12 +4,31 @@ #include "WorkflowBase.h" #include "ExecutionContext.h" #include "ManifestComparator.h" +#include "TableOutput.h" namespace AppInstaller::CLI::Workflow { using namespace AppInstaller::Repository; + namespace + { + std::string GetMatchCriteriaDescriptor(const ResultMatch& match) + { + if (match.MatchCriteria.Field != ApplicationMatchField::Id && match.MatchCriteria.Field != ApplicationMatchField::Name) + { + std::string result{ ApplicationMatchFieldToString(match.MatchCriteria.Field) }; + result += ": "; + result += match.MatchCriteria.Value; + return result; + } + else + { + return {}; + } + } + } + bool WorkflowTask::operator==(const WorkflowTask& other) const { if (m_isFunc && other.m_isFunc) @@ -133,20 +152,22 @@ namespace AppInstaller::CLI::Workflow { auto& searchResult = context.Get(); Logging::Telemetry().LogSearchResultCount(searchResult.Matches.size()); - for (auto& match : searchResult.Matches) + + Execution::TableOutput<4> table(context.Reporter, { "Name", "Id", "Version", "Matched" }); + + for (size_t i = 0; i < searchResult.Matches.size(); ++i) { - auto app = match.Application.get(); + auto app = searchResult.Matches[i].Application.get(); auto allVersions = app->GetVersions(); - // Assume versions are sorted when returned so we'll use the first one as the latest version - context.Reporter.Info() << app->GetId() << ", " << app->GetName() << ", " << allVersions.at(0).GetVersion().ToString(); + table.OutputLine({ app->GetName(), app->GetId(), allVersions.at(0).GetVersion().ToString(), GetMatchCriteriaDescriptor(searchResult.Matches[i]) }); + } - if (match.MatchCriteria.Field != ApplicationMatchField::Id && match.MatchCriteria.Field != ApplicationMatchField::Name) - { - context.Reporter.Info() << ", [" << ApplicationMatchFieldToString(match.MatchCriteria.Field) << ": " << match.MatchCriteria.Value << "]"; - } + table.Complete(); - context.Reporter.Info() << std::endl; + if (searchResult.Truncated) + { + context.Reporter.Info() << "" << std::endl; } } diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h index 46a1f3707e..f961de9dff 100644 --- a/src/AppInstallerCLICore/pch.h +++ b/src/AppInstallerCLICore/pch.h @@ -12,11 +12,13 @@ #include +#include #include #include #include #include #include +#include #include #include #include diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp index cc2ce81382..20b29c5e19 100644 --- a/src/AppInstallerCLITests/SQLiteIndex.cpp +++ b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -544,10 +544,10 @@ TEST_CASE("SQLiteIndex_Search_IdExactMatch", "[sqliteindex]") request.Query = RequestMatch(MatchType::Exact, manifest.Id); auto results = index.Search(request); - REQUIRE(results.size() == 1); - REQUIRE(results[0].second.Field == ApplicationMatchField::Id); - REQUIRE(results[0].second.Type == MatchType::Exact); - REQUIRE(results[0].second.Value == manifest.Id); + REQUIRE(results.Matches.size() == 1); + REQUIRE(results.Matches[0].second.Field == ApplicationMatchField::Id); + REQUIRE(results.Matches[0].second.Type == MatchType::Exact); + REQUIRE(results.Matches[0].second.Value == manifest.Id); } TEST_CASE("SQLiteIndex_Search_MultipleMatch", "[sqliteindex]") @@ -566,9 +566,9 @@ TEST_CASE("SQLiteIndex_Search_MultipleMatch", "[sqliteindex]") request.Query = RequestMatch(MatchType::Exact, manifest.Id); auto results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); - auto result = index.GetVersionsById(results[0].first); + auto result = index.GetVersionsById(results.Matches[0].first); REQUIRE(result.size() == 2); } @@ -585,7 +585,7 @@ TEST_CASE("SQLiteIndex_Search_NoMatch", "[sqliteindex]") request.Query = RequestMatch(MatchType::Exact, "THIS DOES NOT MATCH ANYTHING!"); auto results = index.Search(request); - REQUIRE(results.size() == 0); + REQUIRE(results.Matches.size() == 0); } TEST_CASE("SQLiteIndex_IdString", "[sqliteindex]") @@ -601,9 +601,9 @@ TEST_CASE("SQLiteIndex_IdString", "[sqliteindex]") request.Query = RequestMatch(MatchType::Exact, manifest.Id); auto results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); - auto result = index.GetIdStringById(results[0].first); + auto result = index.GetIdStringById(results.Matches[0].first); REQUIRE(result.has_value()); REQUIRE(result.value() == manifest.Id); } @@ -621,9 +621,9 @@ TEST_CASE("SQLiteIndex_NameString", "[sqliteindex]") request.Query = RequestMatch(MatchType::Exact, manifest.Id); auto results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); - auto result = index.GetNameStringById(results[0].first); + auto result = index.GetNameStringById(results.Matches[0].first); REQUIRE(result.has_value()); REQUIRE(result.value() == manifest.Name); } @@ -641,13 +641,13 @@ TEST_CASE("SQLiteIndex_PathString", "[sqliteindex]") request.Query = RequestMatch(MatchType::Exact, manifest.Id); auto results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); - auto specificResult = index.GetPathStringByKey(results[0].first, manifest.Version, manifest.Channel); + auto specificResult = index.GetPathStringByKey(results.Matches[0].first, manifest.Version, manifest.Channel); REQUIRE(specificResult.has_value()); REQUIRE(specificResult.value() == relativePath); - auto latestResult = index.GetPathStringByKey(results[0].first, "", manifest.Channel); + auto latestResult = index.GetPathStringByKey(results.Matches[0].first, "", manifest.Channel); REQUIRE(latestResult.has_value()); REQUIRE(latestResult.value() == relativePath); } @@ -665,9 +665,9 @@ TEST_CASE("SQLiteIndex_Versions", "[sqliteindex]") request.Query = RequestMatch(MatchType::Exact, manifest.Id); auto results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); - auto result = index.GetVersionsById(results[0].first); + auto result = index.GetVersionsById(results.Matches[0].first); REQUIRE(result.size() == 1); REQUIRE(result[0].GetVersion().ToString() == manifest.Version); REQUIRE(result[0].GetChannel().ToString() == manifest.Channel); @@ -705,9 +705,9 @@ TEST_CASE("SQLiteIndex_Search_VersionSorting", "[sqliteindex]") request.Filters.emplace_back(ApplicationMatchField::Id, MatchType::Exact, "Id"); auto results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); - auto result = index.GetVersionsById(results[0].first); + auto result = index.GetVersionsById(results.Matches[0].first); REQUIRE(result.size() == sortedList.size()); for (size_t i = 0; i < result.size(); ++i) @@ -753,21 +753,21 @@ TEST_CASE("SQLiteIndex_PathString_VersionSorting", "[sqliteindex]") request.Filters.emplace_back(ApplicationMatchField::Id, MatchType::Exact, "Id"); auto results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); - auto result = index.GetPathStringByKey(results[0].first, "", ""); + auto result = index.GetPathStringByKey(results.Matches[0].first, "", ""); REQUIRE(result.has_value()); REQUIRE(result.value() == "Path3"); - result = index.GetPathStringByKey(results[0].first, "", "alpha"); + result = index.GetPathStringByKey(results.Matches[0].first, "", "alpha"); REQUIRE(result.has_value()); REQUIRE(result.value() == "Path2"); - result = index.GetPathStringByKey(results[0].first, "", "beta"); + result = index.GetPathStringByKey(results.Matches[0].first, "", "beta"); REQUIRE(result.has_value()); REQUIRE(result.value() == "Path5"); - result = index.GetPathStringByKey(results[0].first, "", "gamma"); + result = index.GetPathStringByKey(results.Matches[0].first, "", "gamma"); REQUIRE(!result.has_value()); } @@ -812,7 +812,7 @@ TEST_CASE("SQLiteIndex_Search_EmptySearch", "[sqliteindex]") SearchRequest request; auto results = index.Search(request); - REQUIRE(results.size() == 3); + REQUIRE(results.Matches.size() == 3); } TEST_CASE("SQLiteIndex_Search_Exact", "[sqliteindex]") @@ -829,7 +829,7 @@ TEST_CASE("SQLiteIndex_Search_Exact", "[sqliteindex]") request.Query = RequestMatch(MatchType::Exact, "Id"); auto results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); } TEST_CASE("SQLiteIndex_Search_Substring", "[sqliteindex]") @@ -846,7 +846,7 @@ TEST_CASE("SQLiteIndex_Search_Substring", "[sqliteindex]") request.Query = RequestMatch(MatchType::Substring, "Id"); auto results = index.Search(request); - REQUIRE(results.size() == 2); + REQUIRE(results.Matches.size() == 2); } TEST_CASE("SQLiteIndex_Search_ExactBeforeSubstring", "[sqliteindex]") @@ -863,10 +863,10 @@ TEST_CASE("SQLiteIndex_Search_ExactBeforeSubstring", "[sqliteindex]") request.Query = RequestMatch(MatchType::Substring, "Id"); auto results = index.Search(request); - REQUIRE(results.size() == 2); + REQUIRE(results.Matches.size() == 2); - REQUIRE(index.GetIdStringById(results[0].first) == "Id"); - REQUIRE(index.GetIdStringById(results[1].first) == "Id2"); + REQUIRE(index.GetIdStringById(results.Matches[0].first) == "Id"); + REQUIRE(index.GetIdStringById(results.Matches[1].first) == "Id2"); } TEST_CASE("SQLiteIndex_Search_SingleFilter", "[sqliteindex]") @@ -884,12 +884,12 @@ TEST_CASE("SQLiteIndex_Search_SingleFilter", "[sqliteindex]") request.Filters.emplace_back(ApplicationMatchField::Name, MatchType::Substring, "a"); auto results = index.Search(request); - REQUIRE(results.size() == 2); + REQUIRE(results.Matches.size() == 2); request.Filters[0].Value = "e"; results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); } TEST_CASE("SQLiteIndex_Search_Multimatch", "[sqliteindex]") @@ -912,7 +912,7 @@ TEST_CASE("SQLiteIndex_Search_Multimatch", "[sqliteindex]") request.Query = RequestMatch(MatchType::Substring, ""); auto results = index.Search(request); - REQUIRE(results.size() == 3); + REQUIRE(results.Matches.size() == 3); } TEST_CASE("SQLiteIndex_Search_QueryAndFilter", "[sqliteindex]") @@ -931,9 +931,9 @@ TEST_CASE("SQLiteIndex_Search_QueryAndFilter", "[sqliteindex]") request.Filters.emplace_back(ApplicationMatchField::Name, MatchType::Substring, "Na"); auto results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); - auto result = index.GetIdStringById(results[0].first); + auto result = index.GetIdStringById(results.Matches[0].first); REQUIRE(result.has_value()); REQUIRE(result.value() == "Id2"); } @@ -960,9 +960,9 @@ TEST_CASE("SQLiteIndex_Search_QueryAndMultipleFilters", "[sqliteindex]") request.Filters.emplace_back(ApplicationMatchField::Moniker, MatchType::Substring, "new"); auto results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); - auto result = index.GetIdStringById(results[0].first); + auto result = index.GetIdStringById(results.Matches[0].first); REQUIRE(result.has_value()); REQUIRE(result.value() == "Id3"); } @@ -983,9 +983,66 @@ TEST_CASE("SQLiteIndex_Search_SimpleICULike", "[sqliteindex]") request.Filters.emplace_back(ApplicationMatchField::Id, MatchType::Substring, u8"\xE4"); auto results = index.Search(request); - REQUIRE(results.size() == 1); + REQUIRE(results.Matches.size() == 1); - auto result = index.GetNameStringById(results[0].first); + auto result = index.GetNameStringById(results.Matches[0].first); REQUIRE(result.has_value()); REQUIRE(result.value() == "HasUmlaut"); } + +TEST_CASE("SQLiteIndex_Search_MaximumResults_Equal", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Nope", "Name", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1" }, + { "Id2", "Na", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path2" }, + { "Id3", "No", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path3" }, + }); + + SearchRequest request; + request.MaximumResults = 3; + + auto results = index.Search(request); + REQUIRE(results.Matches.size() == 3); + REQUIRE(!results.Truncated); +} + +TEST_CASE("SQLiteIndex_Search_MaximumResults_Less", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Nope", "Name", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1" }, + { "Id2", "Na", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path2" }, + { "Id3", "No", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path3" }, + }); + + SearchRequest request; + request.MaximumResults = 2; + + auto results = index.Search(request); + REQUIRE(results.Matches.size() == 2); + REQUIRE(results.Truncated); +} + +TEST_CASE("SQLiteIndex_Search_MaximumResults_Greater", "[sqliteindex]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + SQLiteIndex index = SearchTestSetup(tempFile, { + { "Nope", "Name", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path1" }, + { "Id2", "Na", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path2" }, + { "Id3", "No", "Moniker", "Version", "Channel", { "Tag" }, { "Command" }, "Path3" }, + }); + + SearchRequest request; + request.MaximumResults = 4; + + auto results = index.Search(request); + REQUIRE(results.Matches.size() == 3); + REQUIRE(!results.Truncated); +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp index 2a66a0cf2f..aa93daea4e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -199,7 +199,7 @@ namespace AppInstaller::Repository::Microsoft m_interface->PrepareForPackaging(m_dbconn); } - std::vector> SQLiteIndex::Search(const SearchRequest& request) + Schema::ISQLiteIndex::SearchResult SQLiteIndex::Search(const SearchRequest& request) { AICLI_LOG(Repo, Info, << "Performing search: " << request.ToString()); diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h index 33caf67d0b..2bc66ba410 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h @@ -3,6 +3,7 @@ #pragma once #include "SQLiteWrapper.h" #include "Manifest/Manifest.h" +#include "Microsoft/Schema/ISQLiteIndex.h" #include "Microsoft/Schema/Version.h" #include "Public/AppInstallerRepositorySearch.h" #include @@ -81,7 +82,7 @@ namespace AppInstaller::Repository::Microsoft void PrepareForPackaging(); // Performs a search based on the given criteria. - std::vector> Search(const SearchRequest& request); + Schema::ISQLiteIndex::SearchResult Search(const SearchRequest& request); // Gets the Id string for the given id, if present. std::optional GetIdStringById(IdType id); diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp index aa2c18a38b..d719a56434 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndexSource.cpp @@ -97,10 +97,11 @@ namespace AppInstaller::Repository::Microsoft SearchResult result; std::shared_ptr sharedThis = shared_from_this(); - for (auto& indexResult : indexResults) + for (auto& indexResult : indexResults.Matches) { result.Matches.emplace_back(std::make_unique(sharedThis, indexResult.first), std::move(indexResult.second)); } + result.Truncated = indexResults.Truncated; return result; } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.cpp index bcb4b1bab7..009e08c427 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.cpp @@ -352,18 +352,21 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 builder.Execute(connection); } - std::vector> Interface::Search(SQLite::Connection& connection, const SearchRequest& request) + ISQLiteIndex::SearchResult Interface::Search(SQLite::Connection& connection, const SearchRequest& request) { // If no query or filters, get everything if (!request.Query && request.Filters.empty()) { std::vector ids = IdTable::GetAllRowIds(connection, request.MaximumResults); - std::vector> result; + SearchResult result; for (SQLite::rowid_t id : ids) { - result.emplace_back(std::make_pair(id, ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Wildcard, {}))); + result.Matches.emplace_back(std::make_pair(id, ApplicationMatchFilter(ApplicationMatchField::Id, MatchType::Wildcard, {}))); } + + result.Truncated = (request.MaximumResults && IdTable::GetCount(connection) > request.MaximumResults); + return result; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h index 30e8a873fe..41f5e5ee94 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h @@ -16,7 +16,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 bool UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) override; void RemoveManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) override; void PrepareForPackaging(SQLite::Connection& connection) override; - std::vector> Search(SQLite::Connection& connection, const SearchRequest& request) override; + SearchResult Search(SQLite::Connection& connection, const SearchRequest& request) override; std::optional GetIdStringById(SQLite::Connection& connection, SQLite::rowid_t id) override; std::optional GetNameStringById(SQLite::Connection& connection, SQLite::rowid_t id) override; std::optional GetPathStringByKey(SQLite::Connection& connection, SQLite::rowid_t id, std::string_view version, std::string_view channel) override; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp index f14efbccd5..425c5b6e7d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp @@ -106,7 +106,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 builder.Execute(connection); } - bool OneToOneTableIsEmpty(SQLite::Connection& connection, std::string_view tableName) + uint64_t OneToOneTableGetCount(SQLite::Connection& connection, std::string_view tableName) { SQLite::Builder::StatementBuilder builder; builder.Select(SQLite::Builder::RowCount).From(tableName); @@ -115,7 +115,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 THROW_HR_IF(E_UNEXPECTED, !countStatement.Step()); - return (countStatement.GetColumn(0) == 0); + return static_cast(countStatement.GetColumn(0)); + } + + bool OneToOneTableIsEmpty(SQLite::Connection& connection, std::string_view tableName) + { + return (OneToOneTableGetCount(connection, tableName) == 0); } } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.h index 0615629d36..197ba45b31 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.h @@ -30,6 +30,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Removes the given row by its rowid if it is no longer referenced. void OneToOneTableDeleteIfNotNeededById(SQLite::Connection& connection, std::string_view tableName, std::string_view valueName, SQLite::rowid_t id); + // Gets the total number of rows in the table. + uint64_t OneToOneTableGetCount(SQLite::Connection& connection, std::string_view tableName); + // Determines if the table is empty. bool OneToOneTableIsEmpty(SQLite::Connection& connection, std::string_view tableName); } @@ -104,6 +107,12 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // There is currently nothing to do for these tables. } + // Gets the total number of rows in the table. + static uint64_t GetCount(SQLite::Connection& connection) + { + return details::OneToOneTableGetCount(connection, TableInfo::TableName()); + } + // Determines if the table is empty. static bool IsEmpty(SQLite::Connection& connection) { diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.cpp index ed3a4d9a84..5d319851c0 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.cpp @@ -224,7 +224,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 AICLI_LOG(Repo, Verbose, << "Filter deleted " << m_connection.GetChanges() << " rows"); } - std::vector> SearchResultsTable::GetSearchResults(size_t limit) + ISQLiteIndex::SearchResult SearchResultsTable::GetSearchResults(size_t limit) { constexpr std::string_view tempTableAlias = "t"sv; @@ -247,19 +247,22 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 Join(ManifestTable::TableName()).On(QCol(tempTableAlias, s_SearchResultsTable_Manifest), QCol(ManifestTable::TableName(), SQLite::RowIDName)). GroupBy(QCol(ManifestTable::TableName(), IdTable::ValueName())).OrderBy(QCol(tempTableAlias, s_SearchResultsTable_SortValue)); - if (limit) - { - builder.Limit(limit); - } - SQLite::Statement select = builder.Prepare(m_connection); - std::vector> result; + ISQLiteIndex::SearchResult result; while (select.Step()) { - result.emplace_back(select.GetColumn(0), + if (limit && result.Matches.size() >= limit) + { + break; + } + + result.Matches.emplace_back(select.GetColumn(0), ApplicationMatchFilter(select.GetColumn(1), select.GetColumn(2), select.GetColumn(3))); } + + result.Truncated = (select.GetState() != SQLite::Statement::State::Completed); + return result; } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.h index b60752f8b1..9e5c7f6017 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/SearchResultsTable.h @@ -3,6 +3,7 @@ #pragma once #include "SQLiteWrapper.h" #include "SQLiteTempTable.h" +#include "Microsoft/Schema/ISQLiteIndex.h" #include "AppInstallerRepositorySearch.h" #include @@ -38,7 +39,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 void CompleteFilter(); // Gets the results from the table. - std::vector> GetSearchResults(size_t limit = 0); + ISQLiteIndex::SearchResult GetSearchResults(size_t limit = 0); private: SQLite::Connection& m_connection; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h index 5c2f45806d..ac4cb27ff7 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h @@ -20,6 +20,14 @@ namespace AppInstaller::Repository::Microsoft::Schema { virtual ~ISQLiteIndex() = default; + // The non-version specific return value of Search. + // New fields must have initializers to their down-schema defaults. + struct SearchResult + { + std::vector> Matches; + bool Truncated = false; + }; + // Version 1.0 // Gets the schema version that this index interface is built for. @@ -43,7 +51,7 @@ namespace AppInstaller::Repository::Microsoft::Schema virtual void PrepareForPackaging(SQLite::Connection& connection) = 0; // Performs a search based on the given criteria. - virtual std::vector> Search(SQLite::Connection& connection, const SearchRequest& request) = 0; + virtual SearchResult Search(SQLite::Connection& connection, const SearchRequest& request) = 0; // Gets the Id string for the given id, if present. virtual std::optional GetIdStringById(SQLite::Connection& connection, SQLite::rowid_t id) = 0; diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h index 8776eb4956..b6dd8cd232 100644 --- a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h +++ b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h @@ -108,6 +108,9 @@ namespace AppInstaller::Repository { // The full set of results from the search. std::vector Matches; + + // If true, the results were truncated by the given SearchRequest::MaximumResults. + bool Truncated = false; }; inline std::string_view MatchTypeToString(MatchType type)