From d42e665b319e1eeedf05392b94a238c33995fa14 Mon Sep 17 00:00:00 2001 From: Alexander Lvov Date: Mon, 1 Jun 2026 15:07:18 +0300 Subject: [PATCH 1/8] Implement testing for range-based for statement loop --- tests/Statement_test.cpp | 72 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/Statement_test.cpp b/tests/Statement_test.cpp index a76d06a8..44120244 100644 --- a/tests/Statement_test.cpp +++ b/tests/Statement_test.cpp @@ -1011,6 +1011,78 @@ TEST(Statement, getColumns) } #endif +#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1600) + +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 From b51ba20792165a054c8b5e428c94d5e3d17a244e Mon Sep 17 00:00:00 2001 From: Alexander Lvov Date: Mon, 1 Jun 2026 15:07:19 +0300 Subject: [PATCH 2/8] Implement RowIterator class --- include/SQLiteCpp/Statement.h | 56 +++++++++++++++++++++++++++++++++++ src/Statement.cpp | 33 ++++++++++++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/include/SQLiteCpp/Statement.h b/include/SQLiteCpp/Statement.h index 3b9b0a70..ab4732df 100644 --- a/include/SQLiteCpp/Statement.h +++ b/include/SQLiteCpp/Statement.h @@ -660,6 +660,62 @@ 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 + { + 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() From f889053f585174c2a47accb43623b719281faf52 Mon Sep 17 00:00:00 2001 From: Alexander Lvov Date: Mon, 1 Jun 2026 15:07:20 +0300 Subject: [PATCH 3/8] Implement test to validate row iterator traits --- tests/Statement_test.cpp | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/Statement_test.cpp b/tests/Statement_test.cpp index 44120244..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 @@ -1013,6 +1015,23 @@ TEST(Statement, getColumns) #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 From bcd2db83fd339ff7eacb8fa5236e339c5ce31102 Mon Sep 17 00:00:00 2001 From: Alexander Lvov Date: Mon, 1 Jun 2026 15:07:20 +0300 Subject: [PATCH 4/8] Add traits to RowIterator --- include/SQLiteCpp/Statement.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/SQLiteCpp/Statement.h b/include/SQLiteCpp/Statement.h index ab4732df..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 @@ -679,6 +680,12 @@ class SQLITECPP_API Statement */ 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). From 83bb827d2e068e111e4f8367b9e65ae85eaf8e25 Mon Sep 17 00:00:00 2001 From: Alexander Lvov Date: Mon, 1 Jun 2026 15:21:20 +0300 Subject: [PATCH 5/8] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) 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) + From 1a9abc8506b35fdcac1f201829a9db9cbe726f5c Mon Sep 17 00:00:00 2001 From: Alexander Lvov Date: Fri, 12 Jun 2026 22:38:05 +0300 Subject: [PATCH 6/8] Remove explicit copy constructor from RowIterator --- include/SQLiteCpp/Statement.h | 1 - 1 file changed, 1 deletion(-) diff --git a/include/SQLiteCpp/Statement.h b/include/SQLiteCpp/Statement.h index 7251c6ff..a84b60a4 100644 --- a/include/SQLiteCpp/Statement.h +++ b/include/SQLiteCpp/Statement.h @@ -690,7 +690,6 @@ class SQLITECPP_API Statement /// 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); From e7d12d952c6de4e5a178fc852df5a952e97b088c Mon Sep 17 00:00:00 2001 From: Alexander Lvov Date: Fri, 12 Jun 2026 22:41:52 +0300 Subject: [PATCH 7/8] Restore blank lines removed by accident --- src/Statement.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Statement.cpp b/src/Statement.cpp index 92417a5f..b746ecce 100644 --- a/src/Statement.cpp +++ b/src/Statement.cpp @@ -342,6 +342,7 @@ 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 @@ -387,6 +388,7 @@ Statement::RowIterator Statement::end() return RowIterator{ nullptr }; } + // Prepare SQLite statement object and return shared pointer to this object Statement::TStatementPtr Statement::prepareStatement() { From c996909ecb66dace04b48d0cdfd96c24674d3ad1 Mon Sep 17 00:00:00 2001 From: Alexander Lvov Date: Fri, 12 Jun 2026 23:08:48 +0300 Subject: [PATCH 8/8] Implement equality and postfix increment operators --- include/SQLiteCpp/Statement.h | 6 ++++++ src/Statement.cpp | 14 +++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/include/SQLiteCpp/Statement.h b/include/SQLiteCpp/Statement.h index a84b60a4..47aa69e9 100644 --- a/include/SQLiteCpp/Statement.h +++ b/include/SQLiteCpp/Statement.h @@ -697,6 +697,12 @@ class SQLITECPP_API Statement /// Advance to the next row. Becomes the end sentinel when no rows remain. SQLITECPP_API RowIterator& operator++(); + /// Post-increment: advance to the next row, return a copy of the iterator before advancing. + SQLITECPP_API RowIterator operator++(int); + + /// Return true when two iterators point to the same row. + SQLITECPP_API bool operator==(const RowIterator& aOther) const; + /// Return true when two iterators do not point to the same row. SQLITECPP_API bool operator!=(const RowIterator& aOther) const; diff --git a/src/Statement.cpp b/src/Statement.cpp index b746ecce..cceac5ee 100644 --- a/src/Statement.cpp +++ b/src/Statement.cpp @@ -365,9 +365,21 @@ Statement::RowIterator& Statement::RowIterator::operator++() return *this; } +Statement::RowIterator Statement::RowIterator::operator++(int) +{ + const RowIterator tmp(*this); + ++(*this); + return tmp; +} + +bool Statement::RowIterator::operator==(const RowIterator& aOther) const +{ + return mpStatement == aOther.mpStatement; +} + bool Statement::RowIterator::operator!=(const RowIterator& aOther) const { - return mpStatement != aOther.mpStatement; + return !this->operator==(aOther); } Statement& Statement::RowIterator::operator*() const