commit 8a01471a36aeb75dd9e79b57958ee67229aee542 parent e1ff71f57dfbb5f27bfae82b18cefea203f0d2e6 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Tue, 13 Oct 2020 15:40:26 -0700 Create consistency check functionality (#609) Diffstat:
18 files changed, 379 insertions(+), 3 deletions(-)
diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp @@ -421,6 +421,8 @@ TEST_CASE("SQLiteIndex_RemoveManifest_EnsureConsistentRowId", "[sqliteindex]") // Now remove manifest1 and prepare index.RemoveManifest(manifest1, manifest1Path); index.PrepareForPackaging(); + // Checking consistency will also uncover issues, but not potentially the same ones as below. + REQUIRE(index.CheckConsistency(true)); // Repeat search to ensure consistent ids result = index.Search(request); @@ -1803,3 +1805,61 @@ TEST_CASE("SQLiteIndex_Search_ProductCodeMatch", "[sqliteindex]") REQUIRE(results.Matches.size() == 0); } } + +TEST_CASE("SQLiteIndex_CheckConsistency_Failure", "[sqliteindex][V1_1]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + std::string manifest1Path = "test/id/test.id-1.0.0.yaml"; + Manifest manifest1; + manifest1.Id = "test.id"; + manifest1.Name = "Test Name"; + manifest1.AppMoniker = "testmoniker"; + manifest1.Version = "1.0.0"; + manifest1.Channel = "test"; + manifest1.Tags = { "t1", "t2" }; + manifest1.Commands = { "test1", "test2" }; + + std::string manifest2Path = "test/woah/test.id-1.0.0.yaml"; + Manifest manifest2; + manifest2.Id = "test.woah"; + manifest2.Name = "Test Name WOAH"; + manifest2.AppMoniker = "testmoniker"; + manifest2.Version = "1.0.0"; + manifest2.Channel = "test"; + manifest2.Tags = {}; + manifest2.Commands = { "test1", "test2", "test3" }; + + SQLite::rowid_t manifestRowId = 0; + + { + SQLiteIndex index = SQLiteIndex::CreateNew(tempFile, { 1, 1 }); + + index.AddManifest(manifest1, manifest1Path); + index.AddManifest(manifest2, manifest2Path); + + // Get the first manifest's id for removal + SearchRequest request; + request.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, manifest1.Id)); + auto result = index.Search(request); + + REQUIRE(result.Matches.size() == 1); + manifestRowId = result.Matches[0].first; + } + + { + // Open it directly to modify the table + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite); + + SQLite::Builder::StatementBuilder builder; + builder.DeleteFrom(Schema::V1_0::IdTable::TableName()).Where(SQLite::RowIDName).Equals(manifestRowId); + builder.Execute(connection); + } + + { + SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite); + + REQUIRE(!index.CheckConsistency(true)); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -206,6 +206,17 @@ namespace AppInstaller::Repository::Microsoft m_interface->PrepareForPackaging(m_dbconn); } + bool SQLiteIndex::CheckConsistency(bool log) const + { + AICLI_LOG(Repo, Info, << "Checking index consistency..."); + + bool result = m_interface->CheckConsistency(m_dbconn, log); + + AICLI_LOG(Repo, Info, << "...index *WAS" << (result ? "*" : " NOT*") << " consistent."); + + return result; + } + Schema::ISQLiteIndex::SearchResult SQLiteIndex::Search(const SearchRequest& request) const { AICLI_LOG(Repo, Info, << "Performing search: " << request.ToString()); diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h @@ -87,6 +87,10 @@ namespace AppInstaller::Repository::Microsoft // Removes data that is no longer needed for an index that is to be published. void PrepareForPackaging(); + // Checks the consistency of the index to ensure that every referenced row exists. + // Returns true if index is consistent; false if it is not. + bool CheckConsistency(bool log = false) const; + // Performs a search based on the given criteria. Schema::ISQLiteIndex::SearchResult Search(const SearchRequest& request) const; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface.h @@ -20,6 +20,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 std::pair<bool, SQLite::rowid_t> UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) override; SQLite::rowid_t RemoveManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) override; void PrepareForPackaging(SQLite::Connection& connection) override; + bool CheckConsistency(const SQLite::Connection& connection, bool log) const override; SearchResult Search(const SQLite::Connection& connection, const SearchRequest& request) const override; std::optional<std::string> GetPropertyByManifestId(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionProperty property) const override; std::optional<SQLite::rowid_t> GetManifestIdByKey(const SQLite::Connection& connection, SQLite::rowid_t id, std::string_view version, std::string_view channel) const override; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/Interface_1_0.cpp @@ -348,6 +348,61 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 builder.Execute(connection); } + bool Interface::CheckConsistency(const SQLite::Connection& connection, bool log) const + { + bool result = true; + + // Check the manifest table references to it's 1:1 tables + if (result || log) + { + result = ManifestTable::CheckConsistency<IdTable>(connection, log) && result; + } + + if (result || log) + { + result = ManifestTable::CheckConsistency<NameTable>(connection, log) && result; + } + + if (result || log) + { + result = ManifestTable::CheckConsistency<MonikerTable>(connection, log) && result; + } + + if (result || log) + { + result = ManifestTable::CheckConsistency<VersionTable>(connection, log) && result; + } + + if (result || log) + { + result = ManifestTable::CheckConsistency<ChannelTable>(connection, log) && result; + } + + if (result || log) + { + result = ManifestTable::CheckConsistency<PathPartTable>(connection, log) && result; + } + + // Check the pathpaths table for consistency + if (result || log) + { + result = PathPartTable::CheckConsistency(connection, log) && result; + } + + // Check the 1:N map tables for consistency + if (result || log) + { + result = TagsTable::CheckConsistency(connection, log) && result; + } + + if (result || log) + { + result = CommandsTable::CheckConsistency(connection, log) && result; + } + + return result; + } + ISQLiteIndex::SearchResult Interface::Search(const SQLite::Connection& connection, const SearchRequest& request) const { // If an empty request, get everything diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp @@ -222,6 +222,38 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 builder.Execute(connection); } + + bool ManifestTableCheckConsistency(const SQLite::Connection& connection, const SQLite::Builder::QualifiedColumn& target, bool log) + { + using QCol = SQLite::Builder::QualifiedColumn; + + // Build a select statement to find manifest rows containing references to 1:1 tables with non-existent rowids + // Such as: + // Select manifest.rowid, manifest.id, ids.id from manifest left outer join ids on manifest.id = ids.rowid where ids.id is NULL + SQLite::Builder::StatementBuilder builder; + builder. + Select({ QCol(s_ManifestTable_Table_Name, SQLite::RowIDName), QCol(s_ManifestTable_Table_Name, target.Column) }). + From(s_ManifestTable_Table_Name). + LeftOuterJoin(target.Table).On(QCol(s_ManifestTable_Table_Name, target.Column), QCol(target.Table, SQLite::RowIDName)). + Where(target).IsNull(); + + SQLite::Statement select = builder.Prepare(connection); + bool result = true; + + while (select.Step()) + { + result = false; + + if (!log) + { + break; + } + + AICLI_LOG(Repo, Info, << " [INVALID] manifest [" << select.GetColumn<SQLite::rowid_t>(0) << "] refers to " << target.Table << " [" << select.GetColumn<SQLite::rowid_t>(1) << "]"); + } + + return result; + } } std::string_view ManifestTable::TableName() diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.h @@ -56,6 +56,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Update the value of a single column for the manifest with the given rowid. void ManifestTableUpdateValueIdById(SQLite::Connection& connection, std::string_view valueName, SQLite::rowid_t value, SQLite::rowid_t id); + + // Checks the consistency of the index to ensure that every referenced row exists. + // Returns true if index is consistent; false if it is not. + bool ManifestTableCheckConsistency(const SQLite::Connection& connection, const SQLite::Builder::QualifiedColumn& target, bool log); } // Info on the manifest columns. @@ -156,6 +160,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Removes data that is no longer needed for an index that is to be published. static void PrepareForPackaging_deprecated(SQLite::Connection& connection, std::initializer_list<std::string_view> values); + // Checks the consistency of the index to ensure that every referenced row exists. + // Returns true if index is consistent; false if it is not. + template <typename Table> + static bool CheckConsistency(const SQLite::Connection& connection, bool log) + { + return details::ManifestTableCheckConsistency(connection, SQLite::Builder::QualifiedColumn{ Table::TableName(), Table::ValueName() }, log); + } + // Determines if the table is empty. static bool IsEmpty(SQLite::Connection& connection); }; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToManyTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToManyTable.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "Microsoft/Schema/1_0/OneToManyTable.h" #include "Microsoft/Schema/1_0/OneToOneTable.h" +#include "Microsoft/Schema/1_0/ManifestTable.h" #include "SQLiteStatementBuilder.h" @@ -256,6 +257,79 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 OneToOneTablePrepareForPackaging(connection, tableName, useNamedIndeces, preserveValuesIndex); } + bool OneToManyTableCheckConsistency(const SQLite::Connection& connection, std::string_view tableName, std::string_view valueName, bool log) + { + using QCol = SQLite::Builder::QualifiedColumn; + constexpr std::string_view s_map = "map"sv; + + bool result = true; + + { + // Build a select statement to find map rows containing references to manifests with non-existent rowids + // Such as: + // Select map.rowid, map.manifest from tags_map as map left outer join manifest on map.manifest = manifest.rowid where manifest.id is null + + SQLite::Builder::StatementBuilder builder; + builder. + Select({ QCol(s_map, SQLite::RowIDName), QCol(s_map, s_OneToManyTable_MapTable_ManifestName) }). + From({ tableName, s_OneToManyTable_MapTable_Suffix }).As(s_map). + LeftOuterJoin(ManifestTable::TableName()).On(QCol(s_map, s_OneToManyTable_MapTable_ManifestName), QCol(ManifestTable::TableName(), SQLite::RowIDName)). + Where(QCol(ManifestTable::TableName(), SQLite::RowIDName)).IsNull(); + + SQLite::Statement select = builder.Prepare(connection); + + while (select.Step()) + { + result = false; + + if (!log) + { + break; + } + + AICLI_LOG(Repo, Info, << " [INVALID] " << tableName << s_OneToManyTable_MapTable_Suffix << " [" << select.GetColumn<SQLite::rowid_t>(0) << + "] refers to " << ManifestTable::TableName() << " [" << select.GetColumn<SQLite::rowid_t>(1) << "]"); + } + } + + if (!result && !log) + { + return result; + } + + { + // Build a select statement to find map rows containing references to 1:1 tables with non-existent rowids + // Such as: + // Select map.rowid, map.tag from tags_map as map left outer join tags on map.tag = tags.rowid where tags.tag is null + SQLite::Builder::StatementBuilder builder; + builder. + Select({ QCol(s_map, SQLite::RowIDName), QCol(s_map, valueName) }). + From({ tableName, s_OneToManyTable_MapTable_Suffix }).As(s_map). + LeftOuterJoin(tableName).On(QCol(s_map, valueName), QCol(tableName, SQLite::RowIDName)). + Where(QCol(tableName, valueName)).IsNull(); + + SQLite::Statement select = builder.Prepare(connection); + bool secondaryResult = true; + + while (select.Step()) + { + secondaryResult = false; + + if (!log) + { + break; + } + + AICLI_LOG(Repo, Info, << " [INVALID] " << tableName << s_OneToManyTable_MapTable_Suffix << " [" << select.GetColumn<SQLite::rowid_t>(0) << + "] refers to " << tableName << " [" << select.GetColumn<SQLite::rowid_t>(1) << "]"); + } + + result = result && secondaryResult; + } + + return result; + } + bool OneToManyTableIsEmpty(SQLite::Connection& connection, std::string_view tableName) { SQLite::Builder::StatementBuilder countBuilder; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToManyTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToManyTable.h @@ -36,6 +36,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Removes data that is no longer needed for an index that is to be published. void OneToManyTablePrepareForPackaging(SQLite::Connection& connection, std::string_view tableName, bool useNamedIndeces, bool preserveValuesIndex); + // Checks the consistency of the index to ensure that every referenced row exists. + // Returns true if index is consistent; false if it is not. + bool OneToManyTableCheckConsistency(const SQLite::Connection& connection, std::string_view tableName, std::string_view valueName, bool log); + // Determines if the table is empty. bool OneToManyTableIsEmpty(SQLite::Connection& connection, std::string_view tableName); } @@ -104,6 +108,13 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 details::OneToManyTablePrepareForPackaging(connection, TableInfo::TableName(), false, false); } + // Checks the consistency of the index to ensure that every referenced row exists. + // Returns true if index is consistent; false if it is not. + static bool CheckConsistency(const SQLite::Connection& connection, bool log) + { + return details::OneToManyTableCheckConsistency(connection, TableInfo::TableName(), TableInfo::ValueName(), log); + } + // Determines if the table is empty. static bool IsEmpty(SQLite::Connection& connection) { diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/PathPartTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/PathPartTable.cpp @@ -145,6 +145,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 savepoint.Commit(); } + std::string_view PathPartTable::TableName() + { + return s_PathPartTable_Table_Name; + } + std::string_view PathPartTable::ValueName() { return s_PathPartTable_PartValue_Name; @@ -296,6 +301,41 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 dropIndexBuilder.Execute(connection); } + bool PathPartTable::CheckConsistency(const SQLite::Connection& connection, bool log) + { + using QCol = SQLite::Builder::QualifiedColumn; + + // Build a select statement to find pathpart rows containing references to parents with non-existent rowids + // Such as: + // Select l.rowid, l.parent from pathparts as l left outer join pathparts as r on l.parent = r.rowid where l.parent is not null and r.pathpart is null + constexpr std::string_view s_left = "left"sv; + constexpr std::string_view s_right = "right"sv; + + SQLite::Builder::StatementBuilder builder; + builder. + Select({ QCol(s_left, SQLite::RowIDName), QCol(s_left, s_PathPartTable_ParentValue_Name) }). + From(s_PathPartTable_Table_Name).As(s_left). + LeftOuterJoin(s_PathPartTable_Table_Name).As(s_right).On(QCol(s_left, s_PathPartTable_ParentValue_Name), QCol(s_right, SQLite::RowIDName)). + Where(QCol(s_left, s_PathPartTable_ParentValue_Name)).IsNotNull().And(QCol(s_right, s_PathPartTable_PartValue_Name)).IsNull(); + + SQLite::Statement select = builder.Prepare(connection); + bool result = true; + + while (select.Step()) + { + result = false; + + if (!log) + { + break; + } + + AICLI_LOG(Repo, Info, << " [INVALID] pathparts [" << select.GetColumn<SQLite::rowid_t>(0) << "] refers to " << s_PathPartTable_ParentValue_Name << " [" << select.GetColumn<SQLite::rowid_t>(1) << "]"); + } + + return result; + } + bool PathPartTable::IsEmpty(SQLite::Connection& connection) { SQLite::Builder::StatementBuilder builder; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/PathPartTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/PathPartTable.h @@ -23,6 +23,9 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Creates the table with standard primary keys. static void Create_deprecated(SQLite::Connection& connection); + // Gets the table name. + static std::string_view TableName(); + // Gets the value name. static std::string_view ValueName(); @@ -49,6 +52,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Removes data that is no longer needed for an index that is to be published. static void PrepareForPackaging_deprecated(SQLite::Connection& connection); + // Checks the consistency of the index to ensure that every referenced row exists. + // Returns true if index is consistent; false if it is not. + static bool CheckConsistency(const SQLite::Connection& connection, bool log); + // Determines if the table is empty. static bool IsEmpty(SQLite::Connection& connection); }; diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface.h @@ -17,6 +17,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 std::pair<bool, SQLite::rowid_t> UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) override; SQLite::rowid_t RemoveManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::filesystem::path& relativePath) override; void PrepareForPackaging(SQLite::Connection& connection) override; + bool CheckConsistency(const SQLite::Connection& connection, bool log) const override; SearchResult Search(const SQLite::Connection& connection, const SearchRequest& request) const override; protected: diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface_1_1.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_1/Interface_1_1.cpp @@ -173,6 +173,24 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1 builder.Execute(connection); } + bool Interface::CheckConsistency(const SQLite::Connection& connection, bool log) const + { + bool result = V1_0::Interface::CheckConsistency(connection, log); + + // If the v1.0 index was consistent, or if full logging of inconsistency was requested, check the v1.1 data. + if (result || log) + { + result = PackageFamilyNameTable::CheckConsistency(connection, log) && result; + } + + if (result || log) + { + result = ProductCodeTable::CheckConsistency(connection, log) && result; + } + + return result; + } + ISQLiteIndex::SearchResult Interface::Search(const SQLite::Connection& connection, const SearchRequest& request) const { // Update any system reference strings to be folded diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h @@ -50,6 +50,10 @@ namespace AppInstaller::Repository::Microsoft::Schema // Removes data that is no longer needed for an index that is to be published. virtual void PrepareForPackaging(SQLite::Connection& connection) = 0; + // Checks the consistency of the index to ensure that every referenced row exists. + // Returns true if index is consistent; false if it is not. + virtual bool CheckConsistency(const SQLite::Connection& connection, bool log) const = 0; + // Performs a search based on the given criteria. virtual SearchResult Search(const SQLite::Connection& connection, const SearchRequest& request) const = 0; diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp @@ -350,9 +350,9 @@ namespace AppInstaller::Repository::SQLite::Builder return *this; } - StatementBuilder& StatementBuilder::IsNull() + StatementBuilder& StatementBuilder::IsNull(bool isNull) { - m_stream << " IS NULL"; + m_stream << " IS " << (isNull ? "" : "NOT ") << "NULL"; return *this; } @@ -386,6 +386,24 @@ namespace AppInstaller::Repository::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::LeftOuterJoin(std::string_view table) + { + OutputOperationAndTable(m_stream, " LEFT OUTER JOIN", table); + return *this; + } + + StatementBuilder& StatementBuilder::LeftOuterJoin(QualifiedTable table) + { + OutputOperationAndTable(m_stream, " LEFT OUTER JOIN", table); + return *this; + } + + StatementBuilder& StatementBuilder::LeftOuterJoin(std::initializer_list<std::string_view> table) + { + OutputOperationAndTable(m_stream, " LEFT OUTER JOIN", table); + return *this; + } + StatementBuilder& StatementBuilder::On(const QualifiedColumn& column1, const QualifiedColumn& column2) { m_stream << " ON " << column1 << " = " << column2; diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h @@ -226,7 +226,9 @@ namespace AppInstaller::Repository::SQLite::Builder StatementBuilder& Not(); StatementBuilder& In(); - StatementBuilder& IsNull(); + // IsNull(true) means the value is null; IsNull(false) means the value is not null. + StatementBuilder& IsNull(bool isNull = true); + StatementBuilder& IsNotNull() { return IsNull(false); } // Operators for combining filter clauses. StatementBuilder& And(std::string_view column); @@ -238,6 +240,12 @@ namespace AppInstaller::Repository::SQLite::Builder StatementBuilder& Join(QualifiedTable table); StatementBuilder& Join(std::initializer_list<std::string_view> table); + // Begin a left outer join clause. + // The initializer_list form enables the table name to be constructed from multiple parts. + StatementBuilder& LeftOuterJoin(std::string_view table); + StatementBuilder& LeftOuterJoin(QualifiedTable table); + StatementBuilder& LeftOuterJoin(std::initializer_list<std::string_view> table); + // Set the join constraint. StatementBuilder& On(const QualifiedColumn& column1, const QualifiedColumn& column2); diff --git a/src/WinGetUtil/Exports.cpp b/src/WinGetUtil/Exports.cpp @@ -157,6 +157,21 @@ extern "C" } CATCH_RETURN() + WINGET_UTIL_API WinGetSQLiteIndexCheckConsistency( + WINGET_SQLITE_INDEX_HANDLE index, + BOOL* succeeded) try + { + THROW_HR_IF(E_INVALIDARG, !index); + THROW_HR_IF(E_INVALIDARG, !succeeded); + + bool result = reinterpret_cast<SQLiteIndex*>(index)->CheckConsistency(true); + + *succeeded = (result ? TRUE : FALSE); + + return S_OK; + } + CATCH_RETURN() + WINGET_UTIL_API WinGetValidateManifest( WINGET_STRING manifestPath, BOOL* succeeded, diff --git a/src/WinGetUtil/WinGetUtil.h b/src/WinGetUtil/WinGetUtil.h @@ -68,6 +68,11 @@ extern "C" WINGET_UTIL_API WinGetSQLiteIndexPrepareForPackaging( WINGET_SQLITE_INDEX_HANDLE index); + // Checks the index for consistency, ensuring that at a minimum all referenced rows actually exist. + WINGET_UTIL_API WinGetSQLiteIndexCheckConsistency( + WINGET_SQLITE_INDEX_HANDLE index, + BOOL* succeeded); + // Validates a given manifest. Returns a bool for validation result and // a string representing validation errors if validation failed. WINGET_UTIL_API WinGetValidateManifest(