mirror of
https://github.com/cuberite/SQLiteCpp.git
synced 2025-08-06 10:46:03 -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
|
- #313 [CMake] Add SQLITECPP_INCLUDE_SCRIPT option from past-due
|
||||||
- #314 Add Database constructor for filesystem::path (#296) from ptrks
|
- #314 Add Database constructor for filesystem::path (#296) from ptrks
|
||||||
- #295 Compile internal SQLite library with -ffunction-sections from smichaku
|
- #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/Column.cpp
|
||||||
${PROJECT_SOURCE_DIR}/src/Database.cpp
|
${PROJECT_SOURCE_DIR}/src/Database.cpp
|
||||||
${PROJECT_SOURCE_DIR}/src/Exception.cpp
|
${PROJECT_SOURCE_DIR}/src/Exception.cpp
|
||||||
|
${PROJECT_SOURCE_DIR}/src/Savepoint.cpp
|
||||||
${PROJECT_SOURCE_DIR}/src/Statement.cpp
|
${PROJECT_SOURCE_DIR}/src/Statement.cpp
|
||||||
${PROJECT_SOURCE_DIR}/src/Transaction.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/Column.h
|
||||||
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Database.h
|
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Database.h
|
||||||
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Exception.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/Statement.h
|
||||||
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Transaction.h
|
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Transaction.h
|
||||||
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/VariadicBind.h
|
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/VariadicBind.h
|
||||||
@ -129,6 +131,7 @@ source_group(include FILES ${SQLITECPP_INC})
|
|||||||
set(SQLITECPP_TESTS
|
set(SQLITECPP_TESTS
|
||||||
tests/Column_test.cpp
|
tests/Column_test.cpp
|
||||||
tests/Database_test.cpp
|
tests/Database_test.cpp
|
||||||
|
tests/Savepoint_test.cpp
|
||||||
tests/Statement_test.cpp
|
tests/Statement_test.cpp
|
||||||
tests/Backup_test.cpp
|
tests/Backup_test.cpp
|
||||||
tests/Transaction_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