From db9fbefe6bd4588829921cb9da0cfa5ce080f716 Mon Sep 17 00:00:00 2001 From: Popa Adrian Marius Date: Tue, 21 Jul 2026 10:40:02 +0000 Subject: [PATCH] Mark Statement::execute() and the fetch family [[nodiscard]] (#59) execute() on a SELECT opens the cursor and already fetches the first row, so the natural-looking 'execute(); while (fetchNext())' loop compiles and runs but silently drops row one. Marking execute() and the cursor-movement methods [[nodiscard]] (with a reason message) turns that shape into a compiler warning at every call site. Intentional discards - DDL/DML executes and reads of a known-existing row - are annotated with an explicit (void) cast in the tests, and the README example gains a short note about the idiom. Closes #59 --- README.md | 6 ++++++ src/fb-cpp/Statement.h | 18 ++++++++++++++++++ src/test/BackupManager.cpp | 14 +++++++------- src/test/Batch.cpp | 18 +++++++++--------- src/test/DatabaseManager.cpp | 2 +- src/test/RowSet.cpp | 24 ++++++++++++------------ src/test/ScrollableCursor.cpp | 4 ++-- src/test/Statement.cpp | 22 +++++++++++----------- src/test/Transaction2PC.cpp | 20 ++++++++++---------- 9 files changed, 76 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 3ab9b9e..382793d 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,12 @@ if (statement.execute(transaction)) transaction.commit(); ``` +Note that `execute()` already fetches a SELECT's first row — that is why the loop above is +`if (execute()) do ... while (fetchNext());` and not `execute(); while (fetchNext()) ...`, +which would silently skip the first row. `execute()` is marked `[[nodiscard]]` so the +compiler warns about the latter shape; cast to `void` to deliberately discard the result +(e.g. for DDL/DML, or when a row is known to exist). + ## Using with vcpkg This library is present in [firebird-vcpkg-registry](https://github.com/asfernandes/firebird-vcpkg-registry). diff --git a/src/fb-cpp/Statement.h b/src/fb-cpp/Statement.h index 05faa8a..bd57bcc 100644 --- a/src/fb-cpp/Statement.h +++ b/src/fb-cpp/Statement.h @@ -309,9 +309,15 @@ namespace fbcpp /// /// @brief Executes a prepared statement using the supplied transaction. + /// + /// For a SELECT, this opens the cursor and already fetches the first row - a subsequent + /// `fetchNext()` positions on the second row. Cast to `void` to deliberately discard the + /// result (e.g. for DDL/DML, or when a row is known to exist). + /// /// @param transaction Transaction that will own the execution context. /// @return `true` when execution yields a record. /// + [[nodiscard("execute() already fetches a SELECT's first row; looping on fetchNext() alone skips it")]] bool execute(Transaction& transaction); /// @@ -320,32 +326,44 @@ namespace fbcpp /// /// @brief Fetches the next row in the current result set. + /// @return `true` when a row was fetched. /// + [[nodiscard("without checking the result, the cursor may be past the end")]] bool fetchNext(); /// /// @brief Fetches the previous row in the current result set. + /// @return `true` when a row was fetched. /// + [[nodiscard("without checking the result, the cursor may be before the start")]] bool fetchPrior(); /// /// @brief Positions the cursor on the first row. + /// @return `true` when a row was fetched. /// + [[nodiscard("without checking the result, the cursor position is unknown")]] bool fetchFirst(); /// /// @brief Positions the cursor on the last row. + /// @return `true` when a row was fetched. /// + [[nodiscard("without checking the result, the cursor position is unknown")]] bool fetchLast(); /// /// @brief Positions the cursor on the given absolute row number. + /// @return `true` when a row was fetched. /// + [[nodiscard("without checking the result, the cursor position is unknown")]] bool fetchAbsolute(unsigned position); /// /// @brief Moves the cursor by the requested relative offset. + /// @return `true` when a row was fetched. /// + [[nodiscard("without checking the result, the cursor position is unknown")]] bool fetchRelative(int offset); /// diff --git a/src/test/BackupManager.cpp b/src/test/BackupManager.cpp index b97e086..f132f32 100644 --- a/src/test/BackupManager.cpp +++ b/src/test/BackupManager.cpp @@ -169,11 +169,11 @@ BOOST_DATA_TEST_CASE(backupAndRestoreRoundTrip, data::make(BACKUP_RESTORE_VERBOS Statement create{ attachment, transaction, "create table test(id integer not null primary key, name varchar(20))"}; - create.execute(transaction); + (void) create.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into test(id, name) values (1, 'backup')"}; - insert.execute(transaction); + (void) insert.execute(transaction); transaction.commit(); } @@ -225,11 +225,11 @@ BOOST_AUTO_TEST_CASE(restoreReplace) Transaction transaction{attachment}; Statement create{attachment, transaction, "create table test(id integer not null primary key)"}; - create.execute(transaction); + (void) create.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into test(id) values (7)"}; - insert.execute(transaction); + (void) insert.execute(transaction); transaction.commit(); } @@ -270,12 +270,12 @@ BOOST_AUTO_TEST_CASE(multiFileDatabaseAndBackupRoundTrip) Statement create{ attachment, transaction, "create table test(id integer not null primary key, name varchar(50))"}; - create.execute(transaction); + (void) create.execute(transaction); transaction.commitRetaining(); Statement addFile{ attachment, transaction, "alter database add file '" + sourceSecondaryPath + "' starting at page 241"}; - addFile.execute(transaction); + (void) addFile.execute(transaction); transaction.commitRetaining(); for (int i = 1; i <= 200; ++i) @@ -283,7 +283,7 @@ BOOST_AUTO_TEST_CASE(multiFileDatabaseAndBackupRoundTrip) Statement insert{attachment, transaction, "insert into test(id, name) values (?, ?)"}; insert.setInt32(0, i); insert.setString(1, "row-" + std::to_string(i) + std::string(40, 'x')); - insert.execute(transaction); + (void) insert.execute(transaction); } transaction.commitRetaining(); diff --git a/src/test/Batch.cpp b/src/test/Batch.cpp index c231cba..e95bec9 100644 --- a/src/test/Batch.cpp +++ b/src/test/Batch.cpp @@ -45,7 +45,7 @@ BOOST_AUTO_TEST_CASE(constructorFromStatementAndExecute) { // scope Transaction transaction{attachment}; Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null, name varchar(50))"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commit(); } @@ -111,7 +111,7 @@ BOOST_AUTO_TEST_CASE(constructorFromAttachmentAndExecute) { // scope Transaction transaction{attachment}; Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null, val integer)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commit(); } @@ -184,7 +184,7 @@ BOOST_AUTO_TEST_CASE(moveConstructorTransfersOwnership) { // scope Transaction transaction{attachment}; Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commit(); } @@ -220,7 +220,7 @@ BOOST_AUTO_TEST_CASE(executeReportsNoInfoWhenRecordCountsDisabled) { // scope Transaction transaction{attachment}; Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commit(); } @@ -253,7 +253,7 @@ BOOST_AUTO_TEST_CASE(executeWithBadDataReportsExecuteFailed) { // scope Transaction transaction{attachment}; Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null primary key)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commit(); } @@ -312,7 +312,7 @@ BOOST_AUTO_TEST_CASE(cancelDiscardsMessages) { // scope Transaction transaction{attachment}; Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commit(); } @@ -352,7 +352,7 @@ BOOST_AUTO_TEST_CASE(blobWithIdEngine) { // scope Transaction transaction{attachment}; Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null, data blob)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commit(); } @@ -410,7 +410,7 @@ BOOST_AUTO_TEST_CASE(registerExistingBlob) { // scope Transaction transaction{attachment}; Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null, data blob)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commit(); } @@ -473,7 +473,7 @@ BOOST_AUTO_TEST_CASE(closeReleasesHandle) { // scope Transaction transaction{attachment}; Statement ddl{attachment, transaction, "recreate table batch_test (id integer not null)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commit(); } diff --git a/src/test/DatabaseManager.cpp b/src/test/DatabaseManager.cpp index 13cb5d7..d2b0b6e 100644 --- a/src/test/DatabaseManager.cpp +++ b/src/test/DatabaseManager.cpp @@ -118,7 +118,7 @@ BOOST_AUTO_TEST_CASE(restoreWithReplicaMode) CLIENT, sourceDatabaseUri, AttachmentOptions().setCreateDatabase(true).setConnectionCharSet("UTF8")}; Transaction transaction{attachment}; Statement create{attachment, transaction, "create table test(id integer)"}; - create.execute(transaction); + (void) create.execute(transaction); transaction.commit(); } diff --git a/src/test/RowSet.cpp b/src/test/RowSet.cpp index e8ccd65..e12c71b 100644 --- a/src/test/RowSet.cpp +++ b/src/test/RowSet.cpp @@ -40,14 +40,14 @@ BOOST_AUTO_TEST_CASE(fetchRowsIntoRowSet) Transaction transaction{attachment}; Statement ddl{attachment, transaction, "create table t (col integer)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into t (col) values (?)"}; for (int i = 1; i <= 5; ++i) { insert.setInt32(0, i); - insert.execute(transaction); + (void) insert.execute(transaction); } Statement select{attachment, transaction, "select col from t order by col"}; @@ -79,14 +79,14 @@ BOOST_AUTO_TEST_CASE(fetchFewerRowsThanMaxRows) Transaction transaction{attachment}; Statement ddl{attachment, transaction, "create table t (col integer)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into t (col) values (?)"}; for (int i = 1; i <= 3; ++i) { insert.setInt32(0, i); - insert.execute(transaction); + (void) insert.execute(transaction); } Statement select{attachment, transaction, "select col from t order by col"}; @@ -110,14 +110,14 @@ BOOST_AUTO_TEST_CASE(rowSetIsDisconnectedFromStatement) Transaction transaction{attachment}; Statement ddl{attachment, transaction, "create table t (col integer)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into t (col) values (?)"}; for (int i = 1; i <= 3; ++i) { insert.setInt32(0, i); - insert.execute(transaction); + (void) insert.execute(transaction); } Statement select{attachment, transaction, "select col from t order by col"}; @@ -147,14 +147,14 @@ BOOST_AUTO_TEST_CASE(moveConstructor) Transaction transaction{attachment}; Statement ddl{attachment, transaction, "create table t (col integer)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into t (col) values (?)"}; for (int i = 1; i <= 3; ++i) { insert.setInt32(0, i); - insert.execute(transaction); + (void) insert.execute(transaction); } Statement select{attachment, transaction, "select col from t order by col"}; @@ -182,14 +182,14 @@ BOOST_AUTO_TEST_CASE(moveAssignment) Transaction transaction{attachment}; Statement ddl{attachment, transaction, "create table t (col integer)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into t (col) values (?)"}; for (int i = 1; i <= 5; ++i) { insert.setInt32(0, i); - insert.execute(transaction); + (void) insert.execute(transaction); } Statement select{attachment, transaction, "select col from t order by col"}; @@ -223,14 +223,14 @@ BOOST_AUTO_TEST_CASE(fetchMultipleBatchesFromSameStatement) Transaction transaction{attachment}; Statement ddl{attachment, transaction, "create table t (col integer)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into t (col) values (?)"}; for (int i = 1; i <= 10; ++i) { insert.setInt32(0, i); - insert.execute(transaction); + (void) insert.execute(transaction); } Statement select{attachment, transaction, "select col from t order by col"}; diff --git a/src/test/ScrollableCursor.cpp b/src/test/ScrollableCursor.cpp index 7831880..7dbdde4 100644 --- a/src/test/ScrollableCursor.cpp +++ b/src/test/ScrollableCursor.cpp @@ -45,14 +45,14 @@ BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchMethods) Transaction transaction{attachment}; Statement ddl{attachment, transaction, "create table t (col integer)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into t (col) values (?)"}; for (int i = 1; i <= 5; ++i) { insert.setInt32(0, i); - insert.execute(transaction); + (void) insert.execute(transaction); } Statement select{attachment, transaction, "select col from t order by col", diff --git a/src/test/Statement.cpp b/src/test/Statement.cpp index fba0292..879a1f3 100644 --- a/src/test/Statement.cpp +++ b/src/test/Statement.cpp @@ -216,7 +216,7 @@ BOOST_AUTO_TEST_CASE(getTypeReturnsCorrectStatementType) Statement ddlStmt{attachment, transaction, "create table t (col integer)"}; BOOST_CHECK(ddlStmt.getType() == StatementType::DDL); - ddlStmt.execute(transaction); + (void) ddlStmt.execute(transaction); transaction.commitRetaining(); Statement selectStmt{attachment, transaction, "select col from t"}; @@ -292,7 +292,7 @@ BOOST_AUTO_TEST_CASE(descriptorMetadataFields) " amount numeric(18, 2)," " data blob sub_type text" ")"}; - createTable.execute(transaction); + (void) createTable.execute(transaction); transaction.commit(); Transaction transaction2{attachment}; @@ -425,7 +425,7 @@ BOOST_AUTO_TEST_CASE(setNullParameter) Statement select{attachment, transaction, "select cast(? as integer) from rdb$database"}; select.setNull(0); - select.execute(transaction); + (void) select.execute(transaction); BOOST_REQUIRE(select.execute(transaction)); BOOST_CHECK(select.isNull(0)); BOOST_CHECK(!select.getInt32(0).has_value()); @@ -444,7 +444,7 @@ BOOST_AUTO_TEST_CASE(clearParametersToNull) select.setInt32(0, 1); select.setInt32(1, 2); select.clearParameters(); - select.execute(transaction); + (void) select.execute(transaction); BOOST_REQUIRE(select.execute(transaction)); BOOST_CHECK(select.isNull(0)); BOOST_CHECK(select.isNull(1)); @@ -480,7 +480,7 @@ BOOST_AUTO_TEST_CASE(nullRoundTrip) select.setString(1, std::nullopt); select.setDouble(2, std::nullopt); select.setDate(3, std::nullopt); - select.execute(transaction); + (void) select.execute(transaction); BOOST_REQUIRE(select.execute(transaction)); BOOST_CHECK(!select.getInt32(0).has_value()); BOOST_CHECK(!select.getString(1).has_value()); @@ -1571,14 +1571,14 @@ BOOST_AUTO_TEST_CASE(fetchNextIteratesRows) Transaction transaction{attachment}; Statement ddl{attachment, transaction, "create table t (col integer)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into t (col) values (?)"}; for (int i = 1; i <= 5; ++i) { insert.setInt32(0, i); - insert.execute(transaction); + (void) insert.execute(transaction); } Statement select{attachment, transaction, "select col from t order by col"}; @@ -1620,7 +1620,7 @@ BOOST_AUTO_TEST_CASE(cursorMethodsReturnFalseWithoutResultSet) Transaction transaction{attachment}; Statement ddl{attachment, transaction, "create table t (col integer)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into t (col) values (?)"}; @@ -1645,14 +1645,14 @@ BOOST_AUTO_TEST_CASE(cursorName) Transaction transaction{attachment}; Statement ddl{attachment, transaction, "create table t (col integer)"}; - ddl.execute(transaction); + (void) ddl.execute(transaction); transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into t (col) values (?)"}; for (int i = 1; i <= 3; ++i) { insert.setInt32(0, i); - insert.execute(transaction); + (void) insert.execute(transaction); } Statement select{ @@ -1735,7 +1735,7 @@ BOOST_AUTO_TEST_CASE(setScaledBoostInt128ToNumeric38) Statement insert{attachment, transaction, "select cast(? as numeric(38,4)) from rdb$database"}; insert.setScaledBoostInt128(0, testValue); - insert.execute(transaction); + (void) insert.execute(transaction); auto result = insert.getScaledBoostInt128(0); BOOST_REQUIRE(result.has_value()); diff --git a/src/test/Transaction2PC.cpp b/src/test/Transaction2PC.cpp index 476c634..aad2476 100644 --- a/src/test/Transaction2PC.cpp +++ b/src/test/Transaction2PC.cpp @@ -39,7 +39,7 @@ static int countLimbo(Attachment& attachment) Transaction transaction{attachment, options}; Statement statement{attachment, transaction, "select count(*) from rdb$transactions"}; - statement.execute(transaction); + (void) statement.execute(transaction); int count = statement.getInt32(0).value(); transaction.commit(); return count; @@ -54,7 +54,7 @@ static int countRows(Attachment& attachment, const char* table) std::string sql = "select count(*) from "; sql += table; Statement statement{attachment, transaction, sql}; - statement.execute(transaction); + (void) statement.execute(transaction); int count = statement.getInt32(0).value(); transaction.commit(); return count; @@ -266,7 +266,7 @@ BOOST_AUTO_TEST_CASE(statementAcrossMultipleDatabases) { // scope Transaction setupTx{attachment1}; Statement stmt1{attachment1, setupTx, "create table test_table (id integer)"}; - stmt1.execute(setupTx); + (void) stmt1.execute(setupTx); setupTx.commit(); } @@ -274,7 +274,7 @@ BOOST_AUTO_TEST_CASE(statementAcrossMultipleDatabases) { // scope Transaction setupTx{attachment2}; Statement stmt2{attachment2, setupTx, "create table test_table (id integer)"}; - stmt2.execute(setupTx); + (void) stmt2.execute(setupTx); setupTx.commit(); } @@ -283,13 +283,13 @@ BOOST_AUTO_TEST_CASE(statementAcrossMultipleDatabases) // Insert data in first database { // scope Statement stmt1{attachment1, transaction, "insert into test_table (id) values (1)"}; - stmt1.execute(transaction); + (void) stmt1.execute(transaction); } // Insert data in second database { // scope Statement stmt2{attachment2, transaction, "insert into test_table (id) values (2)"}; - stmt2.execute(transaction); + (void) stmt2.execute(transaction); } // Prepare and commit @@ -344,14 +344,14 @@ BOOST_AUTO_TEST_CASE(prepareRollbackData) { // scope Transaction setupTx1{attachment1}; Statement stmt1{attachment1, setupTx1, "create table test_table (id integer)"}; - stmt1.execute(setupTx1); + (void) stmt1.execute(setupTx1); setupTx1.commit(); } { // scope Transaction setupTx2{attachment2}; Statement stmt2{attachment2, setupTx2, "create table test_table (id integer)"}; - stmt2.execute(setupTx2); + (void) stmt2.execute(setupTx2); setupTx2.commit(); } @@ -362,12 +362,12 @@ BOOST_AUTO_TEST_CASE(prepareRollbackData) { // scope Statement stmt1{attachment1, transaction, "insert into test_table (id) values (1)"}; - stmt1.execute(transaction); + (void) stmt1.execute(transaction); } { // scope Statement stmt2{attachment2, transaction, "insert into test_table (id) values (2)"}; - stmt2.execute(transaction); + (void) stmt2.execute(transaction); } BOOST_CHECK_EQUAL(countLimbo(attachment1), 0);