Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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