diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt
index 76c58be8d4..803710e0f2 100644
--- a/.github/actions/spelling/allow.txt
+++ b/.github/actions/spelling/allow.txt
@@ -2,7 +2,9 @@ abi
ACCESSDENIED
ACTIONDATA
ACTIONSTART
+addfile
addmanifest
+addportablefile
addstore
admins
alloc
@@ -88,6 +90,7 @@ cpprestsdk
cppwinrt
CPRWL
createnew
+createportabletable
createtables
cref
csproj
@@ -179,6 +182,7 @@ FILESINUSE
FILESUBTYPE
filesystem
FILETYPE
+filetype
FILEVERSION
FLUSHEACHLINE
forcerestart
@@ -242,6 +246,7 @@ interop
INVALIDARG
iomanip
iostream
+IPortable
ISAPPROVEDFOROUTPUT
isspace
istream
@@ -315,6 +320,7 @@ Nelon
netcoreapp
netstandard
newid
+NOCASE
NOCLOSEPROCESS
nodiscard
noexcept
@@ -376,6 +382,7 @@ pipssource
PKCS
placeholders
png
+portableindex
posix
powershell
pplx
@@ -416,7 +423,9 @@ REFCLSID
REFCOUNT
regex
regexp
+removefile
removemanifest
+removeportablefile
repolibtest
requeue
rescap
@@ -524,6 +533,7 @@ SWIPECONTROL
SYMED
symlink
symlinks
+symlinktarget
Sys
sz
TARG
@@ -538,6 +548,7 @@ tempdb
terabyte
testcontainer
testmoniker
+TESTPORTABLEFILE
Testrun
testsettingname
TEXTFORMAT
@@ -588,7 +599,9 @@ uninstalling
Unregister
Unregisters
untimes
+updatefile
updatemanifest
+updateportablefile
UPLEVEL
upvote
uregex
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
index bd31a99920..5fe317e436 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
@@ -213,6 +213,7 @@
+
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
index 9c50142299..e4f7c48cfd 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
@@ -143,6 +143,9 @@
Source Files\Repository
+
+ Source Files\Repository
+
Source Files\Repository
diff --git a/src/AppInstallerCLITests/PortableIndex.cpp b/src/AppInstallerCLITests/PortableIndex.cpp
new file mode 100644
index 0000000000..aadc1bf5b9
--- /dev/null
+++ b/src/AppInstallerCLITests/PortableIndex.cpp
@@ -0,0 +1,200 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "TestCommon.h"
+#include
+#include
+#include
+#include
+#include
+
+using namespace std::string_literals;
+using namespace TestCommon;
+using namespace AppInstaller::Repository::Microsoft;
+using namespace AppInstaller::Repository::SQLite;
+using namespace AppInstaller::Repository::Microsoft::Schema;
+
+void CreateFakePortableFile(IPortableIndex::PortableFile& file)
+{
+ file.SetFilePath("testPortableFile.exe");
+ file.FileType = IPortableIndex::PortableFileType::File;
+ file.SHA256 = "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b";
+ file.SymlinkTarget = "testSymlinkTarget.exe";
+}
+
+TEST_CASE("PortableIndexCreateLatestAndReopen", "[portableIndex]")
+{
+ TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
+ INFO("Using temporary file named: " << tempFile.GetPath());
+
+ Schema::Version versionCreated;
+
+ // Create the index
+ {
+ PortableIndex index = PortableIndex::CreateNew(tempFile, Schema::Version::Latest());
+ versionCreated = index.GetVersion();
+ }
+
+ // Reopen the index for read only
+ {
+ INFO("Trying with Read");
+ PortableIndex index = PortableIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::Read);
+ Schema::Version versionRead = index.GetVersion();
+ REQUIRE(versionRead == versionCreated);
+ }
+
+ // Reopen the index for read/write
+ {
+ INFO("Trying with ReadWrite");
+ PortableIndex index = PortableIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
+ Schema::Version versionRead = index.GetVersion();
+ REQUIRE(versionRead == versionCreated);
+ }
+
+ // Reopen the index for immutable read
+ {
+ INFO("Trying with Immutable");
+ PortableIndex index = PortableIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::Immutable);
+ Schema::Version versionRead = index.GetVersion();
+ REQUIRE(versionRead == versionCreated);
+ }
+}
+
+TEST_CASE("PortableIndexAddEntryToTable", "[portableIndex]")
+{
+ TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
+ INFO("Using temporary file named: " << tempFile.GetPath());
+
+ IPortableIndex::PortableFile portableFile;
+ CreateFakePortableFile(portableFile);
+
+ {
+ PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 });
+ index.AddPortableFile(portableFile);
+ }
+
+ {
+ // Open it directly to directly test table state
+ Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite);
+ REQUIRE(!Schema::Portable_V1_0::PortableTable::IsEmpty(connection));
+ }
+
+ {
+ PortableIndex index = PortableIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
+ index.RemovePortableFile(portableFile);
+ }
+
+ {
+ // Open it directly to directly test table state
+ Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite);
+ REQUIRE(Schema::Portable_V1_0::PortableTable::IsEmpty(connection));
+ }
+}
+
+TEST_CASE("PortableIndex_AddUpdateRemove", "[portableIndex]")
+{
+ TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
+ INFO("Using temporary file named: " << tempFile.GetPath());
+
+ IPortableIndex::PortableFile portableFile;
+ CreateFakePortableFile(portableFile);
+
+ PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 });
+ index.AddPortableFile(portableFile);
+
+ // Apply changes to portable file
+ std::string updatedHash = "2db8ae7657c6622b04700137740002c51c36588e566651c9f67b4b096c8ad18b";
+ portableFile.FileType = IPortableIndex::PortableFileType::Symlink;
+ portableFile.SHA256 = updatedHash;
+ portableFile.SymlinkTarget = "fakeSymlinkTarget.exe";
+
+ REQUIRE(index.UpdatePortableFile(portableFile));
+
+ {
+ Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly);
+ auto fileFromIndex = Schema::Portable_V1_0::PortableTable::GetPortableFileById(connection, 1);
+ REQUIRE(fileFromIndex.has_value());
+ REQUIRE(fileFromIndex->GetFilePath() == "testPortableFile.exe");
+ REQUIRE(fileFromIndex->FileType == IPortableIndex::PortableFileType::Symlink);
+ REQUIRE(fileFromIndex->SHA256 == updatedHash);
+ REQUIRE(fileFromIndex->SymlinkTarget == "fakeSymlinkTarget.exe");
+ }
+
+ {
+ PortableIndex index2 = PortableIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
+ index2.RemovePortableFile(portableFile);
+ }
+
+ {
+ // Open it directly to directly test table state
+ Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite);
+ REQUIRE(Schema::Portable_V1_0::PortableTable::IsEmpty(connection));
+ }
+}
+
+TEST_CASE("PortableIndex_UpdateFile_CaseInsensitive", "[portableIndex]")
+{
+ TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
+ INFO("Using temporary file named: " << tempFile.GetPath());
+
+ IPortableIndex::PortableFile portableFile;
+ CreateFakePortableFile(portableFile);
+
+ PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 });
+ index.AddPortableFile(portableFile);
+
+ // By default, portable file path is set to "testPortableFile.exe"
+ // Change file path to all upper case should still successfully update.
+ portableFile.SetFilePath("TESTPORTABLEFILE.exe");
+ std::string updatedHash = "2db8ae7657c6622b04700137740002c51c36588e566651c9f67b4b096c8ad18b";
+ portableFile.FileType = IPortableIndex::PortableFileType::Symlink;
+ portableFile.SHA256 = updatedHash;
+ portableFile.SymlinkTarget = "fakeSymlinkTarget.exe";
+
+ REQUIRE(index.UpdatePortableFile(portableFile));
+
+ {
+ Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadOnly);
+ auto fileFromIndex = Schema::Portable_V1_0::PortableTable::GetPortableFileById(connection, 1);
+ REQUIRE(fileFromIndex.has_value());
+ REQUIRE(fileFromIndex->GetFilePath() == "TESTPORTABLEFILE.exe");
+ REQUIRE(fileFromIndex->FileType == IPortableIndex::PortableFileType::Symlink);
+ REQUIRE(fileFromIndex->SHA256 == updatedHash);
+ REQUIRE(fileFromIndex->SymlinkTarget == "fakeSymlinkTarget.exe");
+ }
+}
+
+TEST_CASE("PortableIndex_AddDuplicateFile", "[portableIndex]")
+{
+ TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
+ INFO("Using temporary file named: " << tempFile.GetPath());
+
+ IPortableIndex::PortableFile portableFile;
+ CreateFakePortableFile(portableFile);
+
+ PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 });
+ index.AddPortableFile(portableFile);
+
+ // Change file path to all upper case. Adding duplicate file should fail.
+ portableFile.SetFilePath("TESTPORTABLEFILE.exe");
+ REQUIRE_THROWS(index.AddPortableFile(portableFile), ERROR_ALREADY_EXISTS);
+}
+
+TEST_CASE("PortableIndex_RemoveWithId", "[portableIndex]")
+{
+ TempFile tempFile{ "repolibtest_tempdb"s, ".db"s };
+ INFO("Using temporary file named: " << tempFile.GetPath());
+
+ IPortableIndex::PortableFile portableFile;
+ CreateFakePortableFile(portableFile);
+
+ PortableIndex index = PortableIndex::CreateNew(tempFile, { 1, 0 });
+ index.AddPortableFile(portableFile);
+
+ {
+ Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::ReadWrite);
+ REQUIRE(Portable_V1_0::PortableTable::ExistsById(connection, 1));
+ Portable_V1_0::PortableTable::DeleteById(connection, 1);
+ REQUIRE_FALSE(Portable_V1_0::PortableTable::ExistsById(connection, 1));
+ }
+}
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/SQLiteIndex.cpp b/src/AppInstallerCLITests/SQLiteIndex.cpp
index 6010a659c2..9884fbe33f 100644
--- a/src/AppInstallerCLITests/SQLiteIndex.cpp
+++ b/src/AppInstallerCLITests/SQLiteIndex.cpp
@@ -368,7 +368,7 @@ TEST_CASE("SQLiteIndexCreateLatestAndReopen", "[sqliteindex]")
// Reopen the index for read only
{
INFO("Trying with Read");
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::Read);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::Read);
Schema::Version versionRead = index.GetVersion();
REQUIRE(versionRead == versionCreated);
}
@@ -376,7 +376,7 @@ TEST_CASE("SQLiteIndexCreateLatestAndReopen", "[sqliteindex]")
// Reopen the index for read/write
{
INFO("Trying with ReadWrite");
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
Schema::Version versionRead = index.GetVersion();
REQUIRE(versionRead == versionCreated);
}
@@ -384,7 +384,7 @@ TEST_CASE("SQLiteIndexCreateLatestAndReopen", "[sqliteindex]")
// Reopen the index for immutable read
{
INFO("Trying with Immutable");
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::Immutable);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::Immutable);
Schema::Version versionRead = index.GetVersion();
REQUIRE(versionRead == versionCreated);
}
@@ -611,7 +611,7 @@ TEST_CASE("SQLiteIndex_DependenciesTable_CheckConsistency", "[sqliteindex][V1_4]
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
REQUIRE(!index.CheckConsistency(true));
}
@@ -729,7 +729,7 @@ TEST_CASE("SQLiteIndex_RemoveManifest", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
// Now remove manifest2
index.RemoveManifest(manifest2, manifest2Path);
@@ -1113,7 +1113,7 @@ TEST_CASE("SQLiteIndex_UpdateManifest", "[sqliteindex][V1_4]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
// Update with no updates should return false
REQUIRE(!index.UpdateManifest(manifest, manifestPath));
@@ -1149,7 +1149,7 @@ TEST_CASE("SQLiteIndex_UpdateManifest", "[sqliteindex][V1_4]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
// Now remove manifest2
index.RemoveManifest(manifest, manifestPath);
@@ -1267,7 +1267,7 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangePath", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
manifestPath = "test/newid/test.newid-1.0.0.yaml";
@@ -1291,7 +1291,7 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangePath", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
// Now remove manifest, with unknown path
index.RemoveManifest(manifest, "");
@@ -1348,7 +1348,7 @@ TEST_CASE("SQLiteIndex_UpdateManifest_Pathless", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
// Update with no updates should return false
REQUIRE(!index.UpdateManifest(manifest));
@@ -1384,7 +1384,7 @@ TEST_CASE("SQLiteIndex_UpdateManifest_Pathless", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
// Now remove manifest2
index.RemoveManifest(manifest);
@@ -1426,7 +1426,7 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangeCase", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
manifest.Id = "Test.Id";
@@ -1435,7 +1435,7 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangeCase", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
manifest.Version = "1.0.0-Test";
@@ -1444,7 +1444,7 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangeCase", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
manifest.Channel = "Test";
@@ -1453,7 +1453,7 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangeCase", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
manifest.DefaultLocalization.Add("test name");
@@ -1462,7 +1462,7 @@ TEST_CASE("SQLiteIndex_UpdateManifestChangeCase", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
// Now remove manifest, with unknown path
index.RemoveManifest(manifest, "");
@@ -1513,7 +1513,7 @@ TEST_CASE("SQLiteIndex_IdCaseInsensitivity", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
index.AddManifest(manifest2, manifest2Path);
@@ -1523,7 +1523,7 @@ TEST_CASE("SQLiteIndex_IdCaseInsensitivity", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
manifest1.Id = "TEST.ID";
@@ -1535,7 +1535,7 @@ TEST_CASE("SQLiteIndex_IdCaseInsensitivity", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
index.RemoveManifest(manifest1, manifest1Path);
@@ -1545,7 +1545,7 @@ TEST_CASE("SQLiteIndex_IdCaseInsensitivity", "[sqliteindex][V1_0]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
index.RemoveManifest(manifest2, manifest2Path);
@@ -2648,7 +2648,7 @@ TEST_CASE("SQLiteIndex_CheckConsistency_Failure", "[sqliteindex][V1_1]")
}
{
- SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteIndex::OpenDisposition::ReadWrite);
+ SQLiteIndex index = SQLiteIndex::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite);
REQUIRE(!index.CheckConsistency(true));
}
diff --git a/src/AppInstallerCLITests/SQLiteWrapper.cpp b/src/AppInstallerCLITests/SQLiteWrapper.cpp
index ab1212db99..129fa9f174 100644
--- a/src/AppInstallerCLITests/SQLiteWrapper.cpp
+++ b/src/AppInstallerCLITests/SQLiteWrapper.cpp
@@ -451,6 +451,40 @@ TEST_CASE("SQLBuilder_Update", "[sqlbuilder]")
SelectFromSimpleTestTableOnlyOneRow(connection, firstVal, secondVal);
}
+TEST_CASE("SQLBuilder_CaseInsensitive", "[sqlbuilder")
+{
+ Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create);
+
+ Builder::StatementBuilder createTable;
+ createTable.CreateTable(s_tableName).Columns({
+ Builder::ColumnBuilder(s_firstColumn, Builder::Type::Text).CollateNoCase()
+ });
+
+ createTable.Execute(connection);
+
+ std::string upperCaseVal = "TEST";
+ std::string lowerCaseVal = "test";
+
+ {
+ INFO("Insert initial value");
+ Builder::StatementBuilder builder;
+ builder.InsertInto(s_tableName)
+ .Columns({ s_firstColumn })
+ .Values(upperCaseVal);
+
+ builder.Execute(connection);
+ }
+
+ {
+ INFO("Retrieve using case-insensitive value");
+ Builder::StatementBuilder builder;
+ builder.Select({ s_firstColumn }).From(s_tableName).Where(s_firstColumn).Equals(lowerCaseVal);
+
+ auto statement = builder.Prepare(connection);
+ REQUIRE(statement.Step());
+ }
+}
+
TEST_CASE("SQLBuilder_CreateTable", "[sqlbuilder]")
{
Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create);
diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj
index 80103826bd..51988e0dce 100644
--- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj
+++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj
@@ -237,6 +237,7 @@
+
@@ -272,11 +273,15 @@
+
+
+
+
@@ -329,6 +334,7 @@
+
@@ -350,9 +356,12 @@
+
+
+
diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters
index 25b7164366..904db25747 100644
--- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters
+++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters
@@ -67,6 +67,9 @@
{84a55def-9fb8-4c90-8d5a-2cedc171940b}
+
+ {edef5ff7-9bfe-48f8-a179-e343d1a8b57f}
+
@@ -300,6 +303,21 @@
Microsoft\Schema\1_6
+
+ Microsoft\Schema\Portable_1_0
+
+
+ Microsoft\Schema\Portable_1_0
+
+
+ Microsoft
+
+
+ Microsoft\Schema
+
+
+ Microsoft
+
@@ -470,6 +488,18 @@
Microsoft\Schema\1_6
+
+ Microsoft
+
+
+ Microsoft\Schema\Portable_1_0
+
+
+ Microsoft\Schema\Portable_1_0
+
+
+ Microsoft
+
diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp
new file mode 100644
index 0000000000..3b09ee00a0
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp
@@ -0,0 +1,102 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "PortableIndex.h"
+#include "SQLiteStorageBase.h"
+#include "Schema/Portable_1_0/PortableIndexInterface.h"
+
+namespace AppInstaller::Repository::Microsoft
+{
+ PortableIndex PortableIndex::CreateNew(const std::string& filePath, Schema::Version version)
+ {
+ AICLI_LOG(Repo, Info, << "Creating new Portable Index [" << version << "] at '" << filePath << "'");
+ PortableIndex result{ filePath, version };
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(result.m_dbconn, "portableindex_createnew");
+
+ // Use calculated version, as incoming version could be 'latest'
+ result.m_version.SetSchemaVersion(result.m_dbconn);
+
+ result.m_interface->CreateTable(result.m_dbconn);
+
+ result.SetLastWriteTime();
+
+ savepoint.Commit();
+
+ return result;
+ }
+
+ PortableIndex::IdType PortableIndex::AddPortableFile(const Schema::IPortableIndex::PortableFile& file)
+ {
+ std::lock_guard lockInterface{ *m_interfaceLock };
+ AICLI_LOG(Repo, Verbose, << "Adding portable file for [" << file.GetFilePath() << "]");
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "portableindex_addfile");
+
+ IdType result = m_interface->AddPortableFile(m_dbconn, file);
+
+ SetLastWriteTime();
+
+ savepoint.Commit();
+
+ return result;
+ }
+
+ void PortableIndex::RemovePortableFile(const Schema::IPortableIndex::PortableFile& file)
+ {
+ AICLI_LOG(Repo, Verbose, << "Removing portable file [" << file.GetFilePath() << "]");
+ std::lock_guard lockInterface{ *m_interfaceLock };
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "portableindex_removefile");
+
+ m_interface->RemovePortableFile(m_dbconn, file);
+
+ SetLastWriteTime();
+
+ savepoint.Commit();
+ }
+
+ bool PortableIndex::UpdatePortableFile(const Schema::IPortableIndex::PortableFile& file)
+ {
+ AICLI_LOG(Repo, Verbose, << "Updating portable file [" << file.GetFilePath() << "]");
+ std::lock_guard lockInterface{ *m_interfaceLock };
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "portableindex_updatefile");
+
+ bool result = m_interface->UpdatePortableFile(m_dbconn, file).first;
+
+ if (result)
+ {
+ SetLastWriteTime();
+ savepoint.Commit();
+ }
+
+ return result;
+ }
+
+ std::unique_ptr PortableIndex::CreateIPortableIndex() const
+ {
+ if (m_version == Schema::Version{ 1, 0 } ||
+ m_version.MajorVersion == 1 ||
+ m_version.IsLatest())
+ {
+ return std::make_unique();
+ }
+
+ THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
+ }
+
+ PortableIndex::PortableIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile) :
+ SQLiteStorageBase(target, disposition, std::move(indexFile))
+ {
+ AICLI_LOG(Repo, Info, << "Opened Portable Index with version [" << m_version << "], last write [" << GetLastWriteTime() << "]");
+ m_interface = CreateIPortableIndex();
+ THROW_HR_IF(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX, disposition == SQLiteStorageBase::OpenDisposition::ReadWrite && m_version != m_interface->GetVersion());
+ }
+
+ PortableIndex::PortableIndex(const std::string& target, Schema::Version version) : SQLiteStorageBase(target, version)
+ {
+ m_interface = CreateIPortableIndex();
+ m_version = m_interface->GetVersion();
+ }
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h
new file mode 100644
index 0000000000..2becefd82c
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "SQLiteWrapper.h"
+#include "Microsoft/Schema/IPortableIndex.h"
+#include "Microsoft/Schema/Portable_1_0/PortableTable.h"
+#include "Microsoft/SQLiteStorageBase.h"
+#include
+
+namespace AppInstaller::Repository::Microsoft
+{
+ struct PortableIndex : SQLiteStorageBase
+ {
+ // An id that refers to a specific portable file.
+ using IdType = SQLite::rowid_t;
+
+ PortableIndex(const PortableIndex&) = delete;
+ PortableIndex& operator=(const PortableIndex&) = delete;
+
+ PortableIndex(PortableIndex&&) = default;
+ PortableIndex& operator=(PortableIndex&&) = default;
+
+ // Creates a new PortableIndex database of the given version.
+ static PortableIndex CreateNew(const std::string& filePath, Schema::Version version = Schema::Version::Latest());
+
+ // Opens an existing PortableIndex database.
+ static PortableIndex Open(const std::string& filePath, OpenDisposition disposition, Utility::ManagedFile&& indexFile = {})
+ {
+ return { filePath, disposition, std::move(indexFile) };
+ }
+
+ IdType AddPortableFile(const Schema::IPortableIndex::PortableFile& file);
+
+ void RemovePortableFile(const Schema::IPortableIndex::PortableFile& file);
+
+ bool UpdatePortableFile(const Schema::IPortableIndex::PortableFile& file);
+
+ private:
+ // Constructor used to open an existing index.
+ PortableIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile);
+
+ // Constructor used to create a new index.
+ PortableIndex(const std::string& target, Schema::Version version);
+
+ // Creates the IPortableIndex interface object for this version.
+ std::unique_ptr CreateIPortableIndex() const;
+
+ std::unique_ptr m_interface;
+ };
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp
index 906ce77744..24df0fa6e2 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp
+++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp
@@ -2,30 +2,20 @@
// Licensed under the MIT License.
#include "pch.h"
#include "SQLiteIndex.h"
-#include "Schema/MetadataTable.h"
+#include "SQLiteStorageBase.h"
#include "ArpVersionValidation.h"
#include
+#include "Schema/1_0/Interface.h"
+#include "Schema/1_1/Interface.h"
+#include "Schema/1_2/Interface.h"
+#include "Schema/1_3/Interface.h"
+#include "Schema/1_4/Interface.h"
+#include "Schema/1_5/Interface.h"
+#include "Schema/1_6/Interface.h"
+
namespace AppInstaller::Repository::Microsoft
{
- namespace
- {
- char const* const GetOpenDispositionString(SQLiteIndex::OpenDisposition disposition)
- {
- switch (disposition)
- {
- case AppInstaller::Repository::Microsoft::SQLiteIndex::OpenDisposition::Read:
- return "Read";
- case AppInstaller::Repository::Microsoft::SQLiteIndex::OpenDisposition::ReadWrite:
- return "ReadWrite";
- case AppInstaller::Repository::Microsoft::SQLiteIndex::OpenDisposition::Immutable:
- return "ImmutableRead";
- default:
- return "Unknown";
- }
- }
- }
-
SQLiteIndex SQLiteIndex::CreateNew(const std::string& filePath, Schema::Version version, CreateOptions options)
{
AICLI_LOG(Repo, Info, << "Creating new SQLite Index [" << version << "] at '" << filePath << "'");
@@ -33,7 +23,6 @@ namespace AppInstaller::Repository::Microsoft
SQLite::Savepoint savepoint = SQLite::Savepoint::Create(result.m_dbconn, "sqliteindex_createnew");
- Schema::MetadataTable::Create(result.m_dbconn);
// Use calculated version, as incoming version could be 'latest'
result.m_version.SetSchemaVersion(result.m_dbconn);
@@ -46,89 +35,66 @@ namespace AppInstaller::Repository::Microsoft
return result;
}
- SQLiteIndex SQLiteIndex::Open(const std::string& filePath, OpenDisposition disposition, Utility::ManagedFile&& indexFile)
+ std::unique_ptr SQLiteIndex::CreateISQLiteIndex() const
{
- AICLI_LOG(Repo, Info, << "Opening SQLite Index for " << GetOpenDispositionString(disposition) << " at '" << filePath << "'");
- switch (disposition)
+ using namespace Schema;
+
+ if (m_version == Version{ 1, 0 })
+ {
+ return std::make_unique();
+ }
+ else if (m_version == Version{ 1, 1 })
+ {
+ return std::make_unique();
+ }
+ else if (m_version == Version{ 1, 2 })
+ {
+ return std::make_unique();
+ }
+ else if (m_version == Version{ 1, 3 })
+ {
+ return std::make_unique();
+ }
+ else if (m_version == Version{ 1, 4 })
{
- case AppInstaller::Repository::Microsoft::SQLiteIndex::OpenDisposition::Read:
- return { filePath, SQLite::Connection::OpenDisposition::ReadOnly, SQLite::Connection::OpenFlags::None, std::move(indexFile) };
- case AppInstaller::Repository::Microsoft::SQLiteIndex::OpenDisposition::ReadWrite:
- return { filePath, SQLite::Connection::OpenDisposition::ReadWrite, SQLite::Connection::OpenFlags::None, std::move(indexFile) };
- case AppInstaller::Repository::Microsoft::SQLiteIndex::OpenDisposition::Immutable:
+ return std::make_unique();
+ }
+ else if (m_version == Version{ 1, 5 })
{
- // Following the algorithm set forth at https://sqlite.org/uri.html [3.1] to convert to a URI path
- // The execution order builds out the string so that it shouldn't require any moves (other than growing)
- std::string target;
- // Add an 'arbitrary' growth size to prevent the majority of needing to grow (adding 'file:/' and '?immutable=1')
- target.reserve(filePath.size() + 20);
-
- target += "file:";
-
- bool wasLastCharSlash = false;
-
- if (filePath.size() >= 2 && filePath[1] == ':' &&
- ((filePath[0] >= 'a' && filePath[0] <= 'z') ||
- (filePath[0] >= 'A' && filePath[0] <= 'Z')))
- {
- target += '/';
- wasLastCharSlash = true;
- }
-
- for (char c : filePath)
- {
- bool wasThisCharSlash = false;
- switch (c)
- {
- case '?': target += "%3f"; break;
- case '#': target += "%23"; break;
- case '\\':
- case '/':
- {
- wasThisCharSlash = true;
- if (!wasLastCharSlash)
- {
- target += '/';
- }
- break;
- }
- default: target += c; break;
- }
-
- wasLastCharSlash = wasThisCharSlash;
- }
-
- target += "?immutable=1";
-
- return { target, SQLite::Connection::OpenDisposition::ReadOnly, SQLite::Connection::OpenFlags::Uri, std::move(indexFile) };
+ return std::make_unique();
}
- default:
- THROW_HR(E_UNEXPECTED);
+ else if (m_version == Version{ 1, 6 } ||
+ m_version.MajorVersion == 1 ||
+ m_version.IsLatest())
+ {
+ return std::make_unique();
}
+
+ // We do not have the capacity to operate on this schema version
+ THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
}
- SQLiteIndex::SQLiteIndex(const std::string& target, SQLite::Connection::OpenDisposition disposition, SQLite::Connection::OpenFlags flags, Utility::ManagedFile&& indexFile) :
- m_dbconn(SQLite::Connection::Create(target, disposition, flags)), m_indexFile(std::move(indexFile))
+ SQLiteIndex::SQLiteIndex(const std::string& target, Schema::Version version) : SQLiteStorageBase(target, version)
{
m_dbconn.EnableICU();
- m_version = Schema::Version::GetSchemaVersion(m_dbconn);
- AICLI_LOG(Repo, Info, << "Opened SQLite Index with version [" << m_version << "], last write [" << GetLastWriteTime() << "]");
- m_interface = m_version.CreateISQLiteIndex();
- THROW_HR_IF(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX, disposition == SQLite::Connection::OpenDisposition::ReadWrite && m_version != m_interface->GetVersion());
+ m_interface = CreateISQLiteIndex();
+ m_version = m_interface->GetVersion();
}
- SQLiteIndex::SQLiteIndex(const std::string& target, Schema::Version version) :
- m_dbconn(SQLite::Connection::Create(target, SQLite::Connection::OpenDisposition::Create))
+ SQLiteIndex::SQLiteIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile) :
+ SQLiteStorageBase(target, disposition, std::move(indexFile))
{
m_dbconn.EnableICU();
- m_interface = version.CreateISQLiteIndex();
- m_version = m_interface->GetVersion();
+ AICLI_LOG(Repo, Info, << "Opened SQLite Index with version [" << m_version << "], last write [" << GetLastWriteTime() << "]");
+ m_interface = CreateISQLiteIndex();
+ THROW_HR_IF(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX, disposition == SQLiteStorageBase::OpenDisposition::ReadWrite && m_version != m_interface->GetVersion());
}
#ifndef AICLI_DISABLE_TEST_HOOKS
void SQLiteIndex::ForceVersion(const Schema::Version& version)
{
- m_interface = version.CreateISQLiteIndex();
+ m_version = version;
+ m_interface = CreateISQLiteIndex();
}
#endif
@@ -324,17 +290,4 @@ namespace AppInstaller::Repository::Microsoft
{
return m_interface->GetDependentsById(m_dbconn, packageId);
}
-
- // Recording last write time based on MSDN documentation stating that time returns a POSIX epoch time and thus
- // should be consistent across systems.
- void SQLiteIndex::SetLastWriteTime()
- {
- Schema::MetadataTable::SetNamedValue(m_dbconn, Schema::s_MetadataValueName_LastWriteTime, Utility::GetCurrentUnixEpoch());
- }
-
- std::chrono::system_clock::time_point SQLiteIndex::GetLastWriteTime()
- {
- int64_t lastWriteTime = Schema::MetadataTable::GetNamedValue(m_dbconn, Schema::s_MetadataValueName_LastWriteTime);
- return Utility::ConvertUnixEpochToSystemClock(lastWriteTime);
- }
}
diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h
index 05fe0e613d..f8201adc7e 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h
+++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h
@@ -4,6 +4,7 @@
#include "SQLiteWrapper.h"
#include "Microsoft/Schema/ISQLiteIndex.h"
#include "Microsoft/Schema/Version.h"
+#include "Microsoft/SQLiteStorageBase.h"
#include "ISource.h"
#include
#include
@@ -11,7 +12,6 @@
#include
#include
-#include
#include
#include
#include
@@ -23,7 +23,7 @@
namespace AppInstaller::Repository::Microsoft
{
// Holds the connection to the database, as well as the appropriate functionality to interface with it.
- struct SQLiteIndex
+ struct SQLiteIndex : SQLiteStorageBase
{
// An id that refers to a specific application.
using IdType = SQLite::rowid_t;
@@ -46,22 +46,11 @@ namespace AppInstaller::Repository::Microsoft
// Creates a new index database of the given version.
static SQLiteIndex CreateNew(const std::string& filePath, Schema::Version version = Schema::Version::Latest(), CreateOptions options = CreateOptions::None);
- // The disposition for opening the index.
- enum class OpenDisposition
+ // Opens an existing SQLiteIndex database.
+ static SQLiteIndex Open(const std::string& filePath, OpenDisposition disposition, Utility::ManagedFile&& indexFile = {})
{
- // Open for read only.
- Read,
- // Open for read and write.
- ReadWrite,
- // The database will not change while in use; open for immutable read.
- Immutable,
- };
-
- // Opens an existing index database.
- static SQLiteIndex Open(const std::string& filePath, OpenDisposition disposition, Utility::ManagedFile&& indexFile = {});
-
- // Gets the schema version of the index.
- Schema::Version GetVersion() const { return m_version; }
+ return { filePath, disposition, std::move(indexFile) };
+ }
#ifndef AICLI_DISABLE_TEST_HOOKS
// Changes the version of the interface being used to operate on the database.
@@ -69,9 +58,6 @@ namespace AppInstaller::Repository::Microsoft
void ForceVersion(const Schema::Version& version);
#endif
- // Gets the last write time for the index.
- std::chrono::system_clock::time_point GetLastWriteTime();
-
// Adds the manifest at the repository relative path to the index.
// If the function succeeds, the manifest has been added.
// Returns the manifest id.
@@ -151,23 +137,19 @@ namespace AppInstaller::Repository::Microsoft
std::set> GetDependenciesByManifestRowId(SQLite::rowid_t manifestRowId) const;
std::vector> GetDependentsById(AppInstaller::Manifest::string_t packageId) const;
private:
- // Constructor used to open an existing index.
- SQLiteIndex(const std::string& target, SQLite::Connection::OpenDisposition disposition, SQLite::Connection::OpenFlags flags, Utility::ManagedFile&& indexFile);
-
// Constructor used to create a new index.
SQLiteIndex(const std::string& target, Schema::Version version);
+ // Constructor used to open an existing index.
+ SQLiteIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile);
+
// Internal functions to normalize on the relativePath being present.
IdType AddManifestInternal(const Manifest::Manifest& manifest, const std::optional& relativePath);
bool UpdateManifestInternal(const Manifest::Manifest& manifest, const std::optional& relativePath);
- // Sets the last write time metadata value in the index.
- void SetLastWriteTime();
+ // Creates the ISQLiteIndex interface object for this version.
+ std::unique_ptr CreateISQLiteIndex() const;
- Utility::ManagedFile m_indexFile;
- SQLite::Connection m_dbconn;
- Schema::Version m_version;
std::unique_ptr m_interface;
- std::unique_ptr m_interfaceLock = std::make_unique();
};
-}
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteStorageBase.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteStorageBase.cpp
new file mode 100644
index 0000000000..e2e3fc7804
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteStorageBase.cpp
@@ -0,0 +1,114 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "SQLiteStorageBase.h"
+#include "Schema/MetadataTable.h"
+
+namespace AppInstaller::Repository::Microsoft
+{
+ namespace
+ {
+ static char const* const GetOpenDispositionString(SQLiteStorageBase::OpenDisposition disposition)
+ {
+ switch (disposition)
+ {
+ case AppInstaller::Repository::Microsoft::SQLiteStorageBase::OpenDisposition::Read:
+ return "Read";
+ case AppInstaller::Repository::Microsoft::SQLiteStorageBase::OpenDisposition::ReadWrite:
+ return "ReadWrite";
+ case AppInstaller::Repository::Microsoft::SQLiteStorageBase::OpenDisposition::Immutable:
+ return "ImmutableRead";
+ default:
+ return "Unknown";
+ }
+ }
+ }
+
+ // One method for converting open disposition to proper open disposition
+ // another method for obtaining the right flags
+ void SQLiteStorageBase::SetLastWriteTime()
+ {
+ Schema::MetadataTable::SetNamedValue(m_dbconn, Schema::s_MetadataValueName_LastWriteTime, Utility::GetCurrentUnixEpoch());
+ }
+
+ // Recording last write time based on MSDN documentation stating that time returns a POSIX epoch time and thus
+ // should be consistent across systems.
+ std::chrono::system_clock::time_point SQLiteStorageBase::GetLastWriteTime()
+ {
+ int64_t lastWriteTime = Schema::MetadataTable::GetNamedValue(m_dbconn, Schema::s_MetadataValueName_LastWriteTime);
+ return Utility::ConvertUnixEpochToSystemClock(lastWriteTime);
+ }
+
+ SQLiteStorageBase::SQLiteStorageBase(const std::string& filePath, OpenDisposition disposition, Utility::ManagedFile&& indexFile) :
+ m_indexFile(std::move(indexFile))
+ {
+ AICLI_LOG(Repo, Info, << "Opening SQLite Index for " << GetOpenDispositionString(disposition) << " at '" << filePath << "'");
+ switch (disposition)
+ {
+ case OpenDisposition::Read:
+ m_dbconn = SQLite::Connection::Create(filePath, SQLite::Connection::OpenDisposition::ReadOnly, SQLite::Connection::OpenFlags::None);
+ break;
+ case OpenDisposition::ReadWrite:
+ m_dbconn = SQLite::Connection::Create(filePath, SQLite::Connection::OpenDisposition::ReadWrite, SQLite::Connection::OpenFlags::None);
+ break;
+ case OpenDisposition::Immutable:
+ {
+ // Following the algorithm set forth at https://sqlite.org/uri.html [3.1] to convert to a URI path
+ // The execution order builds out the string so that it shouldn't require any moves (other than growing)
+ std::string target;
+ // Add an 'arbitrary' growth size to prevent the majority of needing to grow (adding 'file:/' and '?immutable=1')
+ target.reserve(filePath.size() + 20);
+
+ target += "file:";
+
+ bool wasLastCharSlash = false;
+
+ if (filePath.size() >= 2 && filePath[1] == ':' &&
+ ((filePath[0] >= 'a' && filePath[0] <= 'z') ||
+ (filePath[0] >= 'A' && filePath[0] <= 'Z')))
+ {
+ target += '/';
+ wasLastCharSlash = true;
+ }
+
+ for (char c : filePath)
+ {
+ bool wasThisCharSlash = false;
+ switch (c)
+ {
+ case '?': target += "%3f"; break;
+ case '#': target += "%23"; break;
+ case '\\':
+ case '/':
+ {
+ wasThisCharSlash = true;
+ if (!wasLastCharSlash)
+ {
+ target += '/';
+ }
+ break;
+ }
+ default: target += c; break;
+ }
+
+ wasLastCharSlash = wasThisCharSlash;
+ }
+
+ target += "?immutable=1";
+ m_dbconn = SQLite::Connection::Create(filePath, SQLite::Connection::OpenDisposition::ReadOnly, SQLite::Connection::OpenFlags::Uri);
+ break;
+ }
+ default:
+ THROW_HR(E_UNEXPECTED);
+ }
+
+ m_version = Schema::Version::GetSchemaVersion(m_dbconn);
+ }
+
+ SQLiteStorageBase::SQLiteStorageBase(const std::string& target, Schema::Version version) :
+ m_dbconn(SQLite::Connection::Create(target, SQLite::Connection::OpenDisposition::Create))
+ {
+ m_version = version;
+ Schema::MetadataTable::Create(m_dbconn);
+ }
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteStorageBase.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteStorageBase.h
new file mode 100644
index 0000000000..26d86c5f40
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteStorageBase.h
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "SQLiteWrapper.h"
+#include "Microsoft/Schema/Version.h"
+#include
+#include
+
+#include
+
+namespace AppInstaller::Repository::Microsoft
+{
+ struct SQLiteStorageBase
+ {
+ // The disposition for opening the index.
+ enum class OpenDisposition
+ {
+ // Open for read only.
+ Read,
+ // Open for read and write.
+ ReadWrite,
+ // The database will not change while in use; open for immutable read.
+ Immutable,
+ };
+
+ // Gets the last write time for the index.
+ std::chrono::system_clock::time_point GetLastWriteTime();
+
+ // Gets the schema version of the index.
+ Schema::Version GetVersion() const { return m_version; }
+
+ protected:
+ SQLiteStorageBase(const std::string& target, Schema::Version version);
+
+ SQLiteStorageBase(const std::string& filePath, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile);
+
+ // Sets the last write time metadata value in the index.
+ void SetLastWriteTime();
+
+ // Gets the corresponding OpenFlags based on the disposition.
+ SQLite::Connection::OpenFlags GetOpenFlags(SQLiteStorageBase::OpenDisposition disposition);
+
+ Utility::ManagedFile m_indexFile;
+ SQLite::Connection m_dbconn;
+ Schema::Version m_version;
+ std::unique_ptr m_interfaceLock = std::make_unique();
+ };
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h
new file mode 100644
index 0000000000..2ac19f9543
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/IPortableIndex.h
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "SQLiteWrapper.h"
+#include "Microsoft/Schema/Version.h"
+#include
+
+namespace AppInstaller::Repository::Microsoft::Schema
+{
+ struct IPortableIndex
+ {
+ // File type enum of the portable file
+ enum class PortableFileType
+ {
+ Unknown,
+ File,
+ Directory,
+ Symlink
+ };
+
+ // Metadata representation of a portable file placed down during installation
+ struct PortableFile
+ {
+ // Version 1.0
+ PortableFileType FileType = PortableFileType::Unknown;
+ std::string SHA256;
+ std::string SymlinkTarget;
+
+ void SetFilePath(const std::filesystem::path& path) { m_filePath = std::filesystem::weakly_canonical(path); };
+
+ std::filesystem::path GetFilePath() const { return m_filePath; };
+
+ private:
+ std::filesystem::path m_filePath;
+ };
+
+ virtual ~IPortableIndex() = default;
+
+ // Gets the schema version that this index interface is built for.
+ virtual Schema::Version GetVersion() const = 0;
+
+ // Creates all of the version dependent tables within the database.
+ virtual void CreateTable(SQLite::Connection& connection) = 0;
+
+ // Version 1.0
+ // Adds a portable file to the index.
+ virtual SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const PortableFile& file) = 0;
+
+ // Removes a portable file from the index.
+ virtual SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const PortableFile& file) = 0;
+
+ // Updates the file with matching FilePath in the index.
+ // The return value indicates whether the index was modified by the function.
+ virtual std::pair UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file) = 0;
+ };
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h
new file mode 100644
index 0000000000..b87f07d5a6
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface.h
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "Microsoft/Schema/IPortableIndex.h"
+#include "Microsoft/Schema/Portable_1_0/PortableTable.h"
+
+namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
+{
+ struct PortableIndexInterface : public IPortableIndex
+ {
+ // Version 1.0
+ Schema::Version GetVersion() const override;
+ void CreateTable(SQLite::Connection& connection) override;
+
+ private:
+ SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const PortableFile& file) override;
+ SQLite::rowid_t RemovePortableFile(SQLite::Connection& connection, const PortableFile& file) override;
+ std::pair UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file) override;
+ };
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp
new file mode 100644
index 0000000000..d7e49588ac
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableIndexInterface_1_0.cpp
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "Microsoft/Schema/Portable_1_0/PortableIndexInterface.h"
+#include "Microsoft/Schema/Portable_1_0/PortableTable.h"
+
+namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
+{
+ namespace
+ {
+ std::optional GetExistingPortableFileId(const SQLite::Connection& connection, const IPortableIndex::PortableFile& file)
+ {
+ auto result = PortableTable::SelectByFilePath(connection, file.GetFilePath());
+
+ if (!result)
+ {
+ AICLI_LOG(Repo, Verbose, << "Did not find a portable file with the path { " << file.GetFilePath() << " }");
+ }
+
+ return result;
+ }
+ }
+
+ Schema::Version PortableIndexInterface::GetVersion() const
+ {
+ return { 1, 0 };
+ }
+
+ void PortableIndexInterface::CreateTable(SQLite::Connection& connection)
+ {
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createportabletable_v1_0");
+ Portable_V1_0::PortableTable::Create(connection);
+ savepoint.Commit();
+ }
+
+ SQLite::rowid_t PortableIndexInterface::AddPortableFile(SQLite::Connection& connection, const PortableFile& file)
+ {
+ auto portableEntryResult = GetExistingPortableFileId(connection, file);
+
+ THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), portableEntryResult.has_value());
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addportablefile_v1_0");
+ SQLite::rowid_t portableFileId = PortableTable::AddPortableFile(connection, file);
+
+ savepoint.Commit();
+ return portableFileId;
+ }
+
+ SQLite::rowid_t PortableIndexInterface::RemovePortableFile(SQLite::Connection& connection, const PortableFile& file)
+ {
+ auto portableEntryResult = GetExistingPortableFileId(connection, file);
+
+ // If the portable file doesn't actually exist, fail the remove.
+ THROW_HR_IF(E_NOT_SET, !portableEntryResult);
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "removeportablefile_v1_0");
+ PortableTable::RemovePortableFileById(connection, portableEntryResult.value());
+
+ savepoint.Commit();
+ return portableEntryResult.value();
+ }
+
+ std::pair PortableIndexInterface::UpdatePortableFile(SQLite::Connection& connection, const PortableFile& file)
+ {
+ auto portableEntryResult = GetExistingPortableFileId(connection, file);
+
+ // If the portable file doesn't actually exist, fail the update.
+ THROW_HR_IF(E_NOT_SET, !portableEntryResult);
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "updateportablefile_v1_0");
+ bool status = PortableTable::UpdatePortableFileById(connection, portableEntryResult.value(), file);
+
+ savepoint.Commit();
+ return { status, portableEntryResult.value() };
+ }
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp
new file mode 100644
index 0000000000..b6c0c830d1
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.cpp
@@ -0,0 +1,153 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "PortableTable.h"
+#include "SQLiteStatementBuilder.h"
+#include "Microsoft/Schema/IPortableIndex.h"
+
+namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
+{
+ using namespace std::string_view_literals;
+ static constexpr std::string_view s_PortableTable_Table_Name = "portable"sv;
+ static constexpr std::string_view s_PortableTable_FilePath_Column = "filepath"sv;
+ static constexpr std::string_view s_PortableTable_FileType_Column = "filetype"sv;
+ static constexpr std::string_view s_PortableTable_SHA256_Column = "sha256"sv;
+ static constexpr std::string_view s_PortableTable_SymlinkTarget_Column = "symlinktarget"sv;
+
+ std::string_view PortableTable::TableName()
+ {
+ return s_PortableTable_Table_Name;
+ }
+
+ void PortableTable::Create(SQLite::Connection& connection)
+ {
+ using namespace SQLite::Builder;
+
+ SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createPortableTable_v1_0");
+
+ StatementBuilder createTableBuilder;
+ createTableBuilder.CreateTable(s_PortableTable_Table_Name).BeginColumns();
+
+ createTableBuilder.Column(ColumnBuilder(s_PortableTable_FilePath_Column, Type::Text).NotNull().Unique().CollateNoCase());
+ createTableBuilder.Column(ColumnBuilder(s_PortableTable_FileType_Column, Type::Int64).NotNull());
+ createTableBuilder.Column(ColumnBuilder(s_PortableTable_SHA256_Column, Type::Blob));
+ createTableBuilder.Column(ColumnBuilder(s_PortableTable_SymlinkTarget_Column, Type::Text));
+
+ createTableBuilder.EndColumns();
+ createTableBuilder.Execute(connection);
+
+ savepoint.Commit();
+ }
+
+ std::optional PortableTable::SelectByFilePath(const SQLite::Connection& connection, const std::filesystem::path& path)
+ {
+ SQLite::Builder::StatementBuilder builder;
+ builder.Select(SQLite::RowIDName).From(s_PortableTable_Table_Name).Where(s_PortableTable_FilePath_Column);
+ builder.Equals(path.u8string());
+
+ SQLite::Statement select = builder.Prepare(connection);
+
+ if (select.Step())
+ {
+ return select.GetColumn(0);
+ }
+ else
+ {
+ return {};
+ }
+ }
+
+ void PortableTable::RemovePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id)
+ {
+ SQLite::Builder::StatementBuilder builder;
+ builder.DeleteFrom(s_PortableTable_Table_Name).Where(SQLite::RowIDName).Equals(id);
+ builder.Execute(connection);
+ }
+
+ SQLite::rowid_t PortableTable::AddPortableFile(SQLite::Connection& connection, const IPortableIndex::PortableFile& file)
+ {
+ SQLite::Builder::StatementBuilder builder;
+ builder.InsertInto(s_PortableTable_Table_Name)
+ .Columns({ s_PortableTable_FilePath_Column,
+ s_PortableTable_FileType_Column,
+ s_PortableTable_SHA256_Column,
+ s_PortableTable_SymlinkTarget_Column })
+ .Values(file.GetFilePath().u8string(), file.FileType, file.SHA256, file.SymlinkTarget);
+
+ builder.Execute(connection);
+ return connection.GetLastInsertRowID();
+ }
+
+ bool PortableTable::UpdatePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id, const IPortableIndex::PortableFile& file)
+ {
+ SQLite::Builder::StatementBuilder builder;
+ builder.Update(s_PortableTable_Table_Name).Set()
+ .Column(s_PortableTable_FilePath_Column).Equals(file.GetFilePath().u8string())
+ .Column(s_PortableTable_FileType_Column).Equals(file.FileType)
+ .Column(s_PortableTable_SHA256_Column).Equals(file.SHA256)
+ .Column(s_PortableTable_SymlinkTarget_Column).Equals(file.SymlinkTarget)
+ .Where(SQLite::RowIDName).Equals(id);
+
+ builder.Execute(connection);
+ return connection.GetChanges() != 0;
+ }
+
+ std::optional PortableTable::GetPortableFileById(const SQLite::Connection& connection, SQLite::rowid_t id)
+ {
+ SQLite::Builder::StatementBuilder builder;
+ builder.Select({ s_PortableTable_FilePath_Column,
+ s_PortableTable_FileType_Column,
+ s_PortableTable_SHA256_Column,
+ s_PortableTable_SymlinkTarget_Column})
+ .From(s_PortableTable_Table_Name).Where(SQLite::RowIDName).Equals(id);
+
+ SQLite::Statement select = builder.Prepare(connection);
+
+ IPortableIndex::PortableFile portableFile;
+ if (select.Step())
+ {
+ auto [filePath, fileType, sha256, symlinkTarget] = select.GetRow();
+ portableFile.SetFilePath(std::move(filePath));
+ portableFile.FileType = fileType;
+ portableFile.SHA256 = std::move(sha256);
+ portableFile.SymlinkTarget = std::move(symlinkTarget);
+ return portableFile;
+ }
+ else
+ {
+ return {};
+ }
+ }
+
+ bool PortableTable::ExistsById(const SQLite::Connection& connection, SQLite::rowid_t id)
+ {
+ SQLite::Builder::StatementBuilder builder;
+ builder.Select(SQLite::Builder::RowCount).From(s_PortableTable_Table_Name).Where(SQLite::RowIDName).Equals(id);
+
+ SQLite::Statement countStatement = builder.Prepare(connection);
+
+ THROW_HR_IF(E_UNEXPECTED, !countStatement.Step());
+
+ return (countStatement.GetColumn(0) != 0);
+ }
+
+ void PortableTable::DeleteById(SQLite::Connection& connection, SQLite::rowid_t id)
+ {
+ SQLite::Builder::StatementBuilder builder;
+ builder.DeleteFrom(s_PortableTable_Table_Name).Where(SQLite::RowIDName).Equals(id);
+
+ builder.Execute(connection);
+ }
+
+ bool PortableTable::IsEmpty(SQLite::Connection& connection)
+ {
+ SQLite::Builder::StatementBuilder builder;
+ builder.Select(SQLite::Builder::RowCount).From(s_PortableTable_Table_Name);
+
+ SQLite::Statement countStatement = builder.Prepare(connection);
+
+ THROW_HR_IF(E_UNEXPECTED, !countStatement.Step());
+
+ return (countStatement.GetColumn(0) == 0);
+ }
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h
new file mode 100644
index 0000000000..513e100010
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Portable_1_0/PortableTable.h
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "SQLiteWrapper.h"
+#include "SQLiteStatementBuilder.h"
+#include "Microsoft/Schema/IPortableIndex.h"
+#include
+
+namespace AppInstaller::Repository::Microsoft::Schema::Portable_V1_0
+{
+ // A table the represents a single portable file
+ struct PortableTable
+ {
+ // Get the table name.
+ static std::string_view TableName();
+
+ // Creates the table with named indices.
+ static void Create(SQLite::Connection& connection);
+
+ // Gets a value indicating whether the portable file with rowid id exists.
+ static bool ExistsById(const SQLite::Connection& connection, SQLite::rowid_t id);
+
+ // Deletes the portable file row with the given rowid
+ static void DeleteById(SQLite::Connection& connection, SQLite::rowid_t id);
+
+ // Gets a value indicating whether the table is empty.
+ static bool IsEmpty(SQLite::Connection& connection);
+
+ // Selects the portable file by filepath from the table, returning the rowid if it exists.
+ static std::optional SelectByFilePath(const SQLite::Connection& connection, const std::filesystem::path& path);
+
+ // Selects the portable file by rowid from the table, returning the portable file object if it exists.
+ static std::optional GetPortableFileById(const SQLite::Connection& connection, SQLite::rowid_t id);
+
+ // Adds the portable file into the table.
+ static SQLite::rowid_t AddPortableFile(SQLite::Connection& connection, const IPortableIndex::PortableFile& file);
+
+ // Removes the portable file from the table by id.
+ static void RemovePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id);
+
+ // Updates the portable file in the table by id.
+ static bool UpdatePortableFileById(SQLite::Connection& connection, SQLite::rowid_t id, const IPortableIndex::PortableFile& file);
+ };
+}
\ No newline at end of file
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp
index 0c6cc20f0f..f5a7429596 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp
@@ -4,14 +4,6 @@
#include "Version.h"
#include "MetadataTable.h"
-#include "1_0/Interface.h"
-#include "1_1/Interface.h"
-#include "1_2/Interface.h"
-#include "1_3/Interface.h"
-#include "1_4/Interface.h"
-#include "1_5/Interface.h"
-#include "1_6/Interface.h"
-
namespace AppInstaller::Repository::Microsoft::Schema
{
Version Version::GetSchemaVersion(SQLite::Connection& connection)
@@ -32,44 +24,6 @@ namespace AppInstaller::Repository::Microsoft::Schema
savepoint.Commit();
}
- // Creates the interface object for this version.
- std::unique_ptr Version::CreateISQLiteIndex() const
- {
- if (*this == Version{ 1, 0 })
- {
- return std::make_unique();
- }
- else if (*this == Version{ 1, 1 })
- {
- return std::make_unique();
- }
- else if (*this == Version{ 1, 2 })
- {
- return std::make_unique();
- }
- else if (*this == Version{ 1, 3 })
- {
- return std::make_unique();
- }
- else if (*this == Version{ 1, 4 })
- {
- return std::make_unique();
- }
- else if (*this == Version{ 1, 5 })
- {
- return std::make_unique();
- }
- else if (*this == Version{ 1, 6 } ||
- this->MajorVersion == 1 ||
- this->IsLatest())
- {
- return std::make_unique();
- }
-
- // We do not have the capacity to operate on this schema version
- THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
- }
-
std::ostream& operator<<(std::ostream& out, const Version& version)
{
return (out << version.MajorVersion << '.' << version.MinorVersion);
diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.h
index a898517e82..f435c08b62 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.h
+++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.h
@@ -2,16 +2,11 @@
// Licensed under the MIT License.
#pragma once
#include "SQLiteWrapper.h"
-#include "Microsoft/Schema/ISQLiteIndex.h"
-
#include
#include
namespace AppInstaller::Repository::Microsoft::Schema
{
- // Forward declarations
- struct ISQLiteIndex;
-
// Represents the schema version of the index.
struct Version
{
@@ -56,9 +51,6 @@ namespace AppInstaller::Repository::Microsoft::Schema
// Writes the current version to the given index.
void SetSchemaVersion(SQLite::Connection& connection);
-
- // Creates the interface object for this version.
- std::unique_ptr CreateISQLiteIndex() const;
};
// Output the version
diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h b/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h
index 1930ca95f3..2ec600e7eb 100644
--- a/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h
+++ b/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h
@@ -2,6 +2,8 @@
// Licensed under the MIT License.
#pragma once
#include "Microsoft/Schema/Version.h"
+#include
+#include
#include
#include
diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp
index e431375fb5..216acaac33 100644
--- a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp
+++ b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp
@@ -174,6 +174,15 @@ namespace AppInstaller::Repository::SQLite::Builder
return *this;
}
+ ColumnBuilder& ColumnBuilder::CollateNoCase(bool isTrue)
+ {
+ if (isTrue)
+ {
+ m_stream << " COLLATE NOCASE";
+ }
+ return *this;
+ }
+
ColumnBuilder& ColumnBuilder::Default(int64_t value)
{
m_stream << " DEFAULT " << value;
diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h
index 3bcb632d6e..193728b636 100644
--- a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h
+++ b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h
@@ -151,6 +151,10 @@ namespace AppInstaller::Repository::SQLite::Builder
// Allow for data driven construction with input value.
ColumnBuilder& NotNull(bool isTrue = true);
+ // Indicate that the column is case-insensitive.
+ // Allow for data driven construction with input value.
+ ColumnBuilder& CollateNoCase(bool isTrue = true);
+
// Indicate the default value for the column.
// Note that a default value is not considered constant if it is bound,
// so this function directly places the incoming value into the SQL statement.