Merge #299 Added Savepoint support from catalogm

This commit is contained in:
Sébastien Rombauts 2021-01-06 11:06:50 +01:00
commit cac0000ada
5 changed files with 271 additions and 0 deletions

View File

@ -195,3 +195,4 @@ Version 3.x - 2021
- #313 [CMake] Add SQLITECPP_INCLUDE_SCRIPT option from past-due
- #314 Add Database constructor for filesystem::path (#296) from ptrks
- #295 Compile internal SQLite library with -ffunction-sections from smichaku
- #299 Added Savepoint support from catalogm

View File

@ -105,6 +105,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
)
@ -118,6 +119,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
@ -129,6 +131,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

View File

@ -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 <SQLiteCpp/Exception.h>
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

65
src/Savepoint.cpp Normal file
View File

@ -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 <SQLiteCpp/Assertion.h>
#include <SQLiteCpp/Database.h>
#include <SQLiteCpp/Savepoint.h>
#include <SQLiteCpp/Statement.h>
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

108
tests/Savepoint_test.cpp Normal file
View File

@ -0,0 +1,108 @@
/**
* @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 <SQLiteCpp/Database.h>
#include <SQLiteCpp/Exception.h>
#include <SQLiteCpp/Savepoint.h>
#include <SQLiteCpp/Statement.h>
#include <SQLiteCpp/Transaction.h>
#include <gtest/gtest.h>
#include <cstdio>
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);
EXPECT_THROW(savepoint.rollback(), 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);
}