Skip to content

Commit cac0000

Browse files
committed
Merge #299 Added Savepoint support from catalogm
2 parents a9ef8c9 + 61bdad3 commit cac0000

File tree

5 files changed

+271
-0
lines changed

5 files changed

+271
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,4 @@ Version 3.x - 2021
195195
- #313 [CMake] Add SQLITECPP_INCLUDE_SCRIPT option from past-due
196196
- #314 Add Database constructor for filesystem::path (#296) from ptrks
197197
- #295 Compile internal SQLite library with -ffunction-sections from smichaku
198+
- #299 Added Savepoint support from catalogm

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ set(SQLITECPP_SRC
105105
${PROJECT_SOURCE_DIR}/src/Column.cpp
106106
${PROJECT_SOURCE_DIR}/src/Database.cpp
107107
${PROJECT_SOURCE_DIR}/src/Exception.cpp
108+
${PROJECT_SOURCE_DIR}/src/Savepoint.cpp
108109
${PROJECT_SOURCE_DIR}/src/Statement.cpp
109110
${PROJECT_SOURCE_DIR}/src/Transaction.cpp
110111
)
@@ -118,6 +119,7 @@ set(SQLITECPP_INC
118119
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Column.h
119120
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Database.h
120121
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Exception.h
122+
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Savepoint.h
121123
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Statement.h
122124
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Transaction.h
123125
${PROJECT_SOURCE_DIR}/include/SQLiteCpp/VariadicBind.h
@@ -129,6 +131,7 @@ source_group(include FILES ${SQLITECPP_INC})
129131
set(SQLITECPP_TESTS
130132
tests/Column_test.cpp
131133
tests/Database_test.cpp
134+
tests/Savepoint_test.cpp
132135
tests/Statement_test.cpp
133136
tests/Backup_test.cpp
134137
tests/Transaction_test.cpp

include/SQLiteCpp/Savepoint.h

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* @file Savepoint.h
3+
* @ingroup SQLiteCpp
4+
* @brief A Savepoint is a way to group multiple SQL statements into an atomic
5+
* secured operation. Similar to a transaction while allowing child savepoints.
6+
*
7+
* Copyright (c) 2020 Kelvin Hammond ([email protected])
8+
*
9+
* Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt or
10+
* copy at http://opensource.org/licenses/MIT)
11+
*/
12+
#pragma once
13+
14+
#include <SQLiteCpp/Exception.h>
15+
16+
namespace SQLite {
17+
18+
// Foward declaration
19+
class Database;
20+
21+
/**
22+
* @brief RAII encapsulation of a SQLite Savepoint.
23+
*
24+
* A Savepoint is a way to group multiple SQL statements into an atomic
25+
* secureced operation; either it succeeds, with all the changes commited to the
26+
* database file, or if it fails, all the changes are rolled back to the initial
27+
* state at the start of the savepoint.
28+
*
29+
* This method also offers big performances improvements compared to
30+
* individually executed statements.
31+
*
32+
* Caveats:
33+
*
34+
* 1) Calling COMMIT or commiting a parent transaction or RELEASE on a parent
35+
* savepoint will cause this savepoint to be released.
36+
*
37+
* 2) Calling ROLLBACK or rolling back a parent savepoint will cause this
38+
* savepoint to be rolled back.
39+
*
40+
* 3) This savepoint is not saved to the database until this and all savepoints
41+
* or transaction in the savepoint stack have been released or commited.
42+
*
43+
* See also: https://sqlite.org/lang_savepoint.html
44+
*
45+
* Thread-safety: a Transaction object shall not be shared by multiple threads,
46+
* because:
47+
*
48+
* 1) in the SQLite "Thread Safe" mode, "SQLite can be safely used by multiple
49+
* threads provided that no single database connection is used simultaneously in
50+
* two or more threads."
51+
*
52+
* 2) the SQLite "Serialized" mode is not supported by SQLiteC++, because of the
53+
* way it shares the underling SQLite precompiled statement in a custom shared
54+
* pointer (See the inner class "Statement::Ptr").
55+
*/
56+
57+
class Savepoint {
58+
public:
59+
/**
60+
* @brief Begins the SQLite savepoint
61+
*
62+
* @param[in] aDatabase the SQLite Database Connection
63+
* @param[in] aName the name of the Savepoint
64+
*
65+
* Exception is thrown in case of error, then the Savepoint is NOT
66+
* initiated.
67+
*/
68+
Savepoint(Database& aDatabase, std::string name);
69+
70+
// Savepoint is non-copyable
71+
Savepoint(const Savepoint&) = delete;
72+
Savepoint& operator=(const Savepoint&) = delete;
73+
74+
/**
75+
* @brief Safely rollback the savepoint if it has not been commited.
76+
*/
77+
~Savepoint();
78+
79+
/**
80+
* @brief Commit and release the savepoint.
81+
*/
82+
void release();
83+
84+
/**
85+
* @brief Rollback the savepoint
86+
*/
87+
void rollback();
88+
89+
private:
90+
Database& mDatabase; ///< Reference to the SQLite Database Connection
91+
std::string msName; ///< Name of the Savepoint
92+
bool mbReleased; ///< True when release has been called
93+
};
94+
} // namespace SQLite

src/Savepoint.cpp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* @file Savepoint.cpp
3+
* @ingroup SQLiteCpp
4+
* @brief A Savepoint is a way to group multiple SQL statements into an atomic
5+
* secured operation. Similar to a transaction while allowing child savepoints.
6+
*
7+
* Copyright (c) 2020 Kelvin Hammond ([email protected])
8+
*
9+
* Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt or
10+
* copy at http://opensource.org/licenses/MIT)
11+
*/
12+
13+
#include <SQLiteCpp/Assertion.h>
14+
#include <SQLiteCpp/Database.h>
15+
#include <SQLiteCpp/Savepoint.h>
16+
#include <SQLiteCpp/Statement.h>
17+
18+
namespace SQLite {
19+
20+
// Begins the SQLite savepoint
21+
Savepoint::Savepoint(Database& aDatabase, std::string aName)
22+
: mDatabase(aDatabase), msName(aName), mbReleased(false) {
23+
// workaround because you cannot bind to SAVEPOINT
24+
// escape name for use in query
25+
Statement stmt(mDatabase, "SELECT quote(?)");
26+
stmt.bind(1, msName);
27+
stmt.executeStep();
28+
msName = stmt.getColumn(0).getText();
29+
30+
mDatabase.exec(std::string("SAVEPOINT ") + msName);
31+
}
32+
33+
// Safely rollback the savepoint if it has not been committed.
34+
Savepoint::~Savepoint() {
35+
if (!mbReleased) {
36+
try {
37+
rollback();
38+
} catch (SQLite::Exception&) {
39+
// Never throw an exception in a destructor: error if already rolled
40+
// back or released, but no harm is caused by this.
41+
}
42+
}
43+
}
44+
45+
// Release the savepoint and commit
46+
void Savepoint::release() {
47+
if (!mbReleased) {
48+
mDatabase.exec(std::string("RELEASE SAVEPOINT ") + msName);
49+
mbReleased = true;
50+
} else {
51+
throw SQLite::Exception("Savepoint already released or rolled back.");
52+
}
53+
}
54+
55+
// Rollback the savepoint
56+
void Savepoint::rollback() {
57+
if (!mbReleased) {
58+
mDatabase.exec(std::string("ROLLBACK TO SAVEPOINT ") + msName);
59+
mbReleased = true;
60+
} else {
61+
throw SQLite::Exception("Savepoint already released or rolled back.");
62+
}
63+
}
64+
65+
} // namespace SQLite

tests/Savepoint_test.cpp

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* @file Savepoint_test.cpp
3+
* @ingroup tests
4+
* @brief Test of a SQLite Savepoint.
5+
*
6+
* Copyright (c) 2020 Kelvin Hammond ([email protected])
7+
*
8+
* Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt or
9+
* copy at http://opensource.org/licenses/MIT)
10+
*/
11+
12+
#include <SQLiteCpp/Database.h>
13+
#include <SQLiteCpp/Exception.h>
14+
#include <SQLiteCpp/Savepoint.h>
15+
#include <SQLiteCpp/Statement.h>
16+
#include <SQLiteCpp/Transaction.h>
17+
#include <gtest/gtest.h>
18+
19+
#include <cstdio>
20+
21+
TEST(Savepoint, commitRollback) {
22+
// Create a new database
23+
SQLite::Database db(":memory:",
24+
SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
25+
EXPECT_EQ(SQLite::OK, db.getErrorCode());
26+
27+
{
28+
// Begin savepoint
29+
SQLite::Savepoint savepoint(db, "sp1");
30+
31+
EXPECT_EQ(
32+
0,
33+
db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"));
34+
EXPECT_EQ(SQLite::OK, db.getErrorCode());
35+
36+
// Insert a first valu
37+
EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, 'first')"));
38+
EXPECT_EQ(1, db.getLastInsertRowid());
39+
40+
// release savepoint
41+
savepoint.release();
42+
43+
// Commit again throw an exception
44+
EXPECT_THROW(savepoint.release(), SQLite::Exception);
45+
EXPECT_THROW(savepoint.rollback(), SQLite::Exception);
46+
}
47+
48+
// Auto rollback if no release() before the end of scope
49+
{
50+
// Begin savepoint
51+
SQLite::Savepoint savepoint(db, "sp2");
52+
53+
// Insert a second value (that will be rollbacked)
54+
EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, 'third')"));
55+
EXPECT_EQ(2, db.getLastInsertRowid());
56+
57+
// end of scope: automatic rollback
58+
}
59+
60+
// Auto rollback of a transaction on error / exception
61+
try {
62+
// Begin savepoint
63+
SQLite::Savepoint savepoint(db, "sp3");
64+
65+
// Insert a second value (that will be rollbacked)
66+
EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, 'second')"));
67+
EXPECT_EQ(2, db.getLastInsertRowid());
68+
69+
// Execute with an error => exception with auto-rollback
70+
db.exec(
71+
"DesiredSyntaxError to raise an exception to rollback the "
72+
"transaction");
73+
74+
GTEST_FATAL_FAILURE_("we should never get there");
75+
savepoint.release(); // We should never get there
76+
} catch (std::exception& e) {
77+
std::cout << "SQLite exception: " << e.what() << std::endl;
78+
// expected error, see above
79+
}
80+
81+
// Double rollback with a manual command before the end of scope
82+
{
83+
// Begin savepoint
84+
SQLite::Savepoint savepoint(db, "sp4");
85+
86+
// Insert a second value (that will be rollbacked)
87+
EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, 'third')"));
88+
EXPECT_EQ(2, db.getLastInsertRowid());
89+
90+
// Execute a manual rollback (no real use case I can think of, so no
91+
// rollback() method)
92+
db.exec("ROLLBACK");
93+
94+
// end of scope: the automatic rollback should not raise an error
95+
// because it is harmless
96+
}
97+
98+
// Check the results (expect only one row of result, as all other one have
99+
// been rollbacked)
100+
SQLite::Statement query(db, "SELECT * FROM test");
101+
int nbRows = 0;
102+
while (query.executeStep()) {
103+
nbRows++;
104+
EXPECT_EQ(1, query.getColumn(0).getInt());
105+
EXPECT_STREQ("first", query.getColumn(1).getText());
106+
}
107+
EXPECT_EQ(1, nbRows);
108+
}

0 commit comments

Comments
 (0)