-
Notifications
You must be signed in to change notification settings - Fork 519
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4e3d36a
commit ee6762c
Showing
5 changed files
with
273 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ([email protected]) | ||
* | ||
* 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ([email protected]) | ||
* | ||
* 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/** | ||
* @file Savepoint_test.cpp | ||
* @ingroup tests | ||
* @brief Test of a SQLite Savepoint. | ||
* | ||
* Copyright (c) 2020 Kelvin Hammond ([email protected]) | ||
* | ||
* 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); | ||
} | ||
|
||
// 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); | ||
} |