Not a bug report — the behavior is documented ("@return true when execution yields a record") and the README shows the correct idiom. This is field feedback about how easy the API is to misuse, plus a small suggestion.
What happened
We recently wrote 43 small fb-cpp programs (hands-on companions to a Firebird architecture paper, one per subsystem document — https://github.com/mariuz/conceptual-architecture-for-firebird-paper, samples/fb-cpp/). Three of them independently ended up with this loop:
Statement stmt{att, tra, "select ... order by id"};
stmt.execute(tra);
while (stmt.fetchNext())
// process row — silently starts at row TWO
Since execute() on a SELECT opens the cursor and fetches the first row, this compiles, runs, and quietly drops row one. The failure mode is the bad kind: no error, no warning, just missing data — in our case a lost MON$ATTACHMENTS row, a lost memory-pool summary line, and a "lost update" that sent us hunting a concurrency bug that didn't exist. Developers arriving from the raw OO API (openCursor() then fetchNext() per row) or from ODBC/JDBC-style cursors reach for exactly this shape.
The correct idioms — README's if (execute()) do … while (fetchNext()); or for (bool ok = stmt.execute(tra); ok; ok = stmt.fetchNext()) — work fine once you know. The problem is only that the wrong shape gives no signal.
Suggestion
Mark the row-producing calls [[nodiscard]]:
[[nodiscard]] bool execute(Transaction& transaction);
Discarding execute()'s return is precisely the bug above, and a compiler warning would have caught all three of our cases at build time (fetchNext()/fetchPrior()/etc. are arguably the same — ignoring their result and then reading columns is equally silent). If [[nodiscard]] on execute() is considered too noisy for the INSERT/UPDATE/DDL case (where the return is always true and discarding is natural), an alternative would be a short "pitfalls" note in the README right at the fetch-loop example.
Happy to send a PR for either variant if you'd take one.
Not a bug report — the behavior is documented ("
@return true when execution yields a record") and the README shows the correct idiom. This is field feedback about how easy the API is to misuse, plus a small suggestion.What happened
We recently wrote 43 small fb-cpp programs (hands-on companions to a Firebird architecture paper, one per subsystem document — https://github.com/mariuz/conceptual-architecture-for-firebird-paper,
samples/fb-cpp/). Three of them independently ended up with this loop:Statement stmt{att, tra, "select ... order by id"}; stmt.execute(tra); while (stmt.fetchNext()) // process row — silently starts at row TWOSince
execute()on a SELECT opens the cursor and fetches the first row, this compiles, runs, and quietly drops row one. The failure mode is the bad kind: no error, no warning, just missing data — in our case a lostMON$ATTACHMENTSrow, a lost memory-pool summary line, and a "lost update" that sent us hunting a concurrency bug that didn't exist. Developers arriving from the raw OO API (openCursor()thenfetchNext()per row) or from ODBC/JDBC-style cursors reach for exactly this shape.The correct idioms — README's
if (execute()) do … while (fetchNext());orfor (bool ok = stmt.execute(tra); ok; ok = stmt.fetchNext())— work fine once you know. The problem is only that the wrong shape gives no signal.Suggestion
Mark the row-producing calls
[[nodiscard]]:Discarding
execute()'s return is precisely the bug above, and a compiler warning would have caught all three of our cases at build time (fetchNext()/fetchPrior()/etc. are arguably the same — ignoring their result and then reading columns is equally silent). If[[nodiscard]]onexecute()is considered too noisy for the INSERT/UPDATE/DDL case (where the return is alwaystrueand discarding is natural), an alternative would be a short "pitfalls" note in the README right at the fetch-loop example.Happy to send a PR for either variant if you'd take one.