winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit afc2228364cba053d4e99e8a16d926239be0d204
parent 1a1c9e7d3c5557b8ac8bb628b87639c10039adfb
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Wed,  5 Feb 2020 09:26:58 -0800

Finish the StatementBuilder (#29)


Diffstat:
Mazure-pipelines.yml | 17++++++++++++++++-
Msrc/AppInstallerCLITests/SQLiteWrapper.cpp | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Asrc/AppInstallerCLITests/TEST-AppInstallerCLI-1.xml | 34++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/main.cpp | 5+++++
Msrc/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h | 7+++++++
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp | 70++++++++++++++++++++++++++--------------------------------------------
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToManyTable.cpp | 42+++++++++++++++++++-----------------------
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp | 31++++++++++++-------------------
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/1_0/PathPartTable.cpp | 63++++++++++++++++++---------------------------------------------
Msrc/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp | 401+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/AppInstallerRepositoryCore/SQLiteStatementBuilder.h | 199++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
11 files changed, 864 insertions(+), 187 deletions(-)

diff --git a/azure-pipelines.yml b/azure-pipelines.yml @@ -34,14 +34,29 @@ steps: - task: CmdLine@2 inputs: script: | - AppInstallerCLITests.exe -s -r junit -o TEST-AppInstallerCLI-$(_artifact).xml + AppInstallerCLITests.exe -logto AICLI.log -s -r junit -o TEST-AppInstallerCLI-$(_artifact).xml workingDirectory: 'src\x64\Release\AppInstallerCLITests\' +- task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: 'src\x64\Release\AppInstallerCLITests\AICLI.log' + ArtifactName: 'TestPassLog' + publishLocation: 'Container' + condition: succeededOrFailed() + +- task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: 'src\x64\Release\AppInstallerCLITests\TEST-AppInstallerCLI-$(_artifact).xml' + ArtifactName: 'TestPassOutput' + publishLocation: 'Container' + condition: succeededOrFailed() + - task: PublishTestResults@2 inputs: testResultsFormat: 'JUnit' testResultsFiles: '**/TEST-*.xml' failTaskOnFailedTests: true + condition: succeededOrFailed() - task: PublishBuildArtifacts@1 inputs: diff --git a/src/AppInstallerCLITests/SQLiteWrapper.cpp b/src/AppInstallerCLITests/SQLiteWrapper.cpp @@ -27,17 +27,22 @@ select first, second from simpletest void CreateSimpleTestTable(Connection& connection) { - Statement createTable = Statement::Create(connection, s_CreateSimpleTestTableSQL); + Builder::StatementBuilder builder; + builder.CreateTable(s_tableName).Columns({ + Builder::ColumnBuilder(s_firstColumn, Builder::Type::Int), + Builder::ColumnBuilder(s_secondColumn, Builder::Type::Text), + }); + + Statement createTable = builder.Prepare(connection); REQUIRE_FALSE(createTable.Step()); REQUIRE(createTable.GetState() == Statement::State::Completed); } void InsertIntoSimpleTestTable(Connection& connection, int firstVal, const std::string& secondVal) { - Statement insert = Statement::Create(connection, s_insertToSimpleTestTableSQL); - - insert.Bind(1, firstVal); - insert.Bind(2, secondVal); + Builder::StatementBuilder builder; + builder.InsertInto(s_tableName).Columns({ s_firstColumn, s_secondColumn }).Values(firstVal, secondVal); + Statement insert = builder.Prepare(connection); REQUIRE_FALSE(insert.Step()); REQUIRE(insert.GetState() == Statement::State::Completed); @@ -45,9 +50,9 @@ void InsertIntoSimpleTestTable(Connection& connection, int firstVal, const std:: void InsertIntoSimpleTestTableWithNull(Connection& connection, int firstVal) { - Statement insert = Statement::Create(connection, s_insertToSimpleTestTableSQL); - - insert.Bind(1, firstVal); + Builder::StatementBuilder builder; + builder.InsertInto(s_tableName).Columns({ s_firstColumn, s_secondColumn }).Values(firstVal, nullptr); + Statement insert = builder.Prepare(connection); REQUIRE_FALSE(insert.Step()); REQUIRE(insert.GetState() == Statement::State::Completed); @@ -299,3 +304,164 @@ TEST_CASE("SQLBuilder_SimpleSelectOptional", "[sqlbuilder]") REQUIRE(!statement.Step()); } } + +TEST_CASE("SQLBuilder_CreateTable", "[sqlbuilder]") +{ + Connection connection = Connection::Create(SQLITE_MEMORY_DB_CONNECTION_TARGET, Connection::OpenDisposition::Create); + + int testRun = GENERATE(0, 1, 2, 3, 4, 5, 6, 7); + + bool notNull = ((testRun & 1) != 0); + bool unique = ((testRun & 2) != 0); + bool pk = ((testRun & 4) != 0); + CAPTURE(notNull, unique, pk); + + Builder::StatementBuilder createTable; + createTable.CreateTable(s_tableName).Columns({ + Builder::ColumnBuilder(s_firstColumn, Builder::Type::Int).NotNull(notNull).Unique(unique).PrimaryKey(pk) + }); + + createTable.Execute(connection); + + Builder::StatementBuilder insertBuilder; + insertBuilder.InsertInto(s_tableName).Columns(s_firstColumn).Values(Builder::Unbound); + + Statement insertStatement = insertBuilder.Prepare(connection); + + { + INFO("Insert NULL"); + insertStatement.Bind(1, nullptr); + + if (notNull) + { + REQUIRE_THROWS_HR(insertStatement.Execute(), MAKE_HRESULT(SEVERITY_ERROR, FACILITY_SQLITE, SQLITE_CONSTRAINT_NOTNULL)); + } + else + { + insertStatement.Execute(); + } + } + + { + INFO("Insert unique values"); + insertStatement.Reset(); + insertStatement.Bind(1, 1); + insertStatement.Execute(); + + insertStatement.Reset(); + insertStatement.Bind(1, 2); + insertStatement.Execute(); + } + + { + INFO("Insert duplicate values"); + insertStatement.Reset(); + insertStatement.Bind(1, 1); + + if (unique || pk) + { + HRESULT expectedHR = S_OK; + if (pk) + { + expectedHR = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_SQLITE, SQLITE_CONSTRAINT_PRIMARYKEY); + } + else + { + expectedHR = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_SQLITE, SQLITE_CONSTRAINT_UNIQUE); + } + REQUIRE_THROWS_HR(insertStatement.Execute(), expectedHR); + } + else + { + insertStatement.Execute(); + } + } +} + +TEST_CASE("SQLBuilder_InsertValueBinding", "[sqlbuilder]") +{ + char const* const columns[] = { "a", "b", "c", "d", "e", "f" }; + + TestCommon::TempFile tempFile{ "repolibtest_tempdb", ".db" }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::Create); + + { + INFO("Create table"); + Builder::StatementBuilder createTable; + createTable.CreateTable(s_tableName).BeginColumns(); + for (const auto c : columns) + { + createTable.Column(Builder::ColumnBuilder(c, Builder::Type::Int)); + } + createTable.EndColumns(); + createTable.Execute(connection); + } + + { + INFO("Insert values"); + Builder::StatementBuilder insertBuilder; + insertBuilder.InsertInto(s_tableName).BeginColumns(); + for (const auto c : columns) + { + insertBuilder.Column(c); + } + insertBuilder.EndColumns().Values(0, 1, 2, 3, 4, 5); + insertBuilder.Execute(connection); + } + + { + INFO("Insert values"); + Builder::StatementBuilder insertBuilder; + insertBuilder.InsertInto(s_tableName).BeginColumns(); + for (const auto c : columns) + { + insertBuilder.Column(c); + } + insertBuilder.EndColumns().BeginValues(); + insertBuilder.Value(5); + insertBuilder.Value(nullptr); + insertBuilder.Value(3); + insertBuilder.Value(std::optional<int>{}); + insertBuilder.Value(std::optional<int>{ 1 }); + insertBuilder.Value(Builder::Unbound); + insertBuilder.EndValues(); + insertBuilder.Execute(connection); + } + + { + INFO("Select values"); + Builder::StatementBuilder selectBuilder; + selectBuilder.Select(); + for (const auto c : columns) + { + selectBuilder.Column(c); + } + selectBuilder.From(s_tableName); + + Statement select = selectBuilder.Prepare(connection); + REQUIRE(select.Step()); + + for (int i = 0; i < ARRAYSIZE(columns); ++i) + { + REQUIRE(i == select.GetColumn<int>(i)); + } + + REQUIRE(select.Step()); + + for (int i = 0; i < ARRAYSIZE(columns); ++i) + { + if (i & 1) + { + REQUIRE(select.GetColumnIsNull(i)); + } + else + { + REQUIRE((5 - i) == select.GetColumn<int>(i)); + } + } + + REQUIRE(!select.Step()); + } +} diff --git a/src/AppInstallerCLITests/TEST-AppInstallerCLI-1.xml b/src/AppInstallerCLITests/TEST-AppInstallerCLI-1.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<testsuites> + <testsuite name="AppInstallerCLITests.exe" errors="0" failures="0" tests="271" hostname="tbd" time="3.0509" timestamp="2020-02-05T01:16:40Z"> + <testcase classname="AppInstallerCLITests.exe.global" name="DownloadValidFileAndVerifyHash" time="0.424575"/> + <testcase classname="AppInstallerCLITests.exe.global" name="DownloadValidFileAndCancel" time="0.907437"/> + <testcase classname="AppInstallerCLITests.exe.global" name="DownloadUnreachableUrl" time="0.082048"/> + <testcase classname="AppInstallerCLITests.exe.global" name="InstallFlowWithTestManifest" time="0.72304"/> + <testcase classname="AppInstallerCLITests.exe.global" name="InstallFlowWithNonApplicableArchitecture" time="0.020354"/> + <testcase classname="AppInstallerCLITests.exe.global" name="DestructionToken" time="0.00016"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLiteIndexCreateLatestAndReopen" time="0.057503"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLiteIndexCreateAndAddManifest" time="0.068016"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLiteIndexCreateAndAddManifestFile" time="0.11345"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLiteIndex_RemoveManifestFile_NotPresent" time="0.034248"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLiteIndex_RemoveManifest" time="0.18614"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLiteIndex_RemoveManifestFile" time="0.14229"/> + <testcase classname="AppInstallerCLITests.exe.global" name="PathPartTable_EnsurePathExists_Negative_Paths" time="0.013909"/> + <testcase classname="AppInstallerCLITests.exe.global" name="PathPartTable_EnsurePathExists" time="0.112738"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLiteWrapperMemoryCreate" time="0.001666"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLiteWrapperFileCreateAndReopen" time="0.045612"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLiteWrapperSavepointRollback" time="0.002914"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLiteWrapperSavepointRollbackOnDestruct" time="0.002808"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLiteWrapperSavepointCommit" time="0.003673"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLBuilder_SimpleSelectBind" time="0.003718"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLBuilder_SimpleSelectUnbound" time="0.00303"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLBuilder_SimpleSelectNull" time="0.002924"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLBuilder_SimpleSelectOptional" time="0.003667"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLBuilder_CreateTable" time="0.005427"/> + <testcase classname="AppInstallerCLITests.exe.global" name="SQLBuilder_InsertValueBinding" time="0.002521"/> + <testcase classname="AppInstallerCLITests.exe.global" name="ReadGoodManifestAndVerifyContents" time="0.019252"/> + <testcase classname="AppInstallerCLITests.exe.global" name="ReadBadManifestAndVerifyThrow" time="0.032321"/> + <system-out/> + <system-err/> + </testsuite> +</testsuites> diff --git a/src/AppInstallerCLITests/main.cpp b/src/AppInstallerCLITests/main.cpp @@ -44,6 +44,11 @@ int main(int argc, char** argv) { AppInstaller::Logging::AddFileLogger(); } + else if ("-logto"s == argv[i]) + { + ++i; + AppInstaller::Logging::AddFileLogger(argv[i]); + } else if ("-tdd"s == argv[i]) { ++i; diff --git a/src/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h b/src/AppInstallerCommonCore/Public/AppInstallerLanguageUtilities.h @@ -45,4 +45,11 @@ namespace AppInstaller // Enables a bool to be used as a destruction indicator. using DestructionToken = ResetWhenMovedFrom<bool>; + + // Enable use of folding to execute functions across parameter packs. + struct FoldHelper + { + template <typename T> + FoldHelper& operator,(T&&) { return *this; } + }; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/ManifestTable.cpp @@ -9,6 +9,8 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 { using namespace std::string_view_literals; static constexpr std::string_view s_ManifestTable_Table_Name = "manifest"sv; + static constexpr std::string_view s_ManifestTable_Index_Separator = "_"sv; + static constexpr std::string_view s_ManifestTable_Index_Suffix = "_index"sv; namespace details { @@ -77,45 +79,39 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 void ManifestTable::Create(SQLite::Connection& connection, std::initializer_list<ManifestColumnInfo> values) { + using namespace SQLite::Builder; + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createManifestTable_v1_0"); - std::ostringstream createTableSQL; - createTableSQL << "CREATE TABLE [" << s_ManifestTable_Table_Name << "] ("; + StatementBuilder createTableBuilder; + createTableBuilder.CreateTable(s_ManifestTable_Table_Name).BeginColumns(); for (const ManifestColumnInfo& value : values) { - createTableSQL << '[' << value.Name << "] INT64 NOT NULL" << (value.Unique ? " UNIQUE" : "") << ","; + createTableBuilder.Column(ColumnBuilder(value.Name, Type::Int64).NotNull().Unique(value.Unique)); } - createTableSQL << "PRIMARY KEY("; - - bool isFirst = true; + PrimaryKeyBuilder pkBuilder; for (const ManifestColumnInfo& value : values) { if (value.PrimaryKey) { - createTableSQL << (isFirst ? "[" : ", [") << value.Name << "]"; + pkBuilder.Column(value.Name); } - isFirst = false; } - createTableSQL << "))"; + createTableBuilder.Column(pkBuilder).EndColumns(); - SQLite::Statement createStatement = SQLite::Statement::Create(connection, createTableSQL.str()); - - createStatement.Execute(); + createTableBuilder.Execute(connection); // Create an index on every value to improve performance for (const ManifestColumnInfo& value : values) { - std::ostringstream createIndexSQL; - createIndexSQL << "CREATE INDEX [" << s_ManifestTable_Table_Name << '_' << value.Name << "_index] " - << "ON [" << s_ManifestTable_Table_Name << "](" - << '[' << value.Name << "])"; - - SQLite::Statement createIndex = SQLite::Statement::Create(connection, createIndexSQL.str()); + StatementBuilder createIndexBuilder; + createIndexBuilder.CreateIndex({ s_ManifestTable_Table_Name, s_ManifestTable_Index_Separator, value.Name, s_ManifestTable_Index_Suffix }). + On(s_ManifestTable_Table_Name).Columns(value.Name); - createIndex.Execute(); + createIndexBuilder.Execute(connection); } savepoint.Commit(); @@ -123,48 +119,34 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 SQLite::rowid_t ManifestTable::Insert(SQLite::Connection& connection, std::initializer_list<ManifestOneToOneValue> values) { - std::ostringstream insertSQL; - insertSQL << "INSERT INTO [" << s_ManifestTable_Table_Name << "] ("; + SQLite::Builder::StatementBuilder builder; + builder.InsertInto(s_ManifestTable_Table_Name).BeginColumns(); - bool isFirst = true; for (const ManifestOneToOneValue& value : values) { - insertSQL << (isFirst ? "[" : ",[") << value.Name << "] "; - isFirst = false; - } - - insertSQL << ") VALUES ("; - - for (size_t i = 0; i < values.size(); ++i) - { - insertSQL << (i == 0 ? "?" : ", ?"); + builder.Column(value.Name); } - insertSQL << ')'; + builder.EndColumns().BeginValues(); - SQLite::Statement insert = SQLite::Statement::Create(connection, insertSQL.str()); - - int bindIndex = 1; for (const ManifestOneToOneValue& value : values) { - insert.Bind(bindIndex++, value.Value); + builder.Value(value.Value); } - insert.Execute(); + builder.EndValues(); + + builder.Execute(connection); return connection.GetLastInsertRowID(); } void ManifestTable::DeleteById(SQLite::Connection& connection, SQLite::rowid_t id) { - std::ostringstream deleteSQL; - deleteSQL << "DELETE FROM [" << s_ManifestTable_Table_Name << "] WHERE [" << SQLite::RowIDName << "] = ?"; - - SQLite::Statement deleteStatement = SQLite::Statement::Create(connection, deleteSQL.str()); - - deleteStatement.Bind(1, id); + SQLite::Builder::StatementBuilder builder; + builder.DeleteFrom(s_ManifestTable_Table_Name).Where(SQLite::RowIDName).Equals(id); - deleteStatement.Execute(); + builder.Execute(connection); } bool ManifestTable::IsEmpty(SQLite::Connection& connection) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToManyTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToManyTable.cpp @@ -16,21 +16,22 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 void CreateOneToManyTable(SQLite::Connection& connection, std::string_view tableName, std::string_view valueName) { + using namespace SQLite::Builder; + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, std::string{ tableName } +"_create_v1_0"); // Create the data table as a 1:1 CreateOneToOneTable(connection, tableName, valueName); // Create the mapping table - std::ostringstream createMapTableSQL; - createMapTableSQL << "CREATE TABLE [" << tableName << s_OneToManyTable_MapTable_Suffix << "](" - << "[" << s_OneToManyTable_MapTable_ManifestName << "] INT64 NOT NULL," - << '[' << valueName << "] INT64 NOT NULL," - "PRIMARY KEY([" << s_OneToManyTable_MapTable_ManifestName << "], [" << valueName << "]))"; - - SQLite::Statement createMapStatement = SQLite::Statement::Create(connection, createMapTableSQL.str()); + StatementBuilder createMapTableBuilder; + createMapTableBuilder.CreateTable({ tableName, s_OneToManyTable_MapTable_Suffix }).Columns({ + ColumnBuilder(s_OneToManyTable_MapTable_ManifestName, Type::Int64).NotNull(), + ColumnBuilder(valueName, Type::Int64).NotNull(), + PrimaryKeyBuilder({ s_OneToManyTable_MapTable_ManifestName, valueName }) + }); - createMapStatement.Execute(); + createMapTableBuilder.Execute(connection); savepoint.Commit(); } @@ -42,12 +43,11 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, std::string{ tableName } +"_ensureandinsert_v1_0"); // Create the mapping table insert statement for multiple use - std::ostringstream insertMappingSQL; - insertMappingSQL << "INSERT INTO [" << tableName << s_OneToManyTable_MapTable_Suffix << "] (" - << s_OneToManyTable_MapTable_ManifestName << ", " << valueName << ") VALUES (?, ?)"; + SQLite::Builder::StatementBuilder insertMappingBuilder; + insertMappingBuilder.InsertInto({ tableName, s_OneToManyTable_MapTable_Suffix }). + Columns({ s_OneToManyTable_MapTable_ManifestName, valueName }).Values(manifestId, SQLite::Builder::Unbound); - SQLite::Statement insertMapping = SQLite::Statement::Create(connection, insertMappingSQL.str()); - insertMapping.Bind(1, manifestId); + SQLite::Statement insertMapping = insertMappingBuilder.Prepare(connection); for (const std::string& value : values) { @@ -82,14 +82,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 } // Delete the mapping table rows with the manifest id. - std::ostringstream deleteSQL; - deleteSQL << "DELETE FROM [" << tableName << s_OneToManyTable_MapTable_Suffix << "] WHERE [" << s_OneToManyTable_MapTable_ManifestName << "] = ?"; - - SQLite::Statement deleteStatement = SQLite::Statement::Create(connection, deleteSQL.str()); - - deleteStatement.Bind(1, manifestId); + SQLite::Builder::StatementBuilder deleteBuilder; + deleteBuilder.DeleteFrom({ tableName, s_OneToManyTable_MapTable_Suffix }).Where(s_OneToManyTable_MapTable_ManifestName).Equals(manifestId); - deleteStatement.Execute(); + deleteBuilder.Execute(connection); // For each value, see if any references exist SQLite::Builder::StatementBuilder selectValueMappingBuilder; @@ -97,10 +93,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 SQLite::Statement selectValueMappingStatement = selectValueMappingBuilder.Prepare(connection); - std::ostringstream deleteValueSQL; - deleteValueSQL << "DELETE FROM [" << tableName << "] WHERE [" << SQLite::RowIDName << "] = ?"; + SQLite::Builder::StatementBuilder deleteValueBuilder; + deleteValueBuilder.DeleteFrom(tableName).Where(SQLite::RowIDName).Equals(SQLite::Builder::Unbound); - SQLite::Statement deleteValueStatement = SQLite::Statement::Create(connection, deleteValueSQL.str()); + SQLite::Statement deleteValueStatement = deleteValueBuilder.Prepare(connection); for (SQLite::rowid_t value : values) { diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/OneToOneTable.cpp @@ -12,13 +12,14 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 { void CreateOneToOneTable(SQLite::Connection& connection, std::string_view tableName, std::string_view valueName) { - std::ostringstream createTableSQL; - createTableSQL << "CREATE TABLE [" << tableName << "](" - << '[' << valueName << "] TEXT NOT NULL PRIMARY KEY)"; + using namespace SQLite::Builder; - SQLite::Statement createStatement = SQLite::Statement::Create(connection, createTableSQL.str()); + StatementBuilder createTableBuilder; + createTableBuilder.CreateTable(tableName).Columns({ + ColumnBuilder(valueName, Type::Text).NotNull().PrimaryKey() + }); - createStatement.Execute(); + createTableBuilder.Execute(connection); } SQLite::rowid_t OneToOneTableEnsureExists(SQLite::Connection& connection, std::string_view tableName, std::string_view valueName, std::string_view value) @@ -35,14 +36,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 } } - std::ostringstream insertSQL; - insertSQL << "INSERT INTO [" << tableName << "] ([" << valueName << "]) VALUES (?)"; + SQLite::Builder::StatementBuilder insertBuilder; + insertBuilder.InsertInto(tableName).Columns(valueName).Values(value); - SQLite::Statement insert = SQLite::Statement::Create(connection, insertSQL.str()); - - insert.Bind(1, value); - - insert.Execute(); + insertBuilder.Execute(connection); return connection.GetLastInsertRowID(); } @@ -55,14 +52,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 return; } - std::ostringstream deleteSQL; - deleteSQL << "DELETE FROM [" << tableName << "] WHERE [" << SQLite::RowIDName << "] = ?"; - - SQLite::Statement deleteStatement = SQLite::Statement::Create(connection, deleteSQL.str()); - - deleteStatement.Bind(1, id); + SQLite::Builder::StatementBuilder builder; + builder.DeleteFrom(tableName).Where(SQLite::RowIDName).Equals(id); - deleteStatement.Execute(); + builder.Execute(connection); } bool OneToOneTableIsEmpty(SQLite::Connection& connection, std::string_view tableName) diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/PathPartTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/1_0/PathPartTable.cpp @@ -40,25 +40,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 { THROW_HR_IF(E_INVALIDARG, part.empty()); - std::ostringstream insertPartSQL; - insertPartSQL << "INSERT INTO [" << s_PathPartTable_Table_Name << "] (" - << '[' << s_PathPartTable_ParentValue_Name << "]," - << '[' << s_PathPartTable_PartValue_Name << "])" - << " VALUES (?, ?)"; - - SQLite::Statement insert = SQLite::Statement::Create(connection, insertPartSQL.str()); - - if (parent) - { - insert.Bind(1, parent.value()); - } - else - { - insert.Bind(1, nullptr); - } - insert.Bind(2, part); + SQLite::Builder::StatementBuilder builder; + builder.InsertInto(s_PathPartTable_Table_Name).Columns({ s_PathPartTable_ParentValue_Name, s_PathPartTable_PartValue_Name }).Values(parent, part); - insert.Execute(); + builder.Execute(connection); return connection.GetLastInsertRowID(); } @@ -101,44 +86,32 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_0 // Removes the given part by id. void RemovePartById(SQLite::Connection& connection, SQLite::rowid_t id) { - std::ostringstream deletePartSQL; - deletePartSQL << "DELETE FROM [" << s_PathPartTable_Table_Name << "] WHERE " - << '[' << SQLite::RowIDName << "] = ?"; - - SQLite::Statement deletePart = SQLite::Statement::Create(connection, deletePartSQL.str()); - - deletePart.Bind(1, id); + SQLite::Builder::StatementBuilder builder; + builder.DeleteFrom(s_PathPartTable_Table_Name).Where(SQLite::RowIDName).Equals(id); - deletePart.Execute(); + builder.Execute(connection); } } void PathPartTable::Create(SQLite::Connection& connection) { - SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createPathParts_v1_0"); - - { - std::ostringstream createTableSQL; - createTableSQL << "CREATE TABLE [" << s_PathPartTable_Table_Name << "](" - << '[' << s_PathPartTable_ParentValue_Name << "] INT64," - << '[' << s_PathPartTable_PartValue_Name << "] TEXT NOT NULL," - << "PRIMARY KEY([" << s_PathPartTable_PartValue_Name << "], [" << s_PathPartTable_ParentValue_Name << "]))"; + using namespace SQLite::Builder; - SQLite::Statement createStatement = SQLite::Statement::Create(connection, createTableSQL.str()); + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createPathParts_v1_0"); - createStatement.Execute(); - } + StatementBuilder createTableBuilder; + createTableBuilder.CreateTable(s_PathPartTable_Table_Name).Columns({ + ColumnBuilder(s_PathPartTable_ParentValue_Name, Type::Int64), + ColumnBuilder(s_PathPartTable_PartValue_Name, Type::Text).NotNull(), + PrimaryKeyBuilder({ s_PathPartTable_PartValue_Name, s_PathPartTable_ParentValue_Name }) + }); - { - std::ostringstream createIndexSQL; - createIndexSQL << "CREATE INDEX [" << s_PathPartTable_ParentIndex_Name << "] " - << "ON [" << s_PathPartTable_Table_Name << "](" - << '[' << s_PathPartTable_ParentValue_Name << "])"; + createTableBuilder.Execute(connection); - SQLite::Statement createStatement = SQLite::Statement::Create(connection, createIndexSQL.str()); + StatementBuilder createIndexBuilder; + createIndexBuilder.CreateIndex(s_PathPartTable_ParentIndex_Name).On(s_PathPartTable_Table_Name).Columns(s_PathPartTable_ParentValue_Name); - createStatement.Execute(); - } + createIndexBuilder.Execute(connection); savepoint.Commit(); } diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp @@ -12,39 +12,189 @@ namespace AppInstaller::Repository::SQLite::Builder return out; } - StatementBuilder& StatementBuilder::Select(std::string_view column) + std::ostream& operator<<(std::ostream& out, const details::SubBuilder& column) + { + out << column.GetString(); + return out; + } + + namespace { - m_stream << "SELECT [" << column << ']'; + void OutputColumns(std::ostream& out, std::string_view op, std::string_view column) + { + out << op << '[' << column << ']'; + } + + void OutputColumns(std::ostream& out, std::string_view op, std::initializer_list<std::string_view> columns) + { + out << op; + bool isFirst = true; + for (const auto& c : columns) + { + out << (isFirst ? "[" : ", [") << c << ']'; + isFirst = false; + } + } + + void OutputColumns(std::ostream& out, std::string_view op, const QualifiedColumn& column) + { + out << op << column; + } + + void OutputColumns(std::ostream& out, std::string_view op, std::initializer_list<QualifiedColumn> columns) + { + out << op; + bool isFirst = true; + for (const auto& c : columns) + { + out << (isFirst ? "" : ", ") << c; + isFirst = false; + } + } + + void OutputColumns(std::ostream& out, std::string_view op, std::initializer_list<details::SubBuilder> columns) + { + out << op; + bool isFirst = true; + for (const auto& c : columns) + { + out << (isFirst ? "" : ", ") << c; + isFirst = false; + } + } + + // Use to output operation and table name, such as " FROM [table]" + void OutputOperationAndTable(std::ostream& out, std::string_view op, std::string_view table) + { + out << op << " [" << table << ']'; + } + + void OutputOperationAndTable(std::ostream& out, std::string_view op, std::initializer_list<std::string_view> table) + { + out << op << " ["; + for (std::string_view t : table) + { + out << t; + } + out << ']'; + } + + void OutputType(std::ostream& out, Type type) + { + out << ' '; + switch (type) + { + case Type::Int: + out << "INT"; + break; + case Type::Int64: + out << "INT64"; + break; + case Type::Text: + out << "TEXT"; + break; + default: + THROW_HR(E_UNEXPECTED); + } + } + } + + ColumnBuilder::ColumnBuilder(std::string_view column, Type type) + { + OutputColumns(m_stream, "", column); + OutputType(m_stream, type); + } + + ColumnBuilder& ColumnBuilder::NotNull(bool isTrue) + { + if (isTrue) + { + m_stream << " NOT NULL"; + } return *this; } - StatementBuilder& StatementBuilder::Select(std::initializer_list<std::string_view> columns) + ColumnBuilder& ColumnBuilder::Unique(bool isTrue) { - m_stream << "SELECT"; - bool isFirst = true; - for (const auto& c : columns) + if (isTrue) { - m_stream << (isFirst ? " [" : ", [") << c << ']'; - isFirst = false; + m_stream << " UNIQUE"; } return *this; } - StatementBuilder& StatementBuilder::Select(QualifiedColumn column) + ColumnBuilder& ColumnBuilder::PrimaryKey(bool isTrue) { - m_stream << "SELECT " << column; + if (isTrue) + { + m_stream << " PRIMARY KEY"; + } return *this; } - StatementBuilder& StatementBuilder::Select(std::initializer_list<QualifiedColumn> columns) + PrimaryKeyBuilder::PrimaryKeyBuilder(std::initializer_list<std::string_view> columns) { - m_stream << "SELECT"; - bool isFirst = true; - for (const auto& c : columns) + OutputColumns(m_stream, "PRIMARY KEY(", columns); + m_stream << ')'; + m_needsClosing = false; + } + + PrimaryKeyBuilder::PrimaryKeyBuilder() + { + m_stream << "PRIMARY KEY("; + } + + PrimaryKeyBuilder& PrimaryKeyBuilder::Column(std::string_view column) + { + if (m_isFirst) { - m_stream << (isFirst ? " " : ", ") << c; - isFirst = false; + m_isFirst = false; } + else + { + m_stream << ", "; + } + OutputColumns(m_stream, "", column); + return *this; + } + + PrimaryKeyBuilder::operator details::SubBuilder() + { + if (m_needsClosing) + { + m_stream << ')'; + m_needsClosing = false; + } + return { m_stream.str() }; + } + + StatementBuilder& StatementBuilder::Select() + { + m_stream << "SELECT "; + return *this; + } + + StatementBuilder& StatementBuilder::Select(std::string_view column) + { + OutputColumns(m_stream, "SELECT ", column); + return *this; + } + + StatementBuilder& StatementBuilder::Select(std::initializer_list<std::string_view> columns) + { + OutputColumns(m_stream, "SELECT ", columns); + return *this; + } + + StatementBuilder& StatementBuilder::Select(const QualifiedColumn& column) + { + OutputColumns(m_stream, "SELECT ", column); + return *this; + } + + StatementBuilder& StatementBuilder::Select(std::initializer_list<QualifiedColumn> columns) + { + OutputColumns(m_stream, "SELECT ", columns); return *this; } @@ -56,30 +206,25 @@ namespace AppInstaller::Repository::SQLite::Builder StatementBuilder& StatementBuilder::From(std::string_view table) { - m_stream << " FROM [" << table << ']'; + OutputOperationAndTable(m_stream, " FROM", table); return *this; } StatementBuilder& StatementBuilder::From(std::initializer_list<std::string_view> table) { - m_stream << " FROM ["; - for (std::string_view t : table) - { - m_stream << t; - } - m_stream << ']'; + OutputOperationAndTable(m_stream, " FROM", table); return *this; } StatementBuilder& StatementBuilder::Where(std::string_view column) { - m_stream << " WHERE [" << column << ']'; + OutputColumns(m_stream, " WHERE ", column); return *this; } - StatementBuilder& StatementBuilder::Where(QualifiedColumn column) + StatementBuilder& StatementBuilder::Where(const QualifiedColumn& column) { - m_stream << " WHERE " << column; + OutputColumns(m_stream, " WHERE ", column); return *this; } @@ -105,34 +250,29 @@ namespace AppInstaller::Repository::SQLite::Builder StatementBuilder& StatementBuilder::And(std::string_view column) { - m_stream << " AND [" << column << ']'; + OutputColumns(m_stream, " AND ", column); return *this; } - StatementBuilder& StatementBuilder::And(QualifiedColumn column) + StatementBuilder& StatementBuilder::And(const QualifiedColumn& column) { - m_stream << " AND " << column; + OutputColumns(m_stream, " AND ", column); return *this; } StatementBuilder& StatementBuilder::Join(std::string_view table) { - m_stream << " JOIN [" << table << ']'; + OutputOperationAndTable(m_stream, " JOIN", table); return *this; } StatementBuilder& StatementBuilder::Join(std::initializer_list<std::string_view> table) { - m_stream << " JOIN ["; - for (std::string_view t : table) - { - m_stream << t; - } - m_stream << ']'; + OutputOperationAndTable(m_stream, " JOIN", table); return *this; } - StatementBuilder& StatementBuilder::On(QualifiedColumn column1, QualifiedColumn column2) + StatementBuilder& StatementBuilder::On(const QualifiedColumn& column1, const QualifiedColumn& column2) { m_stream << " ON " << column1 << " = " << column2; return *this; @@ -144,14 +284,170 @@ namespace AppInstaller::Repository::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::InsertInto(std::string_view table) + { + OutputOperationAndTable(m_stream, "INSERT INTO", table); + return *this; + } + + StatementBuilder& StatementBuilder::InsertInto(std::initializer_list<std::string_view> table) + { + OutputOperationAndTable(m_stream, "INSERT INTO", table); + return *this; + } + + StatementBuilder& StatementBuilder::Columns(std::string_view column) + { + OutputColumns(m_stream, "(", column); + m_stream << ')'; + return *this; + } + + StatementBuilder& StatementBuilder::Columns(std::initializer_list<std::string_view> columns) + { + OutputColumns(m_stream, "(", columns); + m_stream << ')'; + return *this; + } + + StatementBuilder& StatementBuilder::Columns(const QualifiedColumn& column) + { + OutputColumns(m_stream, "(", column); + m_stream << ')'; + return *this; + } + + StatementBuilder& StatementBuilder::Columns(std::initializer_list<QualifiedColumn> columns) + { + OutputColumns(m_stream, "(", columns); + m_stream << ')'; + return *this; + } + + StatementBuilder& StatementBuilder::Columns(std::initializer_list<details::SubBuilder> columns) + { + OutputColumns(m_stream, "(", columns); + m_stream << ')'; + return *this; + } + + StatementBuilder& StatementBuilder::BeginColumns() + { + m_stream << '('; + m_needsComma = false; + return *this; + } + + StatementBuilder& StatementBuilder::Column(std::string_view column) + { + if (m_needsComma) + { + m_stream << ", "; + } + OutputColumns(m_stream, "", column); + m_needsComma = true; + return *this; + } + + StatementBuilder& StatementBuilder::Column(const QualifiedColumn& column) + { + if (m_needsComma) + { + m_stream << ", "; + } + OutputColumns(m_stream, "", column); + m_needsComma = true; + return *this; + } + + StatementBuilder& StatementBuilder::Column(const details::SubBuilder& column) + { + if (m_needsComma) + { + m_stream << ", "; + } + m_stream << column; + m_needsComma = true; + return *this; + } + + StatementBuilder& StatementBuilder::EndColumns() + { + m_stream << ')'; + m_needsComma = false; + return *this; + } + + StatementBuilder& StatementBuilder::BeginValues() + { + m_stream << " VALUES ("; + m_needsComma = false; + return *this; + } + + StatementBuilder& StatementBuilder::EndValues() + { + m_stream << ')'; + m_needsComma = false; + return *this; + } + + StatementBuilder& StatementBuilder::CreateTable(std::string_view table) + { + OutputOperationAndTable(m_stream, "CREATE TABLE", table); + return *this; + } + + StatementBuilder& StatementBuilder::CreateTable(std::initializer_list<std::string_view> table) + { + OutputOperationAndTable(m_stream, "CREATE TABLE", table); + return *this; + } + + StatementBuilder& StatementBuilder::CreateIndex(std::string_view table) + { + OutputOperationAndTable(m_stream, "CREATE INDEX", table); + return *this; + } + + StatementBuilder& StatementBuilder::CreateIndex(std::initializer_list<std::string_view> table) + { + OutputOperationAndTable(m_stream, "CREATE INDEX", table); + return *this; + } + + StatementBuilder& StatementBuilder::On(std::string_view table) + { + OutputOperationAndTable(m_stream, " ON", table); + return *this; + } + + StatementBuilder& StatementBuilder::On(std::initializer_list<std::string_view> table) + { + OutputOperationAndTable(m_stream, " ON", table); + return *this; + } + + StatementBuilder& StatementBuilder::DeleteFrom(std::string_view table) + { + OutputOperationAndTable(m_stream, "DELETE FROM", table); + return *this; + } + + StatementBuilder& StatementBuilder::DeleteFrom(std::initializer_list<std::string_view> table) + { + OutputOperationAndTable(m_stream, "DELETE FROM", table); + return *this; + } + Statement StatementBuilder::Prepare(Connection& connection, bool persistent) { - m_statement = std::make_unique<Statement>(Statement::Create(connection, m_stream.str(), persistent)); + Statement result = Statement::Create(connection, m_stream.str(), persistent); for (const auto& f : m_binders) { - f(); + f(result); } - return std::move(*(m_statement.release())); + return result; } void StatementBuilder::Execute(Connection& connection) @@ -172,4 +468,29 @@ namespace AppInstaller::Repository::SQLite::Builder return m_bindIndex++; } + + int StatementBuilder::AppendValuesAndBinders(size_t count) + { + m_stream << " VALUES ("; + for (size_t i = 0; i < count; ++i) + { + m_stream << (i == 0 ? "?" : ", ?"); + } + m_stream << ')'; + + int result = m_bindIndex; + m_bindIndex += static_cast<int>(count); + return result; + } + + int StatementBuilder::AppendValueAndBinder() + { + if (m_needsComma) + { + m_stream << ", "; + } + m_stream << '?'; + m_needsComma = true; + return m_bindIndex++; + } } diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once #include "SQLiteWrapper.h" +#include <AppInstallerLanguageUtilities.h> #include <functional> #include <initializer_list> @@ -18,6 +19,40 @@ namespace AppInstaller::Repository::SQLite::Builder // Sentinel types to indicate special cases to the builder. struct unbound_t {}; struct rowcount_t {}; + + // Class for intake from external functions. + struct SubBuilder + { + SubBuilder(std::string&& s) : m_string(std::move(s)) {} + + SubBuilder(const SubBuilder&) = default; + SubBuilder& operator=(const SubBuilder&) = default; + + SubBuilder(SubBuilder&&) noexcept = default; + SubBuilder& operator=(SubBuilder&&) noexcept = default; + + const std::string& GetString() const { return m_string; } + + protected: + std::string m_string; + }; + + // Base class for all sub-builders. + struct SubBuilderBase + { + SubBuilderBase() = default; + + SubBuilderBase(const SubBuilderBase&) = default; + SubBuilderBase& operator=(const SubBuilderBase&) = default; + + SubBuilderBase(SubBuilderBase&&) noexcept = default; + SubBuilderBase& operator=(SubBuilderBase&&) noexcept = default; + + virtual operator SubBuilder() { return { m_stream.str() }; } + + protected: + std::ostringstream m_stream; + }; } // Pass this value to indicate that the caller will bind the value later. @@ -36,6 +71,61 @@ namespace AppInstaller::Repository::SQLite::Builder explicit QualifiedColumn(std::string_view table, std::string_view column) : Table(table), Column(column) {} }; + // SQLite types as an enum. + enum class Type + { + Int, + Int64, + Text, + }; + + // Helper used when creating a table. + struct ColumnBuilder : public details::SubBuilderBase + { + // Specify the column name and type when creating the builder. + ColumnBuilder(std::string_view column, Type type); + + ColumnBuilder(const ColumnBuilder&) = default; + ColumnBuilder& operator=(const ColumnBuilder&) = default; + + ColumnBuilder(ColumnBuilder&&) noexcept = default; + ColumnBuilder& operator=(ColumnBuilder&&) noexcept = default; + + // Indicate that the column is not able to be null. + // Allow for data driven construction with input value. + ColumnBuilder& NotNull(bool isTrue = true); + + // Indicate that the column is unique. + // Allow for data driven construction with input value. + ColumnBuilder& Unique(bool isTrue = true); + + // Indicate that the column is the primary key. + // Allow for data driven construction with input value. + ColumnBuilder& PrimaryKey(bool isTrue = true); + }; + + // Helper used to specify a primary key with multiple columns. + struct PrimaryKeyBuilder : public details::SubBuilderBase + { + PrimaryKeyBuilder(); + PrimaryKeyBuilder(std::initializer_list<std::string_view> columns); + + PrimaryKeyBuilder(const PrimaryKeyBuilder&) = default; + PrimaryKeyBuilder& operator=(const PrimaryKeyBuilder&) = default; + + PrimaryKeyBuilder(PrimaryKeyBuilder&&) noexcept = default; + PrimaryKeyBuilder& operator=(PrimaryKeyBuilder&&) noexcept = default; + + virtual operator details::SubBuilder() override; + + // Add a column to the primary key. + PrimaryKeyBuilder& Column(std::string_view column); + + private: + bool m_isFirst = true; + bool m_needsClosing = true; + }; + // A class that aids in building SQL statements in a more expressive manner than simple strings. struct StatementBuilder { @@ -48,9 +138,10 @@ namespace AppInstaller::Repository::SQLite::Builder StatementBuilder& operator=(StatementBuilder&&) = default; // Begin a select statement for the given columns. + StatementBuilder& Select(); StatementBuilder& Select(std::string_view column); StatementBuilder& Select(std::initializer_list<std::string_view> columns); - StatementBuilder& Select(QualifiedColumn column); + StatementBuilder& Select(const QualifiedColumn& column); StatementBuilder& Select(std::initializer_list<QualifiedColumn> columns); StatementBuilder& Select(details::rowcount_t); @@ -61,7 +152,7 @@ namespace AppInstaller::Repository::SQLite::Builder // Begin a filter clause on the given column. StatementBuilder& Where(std::string_view column); - StatementBuilder& Where(QualifiedColumn column); + StatementBuilder& Where(const QualifiedColumn& column); // Indicate the operation of the filter clause. template <typename ValueType> @@ -90,7 +181,7 @@ namespace AppInstaller::Repository::SQLite::Builder // Operators for combining filter clauses. StatementBuilder& And(std::string_view column); - StatementBuilder& And(QualifiedColumn column); + StatementBuilder& And(const QualifiedColumn& column); // Begin a join clause. // The initializer_list form enables the table name to be constructed from multiple parts. @@ -98,11 +189,69 @@ namespace AppInstaller::Repository::SQLite::Builder StatementBuilder& Join(std::initializer_list<std::string_view> table); // Set the join constraint. - StatementBuilder& On(QualifiedColumn column1, QualifiedColumn column2); + StatementBuilder& On(const QualifiedColumn& column1, const QualifiedColumn& column2); // Limits the result set to the given number of rows. StatementBuilder& Limit(size_t rowCount); + // Begin an insert statement for the given table. + // The initializer_list form enables the table name to be constructed from multiple parts. + StatementBuilder& InsertInto(std::string_view table); + StatementBuilder& InsertInto(std::initializer_list<std::string_view> table); + + // Set the columns for a statement (typically insert). + StatementBuilder& Columns(std::string_view column); + StatementBuilder& Columns(std::initializer_list<std::string_view> columns); + StatementBuilder& Columns(const QualifiedColumn& column); + StatementBuilder& Columns(std::initializer_list<QualifiedColumn> columns); + + // Set the columns for a create table statement. + StatementBuilder& Columns(std::initializer_list<details::SubBuilder> columns); + StatementBuilder& BeginColumns(); + StatementBuilder& Column(std::string_view column); + StatementBuilder& Column(const QualifiedColumn& column); + StatementBuilder& Column(const details::SubBuilder& column); + StatementBuilder& EndColumns(); + + // Add the values clause for an insert statement. + template <typename... ValueTypes> + StatementBuilder& Values(const ValueTypes&... values) + { + int bindIndexBegin = AppendValuesAndBinders(sizeof...(ValueTypes)); + // Use folding to add a binder for every value, specifically in the order they were given. + // Do not change this expression without understanding the implications to the bind order. + // See: https://en.cppreference.com/w/cpp/language/fold for more details. + (FoldHelper{}, ..., InsertValuesValueBinder(bindIndexBegin++, values)); + return *this; + } + StatementBuilder& BeginValues(); + template <typename ValueType> + StatementBuilder& Value(const ValueType& value) + { + InsertValuesValueBinder(AppendValueAndBinder(), value); + return *this; + } + StatementBuilder& EndValues(); + + // Begin a table creation statement. + // The initializer_list form enables the table name to be constructed from multiple parts. + StatementBuilder& CreateTable(std::string_view table); + StatementBuilder& CreateTable(std::initializer_list<std::string_view> table); + + // Begin an index creation statement. + // The initializer_list form enables the table name to be constructed from multiple parts. + StatementBuilder& CreateIndex(std::string_view table); + StatementBuilder& CreateIndex(std::initializer_list<std::string_view> table); + + // Set index target table. + StatementBuilder& On(std::string_view table); + StatementBuilder& On(std::initializer_list<std::string_view> table); + + // Begin a delete statement. + // The initializer_list form enables the table name to be constructed from multiple parts. + StatementBuilder& DeleteFrom(std::string_view table); + StatementBuilder& DeleteFrom(std::initializer_list<std::string_view> table); + // Prepares and returns the statement, applying any bindings that were requested. Statement Prepare(Connection& connection, bool persistent = false); @@ -118,17 +267,53 @@ namespace AppInstaller::Repository::SQLite::Builder // Appends given the operation. int AppendOpAndBinder(Op op); + // Appends a set of binders for the values clause of an insert. + int AppendValuesAndBinders(size_t count); + + // Appends a binder for the values clause of an insert. + int AppendValueAndBinder(); + // Adds a functor to our list that will bind the given value. template <typename ValueType> void AddBindFunctor(int binderIndex, const ValueType& value) { - m_binders.emplace_back([this, binderIndex, &value]() { this->m_statement->Bind(binderIndex, value); }); + m_binders.emplace_back([binderIndex, value](Statement& s) { s.Bind(binderIndex, value); }); + } + + // Helper template for binding incoming values for an insert. + template <typename ValueType> + StatementBuilder& InsertValuesValueBinder(int bindIndex, const ValueType& value) + { + AddBindFunctor(bindIndex, value); + return *this; + } + template <typename ValueType> + StatementBuilder& InsertValuesValueBinder(int bindIndex, const std::optional<ValueType>& value) + { + if (value) + { + AddBindFunctor(bindIndex, value.value()); + } + else + { + AddBindFunctor(bindIndex, nullptr); + } + return *this; + } + StatementBuilder& InsertValuesValueBinder(int, details::unbound_t) + { + return *this; + } + StatementBuilder& InsertValuesValueBinder(int bindIndex, std::nullptr_t) + { + AddBindFunctor(bindIndex, nullptr); + return *this; } std::ostringstream m_stream; - std::unique_ptr<Statement> m_statement; // Because binding values starts at 1 int m_bindIndex = 1; - std::vector<std::function<void()>> m_binders; + std::vector<std::function<void(Statement&)>> m_binders; + bool m_needsComma = false; }; }