From ee6762c0d93f89653defa580c7dd730156a53646 Mon Sep 17 00:00:00 2001 From: Kelvin Hammond Date: Thu, 10 Sep 2020 18:52:11 -0400 Subject: [PATCH] Added: Savepoint support --- CHANGELOG.md | 5 +- CMakeLists.txt | 3 + include/SQLiteCpp/Savepoint.h | 94 +++++++++++++++++++++++++++++ src/Savepoint.cpp | 65 +++++++++++++++++++++ tests/Savepoint_test.cpp | 107 ++++++++++++++++++++++++++++++++++ 5 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 include/SQLiteCpp/Savepoint.h create mode 100644 src/Savepoint.cpp create mode 100644 tests/Savepoint_test.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index d5a14f4..f49cf64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -185,4 +185,7 @@ Version 3.1.0 - August 11 2020 Version 3.1.1 - August 19 2020 - #292 Fix compilation if using SQLITE_HAS_CODEC from sum01/fix_sqlcipher_compile -- #293 Remove FindSQLiteCpp.cmake from sum01/fix_283 \ No newline at end of file +- #293 Remove FindSQLiteCpp.cmake from sum01/fix_283 + +Version 3.2.0 - September 10 2020 +- Added Savepoint support diff --git a/CMakeLists.txt b/CMakeLists.txt index 85c1061..a0949c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,6 +103,7 @@ set(SQLITECPP_SRC ${PROJECT_SOURCE_DIR}/src/Column.cpp ${PROJECT_SOURCE_DIR}/src/Database.cpp ${PROJECT_SOURCE_DIR}/src/Exception.cpp + ${PROJECT_SOURCE_DIR}/src/Savepoint.cpp ${PROJECT_SOURCE_DIR}/src/Statement.cpp ${PROJECT_SOURCE_DIR}/src/Transaction.cpp ) @@ -116,6 +117,7 @@ set(SQLITECPP_INC ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Column.h ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Database.h ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Exception.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Savepoint.h ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Statement.h ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Transaction.h ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/VariadicBind.h @@ -127,6 +129,7 @@ source_group(include FILES ${SQLITECPP_INC}) set(SQLITECPP_TESTS tests/Column_test.cpp tests/Database_test.cpp + tests/Savepoint_test.cpp tests/Statement_test.cpp tests/Backup_test.cpp tests/Transaction_test.cpp diff --git a/include/SQLiteCpp/Savepoint.h b/include/SQLiteCpp/Savepoint.h new file mode 100644 index 0000000..b2b1aca --- /dev/null +++ b/include/SQLiteCpp/Savepoint.h @@ -0,0 +1,94 @@ +/** + * @file Savepoint.h + * @ingroup SQLiteCpp + * @brief A Savepoint is a way to group multiple SQL statements into an atomic + * secured operation. Similar to a transaction while allowing child savepoints. + * + * Copyright (c) 2020 Kelvin Hammond (hammond.kelvin@gmail.com) + * + * Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt or + * copy at http://opensource.org/licenses/MIT) + */ +#pragma once + +#include + +namespace SQLite { + +// Foward declaration +class Database; + +/** + * @brief RAII encapsulation of a SQLite Savepoint. + * + * A Savepoint is a way to group multiple SQL statements into an atomic + * secureced operation; either it succeeds, with all the changes commited to the + * database file, or if it fails, all the changes are rolled back to the initial + * state at the start of the savepoint. + * + * This method also offers big performances improvements compared to + * individually executed statements. + * + * Caveats: + * + * 1) Calling COMMIT or commiting a parent transaction or RELEASE on a parent + * savepoint will cause this savepoint to be released. + * + * 2) Calling ROLLBACK or rolling back a parent savepoint will cause this + * savepoint to be rolled back. + * + * 3) This savepoint is not saved to the database until this and all savepoints + * or transaction in the savepoint stack have been released or commited. + * + * See also: https://sqlite.org/lang_savepoint.html + * + * Thread-safety: a Transaction object shall not be shared by multiple threads, + * because: + * + * 1) in the SQLite "Thread Safe" mode, "SQLite can be safely used by multiple + * threads provided that no single database connection is used simultaneously in + * two or more threads." + * + * 2) the SQLite "Serialized" mode is not supported by SQLiteC++, because of the + * way it shares the underling SQLite precompiled statement in a custom shared + * pointer (See the inner class "Statement::Ptr"). + */ + +class Savepoint { + public: + /** + * @brief Begins the SQLite savepoint + * + * @param[in] aDatabase the SQLite Database Connection + * @param[in] aName the name of the Savepoint + * + * Exception is thrown in case of error, then the Savepoint is NOT + * initiated. + */ + Savepoint(Database& aDatabase, std::string name); + + // Savepoint is non-copyable + Savepoint(const Savepoint&) = delete; + Savepoint& operator=(const Savepoint&) = delete; + + /** + * @brief Safely rollback the savepoint if it has not been commited. + */ + ~Savepoint(); + + /** + * @brief Commit and release the savepoint. + */ + void release(); + + /** + * @brief Rollback the savepoint + */ + void rollback(); + + private: + Database& mDatabase; ///< Reference to the SQLite Database Connection + std::string msName; ///< Name of the Savepoint + bool mbReleased; ///< True when release has been called +}; +} // namespace SQLite diff --git a/src/Savepoint.cpp b/src/Savepoint.cpp new file mode 100644 index 0000000..b3d13a2 --- /dev/null +++ b/src/Savepoint.cpp @@ -0,0 +1,65 @@ +/** + * @file Savepoint.cpp + * @ingroup SQLiteCpp + * @brief A Savepoint is a way to group multiple SQL statements into an atomic + * secured operation. Similar to a transaction while allowing child savepoints. + * + * Copyright (c) 2020 Kelvin Hammond (hammond.kelvin@gmail.com) + * + * Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt or + * copy at http://opensource.org/licenses/MIT) + */ + +#include +#include +#include +#include + +namespace SQLite { + +// Begins the SQLite savepoint +Savepoint::Savepoint(Database& aDatabase, std::string aName) + : mDatabase(aDatabase), msName(aName), mbReleased(false) { + // workaround because you cannot bind to SAVEPOINT + // escape name for use in query + Statement stmt(mDatabase, "SELECT quote(?)"); + stmt.bind(1, msName); + stmt.executeStep(); + msName = stmt.getColumn(0).getText(); + + mDatabase.exec(std::string("SAVEPOINT ") + msName); +} + +// Safely rollback the savepoint if it has not been committed. +Savepoint::~Savepoint() { + if (!mbReleased) { + try { + rollback(); + } catch (SQLite::Exception&) { + // Never throw an exception in a destructor: error if already rolled + // back or released, but no harm is caused by this. + } + } +} + +// Release the savepoint and commit +void Savepoint::release() { + if (!mbReleased) { + mDatabase.exec(std::string("RELEASE SAVEPOINT ") + msName); + mbReleased = true; + } else { + throw SQLite::Exception("Savepoint already released or rolled back."); + } +} + +// Rollback the savepoint +void Savepoint::rollback() { + if (!mbReleased) { + mDatabase.exec(std::string("ROLLBACK TO SAVEPOINT ") + msName); + mbReleased = true; + } else { + throw SQLite::Exception("Savepoint already released or rolled back."); + } +} + +} // namespace SQLite diff --git a/tests/Savepoint_test.cpp b/tests/Savepoint_test.cpp new file mode 100644 index 0000000..01f9720 --- /dev/null +++ b/tests/Savepoint_test.cpp @@ -0,0 +1,107 @@ +/** + * @file Savepoint_test.cpp + * @ingroup tests + * @brief Test of a SQLite Savepoint. + * + * Copyright (c) 2020 Kelvin Hammond (hammond.kelvin@gmail.com) + * + * Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt or + * copy at http://opensource.org/licenses/MIT) + */ + +#include +#include +#include +#include +#include +#include + +#include + +TEST(Savepoint, commitRollback) { + // Create a new database + SQLite::Database db(":memory:", + SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); + EXPECT_EQ(SQLite::OK, db.getErrorCode()); + + { + // Begin savepoint + SQLite::Savepoint savepoint(db, "sp1"); + + EXPECT_EQ( + 0, + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")); + EXPECT_EQ(SQLite::OK, db.getErrorCode()); + + // Insert a first valu + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, 'first')")); + EXPECT_EQ(1, db.getLastInsertRowid()); + + // release savepoint + savepoint.release(); + + // Commit again throw an exception + EXPECT_THROW(savepoint.release(), SQLite::Exception); + } + + // Auto rollback if no release() before the end of scope + { + // Begin savepoint + SQLite::Savepoint savepoint(db, "sp2"); + + // Insert a second value (that will be rollbacked) + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, 'third')")); + EXPECT_EQ(2, db.getLastInsertRowid()); + + // end of scope: automatic rollback + } + + // Auto rollback of a transaction on error / exception + try { + // Begin savepoint + SQLite::Savepoint savepoint(db, "sp3"); + + // Insert a second value (that will be rollbacked) + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, 'second')")); + EXPECT_EQ(2, db.getLastInsertRowid()); + + // Execute with an error => exception with auto-rollback + db.exec( + "DesiredSyntaxError to raise an exception to rollback the " + "transaction"); + + GTEST_FATAL_FAILURE_("we should never get there"); + savepoint.release(); // We should never get there + } catch (std::exception& e) { + std::cout << "SQLite exception: " << e.what() << std::endl; + // expected error, see above + } + + // Double rollback with a manual command before the end of scope + { + // Begin savepoint + SQLite::Savepoint savepoint(db, "sp4"); + + // Insert a second value (that will be rollbacked) + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, 'third')")); + EXPECT_EQ(2, db.getLastInsertRowid()); + + // Execute a manual rollback (no real use case I can think of, so no + // rollback() method) + db.exec("ROLLBACK"); + + // end of scope: the automatic rollback should not raise an error + // because it is harmless + } + + // Check the results (expect only one row of result, as all other one have + // been rollbacked) + SQLite::Statement query(db, "SELECT * FROM test"); + int nbRows = 0; + while (query.executeStep()) { + nbRows++; + EXPECT_EQ(1, query.getColumn(0).getInt()); + EXPECT_STREQ("first", query.getColumn(1).getText()); + } + EXPECT_EQ(1, nbRows); +}