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)