From a58ba7d69979a965d0c5541d706296c4f563214d Mon Sep 17 00:00:00 2001 From: Oleg Smolsky Date: Sat, 6 Jul 2019 11:16:36 -0700 Subject: [PATCH 1/2] Implement Database::loadOrSaveDb() - it is possible to 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 --- include/SQLiteCpp/Database.h | 29 ++++++++++++++++-------- src/Database.cpp | 44 ++++++++++++++++++++++++++++++++++++ tests/Database_test.cpp | 26 +++++++++++++++++++-- 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/include/SQLiteCpp/Database.h b/include/SQLiteCpp/Database.h index 1b402877..64b0861b 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,17 @@ 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. + */ + int loadOrSaveDb(const char* zFilename, int isSave); + private: /// @{ Database must be non-copyable Database(const Database&); diff --git a/src/Database.cpp b/src/Database.cpp index a97459fe..d34fe10f 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::loadOrSaveDb(const char* zFilename, int isSave) { + /* 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 = (isSave ? mpSQLite : pFile); + sqlite3* pTo = (isSave ? 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..98231263 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.loadOrSaveDb("backup", 1 /* 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.loadOrSaveDb("backup", 0 /* save */), 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) { From 187dd8215527ddbac6c7dee74779a705546ed8fb Mon Sep 17 00:00:00 2001 From: Oleg Smolsky Date: Mon, 8 Jul 2019 17:25:13 -0700 Subject: [PATCH 2/2] Tune the API name --- include/SQLiteCpp/Database.h | 3 ++- src/Database.cpp | 6 +++--- tests/Database_test.cpp | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/include/SQLiteCpp/Database.h b/include/SQLiteCpp/Database.h index 64b0861b..08365112 100644 --- a/include/SQLiteCpp/Database.h +++ b/include/SQLiteCpp/Database.h @@ -443,7 +443,8 @@ class Database * * @return SQLITE_OK on success or an error code from SQLite. */ - int loadOrSaveDb(const char* zFilename, int isSave); + enum class BackupType { Save, Load }; + int backup(const char* zFilename, BackupType type); private: /// @{ Database must be non-copyable diff --git a/src/Database.cpp b/src/Database.cpp index d34fe10f..0a10bbdc 100644 --- a/src/Database.cpp +++ b/src/Database.cpp @@ -303,7 +303,7 @@ bool Database::isUnencrypted(const std::string& aFilename) // This is a reference implementation of live backup taken from the official sit: // https://www.sqlite.org/backup.html -int Database::loadOrSaveDb(const char* zFilename, int isSave) { +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); @@ -314,8 +314,8 @@ int Database::loadOrSaveDb(const char* zFilename, int isSave) { ** 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 = (isSave ? mpSQLite : pFile); - sqlite3* pTo = (isSave ? pFile : mpSQLite); + 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. diff --git a/tests/Database_test.cpp b/tests/Database_test.cpp index 98231263..030b653c 100644 --- a/tests/Database_test.cpp +++ b/tests/Database_test.cpp @@ -140,14 +140,14 @@ TEST(Database, import_export) // Export the data into a file remove("backup"); - EXPECT_EQ(db.loadOrSaveDb("backup", 1 /* save */), SQLITE_OK); + 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.loadOrSaveDb("backup", 0 /* save */), SQLITE_OK); + EXPECT_EQ(db.backup("backup", SQLite::Database::BackupType::Load), SQLITE_OK); EXPECT_TRUE(db.tableExists("test")); }