Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

63 changes: 63 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,68 @@ 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;
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
Expand Down
33 changes: 32 additions & 1 deletion src/Statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
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 @@ -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<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