diff --git a/CHANGELOG.txt b/CHANGELOG.txt index fc69057..e62d082 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -34,13 +34,30 @@ Version 0.5.1 - April 7 2013 Version 0.6.0 - November 22 2013 Renamed Column::getName() to Column::getOriginName() - Added a new Column::getName() + Added Column::getName() Version 0.7.0 - January 9 2014 - Added a new Database::createFunction() API + Added Database::createFunction() Added std::string version of existing APIs Improved CMake with more build options and Doxygen auto-detection -Version 0.8.0 - Februrary 26 2014 +Version 0.8.0 - February 26 2014 + Database constructor support opening a database with a custom VFS (default to NULL) Changed Column::getText() to return empty string "" by default instead of NULL pointer (to handle std::string conversion) +Version 1.0.0 - May 3 2015 + Public headers file moved to include/ dir + Added support to biicode in CMakeLists.txt + Added Unit Tests + Added aBusyTimeoutMs parameter to Database() constructors + Added Database::getTotalChanges() + Added Database::getErrorCode() + Added Statement::clearBindings() + Added Statement::getColumn(aName) + Added Statement::getErrorCode() + Added Statement::getColumnName(aIndex) + Added Statement::getColumnOriginName(aIndex) + +Version 1.1.0 - May 2015 ? + Fix valgrind error on Database destructor + Added Database::loadExtension \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index e907d84..97ea398 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,47 +1,31 @@ -if (BIICODE) - - # biicode doesn't process files bigger than 5Mb - list(APPEND BII_LIB_SRC sqlite3/sqlite3.c) - # Include base block dir - ADD_BIICODE_TARGETS() - - # Link target with dl for linux - if (UNIX) - TARGET_LINK_LIBRARIES(${BII_BLOCK_TARGET} INTERFACE pthread) - if(NOT APPLE) - TARGET_LINK_LIBRARIES(${BII_BLOCK_TARGET} INTERFACE dl) - endif() - endif() -else (BIICODE) - # Main CMake file for compiling the library itself, examples and tests. # # Copyright (c) 2012-2015 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) +if (NOT BIICODE) + cmake_minimum_required(VERSION 2.6) project(SQLiteCpp) -option(SQLITE_ENABLE_COLUMN_METADATA "Enable Column::getName(). Require support from sqlite3 library." OFF) -if (SQLITE_ENABLE_COLUMN_METADATA) - # Enable the use of SQLite column metadata and Column::getName() method, - # Require that the sqlite3 library is also compiled with this flag (default under Debian/Ubuntu, but not on Mac OS X). - add_definitions(-DSQLITE_ENABLE_COLUMN_METADATA) -endif (SQLITE_ENABLE_COLUMN_METADATA) - -option(SQLITE_ENABLE_ASSERT_HANDLER "Enable the user defintion of a assertion_failed() handler." OFF) -if (SQLITE_ENABLE_ASSERT_HANDLER) - # Enable the user defintion of a assertion_failed() handler (default to false, easier to handler for begginers). - add_definitions(-DSQLITECPP_ENABLE_ASSERT_HANDLER) -endif (SQLITE_ENABLE_ASSERT_HANDLER) - # Define useful variables to handle OS differences: if (WIN32) set(DEV_NULL "NUL") -else (WIN32) + # build the SQLite3 C library for Windows (for ease of use) + set(SQLITECPP_INTERNAL_SQLITE_DEFAULT ON) + set(SQLITE_ENABLE_COLUMN_METADATA_DEFAULT ON) +else (WIN32) # UNIX set(DEV_NULL "/dev/null") + # do not build the SQLite3 C library, but uses the Linux/Mac OS X sqlite3-dev package + set(SQLITECPP_INTERNAL_SQLITE_DEFAULT OFF) + if (APPLE) + set(SQLITE_ENABLE_COLUMN_METADATA_DEFAULT OFF) + else (APPLE) + set(SQLITE_ENABLE_COLUMN_METADATA_DEFAULT ON) + endif (APPLE) endif (WIN32) + # then Compiler/IDE differences: if (MSVC) set(CPPLINT_ARG_OUTPUT "--output=vs7") @@ -69,6 +53,22 @@ set(CPPLINT_ARG_VERBOSE "--verbose=3") set(CPPLINT_ARG_LINELENGTH "--linelength=120") +# Options relative to SQLite and SQLiteC++ functions + +option(SQLITE_ENABLE_COLUMN_METADATA "Enable Column::getName(). Require support from sqlite3 library." ${SQLITE_ENABLE_COLUMN_METADATA_DEFAULT}) +if (SQLITE_ENABLE_COLUMN_METADATA) + # Enable the use of SQLite column metadata and Column::getName() method, + # Require that the sqlite3 library is also compiled with this flag (default under Debian/Ubuntu, but not on Mac OS X). + add_definitions(-DSQLITE_ENABLE_COLUMN_METADATA) +endif (SQLITE_ENABLE_COLUMN_METADATA) + +option(SQLITE_ENABLE_ASSERT_HANDLER "Enable the user defintion of a assertion_failed() handler." OFF) +if (SQLITE_ENABLE_ASSERT_HANDLER) + # Enable the user defintion of a assertion_failed() handler (default to false, easier to handler for begginers). + add_definitions(-DSQLITECPP_ENABLE_ASSERT_HANDLER) +endif (SQLITE_ENABLE_ASSERT_HANDLER) + + ## Core source code ## # adding a new file require explicittly modifing the CMakeLists.txt @@ -97,6 +97,7 @@ source_group(inc FILES ${SQLITECPP_INC}) # list of test files of the library set(SQLITECPP_TESTS + tests/Column_test.cpp tests/Database_test.cpp tests/Statement_test.cpp ) @@ -141,30 +142,29 @@ if (UNIX AND (CMAKE_COMPILER_IS_GNUCXX OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "Cla endif (UNIX AND (CMAKE_COMPILER_IS_GNUCXX OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")) -# SQLite3 library (Windows only) +# SQLite3 library (mostly usefull under Windows) -option (SQLITECPP_INTERNAL_SQLITE "Add the internal SQLite3 source to the project." ON) -if (WIN32) - if (SQLITECPP_INTERNAL_SQLITE) - # build the SQLite3 C library for Windows (for ease of use) versus Linux sqlite3-dev package - add_subdirectory(sqlite3) - include_directories("${PROJECT_SOURCE_DIR}/sqlite3") - endif (SQLITECPP_INTERNAL_SQLITE) -endif (WIN32) +option(SQLITECPP_INTERNAL_SQLITE "Add the internal SQLite3 source to the project." ${SQLITECPP_INTERNAL_SQLITE_DEFAULT}) +if (SQLITECPP_INTERNAL_SQLITE) + # build the SQLite3 C library for Windows (for ease of use) versus Linux sqlite3-dev package + add_subdirectory(sqlite3) + include_directories("${PROJECT_SOURCE_DIR}/sqlite3") +endif (SQLITECPP_INTERNAL_SQLITE) # Optional additional targets: option(SQLITECPP_RUN_CPPLINT "Run cpplint.py tool for Google C++ StyleGuide." ON) if (SQLITECPP_RUN_CPPLINT) - find_package(PythonLibs) - if (PYTHONLIBS_FOUND) + find_package(PythonInterp) + if (PYTHONINTERP_FOUND) # add a cpplint target to the "all" target add_custom_target(SQLiteCpp_cpplint ALL - COMMAND python ${PROJECT_SOURCE_DIR}/cpplint.py ${CPPLINT_ARG_OUTPUT} ${CPPLINT_ARG_VERBOSE} ${CPPLINT_ARG_LINELENGTH} ${SQLITECPP_SRC} ${SQLITECPP_INC} + COMMAND ${PYTHON_EXECUTABLE} ${PROJECT_SOURCE_DIR}/cpplint.py ${CPPLINT_ARG_OUTPUT} ${CPPLINT_ARG_VERBOSE} ${CPPLINT_ARG_LINELENGTH} ${SQLITECPP_SRC} ${SQLITECPP_INC} ) - endif (PYTHONLIBS_FOUND) + message(STATUS "PYTHON_EXECUTABLE=${PYTHON_EXECUTABLE}") + endif (PYTHONINTERP_FOUND) else (SQLITECPP_RUN_CPPLINT) message(STATUS "SQLITECPP_RUN_CPPLINT OFF") endif (SQLITECPP_RUN_CPPLINT) @@ -205,9 +205,9 @@ if (SQLITECPP_BUILD_EXAMPLES) # add the basic example executable add_executable(SQLiteCpp_example1 ${SQLITECPP_EXAMPLES}) target_link_libraries(SQLiteCpp_example1 SQLiteCpp sqlite3) -else(SQLITECPP_BUILD_EXAMPLES) +else (SQLITECPP_BUILD_EXAMPLES) message(STATUS "SQLITECPP_BUILD_EXAMPLES OFF") -endif(SQLITECPP_BUILD_EXAMPLES) +endif (SQLITECPP_BUILD_EXAMPLES) option(SQLITECPP_BUILD_TESTS "Build and run tests." OFF) if (SQLITECPP_BUILD_TESTS) @@ -232,9 +232,25 @@ if (SQLITECPP_BUILD_TESTS) if (SQLITECPP_BUILD_EXAMPLES) # does the example1 runs successfully? add_test(Example1Run SQLiteCpp_example1) - endif(SQLITECPP_BUILD_EXAMPLES) + endif (SQLITECPP_BUILD_EXAMPLES) else (SQLITECPP_BUILD_TESTS) message(STATUS "SQLITECPP_BUILD_TESTS OFF") endif (SQLITECPP_BUILD_TESTS) -endif (BIICODE) + +else (NOT BIICODE) + + # biicode doesn't process files bigger than 5Mb + list(APPEND BII_LIB_SRC sqlite3/sqlite3.c) + # Include base block dir + ADD_BIICODE_TARGETS() + + # Link target with dl for linux + if (UNIX) + target_link_libraries(${BII_BLOCK_TARGET} INTERFACE pthread) + if (NOT APPLE) + target_link_libraries(${BII_BLOCK_TARGET} INTERFACE dl) + endif () + endif () + +endif (NOT BIICODE) diff --git a/README.md b/README.md index 4564b1b..b1bd492 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ SQLiteC++ --------- +[![release](https://img.shields.io/github/release/SRombauts/SQLiteCpp.svg?style=flat-square)](https://github.com/SRombauts/SQLiteCpp/releases) [![Travis CI Linux Build Status](https://travis-ci.org/SRombauts/SQLiteCpp.svg)](https://travis-ci.org/SRombauts/SQLiteCpp "Travis CI Linux Build Status") [![AppVeyor Windows Build status](https://ci.appveyor.com/api/projects/status/github/SRombauts/SQLiteCpp?svg=true)](https://ci.appveyor.com/project/SbastienRombauts/SQLiteCpp "AppVeyor Windows Build status") -[![Build Status](https://webapi.biicode.com/v1/badges/sqlite/sqlite/sqlite/master)](https://www.biicode.com/sqlite/sqlite) +[![Biicode Build Status](https://webapi.biicode.com/v1/badges/sqlite/sqlite/sqlite/master)](https://www.biicode.com/sqlite/sqlite) SQLiteC++ (SQLiteCpp) is a smart and easy to use C++ SQLite3 wrapper. diff --git a/TODO.txt b/TODO.txt index 0bbd02a..80640cd 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,28 +1,34 @@ -Add a full googletest suite - -Add a Tutorial: for SQLite newbies -Create Github Wiki pages with the README.md and FAQ.txt: Installation, Examples, Tutorial, How to contribute - -Publish a versionned ZIP file in Google Project Mirror +Add a Tutorial for SQLite newbies +Add a real example (rework current example?) +Improve Github Wiki pages with the FAQ: Installation, Examples, Tutorial, How to contribute Publish the Doxygen Documentation in the Github Pages (gh-pages branch) -Missing features in v0.9.9: -- getColumnByName() (issue #23) ? std::map getRow() ? +Missing features in v1.1.0: - bind a SQLITE_STATIC value (string/blob) - bind a dynamic value with zerocopy (unlike SQLITE_TRANSIENT) with custom deleter +- #24: executemany() like in Python https://docs.python.org/2/library/sqlite3.html#sqlite3.Connection.executemany +- #34: Better type for getColumn -Missing documentation in v0.9.9: +Missing documentation in v1.1.0: - explain the noncopyable property for RAII design -- comment on returning error code instead of exception that shall not be thrown when exepected (!?) +- comment on returning error code instead of exception that shall not be thrown when expected (!?) + +Missing unit tests in v1.0.0: +- Binding variants +- Create Function +- Assert Handler +- Load Extension (not practicable, and easy to verify by code review) Advanced missing features: +- #39: SAVEPOINT https://www.sqlite.org/lang_savepoint.html + - backup support to/from file/:memory: -- Function ? +- Add optional usage of experimental sqlite3_trace() function to enable statistics - Agregate ? + - support for different transaction mode ? NO: too specific - operator<< binding ? NO: redundant with bind() - ATTACH Database ? NO: can already be done by "ATTACH" Statement -- Add optional usage of experimental sqlite3_trace() function to enable statistics -Post an article to CodeProject: Is there a license issue ? \ No newline at end of file +Post an article to CodeProject: Is there a license issue ? diff --git a/cpplint.py b/cpplint.py index e718083..6f4e751 100755 --- a/cpplint.py +++ b/cpplint.py @@ -4784,7 +4784,8 @@ def main(): ProcessFile(filename, _cpplint_state.verbose_level) _cpplint_state.PrintErrorCounts() - sys.exit(_cpplint_state.error_count > 0) + # SRombauts: do not break build for cpplint style warnings + #sys.exit(_cpplint_state.error_count > 0) if __name__ == '__main__': diff --git a/examples/example1/main.cpp b/examples/example1/main.cpp index c475319..9a37f89 100644 --- a/examples/example1/main.cpp +++ b/examples/example1/main.cpp @@ -2,9 +2,9 @@ * @file main.cpp * @brief A few short examples in a row. * - * Demonstrate how-to use the SQLite++ wrapper + * Demonstrates how-to use the SQLite++ wrapper * - * Copyright (c) 2012-2014 Sebastien Rombauts (sebastien.rombauts@gmail.com) + * Copyright (c) 2012-2015 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) @@ -83,7 +83,11 @@ private: int main () { - // Basic example (1/6) : + std::cout << "SQlite3 version " << SQLITE_VERSION << std::endl; + std::cout << "SQliteC++ version " << SQLITECPP_VERSION << std::endl; + + //////////////////////////////////////////////////////////////////////////// + // Very basic first example (1/7) : try { // Open a database file in readonly mode @@ -91,12 +95,28 @@ int main () std::cout << "SQLite database file '" << db.getFilename().c_str() << "' opened successfully\n"; // Test if the 'test' table exists - bool bExists = db.tableExists("test"); + const bool bExists = db.tableExists("test"); std::cout << "SQLite table 'test' exists=" << bExists << "\n"; // Get a single value result with an easy to use shortcut - std::string value = db.execAndGet("SELECT value FROM test WHERE id=2"); + const std::string value = db.execAndGet("SELECT value FROM test WHERE id=2"); std::cout << "execAndGet=" << value.c_str() << std::endl; + } + catch (std::exception& e) + { + std::cout << "SQLite exception: " << e.what() << std::endl; + return EXIT_FAILURE; // unexpected error : exit the example program + } + + //////////////////////////////////////////////////////////////////////////// + // Simple select query - few variations (2/7) : + try + { + // Open a database file in readonly mode + SQLite::Database db(filename_example_db3); // SQLITE_OPEN_READONLY + std::cout << "SQLite database file '" << db.getFilename().c_str() << "' opened successfully\n"; + + ///// a) Loop to get values of column by index, using auto cast to variable type // Compile a SQL query, containing one parameter (index 1) SQLite::Statement query(db, "SELECT id as test_id, value as test_val, weight as test_weight FROM test WHERE weight > ?"); @@ -108,46 +128,77 @@ int main () // Loop to execute the query step by step, to get one a row of results at a time while (query.executeStep()) { - // Demonstrate how to get some typed column value (and the equivalent explicit call) - int id = query.getColumn(0); // = query.getColumn(0).getInt() - //const char* pvalue = query.getColumn(1); // = query.getColumn(1).getText() - std::string value2 = query.getColumn(1); // = query.getColumn(1).getText() - int bytes = query.getColumn(1).getBytes(); - double weight = query.getColumn(2); // = query.getColumn(2).getInt() - - static bool bFirst = true; - if (bFirst) - { - // Show how to get the aliased names of the result columns. - std::string name0 = query.getColumn(0).getName(); - std::string name1 = query.getColumn(1).getName(); - std::string name2 = query.getColumn(2).getName(); - std::cout << "aliased result [\"" << name0.c_str() << "\", \"" << name1.c_str() << "\", \"" << name2.c_str() << "\"]\n"; -#ifdef SQLITE_ENABLE_COLUMN_METADATA - // Show how to get origin names of the table columns from which theses result columns come from. - // Requires the SQLITE_ENABLE_COLUMN_METADATA preprocessor macro to be - // also defined at compile times of the SQLite library itself. - name0 = query.getColumn(0).getOriginName(); - name1 = query.getColumn(1).getOriginName(); - name2 = query.getColumn(2).getOriginName(); - std::cout << "origin table 'test' [\"" << name0.c_str() << "\", \"" << name1.c_str() << "\", \"" << name2.c_str() << "\"]\n"; -#endif - bFirst = false; - } - std::cout << "row (" << id << ", \"" << value2.c_str() << "\" " << bytes << " bytes, " << weight << ")\n"; + // Demonstrates how to get some typed column value (and the equivalent explicit call) + const int id = query.getColumn(0); // = query.getColumn(0).getInt(); + //const char* pvalue = query.getColumn(1); // = query.getColumn(1).getText(); + const std::string value = query.getColumn(1); // = query.getColumn(1).getText(); + const int bytes = query.getColumn(1).size(); // .getColumn(1).getBytes(); + const double weight = query.getColumn(2); // = query.getColumn(2).getInt(); + std::cout << "row (" << id << ", \"" << value.c_str() << "\"(" << bytes << ") " << weight << ")\n"; } + ///// b) Get aliased column names (and original column names if possible) + + // Reset the query to use it again + query.reset(); + std::cout << "SQLite statement '" << query.getQuery().c_str() << "' reseted (" << query.getColumnCount() << " columns in the result)\n"; + + // Show how to get the aliased names of the result columns. + const std::string name0 = query.getColumnName(0); + const std::string name1 = query.getColumnName(1); + const std::string name2 = query.getColumnName(2); + std::cout << "aliased result [\"" << name0.c_str() << "\", \"" << name1.c_str() << "\", \"" << name2.c_str() << "\"]\n"; + +#ifdef SQLITE_ENABLE_COLUMN_METADATA + // Show how to get origin names of the table columns from which theses result columns come from. + // Requires the SQLITE_ENABLE_COLUMN_METADATA preprocessor macro to be + // also defined at compile times of the SQLite library itself. + const std::string oname0 = query.getColumnOriginName(0); + const std::string oname1 = query.getColumnOriginName(1); + const std::string oname2 = query.getColumnOriginName(2); + std::cout << "origin table 'test' [\"" << oname0.c_str() << "\", \"" << oname1.c_str() << "\", \"" << oname2.c_str() << "\"]\n"; +#endif + // Loop to execute the query step by step, to get one a row of results at a time + while (query.executeStep()) + { + // Demonstrates that inserting column value in a std:ostream is natural + std::cout << "row (" << query.getColumn(0) << ", \"" << query.getColumn(1) << "\", " << query.getColumn(2) << ")\n"; + } + + ///// c) Get columns by name + + // Reset the query to use it again + query.reset(); + std::cout << "SQLite statement '" << query.getQuery().c_str() << "' reseted (" << query.getColumnCount() << " columns in the result)\n"; + + // Loop to execute the query step by step, to get one a row of results at a time + while (query.executeStep()) + { + // Demonstrates how to get column value by aliased name (not the original table names, see above) + const int id = query.getColumn("test_id"); + const std::string value = query.getColumn("test_val"); + const double weight = query.getColumn("test_weight"); + std::cout << "row (" << id << ", \"" << value.c_str() << "\" " << weight << ")\n"; + } + + ///// d) Uses explicit typed getters instead of auto cast operators + // Reset the query to use it again query.reset(); std::cout << "SQLite statement '" << query.getQuery().c_str() << "' reseted (" << query.getColumnCount () << " columns in the result)\n"; // Bind the string value "6" to the first parameter of the SQL query query.bind(1, "6"); std::cout << "binded with string value \"6\" :\n"; - + // Reuses variables: uses assignement operator in the loop instead of constructor with initialization + int id = 0; + std::string value; + double weight = 0.0; while (query.executeStep()) { - // Demonstrate that inserting column value in a std:ostream is natural - std::cout << "row (" << query.getColumn(0) << ", \"" << query.getColumn(1) << "\", " << query.getColumn(2) << ")\n"; + id = query.getColumn(0).getInt(); + value = query.getColumn(1).getText(); + weight = query.getColumn(2).getInt(); + std::cout << "row (" << id << ", \"" << value << "\", " << weight << ")\n"; } } catch (std::exception& e) @@ -157,13 +208,13 @@ int main () } //////////////////////////////////////////////////////////////////////////// - // Object Oriented Basic example (2/6) : + // Object Oriented Basic example (3/7) : try { // Open the database and compile the query Example example; - // Demonstrate the way to use the same query with different parameter values + // Demonstrates the way to use the same query with different parameter values example.ListGreaterThan(8); example.ListGreaterThan(6); example.ListGreaterThan(2); @@ -174,7 +225,7 @@ int main () return EXIT_FAILURE; // unexpected error : exit the example program } - // The execAndGet wrapper example (3/6) : + // The execAndGet wrapper example (4/7) : try { // Open a database file in readonly mode @@ -194,7 +245,7 @@ int main () } //////////////////////////////////////////////////////////////////////////// - // Simple batch queries example (4/6) : + // Simple batch queries example (5/7) : try { // Open a database file in create/write mode @@ -235,7 +286,7 @@ int main () remove("test.db3"); //////////////////////////////////////////////////////////////////////////// - // RAII transaction example (5/6) : + // RAII transaction example (6/7) : try { // Open a database file in create/write mode @@ -303,7 +354,7 @@ int main () remove("transaction.db3"); //////////////////////////////////////////////////////////////////////////// - // Binary blob and in-memory database example (6/6) : + // Binary blob and in-memory database example (7/7) : try { // Open a database file in create/write mode diff --git a/include/SQLiteCpp/Assertion.h b/include/SQLiteCpp/Assertion.h index ae611ab..2486e9e 100644 --- a/include/SQLiteCpp/Assertion.h +++ b/include/SQLiteCpp/Assertion.h @@ -40,7 +40,7 @@ namespace SQLite #else // if no assert handler provided by user code, use standard assert() -// (note: in debug mode, assert() does nothing) +// (note: in release mode assert() does nothing) #define SQLITECPP_ASSERT(expression, message) assert(expression && message) #endif diff --git a/include/SQLiteCpp/Column.h b/include/SQLiteCpp/Column.h index 34ed4d1..591419c 100644 --- a/include/SQLiteCpp/Column.h +++ b/include/SQLiteCpp/Column.h @@ -3,7 +3,7 @@ * @ingroup SQLiteCpp * @brief Encapsulation of a Column in a row of the result pointed by the prepared SQLite::Statement. * - * Copyright (c) 2012-2014 Sebastien Rombauts (sebastien.rombauts@gmail.com) + * Copyright (c) 2012-2015 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) @@ -62,11 +62,13 @@ public: // they copy the Statement::Ptr which in turn increments the reference counter. /** - * @brief Return a pointer to the named assigned to a result column (potentially aliased) + * @brief Return a pointer to the named assigned to this result column (potentially aliased) + * + * @see getOriginName() to get original column name (not aliased) */ const char* getName() const noexcept; // nothrow - #ifdef SQLITE_ENABLE_COLUMN_METADATA +#ifdef SQLITE_ENABLE_COLUMN_METADATA /** * @brief Return a pointer to the table column name that is the origin of this result column * @@ -184,8 +186,9 @@ public: { return getBlob(); } + #ifdef __GNUC__ - // NOTE : the following is required by GCC to cast a Column result in a std::string + // NOTE : the following is required by GCC and Clang to cast a Column result in a std::string // (error: conversion from ‘SQLite::Column’ to non-scalar type ‘std::string {aka std::basic_string}’) // but is not working under Microsoft Visual Studio 2010 and 2012 // (error C2440: 'initializing' : cannot convert from 'SQLite::Column' to 'std::basic_string<_Elem,_Traits,_Ax>' @@ -196,6 +199,16 @@ public: return getText(); } #endif + // NOTE : the following is required by GCC and Clang to cast a Column result in a long/int64_t + /// @brief Inline cast operator to long as 64bits integer + inline operator long() const + { +#ifdef __x86_64__ + return getInt64(); +#else + return getInt(); +#endif + } /// @brief Return UTF-8 encoded English language explanation of the most recent error. inline const char* errmsg() const diff --git a/include/SQLiteCpp/Database.h b/include/SQLiteCpp/Database.h index 7cc44a8..e31f545 100644 --- a/include/SQLiteCpp/Database.h +++ b/include/SQLiteCpp/Database.h @@ -53,13 +53,17 @@ public: * * Exception is thrown in case of error, then the Database object is NOT constructed. * - * @param[in] apFilename UTF-8 path/uri to the database file ("filename" sqlite3 parameter) - * @param[in] aFlags SQLITE_OPEN_READONLY/SQLITE_OPEN_READWRITE/SQLITE_OPEN_CREATE... - * @param[in] apVfs UTF-8 name of custom VFS to use, or nullptr for sqlite3 default + * @param[in] apFilename UTF-8 path/uri to the database file ("filename" sqlite3 parameter) + * @param[in] aFlags SQLITE_OPEN_READONLY/SQLITE_OPEN_READWRITE/SQLITE_OPEN_CREATE... + * @param[in] aBusyTimeoutMs Amount of milliseconds to wait before returning SQLITE_BUSY (see setBusyTimeout()) + * @param[in] apVfs UTF-8 name of custom VFS to use, or nullptr for sqlite3 default * * @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 int aBusyTimeoutMs = 0, + const char* apVfs = NULL); /** * @brief Open the provided database UTF-8 filename. @@ -71,13 +75,17 @@ public: * * Exception is thrown in case of error, then the Database object is NOT constructed. * - * @param[in] aFilename UTF-8 path/uri to the database file ("filename" sqlite3 parameter) - * @param[in] aFlags SQLITE_OPEN_READONLY/SQLITE_OPEN_READWRITE/SQLITE_OPEN_CREATE... - * @param[in] aVfs UTF-8 name of custom VFS to use, or empty string for sqlite3 default + * @param[in] aFilename UTF-8 path/uri to the database file ("filename" sqlite3 parameter) + * @param[in] aFlags SQLITE_OPEN_READONLY/SQLITE_OPEN_READWRITE/SQLITE_OPEN_CREATE... + * @param[in] aBusyTimeoutMs Amount of milliseconds to wait before returning SQLITE_BUSY (see setBusyTimeout()) + * @param[in] aVfs UTF-8 name of custom VFS to use, or empty string for sqlite3 default * * @throw SQLite::Exception in case of error */ - Database(const std::string& aFilename, const int aFlags = SQLITE_OPEN_READONLY, const std::string& aVfs = ""); + Database(const std::string& aFilename, + const int aFlags = SQLITE_OPEN_READONLY, + const int aBusyTimeoutMs = 0, + const std::string& aVfs = ""); /** * @brief Close the SQLite database connection. @@ -89,6 +97,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] aBusyTimeoutMs Amount of milliseconds to wait before returning SQLITE_BUSY + * + * @throw SQLite::Exception in case of error + */ + void setBusyTimeout(const int aBusyTimeoutMs) noexcept; // nothrow + /** * @brief Shortcut to execute one or multiple statements without results. * @@ -206,16 +229,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. * @@ -323,6 +336,25 @@ public: apApp, apFunc, apStep, apFinal, apDestroy); } + + /** + * @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. + * + * @see http://www.sqlite.org/c3ref/load_extension.html + * + * @note UTF-8 text encoding assumed. + * + * @param[in] apExtensionName Name of the shared library containing extension + * @param[in] apEntryPointName Name of the entry point (NULL to let sqlite work it out) + * + * @throw SQLite::Exception in case of error + */ + void loadExtension(const char* apExtensionName, + const char *apEntryPointName); + private: /// @{ Database must be non-copyable Database(const Database&); @@ -332,7 +364,13 @@ private: /** * @brief Check if aRet equal SQLITE_OK, else throw a SQLite::Exception with the SQLite error message */ - void check(const int aRet) const; + inline void check(const int aRet) const + { + if (SQLITE_OK != aRet) + { + throw SQLite::Exception(sqlite3_errmsg(mpSQLite)); + } + } private: sqlite3* mpSQLite; //!< Pointer to SQLite Database Connection Handle diff --git a/include/SQLiteCpp/SQLiteCpp.h b/include/SQLiteCpp/SQLiteCpp.h index 6228001..7bb1dea 100644 --- a/include/SQLiteCpp/SQLiteCpp.h +++ b/include/SQLiteCpp/SQLiteCpp.h @@ -5,7 +5,7 @@ * * Include this main header file in your project to gain access to all functionality provided by the wrapper. * - * Copyright (c) 2012-2014 Sebastien Rombauts (sebastien.rombauts@gmail.com) + * Copyright (c) 2012-2015 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) @@ -38,5 +38,5 @@ * with the value (X*1000000 + Y*1000 + Z) where X, Y, and Z are the same * numbers used in [SQLITECPP_VERSION]. */ -#define SQLITECPP_VERSION "0.9.9" -#define SQLITECPP_VERSION_NUMBER 0009009 +#define SQLITECPP_VERSION "1.1.0" +#define SQLITECPP_VERSION_NUMBER 1001000 diff --git a/include/SQLiteCpp/Statement.h b/include/SQLiteCpp/Statement.h index 521d38f..231c8e0 100644 --- a/include/SQLiteCpp/Statement.h +++ b/include/SQLiteCpp/Statement.h @@ -12,6 +12,7 @@ #include #include +#include #include @@ -78,8 +79,12 @@ public: /** * @brief Clears away all the bindings of a prepared statement. + * + * Contrary to the intuition of many, reset() does not reset + * the bindings on a prepared statement. + * Use this routine to reset all parameters to NULL. */ - void clearBindings(void); // throw(SQLite::Exception) + void clearBindings(); // throw(SQLite::Exception) //////////////////////////////////////////////////////////////////////////// // Bind a value to a parameter of the SQL statement, @@ -274,7 +279,8 @@ public: * Can be used to access the data of the current row of result when applicable, * while the executeStep() method returns true. * - * Throw an exception if there is no row to return a Column from : + * Throw an exception if there is no row to return a Column from: + * - if provided index is out of bound * - before any executeStep() call * - after the last executeStep() returned false * - after a reset() call @@ -283,8 +289,7 @@ public: * * @param[in] aIndex Index of the column, starting at 0 * - * @note This method is no more const, starting in v0.5, - * which reflects the fact that the returned Column object will + * @note This method is not const, reflecting the fact that the returned Column object will * share the ownership of the underlying sqlite3_stmt. * * @warning The resulting Column object must not be memorized "as-is". @@ -295,15 +300,68 @@ public: */ Column getColumn(const int aIndex); + /** + * @brief Return a copy of the column data specified by its column name (less efficient than using an index) + * + * Can be used to access the data of the current row of result when applicable, + * while the executeStep() method returns true. + * + * Throw an exception if there is no row to return a Column from : + * - if provided name is not one of the aliased column names + * - before any executeStep() call + * - after the last executeStep() returned false + * - after a reset() call + * + * Throw an exception if the specified name is not an on of the aliased name of the columns in the result. + * + * @param[in] apName Aliased name of the column, that is, the named specified in the query (not the original name) + * + * @note Uses a map of column names to indexes, build on first call. + * + * @note This method is not const, reflecting the fact that the returned Column object will + * share the ownership of the underlying sqlite3_stmt. + * + * @warning The resulting Column object must not be memorized "as-is". + * Is is only a wrapper around the current result row, so it is only valid + * while the row from the Statement remains valid, that is only until next executeStep() call. + * Thus, you should instead extract immediately its data (getInt(), getText()...) + * and use or copy this data for any later usage. + */ + Column getColumn(const char* apName); + /** * @brief Test if the column value is NULL * * @param[in] aIndex Index of the column, starting at 0 * * @return true if the column value is NULL + * + * Throw an exception if the specified index is out of the [0, getColumnCount()) range. */ bool isColumnNull(const int aIndex) const; + /** + * @brief Return a pointer to the named assigned to the specified result column (potentially aliased) + * + * @see getColumnOriginName() to get original column name (not aliased) + * + * Throw an exception if the specified index is out of the [0, getColumnCount()) range. + */ + const char* getColumnName(const int aIndex) const; + +#ifdef SQLITE_ENABLE_COLUMN_METADATA + /** + * @brief Return a pointer to the table column name that is the origin of the specified result column + * + * Require definition of the SQLITE_ENABLE_COLUMN_METADATA preprocessor macro : + * - when building the SQLite library itself (which is the case for the Debian libsqlite3 binary for instance), + * - and also when compiling this wrapper. + * + * Throw an exception if the specified index is out of the [0, getColumnCount()) range. + */ + const char* getColumnOriginName(const int aIndex) const; +#endif + //////////////////////////////////////////////////////////////////////////// /// @brief Return the UTF-8 SQL Query. @@ -395,12 +453,44 @@ private: * * @param[in] SQLite return code to test against the SQLITE_OK expected value */ - void check(const int aRet); + inline void check(const int aRet) const + { + if (SQLITE_OK != aRet) + { + throw SQLite::Exception(sqlite3_errmsg(mStmtPtr)); + } + } + + /** + * @brief Check if there is a row of result returnes by executeStep(), else throw a SQLite::Exception. + */ + inline void checkRow() const + { + if (false == mbOk) + { + throw SQLite::Exception("No row to get a column from. executeStep() was not called, or returned false."); + } + } + + /** + * @brief Check if there is a Column index is in the range of columns in the result. + */ + inline void checkIndex(const int aIndex) const + { + if ((aIndex < 0) || (aIndex >= mColumnCount)) + { + throw SQLite::Exception("Column index out of range."); + } + } + +private: + typedef std::map TColumnNames; private: std::string mQuery; //!< UTF-8 SQL Query Ptr mStmtPtr; //!< Shared Pointer to the prepared SQLite Statement Object int mColumnCount; //!< Number of columns in the result of the prepared statement + TColumnNames mColumnNames; //!< Map of columns index by name bool mbOk; //!< true when a row has been fetched with executeStep() bool mbDone; //!< true when the last executeStep() had no more row to fetch }; diff --git a/sqlite3/CMakeLists.txt b/sqlite3/CMakeLists.txt index d163f50..7d615be 100644 --- a/sqlite3/CMakeLists.txt +++ b/sqlite3/CMakeLists.txt @@ -1,6 +1,6 @@ # CMake file for compiling the sqlite3 static library under Windows (for ease of use) # -# Copyright (c) 2013-2014 Sebastien Rombauts (sebastien.rombauts@gmail.com) +# Copyright (c) 2013-2015 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) diff --git a/src/Column.cpp b/src/Column.cpp index a679d15..eb68e96 100644 --- a/src/Column.cpp +++ b/src/Column.cpp @@ -3,7 +3,7 @@ * @ingroup SQLiteCpp * @brief Encapsulation of a Column in a row of the result pointed by the prepared SQLite::Statement. * - * Copyright (c) 2012-2014 Sebastien Rombauts (sebastien.rombauts@gmail.com) + * Copyright (c) 2012-2015 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) @@ -31,14 +31,14 @@ Column::~Column() noexcept // nothrow } // Return the named assigned to this result column (potentially aliased) -const char * Column::getName() const noexcept // nothrow +const char* Column::getName() const noexcept // nothrow { return sqlite3_column_name(mStmtPtr, mIndex); } #ifdef SQLITE_ENABLE_COLUMN_METADATA // Return the name of the table column that is the origin of this result column -const char * Column::getOriginName() const noexcept // nothrow +const char* Column::getOriginName() const noexcept // nothrow { return sqlite3_column_origin_name(mStmtPtr, mIndex); } diff --git a/src/Database.cpp b/src/Database.cpp index 11fcc8d..b9440ac 100644 --- a/src/Database.cpp +++ b/src/Database.cpp @@ -26,45 +26,85 @@ namespace SQLite // Open the provided database UTF-8 filename with SQLITE_OPEN_xxx provided flags. -Database::Database(const char* apFilename, const int aFlags /*= SQLITE_OPEN_READONLY*/, const char* apVfs /*= NULL*/) : +Database::Database(const char* apFilename, + const int aFlags /* = SQLITE_OPEN_READONLY*/, + const int aBusyTimeoutMs /* = 0 */, + const char* apVfs /* = NULL*/) : 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); sqlite3_close(mpSQLite); // close is required even in case of error on opening throw SQLite::Exception(strerr); } + + if (aBusyTimeoutMs > 0) + { + setBusyTimeout(aBusyTimeoutMs); + } } // Open the provided database UTF-8 filename with SQLITE_OPEN_xxx provided flags. -Database::Database(const std::string& aFilename, const int aFlags /*= SQLITE_OPEN_READONLY*/, const std::string& aVfs) : +Database::Database(const std::string& aFilename, + const int aFlags /* = SQLITE_OPEN_READONLY*/, + const int aBusyTimeoutMs /* = 0 */, + const std::string& aVfs /* = "" */) : 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); sqlite3_close(mpSQLite); // close is required even in case of error on opening throw SQLite::Exception(strerr); } + + if (aBusyTimeoutMs > 0) + { + setBusyTimeout(aBusyTimeoutMs); + } } // Close the SQLite database connection. Database::~Database() noexcept // nothrow { - 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 + const int ret = sqlite3_close(mpSQLite); + + // Avoid unreferenced variable warning when build in release mode + (void) ret; + + // Only case of error is SQLITE_BUSY: "database is locked" (some statements are not finalized) + // Never throw an exception in a destructor : + SQLITECPP_ASSERT(SQLITE_OK == ret, "database is locked"); // 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] aBusyTimeoutMs Amount of milliseconds to wait before returning SQLITE_BUSY + * + * @throw SQLite::Exception in case of error + */ +void Database::setBusyTimeout(const int aBusyTimeoutMs) noexcept // nothrow +{ + const int ret = sqlite3_busy_timeout(mpSQLite, aBusyTimeoutMs); + 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,19 +130,10 @@ 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); } -// Check if aRet equal SQLITE_OK, else throw a SQLite::Exception with the SQLite error message -void Database::check(const int aRet) const -{ - if (SQLITE_OK != aRet) - { - throw SQLite::Exception(sqlite3_errmsg(mpSQLite)); - } -} - // Attach a custom function to your sqlite database. Assumes UTF8 text representation. // Parameter details can be found here: http://www.sqlite.org/c3ref/create_function.html void Database::createFunction(const char* apFuncName, @@ -119,11 +150,31 @@ 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); } +// Load an extension into the sqlite database. Only affects the current connection. +// Parameter details can be found here: http://www.sqlite.org/c3ref/load_extension.html +void Database::loadExtension(const char* apExtensionName, + const char *apEntryPointName) +{ +#ifdef SQLITE_OMIT_LOAD_EXTENSION +# + throw std::runtime_error("sqlite extensions are disabled"); +# +#else +# + int ret = sqlite3_enable_load_extension(mpSQLite, 1); + + check(ret); + + ret = sqlite3_load_extension(mpSQLite, apExtensionName, apEntryPointName, 0); + + check(ret); +# +#endif +} } // namespace SQLite diff --git a/src/Statement.cpp b/src/Statement.cpp index d606e4d..86e5919 100644 --- a/src/Statement.cpp +++ b/src/Statement.cpp @@ -54,67 +54,66 @@ void Statement::reset() { mbOk = false; mbDone = false; - int ret = sqlite3_reset(mStmtPtr); + const int ret = sqlite3_reset(mStmtPtr); check(ret); } // Clears away all the bindings of a prepared statement. -// "Contrary to the intuition of many, [sqlite3_reset()] does not reset -// ** the [sqlite3_bind_blob | bindings] on a [prepared statement]." -void Statement::clearBindings(void) +void Statement::clearBindings() { mbOk = false; mbDone = false; - int ret = sqlite3_clear_bindings(mStmtPtr); + const int ret = sqlite3_clear_bindings(mStmtPtr); check(ret); } // Bind an int value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const int aIndex, const int& aValue) { - int ret = sqlite3_bind_int(mStmtPtr, aIndex, aValue); + const int ret = sqlite3_bind_int(mStmtPtr, aIndex, aValue); check(ret); } // Bind a 64bits int value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const int aIndex, const sqlite3_int64& aValue) { - int ret = sqlite3_bind_int64(mStmtPtr, aIndex, aValue); + const int ret = sqlite3_bind_int64(mStmtPtr, aIndex, aValue); check(ret); } // Bind a double (64bits float) value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const int aIndex, const double& aValue) { - int ret = sqlite3_bind_double(mStmtPtr, aIndex, aValue); + const int ret = sqlite3_bind_double(mStmtPtr, aIndex, aValue); check(ret); } // Bind a string value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const int aIndex, const std::string& aValue) { - int ret = sqlite3_bind_text(mStmtPtr, aIndex, aValue.c_str(), static_cast(aValue.size()), SQLITE_TRANSIENT); + const int ret = sqlite3_bind_text(mStmtPtr, aIndex, aValue.c_str(), + static_cast(aValue.size()), SQLITE_TRANSIENT); check(ret); } // Bind a text value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const int aIndex, const char* apValue) { - int ret = sqlite3_bind_text(mStmtPtr, aIndex, apValue, -1, SQLITE_TRANSIENT); + const int ret = sqlite3_bind_text(mStmtPtr, aIndex, apValue, -1, SQLITE_TRANSIENT); check(ret); } // Bind a binary blob value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const int aIndex, const void* apValue, const int aSize) { - int ret = sqlite3_bind_blob(mStmtPtr, aIndex, apValue, aSize, SQLITE_TRANSIENT); + const int ret = sqlite3_bind_blob(mStmtPtr, aIndex, apValue, aSize, SQLITE_TRANSIENT); check(ret); } // Bind a NULL value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const int aIndex) { - int ret = sqlite3_bind_null(mStmtPtr, aIndex); + const int ret = sqlite3_bind_null(mStmtPtr, aIndex); check(ret); } @@ -122,56 +121,57 @@ void Statement::bind(const int aIndex) // Bind an int value to a parameter "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const char* apName, const int& aValue) { - int index = sqlite3_bind_parameter_index(mStmtPtr, apName); - int ret = sqlite3_bind_int(mStmtPtr, index, aValue); + const int index = sqlite3_bind_parameter_index(mStmtPtr, apName); + const int ret = sqlite3_bind_int(mStmtPtr, index, aValue); check(ret); } // Bind a 64bits int value to a parameter "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const char* apName, const sqlite3_int64& aValue) { - int index = sqlite3_bind_parameter_index(mStmtPtr, apName); - int ret = sqlite3_bind_int64(mStmtPtr, index, aValue); + const int index = sqlite3_bind_parameter_index(mStmtPtr, apName); + const int ret = sqlite3_bind_int64(mStmtPtr, index, aValue); check(ret); } // Bind a double (64bits float) value to a parameter "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const char* apName, const double& aValue) { - int index = sqlite3_bind_parameter_index(mStmtPtr, apName); - int ret = sqlite3_bind_double(mStmtPtr, index, aValue); + const int index = sqlite3_bind_parameter_index(mStmtPtr, apName); + const int ret = sqlite3_bind_double(mStmtPtr, index, aValue); check(ret); } // Bind a string value to a parameter "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const char* apName, const std::string& aValue) { - int index = sqlite3_bind_parameter_index(mStmtPtr, apName); - int ret = sqlite3_bind_text(mStmtPtr, index, aValue.c_str(), static_cast(aValue.size()), SQLITE_TRANSIENT); + const int index = sqlite3_bind_parameter_index(mStmtPtr, apName); + const int ret = sqlite3_bind_text(mStmtPtr, index, aValue.c_str(), + static_cast(aValue.size()), SQLITE_TRANSIENT); check(ret); } // Bind a text value to a parameter "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const char* apName, const char* apValue) { - int index = sqlite3_bind_parameter_index(mStmtPtr, apName); - int ret = sqlite3_bind_text(mStmtPtr, index, apValue, -1, SQLITE_TRANSIENT); + const int index = sqlite3_bind_parameter_index(mStmtPtr, apName); + const int ret = sqlite3_bind_text(mStmtPtr, index, apValue, -1, SQLITE_TRANSIENT); check(ret); } // Bind a binary blob value to a parameter "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const char* apName, const void* apValue, const int aSize) { - int index = sqlite3_bind_parameter_index(mStmtPtr, apName); - int ret = sqlite3_bind_blob(mStmtPtr, index, apValue, aSize, SQLITE_TRANSIENT); + const int index = sqlite3_bind_parameter_index(mStmtPtr, apName); + const int ret = sqlite3_bind_blob(mStmtPtr, index, apValue, aSize, SQLITE_TRANSIENT); check(ret); } // Bind a NULL value to a parameter "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement void Statement::bind(const char* apName) { - int index = sqlite3_bind_parameter_index(mStmtPtr, apName); - int ret = sqlite3_bind_null(mStmtPtr, index); + const int index = sqlite3_bind_parameter_index(mStmtPtr, apName); + const int ret = sqlite3_bind_null(mStmtPtr, index); check(ret); } @@ -181,7 +181,7 @@ bool Statement::executeStep() { if (false == mbDone) { - int ret = sqlite3_step(mStmtPtr); + const int ret = sqlite3_step(mStmtPtr); if (SQLITE_ROW == ret) // one row is ready : call getColumn(N) to access it { mbOk = true; @@ -200,7 +200,7 @@ bool Statement::executeStep() } else { - throw SQLite::Exception("Statement needs to be reset"); + throw SQLite::Exception("Statement needs to be reseted."); } return mbOk; // true only if one row is accessible by getColumn(N) @@ -211,7 +211,7 @@ int Statement::exec() { if (false == mbDone) { - int ret = sqlite3_step(mStmtPtr); + const int ret = sqlite3_step(mStmtPtr); if (SQLITE_DONE == ret) // the statement has finished executing successfully { mbOk = false; @@ -221,7 +221,7 @@ int Statement::exec() { mbOk = false; mbDone = false; - throw SQLite::Exception("exec() does not expect results"); + throw SQLite::Exception("exec() does not expect results. Use executeStep."); } else { @@ -232,7 +232,7 @@ int Statement::exec() } else { - throw SQLite::Exception("Statement need to be reseted"); + throw SQLite::Exception("Statement need to be reseted."); } // Return the number of rows modified by those SQL statements (INSERT, UPDATE or DELETE) @@ -243,43 +243,61 @@ int Statement::exec() // (use the Column copy-constructor) Column Statement::getColumn(const int aIndex) { - if (false == mbOk) - { - throw SQLite::Exception("No row to get a column from"); - } - else if ((aIndex < 0) || (aIndex >= mColumnCount)) - { - throw SQLite::Exception("Column index out of range"); - } + checkRow(); + checkIndex(aIndex); // Share the Statement Object handle with the new Column created return Column(mStmtPtr, aIndex); } +// Return a copy of the column data specified by its column name starting at 0 +// (use the Column copy-constructor) +Column Statement::getColumn(const char* apName) +{ + checkRow(); + + if (mColumnNames.empty()) + { + for (int i = 0; i < mColumnCount; ++i) + { + const char* pName = sqlite3_column_name(mStmtPtr, i); + mColumnNames[pName] = i; + } + } + + const TColumnNames::const_iterator iIndex = mColumnNames.find(apName); + if (iIndex == mColumnNames.end()) + { + throw SQLite::Exception("Unknown column name."); + } + + // Share the Statement Object handle with the new Column created + return Column(mStmtPtr, (*iIndex).second); +} + // Test if the column is NULL bool Statement::isColumnNull(const int aIndex) const { - if (false == mbOk) - { - throw SQLite::Exception("No row to get a column from"); - } - else if ((aIndex < 0) || (aIndex >= mColumnCount)) - { - throw SQLite::Exception("Column index out of range"); - } - + checkRow(); + checkIndex(aIndex); return (SQLITE_NULL == sqlite3_column_type(mStmtPtr, aIndex)); } -// Check if aRet equal SQLITE_OK, else throw a SQLite::Exception with the SQLite error message -void Statement::check(const int aRet) +// Return the named assigned to the specified result column (potentially aliased) +const char* Statement::getColumnName(const int aIndex) const { - if (SQLITE_OK != aRet) - { - throw SQLite::Exception(sqlite3_errmsg(mStmtPtr)); - } + checkIndex(aIndex); + return sqlite3_column_name(mStmtPtr, aIndex); } +#ifdef SQLITE_ENABLE_COLUMN_METADATA +// Return the named assigned to the specified result column (potentially aliased) +const char* Statement::getColumnOriginName(const int aIndex) const +{ + checkIndex(aIndex); + return sqlite3_column_origin_name(mStmtPtr, aIndex); +} +#endif //////////////////////////////////////////////////////////////////////////////// // Internal class : shared pointer to the sqlite3_stmt SQLite Statement Object @@ -296,7 +314,7 @@ Statement::Ptr::Ptr(sqlite3* apSQLite, std::string& aQuery) : mpStmt(NULL), mpRefCount(NULL) { - int ret = sqlite3_prepare_v2(apSQLite, aQuery.c_str(), static_cast(aQuery.size()), &mpStmt, NULL); + const int ret = sqlite3_prepare_v2(apSQLite, aQuery.c_str(), static_cast(aQuery.size()), &mpStmt, NULL); if (SQLITE_OK != ret) { throw SQLite::Exception(sqlite3_errmsg(mpSQLite)); diff --git a/src/Transaction.cpp b/src/Transaction.cpp index 5971d97..1b60aa5 100644 --- a/src/Transaction.cpp +++ b/src/Transaction.cpp @@ -38,6 +38,7 @@ Transaction::~Transaction() noexcept // nothrow catch (SQLite::Exception& e) { // Never throw an exception in a destructor + (void)e; // warning proof SQLITECPP_ASSERT(false, e.what()); // See SQLITECPP_ENABLE_ASSERT_HANDLER } } @@ -53,7 +54,7 @@ void Transaction::commit() } else { - throw SQLite::Exception("Transaction already commited"); + throw SQLite::Exception("Transaction already commited."); } } diff --git a/tests/Column_test.cpp b/tests/Column_test.cpp new file mode 100644 index 0000000..030449c --- /dev/null +++ b/tests/Column_test.cpp @@ -0,0 +1,169 @@ +/** + * @file Column_test.cpp + * @ingroup tests + * @brief Test of a SQLiteCpp Column. + * + * Copyright (c) 2012-2015 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 +#include + +#include + +#include +#include + + +TEST(Column, basis) { + // Create a new database + SQLite::Database db(":memory:", SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE); + EXPECT_EQ(SQLITE_OK, db.getErrorCode()); + EXPECT_EQ(SQLITE_OK, db.getExtendedErrorCode()); + + // Create a new table + EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, msg TEXT, int INTEGER, double REAL, binary BLOB, empty TEXT)")); + EXPECT_TRUE(db.tableExists("test")); + EXPECT_TRUE(db.tableExists(std::string("test"))); + EXPECT_EQ(0, db.getLastInsertRowid()); + + // Create a first row (autoid: 1) with all kind of data and a null value + SQLite::Statement insert(db, "INSERT INTO test VALUES (NULL, \"first\", 123, 0.123, ?, NULL)"); + // Bind the blob value to the first parameter of the SQL query + char buffer[] = "blob"; + void* blob = &buffer; + int size = sizeof(buffer); + insert.bind(1, blob, size); + // Execute the one-step query to insert the row + EXPECT_EQ(1, insert.exec()); + + EXPECT_EQ(1, db.getLastInsertRowid()); + EXPECT_EQ(1, db.getTotalChanges()); + + // Compile a SQL query + SQLite::Statement query(db, "SELECT * FROM test"); + EXPECT_STREQ("SELECT * FROM test", query.getQuery().c_str()); + EXPECT_EQ(6, query.getColumnCount ()); + query.executeStep(); + EXPECT_TRUE (query.isOk()); + EXPECT_FALSE(query.isDone()); + + // validates every variant of cast operators, and conversions of types + { + sqlite3_int64 id = query.getColumn(0); // operator sqlite3_int64() + int64_t id2 = query.getColumn(0); // operator sqlite3_int64() (or long() with GCC 64bits) + long id3 = query.getColumn(0); // operator sqlite3_int64() (or long() with GCC 64bits) + const char* ptxt = query.getColumn(1); // operator const char*() + const std::string msg = query.getColumn(1); // operator std::string() (or const char* with MSVC) + const int integer = query.getColumn(2); // operator int() + const double real = query.getColumn(3); // operator double() + const void* pblob = query.getColumn(4); // operator void*() + const void* pempty = query.getColumn(5); // operator void*() + EXPECT_EQ(1, id); + EXPECT_EQ(1, id2); + EXPECT_EQ(1, id3); + EXPECT_STREQ("first", ptxt); + EXPECT_EQ("first", msg); + EXPECT_EQ(123, integer); + EXPECT_EQ(0.123, real); + EXPECT_EQ(0, memcmp("blob", pblob, size)); + EXPECT_EQ(NULL, pempty); + } + + // validates every variant of explicit getters + { + int64_t id = query.getColumn(0).getInt64(); + const char* ptxt = query.getColumn(1).getText(); + const std::string msg = query.getColumn(1).getText(); + const int integer = query.getColumn(2).getInt(); + const double real = query.getColumn(3).getDouble(); + const void* pblob = query.getColumn(1).getBlob(); + EXPECT_EQ(1, id); + EXPECT_STREQ("first", ptxt); + EXPECT_EQ("first", msg); + EXPECT_EQ(123, integer); + EXPECT_EQ(0.123, real); + EXPECT_EQ(0, memcmp("first", pblob, 5)); + } + + // Validate getBytes(), getType(), isInteger(), isNull()... + EXPECT_EQ(SQLITE_INTEGER, query.getColumn(0).getType()); + EXPECT_EQ(true, query.getColumn(0).isInteger()); + EXPECT_EQ(false, query.getColumn(0).isFloat()); + EXPECT_EQ(false, query.getColumn(0).isText()); + EXPECT_EQ(false, query.getColumn(0).isBlob()); + EXPECT_EQ(false, query.getColumn(0).isNull()); + EXPECT_STREQ("1", query.getColumn(0).getText()); // convert to string + EXPECT_EQ(1, query.getColumn(0).getBytes()); // size of the string "1" without the null terminator + EXPECT_EQ(SQLITE_TEXT, query.getColumn(1).getType()); + EXPECT_EQ(false, query.getColumn(1).isInteger()); + EXPECT_EQ(false, query.getColumn(1).isFloat()); + EXPECT_EQ(true, query.getColumn(1).isText()); + EXPECT_EQ(false, query.getColumn(1).isBlob()); + EXPECT_EQ(false, query.getColumn(1).isNull()); + EXPECT_STREQ("first", query.getColumn(1).getText()); // convert to string + EXPECT_EQ(5, query.getColumn(1).getBytes()); // size of the string "first" + EXPECT_EQ(SQLITE_INTEGER, query.getColumn(2).getType()); + EXPECT_EQ(true, query.getColumn(2).isInteger()); + EXPECT_EQ(false, query.getColumn(2).isFloat()); + EXPECT_EQ(false, query.getColumn(2).isText()); + EXPECT_EQ(false, query.getColumn(2).isBlob()); + EXPECT_EQ(false, query.getColumn(2).isNull()); + EXPECT_STREQ("123", query.getColumn(2).getText()); // convert to string + EXPECT_EQ(3, query.getColumn(2).getBytes()); // size of the string "123" + EXPECT_EQ(SQLITE_FLOAT, query.getColumn(3).getType()); + EXPECT_EQ(false, query.getColumn(3).isInteger()); + EXPECT_EQ(true, query.getColumn(3).isFloat()); + EXPECT_EQ(false, query.getColumn(3).isText()); + EXPECT_EQ(false, query.getColumn(3).isBlob()); + EXPECT_EQ(false, query.getColumn(3).isNull()); + EXPECT_STREQ("0.123", query.getColumn(3).getText()); // convert to string + EXPECT_EQ(5, query.getColumn(3).getBytes()); // size of the string "0.123" + EXPECT_EQ(SQLITE_BLOB, query.getColumn(4).getType()); + EXPECT_EQ(false, query.getColumn(4).isInteger()); + EXPECT_EQ(false, query.getColumn(4).isFloat()); + EXPECT_EQ(false, query.getColumn(4).isText()); + EXPECT_EQ(true, query.getColumn(4).isBlob()); + EXPECT_EQ(false, query.getColumn(4).isNull()); + EXPECT_STREQ("blob", query.getColumn(4).getText()); // convert to string + EXPECT_EQ(5, query.getColumn(4).getBytes()); // size of the string "blob" WITH the null terminator (blob) + EXPECT_EQ(SQLITE_NULL, query.getColumn(5).getType()); + EXPECT_EQ(false, query.getColumn(5).isInteger()); + EXPECT_EQ(false, query.getColumn(5).isFloat()); + EXPECT_EQ(false, query.getColumn(5).isText()); + EXPECT_EQ(false, query.getColumn(5).isBlob()); + EXPECT_EQ(true, query.getColumn(5).isNull()); + EXPECT_STREQ("", query.getColumn(5).getText()); // convert to string + EXPECT_EQ(0, query.getColumn(5).getBytes()); // size of the string "" without the null terminator +} + +TEST(Column, getName) { + // Create a new database + SQLite::Database db(":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, msg TEXT)")); + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\")")); + + // Compile a SQL query, using the "id" column name as-is, but aliasing the "msg" column with new name "value" + SQLite::Statement query(db, "SELECT id, msg as value FROM test"); + query.executeStep(); + + // Show how to get the aliased names of the result columns. + const std::string name0 = query.getColumn(0).getName(); + const std::string name1 = query.getColumn(1).getName(); + EXPECT_EQ("id", name0); + EXPECT_EQ("value", name1); + +#ifdef SQLITE_ENABLE_COLUMN_METADATA + // Show how to get origin names of the table columns from which theses result columns come from. + // Requires the SQLITE_ENABLE_COLUMN_METADATA preprocessor macro to be + // also defined at compile times of the SQLite library itself. + const std::string oname0 = query.getColumn(0).getOriginName(); + const std::string oname1 = query.getColumn(1).getOriginName(); + EXPECT_EQ("id", oname0); + EXPECT_EQ("msg", oname1); +#endif +} \ No newline at end of file diff --git a/tests/Database_test.cpp b/tests/Database_test.cpp index c92f0fb..7441fc9 100644 --- a/tests/Database_test.cpp +++ b/tests/Database_test.cpp @@ -21,7 +21,7 @@ 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 test that this assertion callback get called + // TODO: unit test that this assertion callback get called (already tested manually) std::cout << "assertion_failed(" << apFile << ", " << apLine << ", " << apFunc << ", " << apExpr << ", " << apMsg << ")\n"; } } @@ -32,7 +32,8 @@ TEST(Database, ctorExecCreateDropExist) { remove("test.db3"); { // Try to open an unexisting database - EXPECT_THROW(SQLite::Database not_found("test.db3"), SQLite::Exception); + std::string filename = "test.db3"; + EXPECT_THROW(SQLite::Database not_found(filename), SQLite::Exception); // Create a new database SQLite::Database db("test.db3", SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE); @@ -54,122 +55,182 @@ TEST(Database, ctorExecCreateDropExist) { remove("test.db3"); } -TEST(Database, exec) { +TEST(Database, createCloseReopen) { remove("test.db3"); { + // Try to open the unexisting 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); - - // 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("test.db3", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); 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()); -#endif - + 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 +} + +#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()); + } +} +#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.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()); +#endif +} + 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); + + // 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()); } + +// TODO: test Database::createFunction() diff --git a/tests/Statement_test.cpp b/tests/Statement_test.cpp index 4a16dc1..c76dcc5 100644 --- a/tests/Statement_test.cpp +++ b/tests/Statement_test.cpp @@ -18,62 +18,131 @@ TEST(Statement, invalid) { - 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|SQLITE_OPEN_CREATE); + EXPECT_EQ(SQLITE_OK, db.getErrorCode()); + EXPECT_EQ(SQLITE_OK, db.getExtendedErrorCode()); - // Compile a SQL query, but without any table in the database - EXPECT_THROW(SQLite::Statement query(db, "SELECT * FROM test"), SQLite::Exception); - EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); - EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); + // Compile a SQL query, but without any table in the database + EXPECT_THROW(SQLite::Statement query(db, "SELECT * FROM test"), SQLite::Exception); + EXPECT_EQ(SQLITE_ERROR, db.getErrorCode()); + EXPECT_EQ(SQLITE_ERROR, db.getExtendedErrorCode()); - EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")); - EXPECT_EQ(SQLITE_OK, db.getErrorCode()); - EXPECT_EQ(SQLITE_OK, db.getExtendedErrorCode()); + EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")); + EXPECT_EQ(SQLITE_OK, db.getErrorCode()); + EXPECT_EQ(SQLITE_OK, db.getExtendedErrorCode()); - // Compile a SQL query with no parameter - SQLite::Statement query(db, "SELECT * FROM test"); - EXPECT_STREQ("SELECT * FROM test", query.getQuery().c_str()); - EXPECT_EQ(2, query.getColumnCount ()); - EXPECT_FALSE(query.isOk()); - EXPECT_FALSE(query.isDone()); - EXPECT_THROW(query.isColumnNull(-1), SQLite::Exception); - EXPECT_THROW(query.isColumnNull(0), SQLite::Exception); - EXPECT_THROW(query.isColumnNull(1), SQLite::Exception); - EXPECT_THROW(query.isColumnNull(2), SQLite::Exception); - EXPECT_THROW(query.getColumn(-1), SQLite::Exception); - EXPECT_THROW(query.getColumn(0), SQLite::Exception); - EXPECT_THROW(query.getColumn(1), SQLite::Exception); - EXPECT_THROW(query.getColumn(2), SQLite::Exception); + // Compile a SQL query with no parameter + SQLite::Statement query(db, "SELECT * FROM test"); + EXPECT_STREQ("SELECT * FROM test", query.getQuery().c_str()); + EXPECT_EQ(2, query.getColumnCount ()); + EXPECT_FALSE(query.isOk()); + EXPECT_FALSE(query.isDone()); + EXPECT_THROW(query.isColumnNull(-1), SQLite::Exception); + EXPECT_THROW(query.isColumnNull(0), SQLite::Exception); + EXPECT_THROW(query.isColumnNull(1), SQLite::Exception); + EXPECT_THROW(query.isColumnNull(2), SQLite::Exception); + EXPECT_THROW(query.getColumn(-1), SQLite::Exception); + EXPECT_THROW(query.getColumn(0), SQLite::Exception); + EXPECT_THROW(query.getColumn(1), SQLite::Exception); + EXPECT_THROW(query.getColumn(2), SQLite::Exception); - query.reset(); - EXPECT_FALSE(query.isOk()); - EXPECT_FALSE(query.isDone()); + query.reset(); + EXPECT_FALSE(query.isOk()); + EXPECT_FALSE(query.isDone()); - query.exec(); - EXPECT_FALSE(query.isOk()); - EXPECT_TRUE( query.isDone()); - query.reset(); - EXPECT_FALSE(query.isOk()); - EXPECT_FALSE(query.isDone()); + query.executeStep(); + EXPECT_FALSE(query.isOk()); + EXPECT_TRUE( query.isDone()); + query.reset(); + EXPECT_FALSE(query.isOk()); + EXPECT_FALSE(query.isDone()); - query.reset(); - EXPECT_THROW(query.bind(-1, 123), SQLite::Exception); - EXPECT_THROW(query.bind(0, 123), SQLite::Exception); - EXPECT_THROW(query.bind(1, 123), SQLite::Exception); - EXPECT_THROW(query.bind(2, 123), SQLite::Exception); - EXPECT_THROW(query.bind(0, "abc"), SQLite::Exception); - EXPECT_THROW(query.bind(0), SQLite::Exception); - EXPECT_EQ(SQLITE_RANGE, db.getErrorCode()); - EXPECT_EQ(SQLITE_RANGE, db.getExtendedErrorCode()); + query.reset(); + EXPECT_THROW(query.bind(-1, 123), SQLite::Exception); + EXPECT_THROW(query.bind(0, 123), SQLite::Exception); + EXPECT_THROW(query.bind(1, 123), SQLite::Exception); + EXPECT_THROW(query.bind(2, 123), SQLite::Exception); + EXPECT_THROW(query.bind(0, "abc"), SQLite::Exception); + EXPECT_THROW(query.bind(0), SQLite::Exception); + EXPECT_EQ(SQLITE_RANGE, db.getErrorCode()); + EXPECT_EQ(SQLITE_RANGE, db.getExtendedErrorCode()); - query.exec(); - EXPECT_THROW(query.isColumnNull(0), SQLite::Exception); - EXPECT_THROW(query.getColumn(0), SQLite::Exception); + query.exec(); // exec() instead of executeStep() as there is no result + EXPECT_THROW(query.isColumnNull(0), SQLite::Exception); + EXPECT_THROW(query.getColumn(0), SQLite::Exception); - } // Close DB test.db3 - remove("test.db3"); + // Add a first row + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\")")); + EXPECT_EQ(1, db.getLastInsertRowid()); + EXPECT_EQ(1, db.getTotalChanges()); + + query.reset(); + EXPECT_FALSE(query.isOk()); + EXPECT_FALSE(query.isDone()); + + EXPECT_THROW(query.exec(), SQLite::Exception); // exec() shall throw as it does not expect a result +} + +// TODO: test every kind of binding + clearBindings() + +TEST(Statement, getColumnByName) { + // Create a new database + SQLite::Database db(":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + EXPECT_EQ(SQLITE_OK, db.getErrorCode()); + EXPECT_EQ(SQLITE_OK, db.getExtendedErrorCode()); + + // Create a new table + EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, msg TEXT, int INTEGER, double REAL)")); + EXPECT_EQ(SQLITE_OK, db.getErrorCode()); + EXPECT_EQ(SQLITE_OK, db.getExtendedErrorCode()); + + // Create a first row + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, \"first\", 123, 0.123)")); + EXPECT_EQ(1, db.getLastInsertRowid()); + EXPECT_EQ(1, db.getTotalChanges()); + + // Compile a SQL query + SQLite::Statement query(db, "SELECT * FROM test"); + EXPECT_STREQ("SELECT * FROM test", query.getQuery().c_str()); + EXPECT_EQ(4, query.getColumnCount()); + query.executeStep(); + EXPECT_TRUE (query.isOk()); + EXPECT_FALSE(query.isDone()); + + // Look for unexisting columns + EXPECT_THROW(query.getColumn("unknown"), SQLite::Exception); + EXPECT_THROW(query.getColumn(""), SQLite::Exception); + + const std::string msg = query.getColumn("msg"); + const int integer = query.getColumn("int"); + const double real = query.getColumn("double"); + EXPECT_EQ("first", msg); + EXPECT_EQ(123, integer); + EXPECT_EQ(0.123, real); +} + +TEST(Statement, getName) { + // Create a new database + SQLite::Database db(":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); + EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, msg TEXT)")); + + // Compile a SQL query, using the "id" column name as-is, but aliasing the "msg" column with new name "value" + SQLite::Statement query(db, "SELECT id, msg as value FROM test"); + query.executeStep(); + + const std::string name0 = query.getColumnName(0); + const std::string name1 = query.getColumnName(1); + EXPECT_EQ("id", name0); + EXPECT_EQ("value", name1); + +#ifdef SQLITE_ENABLE_COLUMN_METADATA + // Show how to get origin names of the table columns from which theses result columns come from. + // Requires the SQLITE_ENABLE_COLUMN_METADATA preprocessor macro to be + // also defined at compile times of the SQLite library itself. + const std::string oname0 = query.getColumnOriginName(0); + const std::string oname1 = query.getColumnOriginName(1); + EXPECT_EQ("id", oname0); + EXPECT_EQ("msg", oname1); +#endif }