diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 5665f59508..056a8d0a63 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -43,6 +43,11 @@ if (GTEST_FOUND AND GMOCK_FOUND) ../openmw/options.cpp openmw/options.cpp + + sqlite3/db.cpp + sqlite3/request.cpp + sqlite3/statement.cpp + sqlite3/transaction.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) diff --git a/apps/openmw_test_suite/sqlite3/db.cpp b/apps/openmw_test_suite/sqlite3/db.cpp new file mode 100644 index 0000000000..d2c0cd8674 --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/db.cpp @@ -0,0 +1,15 @@ +#include + +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + TEST(Sqlite3DbTest, makeDbShouldCreateInMemoryDbWithSchema) + { + const auto db = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + EXPECT_NE(db, nullptr); + } +} diff --git a/apps/openmw_test_suite/sqlite3/request.cpp b/apps/openmw_test_suite/sqlite3/request.cpp new file mode 100644 index 0000000000..e0094827d1 --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/request.cpp @@ -0,0 +1,270 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + template + struct InsertInt + { + static std::string_view text() noexcept { return "INSERT INTO ints (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, T value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertReal + { + static std::string_view text() noexcept { return "INSERT INTO reals (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, double value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertText + { + static std::string_view text() noexcept { return "INSERT INTO texts (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, std::string_view value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertBlob + { + static std::string_view text() noexcept { return "INSERT INTO blobs (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, const std::vector& value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetAll + { + std::string mQuery; + + explicit GetAll(const std::string& table) : mQuery("SELECT value FROM " + table + " ORDER BY value") {} + + std::string_view text() noexcept { return mQuery; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + template + struct GetExact + { + std::string mQuery; + + explicit GetExact(const std::string& table) : mQuery("SELECT value FROM " + table + " WHERE value = :value") {} + + std::string_view text() noexcept { return mQuery; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, const T& value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetInt64 + { + static std::string_view text() noexcept { return "SELECT value FROM ints WHERE value = :value"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, std::int64_t value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetNull + { + static std::string_view text() noexcept { return "SELECT NULL"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct Int + { + int mValue = 0; + + Int() = default; + + explicit Int(int value) : mValue(value) {} + + Int& operator=(int value) + { + mValue = value; + return *this; + } + + friend bool operator==(const Int& l, const Int& r) + { + return l.mValue == r.mValue; + } + }; + + constexpr const char schema[] = R"( + CREATE TABLE ints ( value INTEGER ); + CREATE TABLE reals ( value REAL ); + CREATE TABLE texts ( value TEXT ); + CREATE TABLE blobs ( value BLOB ); + )"; + + struct Sqlite3RequestTest : Test + { + const Db mDb = makeDb(":memory:", schema); + }; + + TEST_F(Sqlite3RequestTest, executeShouldSupportInt) + { + Statement insert(*mDb, InsertInt {}); + EXPECT_EQ(execute(*mDb, insert, 13), 1); + EXPECT_EQ(execute(*mDb, insert, 42), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(13), std::tuple(42))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportInt64) + { + Statement insert(*mDb, InsertInt {}); + const std::int64_t value = 1099511627776; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportReal) + { + Statement insert(*mDb, InsertReal {}); + EXPECT_EQ(execute(*mDb, insert, 3.14), 1); + Statement select(*mDb, GetAll("reals")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(3.14))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportText) + { + Statement insert(*mDb, InsertText {}); + const std::string text = "foo"; + EXPECT_EQ(execute(*mDb, insert, text), 1); + Statement select(*mDb, GetAll("texts")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(text))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportBlob) + { + Statement insert(*mDb, InsertBlob {}); + const std::vector blob({std::byte(42), std::byte(13)}); + EXPECT_EQ(execute(*mDb, insert, blob), 1); + Statement select(*mDb, GetAll("blobs")); + std::vector>> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(blob))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportInt) + { + Statement insert(*mDb, InsertInt {}); + const int value = 42; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportInt64) + { + Statement insert(*mDb, InsertInt {}); + const std::int64_t value = 1099511627776; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportReal) + { + Statement insert(*mDb, InsertReal {}); + const double value = 3.14; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("reals")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportText) + { + Statement insert(*mDb, InsertText {}); + const std::string text = "foo"; + EXPECT_EQ(execute(*mDb, insert, text), 1); + Statement select(*mDb, GetExact("texts")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), text); + EXPECT_THAT(result, ElementsAre(std::tuple(text))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportBlob) + { + Statement insert(*mDb, InsertBlob {}); + const std::vector blob({std::byte(42), std::byte(13)}); + EXPECT_EQ(execute(*mDb, insert, blob), 1); + Statement select(*mDb, GetExact>("blobs")); + std::vector>> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), blob); + EXPECT_THAT(result, ElementsAre(std::tuple(blob))); + } + + TEST_F(Sqlite3RequestTest, requestResultShouldSupportNull) + { + Statement select(*mDb, GetNull {}); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(nullptr))); + } + + TEST_F(Sqlite3RequestTest, requestResultShouldSupportConstructibleFromInt) + { + Statement insert(*mDb, InsertInt {}); + const int value = 42; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(Int(value)))); + } + + TEST_F(Sqlite3RequestTest, requestShouldLimitOutput) + { + Statement insert(*mDb, InsertInt {}); + EXPECT_EQ(execute(*mDb, insert, 13), 1); + EXPECT_EQ(execute(*mDb, insert, 42), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), 1); + EXPECT_THAT(result, ElementsAre(std::tuple(13))); + } +} diff --git a/apps/openmw_test_suite/sqlite3/statement.cpp b/apps/openmw_test_suite/sqlite3/statement.cpp new file mode 100644 index 0000000000..fb53ceebb2 --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/statement.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + struct Query + { + static std::string_view text() noexcept { return "SELECT 1"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + TEST(Sqlite3StatementTest, makeStatementShouldCreateStatementWithPreparedQuery) + { + const auto db = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + const Statement statement(*db, Query {}); + EXPECT_FALSE(statement.mNeedReset); + EXPECT_NE(statement.mHandle, nullptr); + EXPECT_EQ(statement.mQuery.text(), "SELECT 1"); + } +} diff --git a/apps/openmw_test_suite/sqlite3/transaction.cpp b/apps/openmw_test_suite/sqlite3/transaction.cpp new file mode 100644 index 0000000000..913fd34bce --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/transaction.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + struct InsertId + { + static std::string_view text() noexcept { return "INSERT INTO test (id) VALUES (42)"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct GetIds + { + static std::string_view text() noexcept { return "SELECT id FROM test"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct Sqlite3TransactionTest : Test + { + const Db mDb = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + + void insertId() const + { + Statement insertId(*mDb, InsertId {}); + EXPECT_EQ(execute(*mDb, insertId), 1); + } + + std::vector> getIds() const + { + Statement getIds(*mDb, GetIds {}); + std::vector> result; + request(*mDb, getIds, std::back_inserter(result), std::numeric_limits::max()); + return result; + } + }; + + TEST_F(Sqlite3TransactionTest, shouldRollbackOnDestruction) + { + { + const Transaction transaction(*mDb); + insertId(); + } + EXPECT_THAT(getIds(), IsEmpty()); + } + + TEST_F(Sqlite3TransactionTest, commitShouldCommitTransaction) + { + { + Transaction transaction(*mDb); + insertId(); + transaction.commit(); + } + EXPECT_THAT(getIds(), ElementsAre(std::tuple(42))); + } +} diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 7ee3d184d8..8fc8c2046b 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -201,6 +201,12 @@ add_component_dir(loadinglistener reporter ) +add_component_dir(sqlite3 + db + statement + transaction +) + set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ) @@ -236,6 +242,8 @@ endif () include_directories(${BULLET_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR}) +find_package(SQLite3 REQUIRED) + add_library(components STATIC ${COMPONENT_FILES}) target_link_libraries(components @@ -268,6 +276,7 @@ target_link_libraries(components RecastNavigation::Recast Base64 + SQLite::SQLite3 ) target_link_libraries(components ${BULLET_LIBRARIES}) diff --git a/components/sqlite3/db.cpp b/components/sqlite3/db.cpp new file mode 100644 index 0000000000..54a156057d --- /dev/null +++ b/components/sqlite3/db.cpp @@ -0,0 +1,31 @@ +#include "db.hpp" + +#include + +#include +#include +#include + +namespace Sqlite3 +{ + void CloseSqlite3::operator()(sqlite3* handle) const noexcept + { + sqlite3_close(handle); + } + + Db makeDb(std::string_view path, const char* schema) + { + sqlite3* handle = nullptr; + const int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; + if (const int ec = sqlite3_open_v2(std::string(path).c_str(), &handle, flags, nullptr); ec != SQLITE_OK) + { + const std::string message(sqlite3_errmsg(handle)); + sqlite3_close(handle); + throw std::runtime_error("Failed to open database: " + message); + } + Db result(handle); + if (const int ec = sqlite3_exec(result.get(), schema, nullptr, nullptr, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed create database schema: " + std::string(sqlite3_errmsg(handle))); + return result; + } +} diff --git a/components/sqlite3/db.hpp b/components/sqlite3/db.hpp new file mode 100644 index 0000000000..293ace4375 --- /dev/null +++ b/components/sqlite3/db.hpp @@ -0,0 +1,21 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_DB_H +#define OPENMW_COMPONENTS_SQLITE3_DB_H + +#include +#include + +struct sqlite3; + +namespace Sqlite3 +{ + struct CloseSqlite3 + { + void operator()(sqlite3* handle) const noexcept; + }; + + using Db = std::unique_ptr; + + Db makeDb(std::string_view path, const char* schema); +} + +#endif diff --git a/components/sqlite3/request.hpp b/components/sqlite3/request.hpp new file mode 100644 index 0000000000..378dd5fdf7 --- /dev/null +++ b/components/sqlite3/request.hpp @@ -0,0 +1,275 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_REQUEST_H +#define OPENMW_COMPONENTS_SQLITE3_REQUEST_H + +#include "statement.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Sqlite3 +{ + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, int value) + { + if (const int ec = sqlite3_bind_int(&stmt, index, value); ec != SQLITE_OK) + throw std::runtime_error("Failed to bind int to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, std::int64_t value) + { + if (const int ec = sqlite3_bind_int64(&stmt, index, value); ec != SQLITE_OK) + throw std::runtime_error("Failed to bind int64 to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, double value) + { + if (const int ec = sqlite3_bind_double(&stmt, index, value); ec != SQLITE_OK) + throw std::runtime_error("Failed to bind double to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, std::string_view value) + { + if (sqlite3_bind_text(&stmt, index, value.data(), static_cast(value.size()), SQLITE_STATIC) != SQLITE_OK) + throw std::runtime_error("Failed to bind text to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, const std::vector& value) + { + if (sqlite3_bind_blob(&stmt, index, value.data(), static_cast(value.size()), SQLITE_STATIC) != SQLITE_OK) + throw std::runtime_error("Failed to bind blob to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + template + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, const char* name, const T& value) + { + const int index = sqlite3_bind_parameter_index(&stmt, name); + if (index == 0) + throw std::logic_error("Parameter \"" + std::string(name) + "\" is not found"); + bindParameter(db, stmt, index, value); + } + + inline std::string sqliteTypeToString(int value) + { + switch (value) + { + case SQLITE_INTEGER: return "SQLITE_INTEGER"; + case SQLITE_FLOAT: return "SQLITE_FLOAT"; + case SQLITE_TEXT: return "SQLITE_TEXT"; + case SQLITE_BLOB: return "SQLITE_BLOB"; + case SQLITE_NULL: return "SQLITE_NULL"; + } + return "unsupported(" + std::to_string(value) + ")"; + } + + template + inline auto copyColumn(sqlite3& /*db*/, sqlite3_stmt& /*statement*/, int index, int type, T*& value) + { + if (type != SQLITE_NULL) + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_INTEGER or SQLITE_FLOAT"); + value = nullptr; + } + + template + inline auto copyColumn(sqlite3& /*db*/, sqlite3_stmt& statement, int index, int type, T& value) + { + switch (type) + { + case SQLITE_INTEGER: + value = static_cast(sqlite3_column_int64(&statement, index)); + return; + case SQLITE_FLOAT: + value = static_cast(sqlite3_column_double(&statement, index)); + return; + case SQLITE_NULL: + value = std::decay_t{}; + return; + } + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_INTEGER or SQLITE_FLOAT or SQLITE_NULL"); + } + + inline void copyColumn(sqlite3& db, sqlite3_stmt& statement, int index, int type, std::string& value) + { + if (type != SQLITE_TEXT) + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_TEXT"); + const unsigned char* const text = sqlite3_column_text(&statement, index); + if (text == nullptr) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to read text from column " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + const int size = sqlite3_column_bytes(&statement, index); + if (size <= 0) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to get column bytes " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + value.reserve(static_cast(size)); + value.assign(reinterpret_cast(text), reinterpret_cast(text) + size); + } + + inline void copyColumn(sqlite3& db, sqlite3_stmt& statement, int index, int type, std::vector& value) + { + if (type != SQLITE_BLOB) + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_BLOB"); + const void* const blob = sqlite3_column_blob(&statement, index); + if (blob == nullptr) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to read blob from column " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + const int size = sqlite3_column_bytes(&statement, index); + if (size <= 0) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to get column bytes " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + value.reserve(static_cast(size)); + value.assign(static_cast(blob), static_cast(blob) + size); + } + + template + inline void getColumnsImpl(sqlite3& db, sqlite3_stmt& statement, T& row) + { + if constexpr (0 < index && index <= std::tuple_size_v) + { + const int column = index - 1; + if (const int number = sqlite3_column_count(&statement); column >= number) + throw std::out_of_range("Column number is out of range: " + std::to_string(column) + + " >= " + std::to_string(number)); + const int type = sqlite3_column_type(&statement, column); + switch (type) + { + case SQLITE_INTEGER: + case SQLITE_FLOAT: + case SQLITE_TEXT: + case SQLITE_BLOB: + case SQLITE_NULL: + copyColumn(db, statement, column, type, std::get(row)); + break; + default: + throw std::runtime_error("Column " + std::to_string(column) + + " has unnsupported column type: " + sqliteTypeToString(type)); + } + getColumnsImpl(db, statement, row); + } + } + + template + inline void getColumns(sqlite3& db, sqlite3_stmt& statement, T& row) + { + getColumnsImpl>(db, statement, row); + } + + template + inline void getRow(sqlite3& db, sqlite3_stmt& statement, T& row) + { + auto tuple = std::tie(row); + getColumns(db, statement, tuple); + } + + template + inline void getRow(sqlite3& db, sqlite3_stmt& statement, std::tuple& row) + { + getColumns(db, statement, row); + } + + template + inline void getRow(sqlite3& db, sqlite3_stmt& statement, std::back_insert_iterator& it) + { + typename T::value_type row; + getRow(db, statement, row); + it = std::move(row); + } + + template + inline void prepare(sqlite3& db, Statement& statement, Args&& ... args) + { + if (statement.mNeedReset) + { + if (sqlite3_reset(statement.mHandle.get()) == SQLITE_OK + && sqlite3_clear_bindings(statement.mHandle.get()) == SQLITE_OK) + statement.mNeedReset = false; + else + statement.mHandle = makeStatementHandle(db, statement.mQuery.text()); + } + statement.mQuery.bind(db, *statement.mHandle, std::forward(args) ...); + } + + template + inline bool executeStep(sqlite3& db, const Statement& statement) + { + switch (sqlite3_step(statement.mHandle.get())) + { + case SQLITE_ROW: return true; + case SQLITE_DONE: return false; + } + throw std::runtime_error("Failed to execute statement step: " + std::string(sqlite3_errmsg(&db))); + } + + template + inline I request(sqlite3& db, Statement& statement, I out, std::size_t max, Args&& ... args) + { + try + { + statement.mNeedReset = true; + prepare(db, statement, std::forward(args) ...); + for (std::size_t i = 0; executeStep(db, statement) && i < max; ++i) + getRow(db, *statement.mHandle, *out++); + return out; + } + catch (const std::exception& e) + { + throw std::runtime_error("Failed perform request \"" + std::string(statement.mQuery.text()) + + "\": " + std::string(e.what())); + } + } + + template + inline int execute(sqlite3& db, Statement& statement, Args&& ... args) + { + try + { + statement.mNeedReset = true; + prepare(db, statement, std::forward(args) ...); + if (executeStep(db, statement)) + throw std::logic_error("Execute cannot return rows"); + return sqlite3_changes(&db); + } + catch (const std::exception& e) + { + throw std::runtime_error("Failed to execute statement \"" + std::string(statement.mQuery.text()) + + "\": " + std::string(e.what())); + } + } +} + +#endif diff --git a/components/sqlite3/statement.cpp b/components/sqlite3/statement.cpp new file mode 100644 index 0000000000..07ca6b8ddf --- /dev/null +++ b/components/sqlite3/statement.cpp @@ -0,0 +1,24 @@ +#include "statement.hpp" + +#include + +#include +#include +#include + +namespace Sqlite3 +{ + void CloseSqlite3Stmt::operator()(sqlite3_stmt* handle) const noexcept + { + sqlite3_finalize(handle); + } + + StatementHandle makeStatementHandle(sqlite3& db, std::string_view query) + { + sqlite3_stmt* stmt = nullptr; + if (const int ec = sqlite3_prepare_v2(&db, query.data(), static_cast(query.size()), &stmt, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed to prepare statement for query \"" + std::string(query) + "\": " + + std::string(sqlite3_errmsg(&db))); + return StatementHandle(stmt); + } +} diff --git a/components/sqlite3/statement.hpp b/components/sqlite3/statement.hpp new file mode 100644 index 0000000000..469e63933c --- /dev/null +++ b/components/sqlite3/statement.hpp @@ -0,0 +1,35 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_STATEMENT_H +#define OPENMW_COMPONENTS_SQLITE3_STATEMENT_H + +#include +#include +#include + +struct sqlite3; +struct sqlite3_stmt; + +namespace Sqlite3 +{ + struct CloseSqlite3Stmt + { + void operator()(sqlite3_stmt* handle) const noexcept; + }; + + using StatementHandle = std::unique_ptr; + + StatementHandle makeStatementHandle(sqlite3& db, std::string_view query); + + template + struct Statement + { + bool mNeedReset = false; + StatementHandle mHandle; + Query mQuery; + + explicit Statement(sqlite3& db, Query query = Query {}) + : mHandle(makeStatementHandle(db, query.text())), + mQuery(std::move(query)) {} + }; +} + +#endif diff --git a/components/sqlite3/transaction.cpp b/components/sqlite3/transaction.cpp new file mode 100644 index 0000000000..3012538652 --- /dev/null +++ b/components/sqlite3/transaction.cpp @@ -0,0 +1,33 @@ +#include "transaction.hpp" + +#include + +#include + +#include +#include + +namespace Sqlite3 +{ + void Rollback::operator()(sqlite3* db) const + { + if (db == nullptr) + return; + if (const int ec = sqlite3_exec(db, "ROLLBACK", nullptr, nullptr, nullptr); ec != SQLITE_OK) + Log(Debug::Warning) << "Failed to rollback SQLite3 transaction: " << std::string(sqlite3_errmsg(db)); + } + + Transaction::Transaction(sqlite3& db) + : mDb(&db) + { + if (const int ec = sqlite3_exec(mDb.get(), "BEGIN", nullptr, nullptr, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed to start transaction: " + std::string(sqlite3_errmsg(mDb.get()))); + } + + void Transaction::commit() + { + if (const int ec = sqlite3_exec(mDb.get(), "COMMIT", nullptr, nullptr, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed to commit transaction: " + std::string(sqlite3_errmsg(mDb.get()))); + (void) mDb.release(); + } +} diff --git a/components/sqlite3/transaction.hpp b/components/sqlite3/transaction.hpp new file mode 100644 index 0000000000..88b780a0a5 --- /dev/null +++ b/components/sqlite3/transaction.hpp @@ -0,0 +1,27 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_TRANSACTION_H +#define OPENMW_COMPONENTS_SQLITE3_TRANSACTION_H + +#include + +struct sqlite3; + +namespace Sqlite3 +{ + struct Rollback + { + void operator()(sqlite3* handle) const; + }; + + class Transaction + { + public: + Transaction(sqlite3& db); + + void commit(); + + private: + std::unique_ptr mDb; + }; +} + +#endif