Skip to content

Commit ff5f33c

Browse files
authored
Range for loop for statements (#545)
Closes #181 Adds a nested `RowIterator` class to `Statement` that enables range-based for loops over query results: ```cpp SQLite::Statement query(db, "SELECT id, name FROM test"); for (SQLite::Statement& row : query) { std::cout << row.getColumn(0).getInt() << "\n"; } ``` Notes: - `begin()` calls `reset()` automatically, so re-iterating the same Statement always starts from the beginning - Bindings set before the loop are preserved across `reset()` - Empty result sets are handled correctly (loop body never executes) - `RowIterator` satisfies `std::input_iterator_tag` with full iterator traits
2 parents a6537fe + 7de8d9e commit ff5f33c

6 files changed

Lines changed: 232 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,4 @@ Version 3.4.0 - 2026 ???
312312
- Fix Savepoint destructor to catch all exceptions and track rollback state to avoid std::terminate (#559)
313313
- Fix Transaction destructor to catch all exceptions to avoid std::terminate (#559)
314314
- Fix the Meson build when the SQLITECPP_DISABLE_STD_FILESYSTEM option is enabled (#560)
315+
- Add Statement::RowIterator to support range-based for loops over query results (#181)

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,19 @@ catch (std::exception& e)
354354
}
355355
```
356356

357+
`Statement` also provides a `RowIterator`, so the loop above can be written as a range-based `for`:
358+
359+
```C++
360+
for (auto&& row : query)
361+
{
362+
int id = row.getColumn(0);
363+
const char* value = row.getColumn(1);
364+
int size = row.getColumn(2);
365+
366+
std::cout << "row: " << id << ", " << value << ", " << size << std::endl;
367+
}
368+
```
369+
357370
### The second sample shows how to manage a transaction:
358371

359372
```C++

examples/example1/main.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,20 @@ int main()
241241
weight = query.getColumn(2).getInt();
242242
std::cout << "row (" << id << ", \"" << value << "\", " << weight << ")\n";
243243
}
244+
245+
///// e) Loop using the range-based for loop provided by Statement::begin()/end()
246+
247+
// Reset the query to use it again
248+
query.reset();
249+
std::cout << "SQLite statement '" << query.getQuery().c_str() << "' reseted (" << query.getColumnCount() << " columns in the result)\n";
250+
251+
for (auto&& row : query)
252+
{
253+
const int rid = row.getColumn(0);
254+
const std::string rvalue = row.getColumn(1);
255+
const double rweight = row.getColumn(2);
256+
std::cout << "row (" << rid << ", \"" << rvalue.c_str() << "\" " << rweight << ")\n";
257+
}
244258
}
245259
catch (std::exception& e)
246260
{

include/SQLiteCpp/Statement.h

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <SQLiteCpp/Utils.h> // SQLITECPP_PURE_FUNC
1616

1717
#include <cstdint>
18+
#include <iterator>
1819
#include <string>
1920
#include <map>
2021
#include <memory>
@@ -660,6 +661,73 @@ class SQLITECPP_API Statement
660661
/// Shared pointer to SQLite Prepared Statement Object
661662
using TStatementPtr = std::shared_ptr<sqlite3_stmt>;
662663

664+
/**
665+
* @brief Input iterator over the rows of a prepared SELECT statement.
666+
*
667+
* Allows range-based for loops over query results:
668+
* @code
669+
* SQLite::Statement query(db, "SELECT id, name FROM test");
670+
* for (SQLite::Statement& row : query)
671+
* {
672+
* std::cout << row.getColumn(0).getInt() << "\n";
673+
* }
674+
* @endcode
675+
*
676+
* Each increment calls executeStep() to advance to the next row.
677+
* Dereferencing returns the Statement itself, giving access to getColumn().
678+
*
679+
* @warning Only one active RowIterator per Statement is supported.
680+
*/
681+
struct RowIterator
682+
{
683+
using iterator_category = std::input_iterator_tag;
684+
using value_type = Statement;
685+
using difference_type = std::ptrdiff_t;
686+
using pointer = Statement*;
687+
using reference = Statement&;
688+
689+
Statement* mpStatement = nullptr; ///< Pointer to the iterated Statement, nullptr when done
690+
691+
/// Construct an end sentinel (no associated Statement).
692+
RowIterator() = default;
693+
694+
/// Construct an iterator pointing to the current row of apStatement.
695+
SQLITECPP_API explicit RowIterator(Statement* apStatement);
696+
697+
/// Advance to the next row. Becomes the end sentinel when no rows remain.
698+
SQLITECPP_API RowIterator& operator++();
699+
700+
/// Post-increment: advance to the next row.
701+
SQLITECPP_API void operator++(int);
702+
703+
/// Return true when both iterators refer to the same statement, or are both the end sentinel.
704+
SQLITECPP_API bool operator==(const RowIterator& aOther) const;
705+
706+
/// Return true when the iterators do not refer to the same statement.
707+
SQLITECPP_API bool operator!=(const RowIterator& aOther) const;
708+
709+
/// Dereference to the Statement, giving access to getColumn().
710+
SQLITECPP_API Statement& operator*() const;
711+
};
712+
713+
/**
714+
* @brief Return an iterator to the first row of the result set.
715+
*
716+
* Calls reset() then executeStep() so that iterating the same Statement
717+
* a second time always starts from the beginning.
718+
* Returns the end iterator immediately if the result set is empty.
719+
*
720+
* @note Bindings set before the loop are preserved across reset().
721+
*
722+
* @throw SQLite::Exception in case of error
723+
*/
724+
RowIterator begin();
725+
726+
/**
727+
* @brief Return the end sentinel iterator (past the last row).
728+
*/
729+
RowIterator end();
730+
663731
private:
664732
/**
665733
* @brief Check if a return code equals SQLITE_OK, else throw a SQLite::Exception with the SQLite error message

src/Statement.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,49 @@ std::string Statement::getExpandedSQL() const {
358358
#endif
359359
}
360360

361+
Statement::RowIterator::RowIterator(Statement* apStatement): mpStatement(apStatement)
362+
{}
363+
364+
Statement::RowIterator& Statement::RowIterator::operator++()
365+
{
366+
if (!mpStatement->executeStep())
367+
mpStatement = nullptr;
368+
return *this;
369+
}
370+
371+
void Statement::RowIterator::operator++(int)
372+
{
373+
++(*this);
374+
}
375+
376+
bool Statement::RowIterator::operator==(const RowIterator& aOther) const
377+
{
378+
return mpStatement == aOther.mpStatement;
379+
}
380+
381+
bool Statement::RowIterator::operator!=(const RowIterator& aOther) const
382+
{
383+
return !this->operator==(aOther);
384+
}
385+
386+
Statement& Statement::RowIterator::operator*() const
387+
{
388+
return *mpStatement;
389+
}
390+
391+
Statement::RowIterator Statement::begin()
392+
{
393+
reset();
394+
if (executeStep())
395+
return RowIterator { this };
396+
return RowIterator { nullptr };
397+
}
398+
399+
Statement::RowIterator Statement::end()
400+
{
401+
return RowIterator{ nullptr };
402+
}
403+
361404

362405
// Prepare SQLite statement object and return shared pointer to this object
363406
Statement::TStatementPtr Statement::prepareStatement()

tests/Statement_test.cpp

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
#include <SQLiteCpp/Database.h>
1313
#include <SQLiteCpp/Statement.h>
1414

15-
#include <cstdint> // for int64_t
16-
#include <sqlite3.h> // for SQLITE_DONE
15+
#include <cstdint> // for int64_t
16+
#include <iterator> // for std::iterator_traits, std::input_iterator_tag
17+
#include <type_traits> // for std::is_same
18+
#include <sqlite3.h> // for SQLITE_DONE
1719

1820
#include <gtest/gtest.h>
1921

@@ -1013,6 +1015,95 @@ TEST(Statement, getColumns)
10131015
}
10141016
#endif
10151017

1018+
#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1600)
1019+
1020+
TEST(Statement, rowIteratorTraits)
1021+
{
1022+
using Iter = SQLite::Statement::RowIterator;
1023+
using Traits = std::iterator_traits<Iter>;
1024+
1025+
static_assert(std::is_same<Traits::iterator_category, std::input_iterator_tag>::value,
1026+
"RowIterator must be an input iterator");
1027+
static_assert(std::is_same<Traits::value_type, SQLite::Statement>::value,
1028+
"value_type must be Statement");
1029+
static_assert(std::is_same<Traits::reference, SQLite::Statement&>::value,
1030+
"reference must be Statement&");
1031+
static_assert(std::is_same<Traits::pointer, SQLite::Statement*>::value,
1032+
"pointer must be Statement*");
1033+
static_assert(std::is_same<Traits::difference_type, std::ptrdiff_t>::value,
1034+
"difference_type must be ptrdiff_t");
1035+
}
1036+
1037+
TEST(Statement, rangeBasedFor)
1038+
{
1039+
// Create a new database
1040+
SQLite::Database db(":memory:", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
1041+
EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, msg TEXT, val INTEGER)"));
1042+
EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (1, 'first', 10)"));
1043+
EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (2, 'second', 20)"));
1044+
EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (3, 'third', 30)"));
1045+
1046+
// Basic range-based for loop: iterator dereferences to the Statement itself
1047+
SQLite::Statement query(db, "SELECT id, msg, val FROM test ORDER BY id");
1048+
int rowCount = 0;
1049+
for (SQLite::Statement& row : query)
1050+
{
1051+
++rowCount;
1052+
EXPECT_EQ(rowCount, row.getColumn(0).getInt());
1053+
EXPECT_EQ(rowCount * 10, row.getColumn(2).getInt());
1054+
}
1055+
EXPECT_EQ(3, rowCount);
1056+
1057+
// Re-iterating the same Statement must reset and start over
1058+
rowCount = 0;
1059+
for (SQLite::Statement& row : query)
1060+
{
1061+
++rowCount;
1062+
EXPECT_EQ(rowCount, row.getColumn(0).getInt());
1063+
}
1064+
EXPECT_EQ(3, rowCount);
1065+
}
1066+
1067+
TEST(Statement, rangeBasedForEmpty)
1068+
{
1069+
// Create a new database
1070+
SQLite::Database db(":memory:", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
1071+
EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY)"));
1072+
1073+
// Empty table: loop body must never execute
1074+
SQLite::Statement query(db, "SELECT * FROM test");
1075+
int rowCount = 0;
1076+
for (SQLite::Statement& row : query)
1077+
{
1078+
(void)row;
1079+
++rowCount;
1080+
}
1081+
EXPECT_EQ(0, rowCount);
1082+
}
1083+
1084+
TEST(Statement, rangeBasedForWithBind)
1085+
{
1086+
// Create a new database
1087+
SQLite::Database db(":memory:", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
1088+
EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, val INTEGER)"));
1089+
EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (1, 5)"));
1090+
EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (2, 15)"));
1091+
EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (3, 25)"));
1092+
1093+
// Only rows with val > 10 should be visited
1094+
SQLite::Statement query(db, "SELECT id, val FROM test WHERE val > ? ORDER BY id");
1095+
query.bind(1, 10);
1096+
int rowCount = 0;
1097+
for (SQLite::Statement& row : query)
1098+
{
1099+
++rowCount;
1100+
EXPECT_GT(row.getColumn(1).getInt(), 10);
1101+
}
1102+
EXPECT_EQ(2, rowCount);
1103+
}
1104+
1105+
#endif // C++11
1106+
10161107
TEST(Statement, getBindParameterCount)
10171108
{
10181109
// Create a new database

0 commit comments

Comments
 (0)