diff --git a/CHANGELOG.md b/CHANGELOG.md index 16bdece6..f01dc635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -289,3 +289,7 @@ Version 3.3.3 - 2025 May 20 - Update googletest to v1.16.0 (#506) - update meson dependencies (#508) +Version 3.3.4 - WIP + +- Add Statement::RowIterator to support range-based for loops over query results (#181) + diff --git a/include/SQLiteCpp/Statement.h b/include/SQLiteCpp/Statement.h index 3b9b0a70..7251c6ff 100644 --- a/include/SQLiteCpp/Statement.h +++ b/include/SQLiteCpp/Statement.h @@ -15,6 +15,7 @@ #include // SQLITECPP_PURE_FUNC #include +#include #include #include #include @@ -660,6 +661,68 @@ class SQLITECPP_API Statement /// Shared pointer to SQLite Prepared Statement Object using TStatementPtr = std::shared_ptr; + /** + * @brief Input iterator over the rows of a prepared SELECT statement. + * + * Allows range-based for loops over query results: + * @code + * SQLite::Statement query(db, "SELECT id, name FROM test"); + * for (SQLite::Statement& row : query) + * { + * std::cout << row.getColumn(0).getInt() << "\n"; + * } + * @endcode + * + * Each increment calls executeStep() to advance to the next row. + * Dereferencing returns the Statement itself, giving access to getColumn(). + * + * @warning Only one active RowIterator per Statement is supported. + */ + struct RowIterator + { + using iterator_category = std::input_iterator_tag; + using value_type = Statement; + using difference_type = std::ptrdiff_t; + using pointer = Statement*; + using reference = Statement&; + + Statement* mpStatement = nullptr; ///< Pointer to the iterated Statement, nullptr when done + + /// Construct an end sentinel (no associated Statement). + RowIterator() = default; + RowIterator(const RowIterator&) = default; + + /// Construct an iterator pointing to the current row of apStatement. + SQLITECPP_API explicit RowIterator(Statement* apStatement); + + /// Advance to the next row. Becomes the end sentinel when no rows remain. + SQLITECPP_API RowIterator& operator++(); + + /// Return true when two iterators do not point to the same row. + SQLITECPP_API bool operator!=(const RowIterator& aOther) const; + + /// Dereference to the Statement, giving access to getColumn(). + SQLITECPP_API Statement& operator*() const; + }; + + /** + * @brief Return an iterator to the first row of the result set. + * + * Calls reset() then executeStep() so that iterating the same Statement + * a second time always starts from the beginning. + * Returns the end iterator immediately if the result set is empty. + * + * @note Bindings set before the loop are preserved across reset(). + * + * @throw SQLite::Exception in case of error + */ + RowIterator begin(); + + /** + * @brief Return the end sentinel iterator (past the last row). + */ + RowIterator end(); + private: /** * @brief Check if a return code equals SQLITE_OK, else throw a SQLite::Exception with the SQLite error message diff --git a/src/Statement.cpp b/src/Statement.cpp index e4a264f4..92417a5f 100644 --- a/src/Statement.cpp +++ b/src/Statement.cpp @@ -342,7 +342,6 @@ const char* Statement::getErrorMsg() const noexcept return sqlite3_errmsg(mpSQLite); } - // Return a UTF-8 string containing the SQL text of prepared statement with bound parameters expanded. std::string Statement::getExpandedSQL() const { #ifdef SQLITECPP_DISABLE_SQLITE3_EXPANDED_SQL @@ -355,6 +354,38 @@ std::string Statement::getExpandedSQL() const { #endif } +Statement::RowIterator::RowIterator(Statement* apStatement): mpStatement(apStatement) +{} + +Statement::RowIterator& Statement::RowIterator::operator++() +{ + if (!mpStatement->executeStep()) + mpStatement = nullptr; + return *this; +} + +bool Statement::RowIterator::operator!=(const RowIterator& aOther) const +{ + return mpStatement != aOther.mpStatement; +} + +Statement& Statement::RowIterator::operator*() const +{ + return *mpStatement; +} + +Statement::RowIterator Statement::begin() +{ + reset(); + if (executeStep()) + return RowIterator { this }; + return RowIterator { nullptr }; +} + +Statement::RowIterator Statement::end() +{ + return RowIterator{ nullptr }; +} // Prepare SQLite statement object and return shared pointer to this object Statement::TStatementPtr Statement::prepareStatement() diff --git a/tests/Statement_test.cpp b/tests/Statement_test.cpp index a76d06a8..ded84b54 100644 --- a/tests/Statement_test.cpp +++ b/tests/Statement_test.cpp @@ -12,8 +12,10 @@ #include #include -#include // for int64_t -#include // for SQLITE_DONE +#include // for int64_t +#include // for std::iterator_traits, std::input_iterator_tag +#include // for std::is_same +#include // for SQLITE_DONE #include @@ -1011,6 +1013,95 @@ TEST(Statement, getColumns) } #endif +#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1600) + +TEST(Statement, rowIteratorTraits) +{ + using Iter = SQLite::Statement::RowIterator; + using Traits = std::iterator_traits; + + static_assert(std::is_same::value, + "RowIterator must be an input iterator"); + static_assert(std::is_same::value, + "value_type must be Statement"); + static_assert(std::is_same::value, + "reference must be Statement&"); + static_assert(std::is_same::value, + "pointer must be Statement*"); + static_assert(std::is_same::value, + "difference_type must be ptrdiff_t"); +} + +TEST(Statement, rangeBasedFor) +{ + // 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, val INTEGER)")); + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (1, 'first', 10)")); + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (2, 'second', 20)")); + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (3, 'third', 30)")); + + // Basic range-based for loop: iterator dereferences to the Statement itself + SQLite::Statement query(db, "SELECT id, msg, val FROM test ORDER BY id"); + int rowCount = 0; + for (SQLite::Statement& row : query) + { + ++rowCount; + EXPECT_EQ(rowCount, row.getColumn(0).getInt()); + EXPECT_EQ(rowCount * 10, row.getColumn(2).getInt()); + } + EXPECT_EQ(3, rowCount); + + // Re-iterating the same Statement must reset and start over + rowCount = 0; + for (SQLite::Statement& row : query) + { + ++rowCount; + EXPECT_EQ(rowCount, row.getColumn(0).getInt()); + } + EXPECT_EQ(3, rowCount); +} + +TEST(Statement, rangeBasedForEmpty) +{ + // 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)")); + + // Empty table: loop body must never execute + SQLite::Statement query(db, "SELECT * FROM test"); + int rowCount = 0; + for (SQLite::Statement& row : query) + { + (void)row; + ++rowCount; + } + EXPECT_EQ(0, rowCount); +} + +TEST(Statement, rangeBasedForWithBind) +{ + // 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, val INTEGER)")); + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (1, 5)")); + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (2, 15)")); + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (3, 25)")); + + // Only rows with val > 10 should be visited + SQLite::Statement query(db, "SELECT id, val FROM test WHERE val > ? ORDER BY id"); + query.bind(1, 10); + int rowCount = 0; + for (SQLite::Statement& row : query) + { + ++rowCount; + EXPECT_GT(row.getColumn(1).getInt(), 10); + } + EXPECT_EQ(2, rowCount); +} + +#endif // C++11 + TEST(Statement, getBindParameterCount) { // Create a new database