From acaed41465b9d343c15e511ba0729b2476b18d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rombauts?= Date: Fri, 1 May 2015 11:50:50 +0200 Subject: [PATCH] setBusyTimeout() now uses check() and throw in case of error - add unit test for setBusyTimeout() - add unit test for in memory databases --- include/SQLiteCpp/Database.h | 27 +++-- src/Database.cpp | 33 ++++-- tests/Database_test.cpp | 216 +++++++++++++++++++---------------- 3 files changed, 160 insertions(+), 116 deletions(-) diff --git a/include/SQLiteCpp/Database.h b/include/SQLiteCpp/Database.h index 7cc44a8..39ae312 100644 --- a/include/SQLiteCpp/Database.h +++ b/include/SQLiteCpp/Database.h @@ -59,7 +59,7 @@ public: * * @throw SQLite::Exception in case of error */ - Database(const char* apFilename, const int aFlags = SQLITE_OPEN_READONLY, const char * apVfs = NULL); + Database(const char* apFilename, const int aFlags = SQLITE_OPEN_READONLY, const char* apVfs = NULL); /** * @brief Open the provided database UTF-8 filename. @@ -89,6 +89,21 @@ public: */ virtual ~Database() noexcept; // nothrow + /** + * @brief Set a busy handler that sleeps for a specified amount of time when a table is locked. + * + * This is usefull in multithreaded program to handle case where a table is locked for writting by a thread. + * Any other thread cannot access the table and will receive a SQLITE_BUSY error: + * setting a timeout will wait and retry up to the time specified before returning this SQLITE_BUSY error. + * Reading the value of timeout for current connection can be done with SQL query "PRAGMA busy_timeout;". + * Default busy timeout is 0ms. + * + * @param[in] aTimeoutMs Amount of milliseconds to wait before returning SQLITE_BUSY + * + * @throw SQLite::Exception in case of error + */ + void setBusyTimeout(int aTimeoutMs) noexcept; // nothrow + /** * @brief Shortcut to execute one or multiple statements without results. * @@ -206,16 +221,6 @@ public: return tableExists(aTableName.c_str()); } - /** - * @brief Set a busy handler that sleeps for a specified amount of time when a table is locked. - * - * @param[in] aTimeoutMs Amount of milliseconds to wait before returning SQLITE_BUSY - */ - inline int setBusyTimeout(int aTimeoutMs) noexcept // nothrow - { - return sqlite3_busy_timeout(mpSQLite, aTimeoutMs); - } - /** * @brief Get the rowid of the most recent successful INSERT into the database from the current connection. * diff --git a/src/Database.cpp b/src/Database.cpp index 11fcc8d..ba5e355 100644 --- a/src/Database.cpp +++ b/src/Database.cpp @@ -30,7 +30,7 @@ Database::Database(const char* apFilename, const int aFlags /*= SQLITE_OPEN_READ mpSQLite(NULL), mFilename(apFilename) { - int ret = sqlite3_open_v2(apFilename, &mpSQLite, aFlags, apVfs); + const int ret = sqlite3_open_v2(apFilename, &mpSQLite, aFlags, apVfs); if (SQLITE_OK != ret) { std::string strerr = sqlite3_errmsg(mpSQLite); @@ -44,7 +44,7 @@ Database::Database(const std::string& aFilename, const int aFlags /*= SQLITE_OPE mpSQLite(NULL), mFilename(aFilename) { - int ret = sqlite3_open_v2(aFilename.c_str(), &mpSQLite, aFlags, aVfs.empty() ? NULL : aVfs.c_str()); + const int ret = sqlite3_open_v2(aFilename.c_str(), &mpSQLite, aFlags, aVfs.empty() ? NULL : aVfs.c_str()); if (SQLITE_OK != ret) { std::string strerr = sqlite3_errmsg(mpSQLite); @@ -56,15 +56,34 @@ Database::Database(const std::string& aFilename, const int aFlags /*= SQLITE_OPE // Close the SQLite database connection. Database::~Database() noexcept // nothrow { - int ret = sqlite3_close(mpSQLite); + const int ret = sqlite3_close(mpSQLite); // Never throw an exception in a destructor SQLITECPP_ASSERT(SQLITE_OK == ret, sqlite3_errmsg(mpSQLite)); // See SQLITECPP_ENABLE_ASSERT_HANDLER } +/** + * @brief Set a busy handler that sleeps for a specified amount of time when a table is locked. + * + * This is usefull in multithreaded program to handle case where a table is locked for writting by a thread. + * Any other thread cannot access the table and will receive a SQLITE_BUSY error: + * setting a timeout will wait and retry up to the time specified before returning this SQLITE_BUSY error. + * Reading the value of timeout for current connection can be done with SQL query "PRAGMA busy_timeout;". + * Default busy timeout is 0ms. + * + * @param[in] aTimeoutMs Amount of milliseconds to wait before returning SQLITE_BUSY + * + * @throw SQLite::Exception in case of error + */ +void Database::setBusyTimeout(int aTimeoutMs) noexcept // nothrow +{ + const int ret = sqlite3_busy_timeout(mpSQLite, aTimeoutMs); + check(ret); +} + // Shortcut to execute one or multiple SQL statements without results (UPDATE, INSERT, ALTER, COMMIT, CREATE...). int Database::exec(const char* apQueries) { - int ret = sqlite3_exec(mpSQLite, apQueries, NULL, NULL, NULL); + const int ret = sqlite3_exec(mpSQLite, apQueries, NULL, NULL, NULL); check(ret); // Return the number of rows modified by those SQL statements (INSERT, UPDATE or DELETE only) @@ -90,7 +109,7 @@ bool Database::tableExists(const char* apTableName) Statement query(*this, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?"); query.bind(1, apTableName); (void)query.executeStep(); // Cannot return false, as the above query always return a result - int Nb = query.getColumn(0); + const int Nb = query.getColumn(0); return (1 == Nb); } @@ -119,8 +138,8 @@ void Database::createFunction(const char* apFuncName, if (abDeterministic) { TextRep = TextRep|SQLITE_DETERMINISTIC; } - int ret = sqlite3_create_function_v2(mpSQLite, apFuncName, aNbArg, TextRep, - apApp, apFunc, apStep, apFinal, apDestroy); + const int ret = sqlite3_create_function_v2(mpSQLite, apFuncName, aNbArg, TextRep, + apApp, apFunc, apStep, apFinal, apDestroy); check(ret); } diff --git a/tests/Database_test.cpp b/tests/Database_test.cpp index 43e3e1d..edb9235 100644 --- a/tests/Database_test.cpp +++ b/tests/Database_test.cpp @@ -54,125 +54,145 @@ TEST(Database, ctorExecCreateDropExist) { remove("test.db3"); } -TEST(Database, exec) { - remove("test.db3"); +TEST(Database, inMemory) { { // Create a new database - SQLite::Database db("test.db3", SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE); - - // 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.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.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(2, db.getLastInsertRowid()); - EXPECT_EQ(2, db.getTotalChanges()); - - // third row : insert the "third" text value into new row of id 3 - EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"third\")")); - 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(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(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"); + SQLite::Database db(":memory:", SQLITE_OPEN_READWRITE); 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()); + 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 +} - // 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(2, db.getLastInsertRowid()); - EXPECT_EQ(7, db.getTotalChanges()); +TEST(Database, busyTimeout) { + // Create a new database + 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 null + db.setBusyTimeout(0); + EXPECT_EQ(0, db.execAndGet("PRAGMA busy_timeout").getInt()); +} + +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.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.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(2, db.getLastInsertRowid()); + EXPECT_EQ(2, db.getTotalChanges()); + + // third row : insert the "third" text value into new row of id 3 + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"third\")")); + 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(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(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(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(4, db.getLastInsertRowid()); - EXPECT_EQ(9, db.getTotalChanges()); + // 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(4, db.getLastInsertRowid()); + EXPECT_EQ(9, db.getTotalChanges()); #endif - // Add a row with too many values (more than rows in the table) - EXPECT_THROW(db.exec("INSERT INTO test VALUES (NULL, \"first\", 123, 0.123)"), SQLite::Exception); - - } // Close DB test.db3 - remove("test.db3"); + // Add a row with too many values (more than rows in the table) + EXPECT_THROW(db.exec("INSERT INTO test VALUES (NULL, \"first\", 123, 0.123)"), SQLite::Exception); } TEST(Database, execAndGet) { - remove("test.db3"); - { - // Create a new database - SQLite::Database db("test.db3", SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE); + // 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)"); + // 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)")); + // 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")); - EXPECT_EQ(3, (int)db.execAndGet("SELECT weight FROM test WHERE value=\"first\"")); - } // Close DB test.db3 - remove("test.db3"); + // 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")); + EXPECT_EQ(3, db.execAndGet("SELECT weight FROM test WHERE value=\"first\"").getInt()); } TEST(Database, execException) { - remove("test.db3"); - { - // Create a new database - SQLite::Database db("test.db3", SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE); - EXPECT_EQ(SQLITE_OK, db.getErrorCode()); - EXPECT_EQ(SQLITE_OK, db.getExtendedErrorCode()); + // 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()); + // 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()); - // 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()); + // 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()); - // 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()); + // 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()); - // exception with SQL error: "No row to get a column from" - EXPECT_THROW(db.execAndGet("SELECT weight FROM test WHERE value=\"first\""), SQLite::Exception); + // 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); - } // Close DB test.db3 - remove("test.db3"); + 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); }