Skip to content

Commit b2d9b02

Browse files
authored
Fix stale bindings leaking between execute_many() parameter sets (#554)
execute_many() reset the statement between parameter sets but never cleared the bindings, so a set that bound fewer values than the previous one silently reused the leftover values (silent data corruption). Call clearBindings() after reset() so each parameter set starts clean. Adds a regression test covering decreasing arity across parameter sets.
2 parents f173e65 + e656cb9 commit b2d9b02

3 files changed

Lines changed: 43 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,4 +305,5 @@ Version 3.4.0 - 2026 ???
305305
- Add unit tests covering error and edge-case paths to raise code coverage (#551)
306306
- Guard against null C pointers in Exception, Database, and Statement to avoid undefined behavior (#552)
307307
- Fix Database::isUnencrypted() to compare the full 16-byte header in binary mode (#553)
308+
- Fix execute_many() to clear stale bindings between parameter sets (#554)
308309
- Fix Column::operator<< to stream the exact column bytes via getString() (#556)

include/SQLiteCpp/ExecuteMany.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ void execute_many(Database& aDatabase, const char* apQuery, Arg&& aArg, Types&&.
5454
}
5555

5656
/**
57-
* \brief Convenience function to reset a statement and call bind_exec to
57+
* \brief Convenience function to reset a statement and call bind_exec to
5858
* bind new values to the statement and execute it
5959
*
6060
* This feature requires a c++14 capable compiler.
@@ -66,6 +66,7 @@ template <typename TupleT>
6666
void reset_bind_exec(Statement& apQuery, TupleT&& aTuple)
6767
{
6868
apQuery.reset();
69+
apQuery.clearBindings();
6970
bind_exec(apQuery, std::forward<TupleT>(aTuple));
7071
}
7172

tests/ExecuteMany_test.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,44 @@ TEST(ExecuteMany, invalid)
5151
EXPECT_EQ(std::make_pair(3,std::string{"three"}), results.at(2));
5252
}
5353
}
54+
55+
TEST(ExecuteMany, decreasingArity)
56+
{
57+
// Create a new database
58+
SQLite::Database db(":memory:", SQLite::OPEN_READWRITE|SQLite::OPEN_CREATE);
59+
60+
EXPECT_EQ(0, db.exec("DROP TABLE IF EXISTS test"));
61+
EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"));
62+
EXPECT_TRUE(db.tableExists("test"));
63+
{
64+
// The first row binds both parameters, the second row binds only the id
65+
// and leaves the value parameter unbound: it must be stored as NULL, not
66+
// reuse the "first" value left over from the previous parameter set.
67+
execute_many(db, "INSERT INTO test VALUES (?, ?)",
68+
std::make_tuple(1, "first"),
69+
std::make_tuple(2)
70+
);
71+
}
72+
// make sure the stale binding did not leak into the second row
73+
{
74+
SQLite::Statement query(db, std::string{"SELECT id, value FROM test ORDER BY id"});
75+
std::vector<std::pair<int, std::string> > results;
76+
std::vector<bool> valueIsNull;
77+
while (query.executeStep())
78+
{
79+
const int id = query.getColumn(0);
80+
valueIsNull.push_back(query.getColumn(1).isNull());
81+
std::string value = query.getColumn(1);
82+
results.emplace_back( id, std::move(value) );
83+
}
84+
EXPECT_EQ(std::size_t(2), results.size());
85+
86+
EXPECT_EQ(std::make_pair(1,std::string{"first"}), results.at(0));
87+
EXPECT_FALSE(valueIsNull.at(0));
88+
89+
// Without clearBindings() the second row would wrongly store "first"
90+
EXPECT_EQ(2, results.at(1).first);
91+
EXPECT_TRUE(valueIsNull.at(1));
92+
}
93+
}
5494
#endif // c++14

0 commit comments

Comments
 (0)