From 80084902265c0d0e9402389db09ba0c707ef5cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rombauts?= Date: Sat, 17 Aug 2024 23:31:14 +0200 Subject: [PATCH] NOTE on macOS FindSQLite3 find an unrelated sqlite3.h from Mono.framework that doesn't match the actual package version! --- examples/example1/main.cpp | 2 +- tests/Database_test.cpp | 1284 ++++++++++++++++++------------------ 2 files changed, 645 insertions(+), 641 deletions(-) diff --git a/examples/example1/main.cpp b/examples/example1/main.cpp index 9c2b3616..c0e9a2de 100644 --- a/examples/example1/main.cpp +++ b/examples/example1/main.cpp @@ -87,7 +87,7 @@ int main() { // Using SQLITE_VERSION would require #include which we want to avoid: use SQLite::VERSION if possible. // std::cout << "SQlite3 version " << SQLITE_VERSION << std::endl; - std::cout << "SQlite3 version " << SQLite::VERSION << " (" << SQLite::getLibVersion() << ")" << std::endl; + std::cout << "SQlite3 compile time header version " << SQLite::VERSION << " (vs dynamic lib version " << SQLite::getLibVersion() << ")" << std::endl; std::cout << "SQliteC++ version " << SQLITECPP_VERSION << std::endl; //////////////////////////////////////////////////////////////////////////// diff --git a/tests/Database_test.cpp b/tests/Database_test.cpp index ec9adefa..427d30d8 100644 --- a/tests/Database_test.cpp +++ b/tests/Database_test.cpp @@ -1,640 +1,644 @@ -/** - * @file Database_test.cpp - * @ingroup tests - * @brief Test of a SQLiteCpp Database. - * - * Copyright (c) 2012-2024 Sebastien Rombauts (sebastien.rombauts@gmail.com) - * - * Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt - * or copy at http://opensource.org/licenses/MIT) - */ - -#include - -#include // for SQLITE_ERROR and SQLITE_VERSION_NUMBER - -#include - -#ifdef SQLITECPP_HAVE_STD_FILESYSTEM -#include -#endif // c++17 - -#include -#include - -#ifdef SQLITECPP_ENABLE_ASSERT_HANDLER -namespace SQLite -{ -/// definition of the assertion handler enabled when SQLITECPP_ENABLE_ASSERT_HANDLER is defined in the project (CMakeList.txt) -void assertion_failed(const char* apFile, const long apLine, const char* apFunc, const char* apExpr, const char* apMsg) -{ - // TODO: unit test that this assertion callback get called (already tested manually) - std::cout << "assertion_failed(" << apFile << ", " << apLine << ", " << apFunc << ", " << apExpr << ", " << apMsg << ")\n"; -} -} -#endif - -#ifdef SQLITECPP_INTERNAL_SQLITE -TEST(SQLiteCpp, version) -{ - EXPECT_STREQ(SQLITE_VERSION, SQLite::VERSION); - EXPECT_EQ (SQLITE_VERSION_NUMBER, SQLite::VERSION_NUMBER); - EXPECT_STREQ(SQLITE_VERSION, SQLite::getLibVersion()); - EXPECT_EQ (SQLITE_VERSION_NUMBER, SQLite::getLibVersionNumber()); -} -#endif - -TEST(Database, ctorExecCreateDropExist) -{ - remove("test.db3"); - { - // Try to open a non-existing database - std::string filename = "test.db3"; - EXPECT_THROW(SQLite::Database not_found(filename), SQLite::Exception); - - // Create a new database using a string or a std::filesystem::path if using c++17 and a - // compatible compiler - #ifdef SQLITECPP_HAVE_STD_FILESYSTEM - SQLite::Database db(std::filesystem::path("test.db3"), SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE); - #else - SQLite::Database db("test.db3", SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE); - #endif // have std::filesystem - - EXPECT_STREQ("test.db3", db.getFilename().c_str()); - EXPECT_FALSE(db.tableExists("test")); - EXPECT_FALSE(db.tableExists(std::string("test"))); - EXPECT_EQ(0, db.getLastInsertRowid()); - - EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")); - 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"))); - EXPECT_EQ(0, db.getLastInsertRowid()); - } // Close DB test.db3 - remove("test.db3"); -} - -#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1600) - -SQLite::Database DatabaseBuilder(const char* apName) -{ - return SQLite::Database(apName, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); -} - -TEST(Database, moveConstructor) -{ - remove("test.db3"); - { - // Create a new database, using the move constructor - SQLite::Database db = DatabaseBuilder("test.db3"); - EXPECT_FALSE(db.tableExists("test")); - EXPECT_TRUE(db.getHandle() != NULL); - SQLite::Database moved = std::move(db); - EXPECT_TRUE(db.getHandle() == NULL); - EXPECT_TRUE(moved.getHandle() != NULL); - EXPECT_FALSE(moved.tableExists("test")); - } // Close DB test.db3 - remove("test.db3"); -} - -#endif - -TEST(Database, createCloseReopen) -{ - remove("test.db3"); - { - // Try to open the non-existing database - EXPECT_THROW(SQLite::Database not_found("test.db3"), SQLite::Exception); - - // Create a new database - SQLite::Database db("test.db3", SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE); - EXPECT_FALSE(db.tableExists("test")); - db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); - EXPECT_TRUE(db.tableExists("test")); - } // Close DB test.db3 - { - // Reopen the database file - SQLite::Database db("test.db3", SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE); - EXPECT_TRUE(db.tableExists("test")); - } // Close DB test.db3 - remove("test.db3"); -} - -TEST(Database, inMemory) -{ - { - // Create a new 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")); - // Create a new database: not shared with the above db - SQLite::Database db2(":memory:"); - EXPECT_FALSE(db2.tableExists("test")); - } // Close an destroy DBs - { - // Create a new database: no more "test" table - SQLite::Database db(":memory:"); - EXPECT_FALSE(db.tableExists("test")); - } // Close an destroy DB -} - -TEST(Database, backup) -{ - // 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.db3"); - EXPECT_NO_THROW(db.backup("backup.db3", SQLite::Database::Save)); - - // Trash the table - db.exec("DROP TABLE test;"); - EXPECT_FALSE(db.tableExists("test")); - - // Import the data back from the file - EXPECT_NO_THROW(db.backup("backup.db3", SQLite::Database::Load)); - remove("backup.db3"); - - EXPECT_TRUE(db.tableExists("test")); -} - -#if SQLITE_VERSION_NUMBER >= 3007015 // SQLite v3.7.15 is first version with PRAGMA busy_timeout -TEST(Database, busyTimeout) -{ - { - // Create a new database with default timeout of 0ms - SQLite::Database db(":memory:"); - // Busy timeout default to 0ms: any contention between threads or process leads to SQLITE_BUSY error - EXPECT_EQ(0, db.execAndGet("PRAGMA busy_timeout").getInt()); - - // Set a non null busy timeout: any contention between threads will leads to as much retry as possible during the time - db.setBusyTimeout(5000); - EXPECT_EQ(5000, db.execAndGet("PRAGMA busy_timeout").getInt()); - - // Reset timeout to 0 - db.setBusyTimeout(0); - EXPECT_EQ(0, db.execAndGet("PRAGMA busy_timeout").getInt()); - } - { - // Create a new database with a non null busy timeout - SQLite::Database db(":memory:", SQLite::OPEN_READWRITE, 5000); - EXPECT_EQ(5000, db.execAndGet("PRAGMA busy_timeout").getInt()); - - // Reset timeout to null - db.setBusyTimeout(0); - EXPECT_EQ(0, db.execAndGet("PRAGMA busy_timeout").getInt()); - } - { - // Create a new database with a non null busy timeout - const std::string memory = ":memory:"; - SQLite::Database db(memory, SQLite::OPEN_READWRITE, 5000); - EXPECT_EQ(5000, db.execAndGet("PRAGMA busy_timeout").getInt()); - - // Reset timeout to null - db.setBusyTimeout(0); - EXPECT_EQ(0, db.execAndGet("PRAGMA busy_timeout").getInt()); - } -} -#endif // SQLITE_VERSION_NUMBER >= 3007015 - -TEST(Database, exec) -{ - // Create a new database - SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); - - // Create a new table with an explicit "id" column aliasing the underlying rowid - // NOTE: here exec() returns 0 only because it is the first statements since database connexion, - // but its return is an undefined value for "CREATE TABLE" statements. - db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); - EXPECT_EQ(0, db.getChanges()); - EXPECT_EQ(0, db.getLastInsertRowid()); - EXPECT_EQ(0, db.getTotalChanges()); - - // first row : insert the "first" text value into new row of id 1 - EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\")")); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(1, db.getLastInsertRowid()); - EXPECT_EQ(1, db.getTotalChanges()); - - // second row : insert the "second" text value into new row of id 2 - EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"second\")")); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(2, db.getLastInsertRowid()); - EXPECT_EQ(2, db.getTotalChanges()); - - // third row : insert the "third" text value into new row of id 3 - const std::string insert("INSERT INTO test VALUES (NULL, \"third\")"); - EXPECT_EQ(1, db.exec(insert)); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(3, db.getLastInsertRowid()); - EXPECT_EQ(3, db.getTotalChanges()); - - // update the second row : update text value to "second_updated" - EXPECT_EQ(1, db.exec("UPDATE test SET value=\"second-updated\" WHERE id='2'")); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(3, db.getLastInsertRowid()); // last inserted row ID is still 3 - EXPECT_EQ(4, db.getTotalChanges()); - - // delete the third row - EXPECT_EQ(1, db.exec("DELETE FROM test WHERE id='3'")); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(3, db.getLastInsertRowid()); - EXPECT_EQ(5, db.getTotalChanges()); - - // drop the whole table, ie the two remaining columns - // NOTE: here exec() returns 1, like the last time, as it is an undefined value for "DROP TABLE" statements - db.exec("DROP TABLE IF EXISTS test"); - EXPECT_FALSE(db.tableExists("test")); - EXPECT_EQ(5, db.getTotalChanges()); - - // Re-Create the same table - // NOTE: here exec() returns 1, like the last time, as it is an undefined value for "CREATE TABLE" statements - db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); - EXPECT_EQ(5, db.getTotalChanges()); - - // insert two rows with two *different* statements => returns only 1, ie. for the second INSERT statement - EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\");INSERT INTO test VALUES (NULL, \"second\");")); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(2, db.getLastInsertRowid()); - EXPECT_EQ(7, db.getTotalChanges()); - -#if (SQLITE_VERSION_NUMBER >= 3007011) - // insert two rows with only one statement (starting with SQLite 3.7.11) => returns 2 - EXPECT_EQ(2, db.exec("INSERT INTO test VALUES (NULL, \"third\"), (NULL, \"fourth\");")); - EXPECT_EQ(2, db.getChanges()); - EXPECT_EQ(4, db.getLastInsertRowid()); - EXPECT_EQ(9, db.getTotalChanges()); -#endif -} - -TEST(Database, tryExec) -{ - // Create a new database - SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); - - // Create a new table with an explicit "id" column aliasing the underlying rowid - EXPECT_EQ(SQLite::OK, db.tryExec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")); - EXPECT_EQ(0, db.getChanges()); - EXPECT_EQ(0, db.getLastInsertRowid()); - EXPECT_EQ(0, db.getTotalChanges()); - - // first row : insert the "first" text value into new row of id 1 - EXPECT_EQ(SQLite::OK, db.tryExec("INSERT INTO test VALUES (NULL, \"first\")")); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(1, db.getLastInsertRowid()); - EXPECT_EQ(1, db.getTotalChanges()); - - // second row : insert the "second" text value into new row of id 2 - EXPECT_EQ(SQLite::OK, db.tryExec("INSERT INTO test VALUES (NULL, \"second\")")); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(2, db.getLastInsertRowid()); - EXPECT_EQ(2, db.getTotalChanges()); - - // third row : insert the "third" text value into new row of id 3 - const std::string insert("INSERT INTO test VALUES (NULL, \"third\")"); - EXPECT_EQ(SQLite::OK, db.tryExec(insert)); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(3, db.getLastInsertRowid()); - EXPECT_EQ(3, db.getTotalChanges()); - - // update the second row : update text value to "second_updated" - EXPECT_EQ(SQLite::OK, db.tryExec("UPDATE test SET value=\"second-updated\" WHERE id='2'")); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(3, db.getLastInsertRowid()); // last inserted row ID is still 3 - EXPECT_EQ(4, db.getTotalChanges()); - - // delete the third row - EXPECT_EQ(SQLite::OK, db.tryExec("DELETE FROM test WHERE id='3'")); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(3, db.getLastInsertRowid()); - EXPECT_EQ(5, db.getTotalChanges()); - - // drop the whole table, ie the two remaining columns - EXPECT_EQ(SQLite::OK, db.tryExec("DROP TABLE IF EXISTS test")); - EXPECT_FALSE(db.tableExists("test")); - EXPECT_EQ(5, db.getTotalChanges()); - - // Re-Create the same table - EXPECT_EQ(SQLite::OK, db.tryExec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")); - EXPECT_EQ(5, db.getTotalChanges()); - - // insert two rows with two *different* statements => only 1 change, ie. for the second INSERT statement - EXPECT_EQ(SQLite::OK, db.tryExec("INSERT INTO test VALUES (NULL, \"first\");INSERT INTO test VALUES (NULL, \"second\");")); - EXPECT_EQ(1, db.getChanges()); - EXPECT_EQ(2, db.getLastInsertRowid()); - EXPECT_EQ(7, db.getTotalChanges()); - -#if (SQLITE_VERSION_NUMBER >= 3007011) - // insert two rows with only one statement (starting with SQLite 3.7.11) - EXPECT_EQ(SQLite::OK, db.tryExec("INSERT INTO test VALUES (NULL, \"third\"), (NULL, \"fourth\");")); - EXPECT_EQ(2, db.getChanges()); - EXPECT_EQ(4, db.getLastInsertRowid()); - EXPECT_EQ(9, db.getTotalChanges()); -#endif -} - -TEST(Database, execAndGet) -{ - // Create a new database - SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); - - // Create a new table with an explicit "id" column aliasing the underlying rowid - db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT, weight INTEGER)"); - - // insert a few rows - EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\", 3)")); - EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"second\", 5)")); - EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"third\", 7)")); - - // Get a single value result with an easy to use shortcut - EXPECT_STREQ("second", db.execAndGet("SELECT value FROM test WHERE id=2")); - EXPECT_STREQ("third", db.execAndGet("SELECT value FROM test WHERE weight=7")); - const std::string query("SELECT weight FROM test WHERE value=\"first\""); - EXPECT_EQ(3, db.execAndGet(query).getInt()); -} - -TEST(Database, execException) -{ - // Create a new database - SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); - EXPECT_EQ(SQLite::OK, db.getErrorCode()); - EXPECT_EQ(SQLite::OK, db.getExtendedErrorCode()); - - // exception with SQL error: "no such table" - EXPECT_THROW(db.exec("INSERT INTO test VALUES (NULL, \"first\", 3)"), SQLite::Exception); - EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); - EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); - EXPECT_STREQ("no such table: test", db.getErrorMsg()); - - // Create a new table - db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT, weight INTEGER)"); - EXPECT_EQ(SQLite::OK, db.getErrorCode()); - EXPECT_EQ(SQLite::OK, db.getExtendedErrorCode()); - EXPECT_STREQ("not an error", db.getErrorMsg()); - - // exception with SQL error: "table test has 3 columns but 2 values were supplied" - EXPECT_THROW(db.exec("INSERT INTO test VALUES (NULL, 3)"), SQLite::Exception); - EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); - EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); - EXPECT_STREQ("table test has 3 columns but 2 values were supplied", db.getErrorMsg()); - - // exception with SQL error: "No row to get a column from" - EXPECT_THROW(db.execAndGet("SELECT weight FROM test WHERE value=\"first\""), SQLite::Exception); - - EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\", 3)")); - // exception with SQL error: "No row to get a column from" - EXPECT_THROW(db.execAndGet("SELECT weight FROM test WHERE value=\"second\""), SQLite::Exception); - - // Add a row with more values than columns in the table: "table test has 3 columns but 4 values were supplied" - EXPECT_THROW(db.exec("INSERT INTO test VALUES (NULL, \"first\", 123, 0.123)"), SQLite::Exception); - EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); - EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); - EXPECT_STREQ("table test has 3 columns but 4 values were supplied", db.getErrorMsg()); -} - -TEST(Database, tryExecError) -{ - // Create a new database - SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); - EXPECT_EQ(SQLite::OK, db.getErrorCode()); - EXPECT_EQ(SQLite::OK, db.getExtendedErrorCode()); - - // Insert into nonexistent table: "no such table" - EXPECT_EQ(SQLITE_ERROR, db.tryExec("INSERT INTO test VALUES (NULL, \"first\", 3)")); - EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); - EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); - EXPECT_STREQ("no such table: test", db.getErrorMsg()); - - // Create a new table - EXPECT_EQ(SQLite::OK, db.tryExec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT, weight INTEGER)")); - EXPECT_EQ(SQLite::OK, db.getErrorCode()); - EXPECT_EQ(SQLite::OK, db.getExtendedErrorCode()); - EXPECT_STREQ("not an error", db.getErrorMsg()); - - // Add a row with fewer values than columns in the table: "table test has 3 columns but 2 values were supplied" - EXPECT_EQ(SQLITE_ERROR, db.tryExec("INSERT INTO test VALUES (NULL, 3)")); - EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); - EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); - EXPECT_STREQ("table test has 3 columns but 2 values were supplied", db.getErrorMsg()); - - // Add a row with more values than columns in the table: "table test has 3 columns but 4 values were supplied" - EXPECT_EQ(SQLITE_ERROR, db.tryExec("INSERT INTO test VALUES (NULL, \"first\", 123, 0.123)")); - EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); - EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); - EXPECT_STREQ("table test has 3 columns but 4 values were supplied", db.getErrorMsg()); - - // Create a first row - EXPECT_EQ(SQLite::OK, db.tryExec("INSERT INTO test VALUES (NULL, \"first\", 3)")); - EXPECT_EQ(1, db.getLastInsertRowid()); - - // Try to insert a new row with the same PRIMARY KEY: "UNIQUE constraint failed: test.id" - EXPECT_EQ(SQLITE_CONSTRAINT, db.tryExec("INSERT INTO test VALUES (1, \"impossible\", 456)")); - EXPECT_EQ(SQLITE_CONSTRAINT, db.getErrorCode()); - EXPECT_EQ(SQLITE_CONSTRAINT_PRIMARYKEY, db.getExtendedErrorCode()); - EXPECT_STREQ("UNIQUE constraint failed: test.id", db.getErrorMsg()); -} - -// From https://stackoverflow.com/a/8283265/1163698 How can I create a user-defined function in SQLite? -static void firstchar(sqlite3_context *context, int argc, sqlite3_value **argv) -{ - if (argc == 1) - { - const unsigned char *text = sqlite3_value_text(argv[0]); - if (text && text[0]) - { - char result[2]; - result[0] = text[0]; result[1] = '\0'; - sqlite3_result_text(context, result, -1, SQLITE_TRANSIENT); - return; - } - } - sqlite3_result_null(context); -} - -TEST(Database, createFunction) -{ - SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); - db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); - - EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\")")); - EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"second\")")); - - // exception with SQL error: "no such function: firstchar" - EXPECT_THROW(db.exec("SELECT firstchar(value) FROM test WHERE id=1"), SQLite::Exception); - - db.createFunction("firstchar", 1, true, nullptr, &firstchar, nullptr, nullptr, nullptr); - - EXPECT_EQ(1, db.exec("SELECT firstchar(value) FROM test WHERE id=1")); -} - -TEST(Database, loadExtension) -{ - SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); - - // Try to load a non-existing extension (no dynamic library found) - EXPECT_THROW(db.loadExtension("non-existing-extension", "entry-point"), SQLite::Exception); - - // TODO: test a proper extension -} - -TEST(Database, getHeaderInfo) -{ - remove("test.db3"); - { - // Call without passing a database file name - EXPECT_THROW(SQLite::Database::getHeaderInfo(""),SQLite::Exception); - - // Call with a non-existent database - EXPECT_THROW(SQLite::Database::getHeaderInfo("test.db3"), SQLite::Exception); - - // Simulate an incomplete header by writing garbage to a file - { - const unsigned char badData[] = "garbage..."; - const char* pBadData = reinterpret_cast(&badData[0]); - - remove("short.db3"); - std::ofstream corruptDb; - corruptDb.open("short.db3", std::ios::app | std::ios::binary); - corruptDb.write(pBadData, sizeof(badData)); - corruptDb.close(); - - EXPECT_THROW(SQLite::Database::getHeaderInfo("short.db3"), SQLite::Exception); - remove("short.db3"); - } - - // Simulate a corrupt header by writing garbage to a file - { - const unsigned char badData[100] = "garbage..."; - const char* pBadData = reinterpret_cast(&badData[0]); - - remove("corrupt.db3"); - std::ofstream corruptDb; - corruptDb.open("corrupt.db3", std::ios::app | std::ios::binary); - corruptDb.write(pBadData, sizeof(badData)); - corruptDb.close(); - - EXPECT_THROW(SQLite::Database::getHeaderInfo("corrupt.db3"), SQLite::Exception); - remove("corrupt.db3"); - } - - // Create a new database - SQLite::Database db("test.db3", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); - db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); - - // Set assorted SQLite header values using associated PRAGMA - db.exec("PRAGMA main.user_version = 12345"); - db.exec("PRAGMA main.application_id = 2468"); - - // Parse header fields from test database - const SQLite::Header h = db.getHeaderInfo(); - - //Test header values explicitly set via PRAGMA statements - EXPECT_EQ(h.userVersion, 12345); - EXPECT_EQ(h.applicationId, 2468); - - //Test header values with expected default values - EXPECT_EQ(h.pageSizeBytes, 4096); - EXPECT_EQ(h.fileFormatWriteVersion,1); - EXPECT_EQ(h.fileFormatReadVersion,1); - EXPECT_EQ(h.reservedSpaceBytes,0); - EXPECT_EQ(h.maxEmbeddedPayloadFrac, 64); - EXPECT_EQ(h.minEmbeddedPayloadFrac, 32); - EXPECT_EQ(h.leafPayloadFrac, 32); - EXPECT_EQ(h.fileChangeCounter, 3); - EXPECT_EQ(h.databaseSizePages, 2); - EXPECT_EQ(h.firstFreelistTrunkPage, 0); - EXPECT_EQ(h.totalFreelistPages, 0); - EXPECT_EQ(h.schemaCookie, 1); - EXPECT_EQ(h.schemaFormatNumber, 4); - EXPECT_EQ(h.defaultPageCacheSizeBytes, 0); - EXPECT_EQ(h.largestBTreePageNumber, 0); - EXPECT_EQ(h.databaseTextEncoding, 1); - EXPECT_EQ(h.incrementalVaccumMode, 0); - EXPECT_EQ(h.versionValidFor, 3); - EXPECT_EQ(h.sqliteVersion, SQLITE_VERSION_NUMBER); - } - remove("test.db3"); -} - -#ifdef SQLITE_HAS_CODEC -TEST(Database, encryptAndDecrypt) -{ - remove("test.db3"); - { - // Try to open the non-existing database - EXPECT_THROW(SQLite::Database not_found("test.db3"), SQLite::Exception); - EXPECT_THROW(SQLite::Database::isUnencrypted("test.db3"), SQLite::Exception); - EXPECT_THROW(SQLite::Database::isUnencrypted(""), SQLite::Exception); - - // Create a new database - SQLite::Database db("test.db3", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); - EXPECT_FALSE(db.tableExists("test")); - db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); - EXPECT_TRUE(db.tableExists("test")); - } // Close DB test.db3 - { - // Reopen the database file and encrypt it - EXPECT_TRUE(SQLite::Database::isUnencrypted("test.db3")); - SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); - // Encrypt the database - db.rekey("123secret"); - } // Close DB test.db3 - { - // Reopen the database file and try to use it - EXPECT_FALSE(SQLite::Database::isUnencrypted("test.db3")); - SQLite::Database db("test.db3", SQLite::OPEN_READONLY); - EXPECT_THROW(db.tableExists("test"), SQLite::Exception); - db.key("123secret"); - EXPECT_TRUE(db.tableExists("test")); - } // Close DB test.db3 - { - // Reopen the database file and decrypt it - EXPECT_FALSE(SQLite::Database::isUnencrypted("test.db3")); - SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); - // Decrypt the database - db.key("123secret"); - db.rekey(""); - } // Close DB test.db3 - { - // Reopen the database file and use it - EXPECT_TRUE(SQLite::Database::isUnencrypted("test.db3")); - SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); - EXPECT_TRUE(db.tableExists("test")); - } // Close DB test.db3 - remove("test.db3"); -} -#else // SQLITE_HAS_CODEC -TEST(Database, encryptAndDecrypt) -{ - remove("test.db3"); - { - // Try to open the non-existing database - EXPECT_THROW(SQLite::Database not_found("test.db3"), SQLite::Exception); - EXPECT_THROW(SQLite::Database::isUnencrypted("test.db3"), SQLite::Exception); - EXPECT_THROW(SQLite::Database::isUnencrypted(""), SQLite::Exception); - - // Create a new database - SQLite::Database db("test.db3", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); - EXPECT_FALSE(db.tableExists("test")); - db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); - EXPECT_TRUE(db.tableExists("test")); - } // Close DB test.db3 - { - // Reopen the database file and encrypt it - EXPECT_TRUE(SQLite::Database::isUnencrypted("test.db3")); - SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); - // Encrypt the database - EXPECT_THROW(db.key("123secret"), SQLite::Exception); - EXPECT_THROW(db.rekey("123secret"), SQLite::Exception); - } // Close DB test.db3 - remove("test.db3"); -} -#endif // SQLITE_HAS_CODEC +/** + * @file Database_test.cpp + * @ingroup tests + * @brief Test of a SQLiteCpp Database. + * + * Copyright (c) 2012-2024 Sebastien Rombauts (sebastien.rombauts@gmail.com) + * + * Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt + * or copy at http://opensource.org/licenses/MIT) + */ + +#include + +#include // for SQLITE_ERROR and SQLITE_VERSION_NUMBER + +#include + +#ifdef SQLITECPP_HAVE_STD_FILESYSTEM +#include +#endif // c++17 + +#include +#include + +#ifdef SQLITECPP_ENABLE_ASSERT_HANDLER +namespace SQLite +{ +/// definition of the assertion handler enabled when SQLITECPP_ENABLE_ASSERT_HANDLER is defined in the project (CMakeList.txt) +void assertion_failed(const char* apFile, const long apLine, const char* apFunc, const char* apExpr, const char* apMsg) +{ + // TODO: unit test that this assertion callback get called (already tested manually) + std::cout << "assertion_failed(" << apFile << ", " << apLine << ", " << apFunc << ", " << apExpr << ", " << apMsg << ")\n"; +} +} +#endif + +// NOTE on macOS FindSQLite3 find an unrelated sqlite3.h from Mono.framework that doesn't match the actual package version! +#if !defined(__APPLE__) || !defined(SQLITECPP_INTERNAL_SQLITE) +TEST(SQLiteCpp, version) +{ + EXPECT_STREQ(SQLITE_VERSION, SQLite::VERSION); + EXPECT_EQ (SQLITE_VERSION_NUMBER, SQLite::VERSION_NUMBER); + EXPECT_STREQ(SQLITE_VERSION, SQLite::getLibVersion()); + EXPECT_EQ (SQLITE_VERSION_NUMBER, SQLite::getLibVersionNumber()); +} +#endif + +TEST(Database, ctorExecCreateDropExist) +{ + remove("test.db3"); + { + // Try to open a non-existing database + std::string filename = "test.db3"; + EXPECT_THROW(SQLite::Database not_found(filename), SQLite::Exception); + + // Create a new database using a string or a std::filesystem::path if using c++17 and a + // compatible compiler + #ifdef SQLITECPP_HAVE_STD_FILESYSTEM + SQLite::Database db(std::filesystem::path("test.db3"), SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE); + #else + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE); + #endif // have std::filesystem + + EXPECT_STREQ("test.db3", db.getFilename().c_str()); + EXPECT_FALSE(db.tableExists("test")); + EXPECT_FALSE(db.tableExists(std::string("test"))); + EXPECT_EQ(0, db.getLastInsertRowid()); + + EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")); + 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"))); + EXPECT_EQ(0, db.getLastInsertRowid()); + } // Close DB test.db3 + remove("test.db3"); +} + +#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1600) + +SQLite::Database DatabaseBuilder(const char* apName) +{ + return SQLite::Database(apName, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); +} + +TEST(Database, moveConstructor) +{ + remove("test.db3"); + { + // Create a new database, using the move constructor + SQLite::Database db = DatabaseBuilder("test.db3"); + EXPECT_FALSE(db.tableExists("test")); + EXPECT_TRUE(db.getHandle() != NULL); + SQLite::Database moved = std::move(db); + EXPECT_TRUE(db.getHandle() == NULL); + EXPECT_TRUE(moved.getHandle() != NULL); + EXPECT_FALSE(moved.tableExists("test")); + } // Close DB test.db3 + remove("test.db3"); +} + +#endif + +TEST(Database, createCloseReopen) +{ + remove("test.db3"); + { + // Try to open the non-existing database + EXPECT_THROW(SQLite::Database not_found("test.db3"), SQLite::Exception); + + // Create a new database + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE); + EXPECT_FALSE(db.tableExists("test")); + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); + EXPECT_TRUE(db.tableExists("test")); + } // Close DB test.db3 + { + // Reopen the database file + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE); + EXPECT_TRUE(db.tableExists("test")); + } // Close DB test.db3 + remove("test.db3"); +} + +TEST(Database, inMemory) +{ + { + // Create a new 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")); + // Create a new database: not shared with the above db + SQLite::Database db2(":memory:"); + EXPECT_FALSE(db2.tableExists("test")); + } // Close an destroy DBs + { + // Create a new database: no more "test" table + SQLite::Database db(":memory:"); + EXPECT_FALSE(db.tableExists("test")); + } // Close an destroy DB +} + +TEST(Database, backup) +{ + // 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.db3"); + EXPECT_NO_THROW(db.backup("backup.db3", SQLite::Database::Save)); + + // Trash the table + db.exec("DROP TABLE test;"); + EXPECT_FALSE(db.tableExists("test")); + + // Import the data back from the file + EXPECT_NO_THROW(db.backup("backup.db3", SQLite::Database::Load)); + remove("backup.db3"); + + EXPECT_TRUE(db.tableExists("test")); +} + +#if SQLITE_VERSION_NUMBER >= 3007015 // SQLite v3.7.15 is first version with PRAGMA busy_timeout +TEST(Database, busyTimeout) +{ + { + // Create a new database with default timeout of 0ms + SQLite::Database db(":memory:"); + // Busy timeout default to 0ms: any contention between threads or process leads to SQLITE_BUSY error + EXPECT_EQ(0, db.execAndGet("PRAGMA busy_timeout").getInt()); + + // Set a non null busy timeout: any contention between threads will leads to as much retry as possible during the time + db.setBusyTimeout(5000); + EXPECT_EQ(5000, db.execAndGet("PRAGMA busy_timeout").getInt()); + + // Reset timeout to 0 + db.setBusyTimeout(0); + EXPECT_EQ(0, db.execAndGet("PRAGMA busy_timeout").getInt()); + } + { + // Create a new database with a non null busy timeout + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE, 5000); + EXPECT_EQ(5000, db.execAndGet("PRAGMA busy_timeout").getInt()); + + // Reset timeout to null + db.setBusyTimeout(0); + EXPECT_EQ(0, db.execAndGet("PRAGMA busy_timeout").getInt()); + } + { + // Create a new database with a non null busy timeout + const std::string memory = ":memory:"; + SQLite::Database db(memory, SQLite::OPEN_READWRITE, 5000); + EXPECT_EQ(5000, db.execAndGet("PRAGMA busy_timeout").getInt()); + + // Reset timeout to null + db.setBusyTimeout(0); + EXPECT_EQ(0, db.execAndGet("PRAGMA busy_timeout").getInt()); + } +} +#endif // SQLITE_VERSION_NUMBER >= 3007015 + +TEST(Database, exec) +{ + // Create a new database + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); + + // Create a new table with an explicit "id" column aliasing the underlying rowid + // NOTE: here exec() returns 0 only because it is the first statements since database connexion, + // but its return is an undefined value for "CREATE TABLE" statements. + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); + EXPECT_EQ(0, db.getChanges()); + EXPECT_EQ(0, db.getLastInsertRowid()); + EXPECT_EQ(0, db.getTotalChanges()); + + // first row : insert the "first" text value into new row of id 1 + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\")")); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(1, db.getLastInsertRowid()); + EXPECT_EQ(1, db.getTotalChanges()); + + // second row : insert the "second" text value into new row of id 2 + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"second\")")); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(2, db.getLastInsertRowid()); + EXPECT_EQ(2, db.getTotalChanges()); + + // third row : insert the "third" text value into new row of id 3 + const std::string insert("INSERT INTO test VALUES (NULL, \"third\")"); + EXPECT_EQ(1, db.exec(insert)); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(3, db.getLastInsertRowid()); + EXPECT_EQ(3, db.getTotalChanges()); + + // update the second row : update text value to "second_updated" + EXPECT_EQ(1, db.exec("UPDATE test SET value=\"second-updated\" WHERE id='2'")); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(3, db.getLastInsertRowid()); // last inserted row ID is still 3 + EXPECT_EQ(4, db.getTotalChanges()); + + // delete the third row + EXPECT_EQ(1, db.exec("DELETE FROM test WHERE id='3'")); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(3, db.getLastInsertRowid()); + EXPECT_EQ(5, db.getTotalChanges()); + + // drop the whole table, ie the two remaining columns + // NOTE: here exec() returns 1, like the last time, as it is an undefined value for "DROP TABLE" statements + db.exec("DROP TABLE IF EXISTS test"); + EXPECT_FALSE(db.tableExists("test")); + EXPECT_EQ(5, db.getTotalChanges()); + + // Re-Create the same table + // NOTE: here exec() returns 1, like the last time, as it is an undefined value for "CREATE TABLE" statements + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); + EXPECT_EQ(5, db.getTotalChanges()); + + // insert two rows with two *different* statements => returns only 1, ie. for the second INSERT statement + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\");INSERT INTO test VALUES (NULL, \"second\");")); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(2, db.getLastInsertRowid()); + EXPECT_EQ(7, db.getTotalChanges()); + +#if (SQLITE_VERSION_NUMBER >= 3007011) + // insert two rows with only one statement (starting with SQLite 3.7.11) => returns 2 + EXPECT_EQ(2, db.exec("INSERT INTO test VALUES (NULL, \"third\"), (NULL, \"fourth\");")); + EXPECT_EQ(2, db.getChanges()); + EXPECT_EQ(4, db.getLastInsertRowid()); + EXPECT_EQ(9, db.getTotalChanges()); +#endif +} + +TEST(Database, tryExec) +{ + // Create a new database + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); + + // Create a new table with an explicit "id" column aliasing the underlying rowid + EXPECT_EQ(SQLite::OK, db.tryExec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")); + EXPECT_EQ(0, db.getChanges()); + EXPECT_EQ(0, db.getLastInsertRowid()); + EXPECT_EQ(0, db.getTotalChanges()); + + // first row : insert the "first" text value into new row of id 1 + EXPECT_EQ(SQLite::OK, db.tryExec("INSERT INTO test VALUES (NULL, \"first\")")); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(1, db.getLastInsertRowid()); + EXPECT_EQ(1, db.getTotalChanges()); + + // second row : insert the "second" text value into new row of id 2 + EXPECT_EQ(SQLite::OK, db.tryExec("INSERT INTO test VALUES (NULL, \"second\")")); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(2, db.getLastInsertRowid()); + EXPECT_EQ(2, db.getTotalChanges()); + + // third row : insert the "third" text value into new row of id 3 + const std::string insert("INSERT INTO test VALUES (NULL, \"third\")"); + EXPECT_EQ(SQLite::OK, db.tryExec(insert)); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(3, db.getLastInsertRowid()); + EXPECT_EQ(3, db.getTotalChanges()); + + // update the second row : update text value to "second_updated" + EXPECT_EQ(SQLite::OK, db.tryExec("UPDATE test SET value=\"second-updated\" WHERE id='2'")); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(3, db.getLastInsertRowid()); // last inserted row ID is still 3 + EXPECT_EQ(4, db.getTotalChanges()); + + // delete the third row + EXPECT_EQ(SQLite::OK, db.tryExec("DELETE FROM test WHERE id='3'")); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(3, db.getLastInsertRowid()); + EXPECT_EQ(5, db.getTotalChanges()); + + // drop the whole table, ie the two remaining columns + EXPECT_EQ(SQLite::OK, db.tryExec("DROP TABLE IF EXISTS test")); + EXPECT_FALSE(db.tableExists("test")); + EXPECT_EQ(5, db.getTotalChanges()); + + // Re-Create the same table + EXPECT_EQ(SQLite::OK, db.tryExec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")); + EXPECT_EQ(5, db.getTotalChanges()); + + // insert two rows with two *different* statements => only 1 change, ie. for the second INSERT statement + EXPECT_EQ(SQLite::OK, db.tryExec("INSERT INTO test VALUES (NULL, \"first\");INSERT INTO test VALUES (NULL, \"second\");")); + EXPECT_EQ(1, db.getChanges()); + EXPECT_EQ(2, db.getLastInsertRowid()); + EXPECT_EQ(7, db.getTotalChanges()); + +#if (SQLITE_VERSION_NUMBER >= 3007011) + // insert two rows with only one statement (starting with SQLite 3.7.11) + EXPECT_EQ(SQLite::OK, db.tryExec("INSERT INTO test VALUES (NULL, \"third\"), (NULL, \"fourth\");")); + EXPECT_EQ(2, db.getChanges()); + EXPECT_EQ(4, db.getLastInsertRowid()); + EXPECT_EQ(9, db.getTotalChanges()); +#endif +} + +TEST(Database, execAndGet) +{ + // Create a new database + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); + + // Create a new table with an explicit "id" column aliasing the underlying rowid + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT, weight INTEGER)"); + + // insert a few rows + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\", 3)")); + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"second\", 5)")); + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"third\", 7)")); + + // Get a single value result with an easy to use shortcut + EXPECT_STREQ("second", db.execAndGet("SELECT value FROM test WHERE id=2")); + EXPECT_STREQ("third", db.execAndGet("SELECT value FROM test WHERE weight=7")); + const std::string query("SELECT weight FROM test WHERE value=\"first\""); + EXPECT_EQ(3, db.execAndGet(query).getInt()); +} + +TEST(Database, execException) +{ + // Create a new database + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); + EXPECT_EQ(SQLite::OK, db.getErrorCode()); + EXPECT_EQ(SQLite::OK, db.getExtendedErrorCode()); + + // exception with SQL error: "no such table" + EXPECT_THROW(db.exec("INSERT INTO test VALUES (NULL, \"first\", 3)"), SQLite::Exception); + EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); + EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); + EXPECT_STREQ("no such table: test", db.getErrorMsg()); + + // Create a new table + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT, weight INTEGER)"); + EXPECT_EQ(SQLite::OK, db.getErrorCode()); + EXPECT_EQ(SQLite::OK, db.getExtendedErrorCode()); + EXPECT_STREQ("not an error", db.getErrorMsg()); + + // exception with SQL error: "table test has 3 columns but 2 values were supplied" + EXPECT_THROW(db.exec("INSERT INTO test VALUES (NULL, 3)"), SQLite::Exception); + EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); + EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); + EXPECT_STREQ("table test has 3 columns but 2 values were supplied", db.getErrorMsg()); + + // exception with SQL error: "No row to get a column from" + EXPECT_THROW(db.execAndGet("SELECT weight FROM test WHERE value=\"first\""), SQLite::Exception); + + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\", 3)")); + // exception with SQL error: "No row to get a column from" + EXPECT_THROW(db.execAndGet("SELECT weight FROM test WHERE value=\"second\""), SQLite::Exception); + + // Add a row with more values than columns in the table: "table test has 3 columns but 4 values were supplied" + EXPECT_THROW(db.exec("INSERT INTO test VALUES (NULL, \"first\", 123, 0.123)"), SQLite::Exception); + EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); + EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); + EXPECT_STREQ("table test has 3 columns but 4 values were supplied", db.getErrorMsg()); +} + +TEST(Database, tryExecError) +{ + // Create a new database + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); + EXPECT_EQ(SQLite::OK, db.getErrorCode()); + EXPECT_EQ(SQLite::OK, db.getExtendedErrorCode()); + + // Insert into nonexistent table: "no such table" + EXPECT_EQ(SQLITE_ERROR, db.tryExec("INSERT INTO test VALUES (NULL, \"first\", 3)")); + EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); + EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); + EXPECT_STREQ("no such table: test", db.getErrorMsg()); + + // Create a new table + EXPECT_EQ(SQLite::OK, db.tryExec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT, weight INTEGER)")); + EXPECT_EQ(SQLite::OK, db.getErrorCode()); + EXPECT_EQ(SQLite::OK, db.getExtendedErrorCode()); + EXPECT_STREQ("not an error", db.getErrorMsg()); + + // Add a row with fewer values than columns in the table: "table test has 3 columns but 2 values were supplied" + EXPECT_EQ(SQLITE_ERROR, db.tryExec("INSERT INTO test VALUES (NULL, 3)")); + EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); + EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); + EXPECT_STREQ("table test has 3 columns but 2 values were supplied", db.getErrorMsg()); + + // Add a row with more values than columns in the table: "table test has 3 columns but 4 values were supplied" + EXPECT_EQ(SQLITE_ERROR, db.tryExec("INSERT INTO test VALUES (NULL, \"first\", 123, 0.123)")); + EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); + EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); + EXPECT_STREQ("table test has 3 columns but 4 values were supplied", db.getErrorMsg()); + + // Create a first row + EXPECT_EQ(SQLite::OK, db.tryExec("INSERT INTO test VALUES (NULL, \"first\", 3)")); + EXPECT_EQ(1, db.getLastInsertRowid()); + + // Try to insert a new row with the same PRIMARY KEY: "UNIQUE constraint failed: test.id" + EXPECT_EQ(SQLITE_CONSTRAINT, db.tryExec("INSERT INTO test VALUES (1, \"impossible\", 456)")); + EXPECT_EQ(SQLITE_CONSTRAINT, db.getErrorCode()); + EXPECT_EQ(SQLITE_CONSTRAINT_PRIMARYKEY, db.getExtendedErrorCode()); + EXPECT_STREQ("UNIQUE constraint failed: test.id", db.getErrorMsg()); +} + +// From https://stackoverflow.com/a/8283265/1163698 How can I create a user-defined function in SQLite? +static void firstchar(sqlite3_context *context, int argc, sqlite3_value **argv) +{ + if (argc == 1) + { + const unsigned char *text = sqlite3_value_text(argv[0]); + if (text && text[0]) + { + char result[2]; + result[0] = text[0]; result[1] = '\0'; + sqlite3_result_text(context, result, -1, SQLITE_TRANSIENT); + return; + } + } + sqlite3_result_null(context); +} + +TEST(Database, createFunction) +{ + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); + + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\")")); + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"second\")")); + + // exception with SQL error: "no such function: firstchar" + EXPECT_THROW(db.exec("SELECT firstchar(value) FROM test WHERE id=1"), SQLite::Exception); + + db.createFunction("firstchar", 1, true, nullptr, &firstchar, nullptr, nullptr, nullptr); + + EXPECT_EQ(1, db.exec("SELECT firstchar(value) FROM test WHERE id=1")); +} + +TEST(Database, loadExtension) +{ + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE); + + // Try to load a non-existing extension (no dynamic library found) + EXPECT_THROW(db.loadExtension("non-existing-extension", "entry-point"), SQLite::Exception); + + // TODO: test a proper extension +} + +TEST(Database, getHeaderInfo) +{ + remove("test.db3"); + { + // Call without passing a database file name + EXPECT_THROW(SQLite::Database::getHeaderInfo(""),SQLite::Exception); + + // Call with a non-existent database + EXPECT_THROW(SQLite::Database::getHeaderInfo("test.db3"), SQLite::Exception); + + // Simulate an incomplete header by writing garbage to a file + { + const unsigned char badData[] = "garbage..."; + const char* pBadData = reinterpret_cast(&badData[0]); + + remove("short.db3"); + std::ofstream corruptDb; + corruptDb.open("short.db3", std::ios::app | std::ios::binary); + corruptDb.write(pBadData, sizeof(badData)); + corruptDb.close(); + + EXPECT_THROW(SQLite::Database::getHeaderInfo("short.db3"), SQLite::Exception); + remove("short.db3"); + } + + // Simulate a corrupt header by writing garbage to a file + { + const unsigned char badData[100] = "garbage..."; + const char* pBadData = reinterpret_cast(&badData[0]); + + remove("corrupt.db3"); + std::ofstream corruptDb; + corruptDb.open("corrupt.db3", std::ios::app | std::ios::binary); + corruptDb.write(pBadData, sizeof(badData)); + corruptDb.close(); + + EXPECT_THROW(SQLite::Database::getHeaderInfo("corrupt.db3"), SQLite::Exception); + remove("corrupt.db3"); + } + + // Create a new database + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); + + // Set assorted SQLite header values using associated PRAGMA + db.exec("PRAGMA main.user_version = 12345"); + db.exec("PRAGMA main.application_id = 2468"); + + // Parse header fields from test database + const SQLite::Header h = db.getHeaderInfo(); + + //Test header values explicitly set via PRAGMA statements + EXPECT_EQ(h.userVersion, 12345); + EXPECT_EQ(h.applicationId, 2468); + + //Test header values with expected default values + EXPECT_EQ(h.pageSizeBytes, 4096); + EXPECT_EQ(h.fileFormatWriteVersion,1); + EXPECT_EQ(h.fileFormatReadVersion,1); + EXPECT_EQ(h.reservedSpaceBytes,0); + EXPECT_EQ(h.maxEmbeddedPayloadFrac, 64); + EXPECT_EQ(h.minEmbeddedPayloadFrac, 32); + EXPECT_EQ(h.leafPayloadFrac, 32); + EXPECT_EQ(h.fileChangeCounter, 3); + EXPECT_EQ(h.databaseSizePages, 2); + EXPECT_EQ(h.firstFreelistTrunkPage, 0); + EXPECT_EQ(h.totalFreelistPages, 0); + EXPECT_EQ(h.schemaCookie, 1); + EXPECT_EQ(h.schemaFormatNumber, 4); + EXPECT_EQ(h.defaultPageCacheSizeBytes, 0); + EXPECT_EQ(h.largestBTreePageNumber, 0); + EXPECT_EQ(h.databaseTextEncoding, 1); + EXPECT_EQ(h.incrementalVaccumMode, 0); + EXPECT_EQ(h.versionValidFor, 3); +// NOTE on macOS FindSQLite3 find an unrelated sqlite3.h from Mono.framework that doesn't match the actual package version! +#if !defined(__APPLE__) || !defined(SQLITECPP_INTERNAL_SQLITE) + EXPECT_EQ(h.sqliteVersion, SQLITE_VERSION_NUMBER); +#endif && !defined(__APPLE__) + } + remove("test.db3"); +} + +#ifdef SQLITE_HAS_CODEC +TEST(Database, encryptAndDecrypt) +{ + remove("test.db3"); + { + // Try to open the non-existing database + EXPECT_THROW(SQLite::Database not_found("test.db3"), SQLite::Exception); + EXPECT_THROW(SQLite::Database::isUnencrypted("test.db3"), SQLite::Exception); + EXPECT_THROW(SQLite::Database::isUnencrypted(""), SQLite::Exception); + + // Create a new database + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); + EXPECT_FALSE(db.tableExists("test")); + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); + EXPECT_TRUE(db.tableExists("test")); + } // Close DB test.db3 + { + // Reopen the database file and encrypt it + EXPECT_TRUE(SQLite::Database::isUnencrypted("test.db3")); + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); + // Encrypt the database + db.rekey("123secret"); + } // Close DB test.db3 + { + // Reopen the database file and try to use it + EXPECT_FALSE(SQLite::Database::isUnencrypted("test.db3")); + SQLite::Database db("test.db3", SQLite::OPEN_READONLY); + EXPECT_THROW(db.tableExists("test"), SQLite::Exception); + db.key("123secret"); + EXPECT_TRUE(db.tableExists("test")); + } // Close DB test.db3 + { + // Reopen the database file and decrypt it + EXPECT_FALSE(SQLite::Database::isUnencrypted("test.db3")); + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); + // Decrypt the database + db.key("123secret"); + db.rekey(""); + } // Close DB test.db3 + { + // Reopen the database file and use it + EXPECT_TRUE(SQLite::Database::isUnencrypted("test.db3")); + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); + EXPECT_TRUE(db.tableExists("test")); + } // Close DB test.db3 + remove("test.db3"); +} +#else // SQLITE_HAS_CODEC +TEST(Database, encryptAndDecrypt) +{ + remove("test.db3"); + { + // Try to open the non-existing database + EXPECT_THROW(SQLite::Database not_found("test.db3"), SQLite::Exception); + EXPECT_THROW(SQLite::Database::isUnencrypted("test.db3"), SQLite::Exception); + EXPECT_THROW(SQLite::Database::isUnencrypted(""), SQLite::Exception); + + // Create a new database + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); + EXPECT_FALSE(db.tableExists("test")); + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); + EXPECT_TRUE(db.tableExists("test")); + } // Close DB test.db3 + { + // Reopen the database file and encrypt it + EXPECT_TRUE(SQLite::Database::isUnencrypted("test.db3")); + SQLite::Database db("test.db3", SQLite::OPEN_READWRITE); + // Encrypt the database + EXPECT_THROW(db.key("123secret"), SQLite::Exception); + EXPECT_THROW(db.rekey("123secret"), SQLite::Exception); + } // Close DB test.db3 + remove("test.db3"); +} +#endif // SQLITE_HAS_CODEC