mirror of
https://github.com/cuberite/SQLiteCpp.git
synced 2025-08-05 10:16:01 -04:00
Merge #299 Added Savepoint support from catalogm
This commit is contained in:
commit
cac0000ada
@ -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
|
||||
|
@ -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
|
||||
|
94
include/SQLiteCpp/Savepoint.h
Normal file
94
include/SQLiteCpp/Savepoint.h
Normal 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
65
src/Savepoint.cpp
Normal 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
108
tests/Savepoint_test.cpp
Normal 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);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user