Skip to content

Statement::execute() already fetches the first row — execute(); while (fetchNext()) silently drops it; consider [[nodiscard]] #59

Description

@mariuz

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions