diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e198ba..4d8279c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,7 @@ set(SQLITECPP_SRC ${PROJECT_SOURCE_DIR}/src/Database.cpp ${PROJECT_SOURCE_DIR}/src/Statement.cpp ${PROJECT_SOURCE_DIR}/src/Transaction.cpp + ${PROJECT_SOURCE_DIR}/src/Backup.cpp ) source_group(src FILES ${SQLITECPP_SRC}) @@ -102,6 +103,7 @@ set(SQLITECPP_INC ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Exception.h ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Statement.h ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Transaction.h + ${PROJECT_SOURCE_DIR}/include/SQLiteCpp/Backup.h ) source_group(inc FILES ${SQLITECPP_INC}) @@ -110,6 +112,7 @@ set(SQLITECPP_TESTS tests/Column_test.cpp tests/Database_test.cpp tests/Statement_test.cpp + tests/Backup_test.cpp ) source_group(tests FILES ${SQLITECPP_TESTS}) diff --git a/include/SQLiteCpp/Backup.h b/include/SQLiteCpp/Backup.h new file mode 100644 index 0000000..660ce80 --- /dev/null +++ b/include/SQLiteCpp/Backup.h @@ -0,0 +1,156 @@ +/** + * @file Backup.h + * @ingroup SQLiteCpp + * @brief Backup is used to backup a database file in a safe and online way. + * + * Copyright (c) 2015-2015 Shibao HONG (shibaohong@outlook.com) + * + * Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt + * or copy at http://opensource.org/licenses/MIT) + */ +#pragma once + +#include + +#include + +#include + +namespace SQLite +{ + +/** + * @brief RAII encapsulation of a SQLite Database Backup process. + * + * A Backup object is used to backup a source database file to a destination database file + * in a safe and online way. + * + * Resource Acquisition Is Initialization (RAII) means that the Backup Resource + * is allocated in the constructor and released in the destructor, so that there is + * no need to worry about memory management or the validity of the underlying SQLite Backup. + * + * Thread-safety: a Backup 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 Backup +{ +public: + /** + * @brief Initialize a SQLite Backup object. + * + * Initialize a SQLite Backup object for the source database and destination database. + * The database name is "main" for the main database, "temp" for the temporary database, + * or the name specified after the AS keyword in an ATTACH statement for an attached database. + * + * Exception is thrown in case of error, then the Backup object is NOT constructed. + * + * @param[in] aDestDatabase Destination database connection + * @param[in] apDestDatabaseName Destination database name + * @param[in] aSrcDatabase Source database connection + * @param[in] apSrcDatabaseName Source database name + * + * @throw SQLite::Exception in case of error + */ + Backup(Database& aDestDatabase, + const char* apDestDatabaseName, + Database& aSrcDatabase, + const char* apSrcDatabaseName); + + /** + * @brief Initialize a SQLite Backup object. + * + * Initialize a SQLite Backup object for source database and destination database. + * The database name is "main" for the main database, "temp" for the temporary database, + * or the name specified after the AS keyword in an ATTACH statement for an attached database. + * + * Exception is thrown in case of error, then the Backup object is NOT constructed. + * + * @param[in] aDestDatabase Destination database connection + * @param[in] aDestDatabaseName Destination database name + * @param[in] aSrcDatabase Source database connection + * @param[in] aSrcDatabaseName Source database name + * + * @throw SQLite::Exception in case of error + */ + Backup(Database& aDestDatabase, + const std::string& aDestDatabaseName, + Database& aSrcDatabase, + const std::string& aSrcDatabaseName); + + /** + * @brief Initialize a SQLite Backup object for main databases. + * + * Initialize a SQLite Backup object for source database and destination database. + * Backup the main databases between the source and the destination. + * + * Exception is thrown in case of error, then the Backup object is NOT constructed. + * + * @param[in] aDestDatabase Destination database connection + * @param[in] aSrcDatabase Source database connection + * + * @throw SQLite::Exception in case of error + */ + Backup(Database& aDestDatabase, + Database& aSrcDatabase); + + /** + * @brief Release the SQLite Backup resource. + */ + virtual ~Backup() noexcept; + + /** + * @brief Execute a step of backup with a given number of source pages to be copied + * + * Exception is thrown when SQLITE_IOERR_XXX, SQLITE_NOMEM, or SQLITE_READONLY is returned + * in sqlite3_backup_step(). These errors are considered fatal, so there is no point + * in retrying the call to executeStep(). + * + * @param[in] aNumPage The number of source pages to be copied, with a negative value meaning all remaining source pages + * + * @return SQLITE_OK/SQLITE_DONE/SQLITE_BUSY/SQLITE_LOCKED + * + * @throw SQLite::Exception in case of error + */ + int executeStep(const int aNumPage = -1); + + /** + * @brief Get the remaining number of source pages to be copied. + * + * @return the remaining number of source pages to be copied + */ + int remainingPageCount(); + + /** + * @brief Get the total number of source pages. + * + * @return the total number of source pages + */ + int totalPageCount(); + + /** + * @brief Return raw pointer to SQLite Database Backup Handle. + * + * This is often needed to mix this wrapper with other libraries or for advance usage not supported by SQLiteCpp. + * + * @return Raw pointer to SQLite Backup Handle + */ + inline sqlite3_backup* getHandle() const noexcept // nothrow + { + return mpSQLiteBackup; + } + +private: + /// @{ Backup must be non-copyable + Backup(const Backup&); + Backup& operator=(const Backup&); + /// @} + +private: + sqlite3_backup* mpSQLiteBackup; //!< Pointer to SQLite Database Backup Handle +}; + +} // namespace SQLite diff --git a/src/Backup.cpp b/src/Backup.cpp new file mode 100644 index 0000000..504042f --- /dev/null +++ b/src/Backup.cpp @@ -0,0 +1,110 @@ +/** + * @file Backup.cpp + * @ingroup SQLiteCpp + * @brief Backup is used to backup a database file in a safe and online way. + * + * Copyright (c) 2015-2015 Shibao HONG (shibaohong@outlook.com) + * + * Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt + * or copy at http://opensource.org/licenses/MIT) + */ + +#include + +#include + +#include + +namespace SQLite +{ + +// Initialize resource for SQLite database backup +Backup::Backup(Database& aDestDatabase, + const char *apDestDatabaseName, + Database& aSrcDatabase, + const char *apSrcDatabaseName) : + mpSQLiteBackup(NULL) +{ + mpSQLiteBackup = sqlite3_backup_init(aDestDatabase.getHandle(), + apDestDatabaseName, + aSrcDatabase.getHandle(), + apSrcDatabaseName); + if (NULL == mpSQLiteBackup) + { + std::string strerr = sqlite3_errmsg(aDestDatabase.getHandle()); + throw SQLite::Exception(strerr); + } +} + +// Initialize resource for SQLite database backup +Backup::Backup(Database &aDestDatabase, + const std::string &aDestDatabaseName, + Database &aSrcDatabase, + const std::string &aSrcDatabaseName) : + mpSQLiteBackup(NULL) +{ + mpSQLiteBackup = sqlite3_backup_init(aDestDatabase.getHandle(), + aDestDatabaseName.c_str(), + aSrcDatabase.getHandle(), + aSrcDatabaseName.c_str()); + if (NULL == mpSQLiteBackup) + { + std::string strerr = sqlite3_errmsg(aDestDatabase.getHandle()); + throw SQLite::Exception(strerr); + } +} + +// Initialize resource for SQLite database backup +Backup::Backup(Database &aDestDatabase, Database &aSrcDatabase) : + mpSQLiteBackup(NULL) +{ + mpSQLiteBackup = sqlite3_backup_init(aDestDatabase.getHandle(), + "main", + aSrcDatabase.getHandle(), + "main"); + if (NULL == mpSQLiteBackup) + { + std::string strerr = sqlite3_errmsg(aDestDatabase.getHandle()); + throw SQLite::Exception(strerr); + } +} + +// Release resource for SQLite database backup +Backup::~Backup() noexcept +{ + if (NULL != mpSQLiteBackup) + { + sqlite3_backup_finish(mpSQLiteBackup); + } +} + +// Execute backup step with a given number of source pages to be copied +int Backup::executeStep(const int aNumPage /* = -1 */) +{ + const int res = sqlite3_backup_step(mpSQLiteBackup, aNumPage); + if (SQLITE_OK != res && SQLITE_DONE != res && + SQLITE_BUSY != res && SQLITE_LOCKED != res) + { + std::string strerr("Backup executeStep error"); +#if SQLITE_VERSION_NUMBER >= 3007015 // SQLite v3.7.15 is the first version with sqlite3_errstr() interface + strerr += "with error message "; + strerr += sqlite3_errstr(res); +#endif + throw SQLite::Exception(strerr); + } + return res; +} + +// Get the number of remaining source pages to be copied in this backup process +int Backup::remainingPageCount() +{ + return sqlite3_backup_remaining(mpSQLiteBackup); +} + +// Get the number of total source pages to be copied in this backup process +int Backup::totalPageCount() +{ + return sqlite3_backup_pagecount(mpSQLiteBackup); +} + +} // namespace SQLite diff --git a/tests/Backup_test.cpp b/tests/Backup_test.cpp new file mode 100644 index 0000000..68888bd --- /dev/null +++ b/tests/Backup_test.cpp @@ -0,0 +1,63 @@ +/** + * @file Backup_test.cpp + * @ingroup tests + * @brief Test of a SQLite Backup. + * + * Copyright (c) 2015-2015 Shibao HONG (shibaohong@outlook.com) + * + * Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt + * or copy at http://opensource.org/licenses/MIT) + */ + +#include +#include +#include +#include + +#include + +#include + +TEST(Backup, executeStep) { + remove("backup_test.db3"); + remove("backup_test.db3.backup"); + SQLite::Database srcDB("backup_test.db3", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + srcDB.exec("CREATE TABLE backup_test (id INTEGER PRIMARY KEY, value TEXT)"); + ASSERT_EQ(1, srcDB.exec("INSERT INTO backup_test VALUES (1, \"first\")")); + ASSERT_EQ(1, srcDB.exec("INSERT INTO backup_test VALUES (2, \"second\")")); + + SQLite::Database destDB("backup_test.db3.backup", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + SQLite::Backup backup(destDB, srcDB); + const int res = backup.executeStep(); + ASSERT_EQ(res, SQLITE_DONE); + + SQLite::Statement query(destDB, "SELECT * FROM backup_test ORDER BY id ASC"); + ASSERT_TRUE(query.executeStep()); + EXPECT_EQ(1, query.getColumn(0).getInt()); + EXPECT_STREQ("first", query.getColumn(1)); + ASSERT_TRUE(query.executeStep()); + EXPECT_EQ(2, query.getColumn(0).getInt()); + EXPECT_STREQ("second", query.getColumn(1)); + remove("backup_test.db3"); + remove("backup_test.db3.backup"); +} + +TEST(Backup, executeStepException) { + remove("backup_test.db3"); + remove("backup_test.db3.backup"); + SQLite::Database srcDB("backup_test.db3", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + srcDB.exec("CREATE TABLE backup_test (id INTEGER PRIMARY KEY, value TEXT)"); + ASSERT_EQ(1, srcDB.exec("INSERT INTO backup_test VALUES (1, \"first\")")); + ASSERT_EQ(1, srcDB.exec("INSERT INTO backup_test VALUES (2, \"second\")")); + { + SQLite::Database destDB("backup_test.db3.backup", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + (void)destDB; + } + { + SQLite::Database destDB("backup_test.db3.backup", SQLITE_OPEN_READONLY); + SQLite::Backup backup(destDB, srcDB); + EXPECT_THROW(backup.executeStep(), SQLite::Exception); + } + remove("backup_test.db3"); + remove("backup_test.db3.backup"); +}