Skip to content

Commit

Permalink
Merge ee6762c into 4e3d36a
Browse files Browse the repository at this point in the history
  • Loading branch information
kelvinhammond committed Sep 10, 2020
2 parents 4e3d36a + ee6762c commit a794c06
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 1 deletion.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- #293 Remove FindSQLiteCpp.cmake from sum01/fix_283

Version 3.2.0 - September 10 2020
- Added Savepoint support
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
Expand All @@ -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
Expand Down
94 changes: 94 additions & 0 deletions include/SQLiteCpp/Savepoint.h
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 (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 changes: 65 additions & 0 deletions src/Savepoint.cpp
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 (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
107 changes: 107 additions & 0 deletions tests/Savepoint_test.cpp
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 (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);
}

// 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);
}

0 comments on commit a794c06

Please sign in to comment.