Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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++
Expand Down
14 changes: 14 additions & 0 deletions examples/example1/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
68 changes: 68 additions & 0 deletions include/SQLiteCpp/Statement.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <SQLiteCpp/Utils.h> // SQLITECPP_PURE_FUNC

#include <cstdint>
#include <iterator>
#include <string>
#include <map>
#include <memory>
Expand Down Expand Up @@ -660,6 +661,73 @@ class SQLITECPP_API Statement
/// Shared pointer to SQLite Prepared Statement Object
using TStatementPtr = std::shared_ptr<sqlite3_stmt>;

/**
* @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;
Comment thread
Alvov1 marked this conversation as resolved.

/// 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
Expand Down
43 changes: 43 additions & 0 deletions src/Statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
95 changes: 93 additions & 2 deletions tests/Statement_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
#include <SQLiteCpp/Database.h>
#include <SQLiteCpp/Statement.h>

#include <cstdint> // for int64_t
#include <sqlite3.h> // for SQLITE_DONE
#include <cstdint> // for int64_t
#include <iterator> // for std::iterator_traits, std::input_iterator_tag
#include <type_traits> // for std::is_same
#include <sqlite3.h> // for SQLITE_DONE

#include <gtest/gtest.h>

Expand Down Expand Up @@ -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<Iter>;

static_assert(std::is_same<Traits::iterator_category, std::input_iterator_tag>::value,
"RowIterator must be an input iterator");
static_assert(std::is_same<Traits::value_type, SQLite::Statement>::value,
"value_type must be Statement");
static_assert(std::is_same<Traits::reference, SQLite::Statement&>::value,
"reference must be Statement&");
static_assert(std::is_same<Traits::pointer, SQLite::Statement*>::value,
"pointer must be Statement*");
static_assert(std::is_same<Traits::difference_type, std::ptrdiff_t>::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
Expand Down
Loading