From 514d7d6846c9e084c6521d159dcb4da880cd0d3c Mon Sep 17 00:00:00 2001 From: Oleg Smolsky Date: Tue, 9 Jul 2019 00:35:31 -0700 Subject: [PATCH] Implement Database::backup() (#211) Easily backup/restore a live DB via the sqlite3_backup_init() API - https://www.sqlite.org/backup.html - https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit Use the reference implementation from the documentation --- include/SQLiteCpp/Database.h | 30 ++++++++++++++++-------- src/Database.cpp | 44 ++++++++++++++++++++++++++++++++++++ tests/Database_test.cpp | 26 +++++++++++++++++++-- 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/include/SQLiteCpp/Database.h b/include/SQLiteCpp/Database.h index 1b402877..08365112 100644 --- a/include/SQLiteCpp/Database.h +++ b/include/SQLiteCpp/Database.h @@ -313,7 +313,7 @@ class Database } /** - * @brief Create or redefine a SQL function or aggregate in the sqlite database. + * @brief Create or redefine a SQL function or aggregate in the sqlite database. * * This is the equivalent of the sqlite3_create_function_v2 command. * @see http://www.sqlite.org/c3ref/create_function.html @@ -341,7 +341,7 @@ class Database void (*apDestroy)(void *)); /** - * @brief Create or redefine a SQL function or aggregate in the sqlite database. + * @brief Create or redefine a SQL function or aggregate in the sqlite database. * * This is the equivalent of the sqlite3_create_function_v2 command. * @see http://www.sqlite.org/c3ref/create_function.html @@ -373,7 +373,7 @@ class Database } /** - * @brief Load a module into the current sqlite database instance. + * @brief Load a module into the current sqlite database instance. * * This is the equivalent of the sqlite3_load_extension call, but additionally enables * module loading support prior to loading the requested module. @@ -392,8 +392,8 @@ class Database /** * @brief Set the key for the current sqlite database instance. * - * This is the equivalent of the sqlite3_key call and should thus be called - * directly after opening the database. + * This is the equivalent of the sqlite3_key call and should thus be called + * directly after opening the database. * Open encrypted database -> call db.key("secret") -> database ready * * @param[in] aKey Key to decode/encode the database @@ -421,10 +421,10 @@ class Database /** * @brief Test if a file contains an unencrypted database. * - * This is a simple test that reads the first bytes of a database file and - * compares them to the standard header for unencrypted databases. If the - * header does not match the standard string, we assume that we have an - * encrypted file. + * This is a simple test that reads the first bytes of a database file and + * compares them to the standard header for unencrypted databases. If the + * header does not match the standard string, we assume that we have an + * encrypted file. * * @param[in] aFilename path/uri to a file * @@ -434,6 +434,18 @@ class Database */ static bool isUnencrypted(const std::string& aFilename); + /** + * @brief Load or save the database content. + * + * This function is used to load the contents of a database file on disk + * into the "main" database of open database connection, or to save the current + * contents of the database into a database file on disk. + * + * @return SQLITE_OK on success or an error code from SQLite. + */ + enum class BackupType { Save, Load }; + int backup(const char* zFilename, BackupType type); + private: /// @{ Database must be non-copyable Database(const Database&); diff --git a/src/Database.cpp b/src/Database.cpp index a97459fe..0a10bbdc 100644 --- a/src/Database.cpp +++ b/src/Database.cpp @@ -300,4 +300,48 @@ bool Database::isUnencrypted(const std::string& aFilename) throw exception; } +// This is a reference implementation of live backup taken from the official sit: +// https://www.sqlite.org/backup.html + +int Database::backup(const char* zFilename, BackupType type) { + /* Open the database file identified by zFilename. Exit early if this fails. */ + sqlite3* pFile; + int rc = sqlite3_open(zFilename, &pFile); + if (rc == SQLITE_OK) + { + /* If this is a 'load' operation (isSave==0), then data is copied + ** from the database file just opened to database mpSQLite. + ** Otherwise, if this is a 'save' operation (isSave==1), then data + ** is copied from mpSQLite to pFile. Set the variables pFrom and + ** pTo accordingly. */ + sqlite3* pFrom = (type == BackupType::Save ? mpSQLite : pFile); + sqlite3* pTo = (type == BackupType::Save ? pFile : mpSQLite); + + /* Set up the backup procedure to copy from the "main" database of + ** connection pFile to the main database of connection mpSQLite. + ** If something goes wrong, pBackup will be set to NULL and an error + ** code and message left in connection pTo. + ** + ** If the backup object is successfully created, call backup_step() + ** to copy data from pFile to mpSQLite. Then call backup_finish() + ** to release resources associated with the pBackup object. If an + ** error occurred, then an error code and message will be left in + ** connection pTo. If no error occurred, then the error code belonging + ** to pTo is set to SQLITE_OK. + */ + sqlite3_backup *pBackup = sqlite3_backup_init(pTo, "main", pFrom, "main"); + if (pBackup) + { + sqlite3_backup_step(pBackup, -1); + sqlite3_backup_finish(pBackup); + } + rc = sqlite3_errcode(pTo); + } + + /* Close the database connection opened on database file zFilename + ** and return the result of this function. */ + sqlite3_close(pFile); + return rc; +} + } // namespace SQLite diff --git a/tests/Database_test.cpp b/tests/Database_test.cpp index 447f15e1..030b653c 100644 --- a/tests/Database_test.cpp +++ b/tests/Database_test.cpp @@ -56,7 +56,7 @@ TEST(Database, ctorExecCreateDropExist) EXPECT_TRUE(db.tableExists("test")); EXPECT_TRUE(db.tableExists(std::string("test"))); EXPECT_EQ(0, db.getLastInsertRowid()); - + EXPECT_EQ(0, db.exec("DROP TABLE IF EXISTS test")); EXPECT_FALSE(db.tableExists("test")); EXPECT_FALSE(db.tableExists(std::string("test"))); @@ -88,7 +88,7 @@ TEST(Database, moveConstructor) remove("test.db3"); } -#endif +#endif TEST(Database, createCloseReopen) { @@ -130,6 +130,28 @@ TEST(Database, inMemory) } // Close an destroy DB } +TEST(Database, import_export) +{ + // Create a new in-memory database + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); + EXPECT_FALSE(db.tableExists("test")); + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); + EXPECT_TRUE(db.tableExists("test")); + + // Export the data into a file + remove("backup"); + EXPECT_EQ(db.backup("backup", SQLite::Database::BackupType::Save), SQLITE_OK); + + // Trash the table + db.exec("DROP TABLE test;"); + EXPECT_FALSE(db.tableExists("test")); + + // Import the data back from the file + EXPECT_EQ(db.backup("backup", SQLite::Database::BackupType::Load), SQLITE_OK); + + EXPECT_TRUE(db.tableExists("test")); +} + #if SQLITE_VERSION_NUMBER >= 3007015 // SQLite v3.7.15 is first version with PRAGMA busy_timeout TEST(Database, busyTimeout) {