diff --git a/CHANGELOG.md b/CHANGELOG.md index e64e028c..8aef3337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -312,3 +312,4 @@ Version 3.4.0 - 2026 ??? - Fix Savepoint destructor to catch all exceptions and track rollback state to avoid std::terminate (#559) - Fix Transaction destructor to catch all exceptions to avoid std::terminate (#559) - Fix the Meson build when the SQLITECPP_DISABLE_STD_FILESYSTEM option is enabled (#560) +- Add Statement::RowIterator to support range-based for loops over query results (#181) diff --git a/README.md b/README.md index d86af2b7..e05a43e1 100644 --- a/README.md +++ b/README.md @@ -354,6 +354,19 @@ catch (std::exception& e) } ``` +`Statement` also provides a `RowIterator`, so the loop above can be written as a range-based `for`: + +```C++ +for (auto&& row : query) +{ + int id = row.getColumn(0); + const char* value = row.getColumn(1); + int size = row.getColumn(2); + + std::cout << "row: " << id << ", " << value << ", " << size << std::endl; +} +``` + ### The second sample shows how to manage a transaction: ```C++ diff --git a/examples/example1/main.cpp b/examples/example1/main.cpp index 79527883..909faf29 100644 --- a/examples/example1/main.cpp +++ b/examples/example1/main.cpp @@ -241,6 +241,20 @@ int main() weight = query.getColumn(2).getInt(); std::cout << "row (" << id << ", \"" << value << "\", " << weight << ")\n"; } + + ///// e) Loop using the range-based for loop provided by Statement::begin()/end() + + // 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"; + + for (auto&& row : query) + { + const int rid = row.getColumn(0); + const std::string rvalue = row.getColumn(1); + const double rweight = row.getColumn(2); + std::cout << "row (" << rid << ", \"" << rvalue.c_str() << "\" " << rweight << ")\n"; + } } catch (std::exception& e) { diff --git a/include/SQLiteCpp/Statement.h b/include/SQLiteCpp/Statement.h index 3b9b0a70..61fccb95 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,73 @@ 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; + + /// 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++(); + + /// Post-increment: advance to the next row. + SQLITECPP_API void operator++(int); + + /// Return true when both iterators refer to the same statement, or are both the end sentinel. + SQLITECPP_API bool operator==(const RowIterator& aOther) const; + + /// Return true when the iterators do not refer to the same statement. + 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 a528c6e5..88f2ca01 100644 --- a/src/Statement.cpp +++ b/src/Statement.cpp @@ -358,6 +358,49 @@ 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; +} + +void Statement::RowIterator::operator++(int) +{ + ++(*this); +} + +bool Statement::RowIterator::operator==(const RowIterator& aOther) const +{ + return mpStatement == aOther.mpStatement; +} + +bool Statement::RowIterator::operator!=(const RowIterator& aOther) const +{ + return !this->operator==(aOther); +} + +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 7bcb1462..2762e9ab 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 @@ -1013,6 +1015,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