diff --git a/src/fb-cpp/Attachment.cpp b/src/fb-cpp/Attachment.cpp index 63f86ef..17a3e9d 100644 --- a/src/fb-cpp/Attachment.cpp +++ b/src/fb-cpp/Attachment.cpp @@ -147,7 +147,8 @@ RowSet Attachment::queryPreparedRowSet(Statement& statement, Transaction& transa throw FbCppException("Cannot use non-query SQL with Attachment::queryRowSet"); } - const auto hasRow = statement.execute(transaction); + statement.execute(transaction); const auto effectiveMaxRows = statement.getType() == StatementType::EXEC_PROCEDURE ? 1u : maxRows; - return RowSet{statement, hasRow ? effectiveMaxRows : 0u, hasRow}; + + return RowSet{statement, effectiveMaxRows}; } diff --git a/src/fb-cpp/RowSet.cpp b/src/fb-cpp/RowSet.cpp index 64bdb02..13032f5 100644 --- a/src/fb-cpp/RowSet.cpp +++ b/src/fb-cpp/RowSet.cpp @@ -33,18 +33,13 @@ using namespace fbcpp::impl; RowSet::RowSet(Statement& statement, unsigned maxRows) - : RowSet{statement, maxRows, false} -{ -} - -RowSet::RowSet(Statement& statement, unsigned maxRows, bool includeCurrentRow) : client{&statement.getAttachment().getClient()}, statusWrapper{statement.getAttachment().getClient()}, numericConverter{statement.getAttachment().getClient()}, calendarConverter{statement.getAttachment().getClient()} { assert(statement.isValid()); - assert(includeCurrentRow || statement.getResultSetHandle()); + assert(statement.hasCurrentRow() || statement.getResultSetHandle()); descriptors = statement.getOutputDescriptors(); @@ -56,13 +51,14 @@ RowSet::RowSet(Statement& statement, unsigned maxRows, bool includeCurrentRow) auto resultSet = statement.getResultSetHandle(); auto* dest = buffer.data(); - if (includeCurrentRow && maxRows > 0u) + if (statement.hasCurrentRow() && maxRows > 0u) { auto& currentRow = statement.getOutputMessage(); assert(currentRow.size() == messageLength); std::copy(currentRow.begin(), currentRow.end(), dest); dest += messageLength; ++count; + statement.clearCurrentRow(); } if (!resultSet) diff --git a/src/fb-cpp/RowSet.h b/src/fb-cpp/RowSet.h index 6a02dd6..ae9b329 100644 --- a/src/fb-cpp/RowSet.h +++ b/src/fb-cpp/RowSet.h @@ -58,26 +58,18 @@ namespace fbcpp public: /// - /// @brief Fetches up to `maxRows` rows from the current result set of - /// `statement`. + /// @brief Fetches up to `maxRows` rows from `statement`. /// - /// The statement must have an open result set (i.e. `execute()` was - /// called and it is a SELECT-type statement). Rows are fetched via - /// `IResultSet::fetchNext()` directly into the internal buffer. + /// If `statement` is positioned on a current row (typically the row already + /// fetched by `execute()` or a later fetch), that row is copied first and then + /// consumed so a subsequent `RowSet` does not duplicate it. Remaining rows are + /// fetched via `IResultSet::fetchNext()` directly into the internal buffer. /// - /// @param statement The statement with an open result set. + /// @param statement The statement with a current row and/or an open result set. /// @param maxRows Maximum number of rows to fetch. /// explicit RowSet(Statement& statement, unsigned maxRows); - /// - /// @brief Fetches up to `maxRows` rows from the current result set of `statement`. - /// - /// When `includeCurrentRow` is true, the current output message already fetched by `statement.execute()` is - /// copied as the first row before fetching the remaining rows from the result set. - /// - explicit RowSet(Statement& statement, unsigned maxRows, bool includeCurrentRow); - RowSet(RowSet&& o) noexcept : client{o.client}, count{o.count}, diff --git a/src/fb-cpp/Statement.cpp b/src/fb-cpp/Statement.cpp index 6c9ac0c..6db794e 100644 --- a/src/fb-cpp/Statement.cpp +++ b/src/fb-cpp/Statement.cpp @@ -196,9 +196,11 @@ Statement::Statement(Statement&& o) noexcept outMessage{std::move(o.outMessage)}, outRow{std::make_unique(attachment->getClient(), outDescriptors, std::span{outMessage})}, type{o.type}, - cursorFlags{o.cursorFlags} + cursorFlags{o.cursorFlags}, + currentRow{o.currentRow} { o.outRow.reset(); + o.currentRow = false; } Statement& Statement::operator=(Statement&& o) noexcept @@ -220,8 +222,10 @@ Statement& Statement::operator=(Statement&& o) noexcept outRow = std::make_unique(attachment->getClient(), outDescriptors, std::span{outMessage}); type = o.type; cursorFlags = o.cursorFlags; + currentRow = o.currentRow; o.outRow.reset(); + o.currentRow = false; } return *this; @@ -237,6 +241,7 @@ void Statement::free() resultSetHandle.reset(); } + currentRow = false; statementHandle->free(&statusWrapper); statementHandle.reset(); } @@ -266,6 +271,8 @@ bool Statement::execute(Transaction& transaction) resultSetHandle.reset(); } + currentRow = false; + const auto outMessageData = outMessage.data(); if (outMessageData) @@ -280,11 +287,13 @@ bool Statement::execute(Transaction& transaction) case StatementType::SELECT_FOR_UPDATE: resultSetHandle.reset(statementHandle->openCursor(&statusWrapper, transaction.getHandle().get(), inMetadata.get(), inMessage.data(), outMetadata.get(), cursorFlags)); - return resultSetHandle->fetchNext(&statusWrapper, outMessageData) == fb::IStatus::RESULT_OK; + currentRow = resultSetHandle->fetchNext(&statusWrapper, outMessageData) == fb::IStatus::RESULT_OK; + return currentRow; default: statementHandle->execute(&statusWrapper, transaction.getHandle().get(), inMetadata.get(), inMessage.data(), outMetadata.get(), outMessageData); + currentRow = !outDescriptors.empty(); return true; } } @@ -298,43 +307,53 @@ bool Statement::fetchNext() { assert(isValid()); - return resultSetHandle && resultSetHandle->fetchNext(&statusWrapper, outMessage.data()) == fb::IStatus::RESULT_OK; + currentRow = + resultSetHandle && resultSetHandle->fetchNext(&statusWrapper, outMessage.data()) == fb::IStatus::RESULT_OK; + return currentRow; } bool Statement::fetchPrior() { assert(isValid()); - return resultSetHandle && resultSetHandle->fetchPrior(&statusWrapper, outMessage.data()) == fb::IStatus::RESULT_OK; + currentRow = + resultSetHandle && resultSetHandle->fetchPrior(&statusWrapper, outMessage.data()) == fb::IStatus::RESULT_OK; + return currentRow; } bool Statement::fetchFirst() { assert(isValid()); - return resultSetHandle && resultSetHandle->fetchFirst(&statusWrapper, outMessage.data()) == fb::IStatus::RESULT_OK; + currentRow = + resultSetHandle && resultSetHandle->fetchFirst(&statusWrapper, outMessage.data()) == fb::IStatus::RESULT_OK; + return currentRow; } bool Statement::fetchLast() { assert(isValid()); - return resultSetHandle && resultSetHandle->fetchLast(&statusWrapper, outMessage.data()) == fb::IStatus::RESULT_OK; + currentRow = + resultSetHandle && resultSetHandle->fetchLast(&statusWrapper, outMessage.data()) == fb::IStatus::RESULT_OK; + return currentRow; } bool Statement::fetchAbsolute(unsigned position) { assert(isValid()); - return resultSetHandle && + currentRow = resultSetHandle && resultSetHandle->fetchAbsolute(&statusWrapper, static_cast(position), outMessage.data()) == - fb::IStatus::RESULT_OK; + fb::IStatus::RESULT_OK; + return currentRow; } bool Statement::fetchRelative(int offset) { assert(isValid()); - return resultSetHandle && + currentRow = resultSetHandle && resultSetHandle->fetchRelative(&statusWrapper, offset, outMessage.data()) == fb::IStatus::RESULT_OK; + return currentRow; } diff --git a/src/fb-cpp/Statement.h b/src/fb-cpp/Statement.h index aa5616c..3141bea 100644 --- a/src/fb-cpp/Statement.h +++ b/src/fb-cpp/Statement.h @@ -141,6 +141,8 @@ namespace fbcpp /// class FB_CPP_EXPORT Statement final { + friend class RowSet; + public: /// /// Prepares an SQL statement. @@ -209,6 +211,17 @@ namespace fbcpp return statementHandle != nullptr; } + /// + /// Returns whether the statement is positioned on a current output row. + /// + /// A current row is present after a successful `execute()` or fetch that produced + /// output, until the row is consumed by `RowSet` or replaced by a later fetch. + /// + bool hasCurrentRow() const noexcept + { + return currentRow; + } + /// /// @brief Provides direct access to the underlying Firebird statement handle. /// @return Smart pointer to the low-level `fb::IStatement` interface. @@ -2260,6 +2273,11 @@ namespace fbcpp private: Client& getClient() noexcept; + void clearCurrentRow() noexcept + { + currentRow = false; + } + private: Attachment* attachment; impl::StatusWrapper statusWrapper; @@ -2276,6 +2294,7 @@ namespace fbcpp std::unique_ptr outRow; StatementType type; unsigned cursorFlags = 0; + bool currentRow = false; }; /// diff --git a/src/test/RowSet.cpp b/src/test/RowSet.cpp index 2e7b12d..34dccc7 100644 --- a/src/test/RowSet.cpp +++ b/src/test/RowSet.cpp @@ -52,20 +52,21 @@ BOOST_AUTO_TEST_CASE(fetchRowsIntoRowSet) Statement select{attachment, transaction, "select col from t order by col"}; BOOST_REQUIRE(select.execute(transaction)); + BOOST_CHECK(select.hasCurrentRow()); - // The first row (1) was fetched by execute(). Fetch remaining rows into RowSet. RowSet rowSet{select, 10}; - BOOST_CHECK_EQUAL(rowSet.getCount(), 4u); + BOOST_CHECK(!select.hasCurrentRow()); + BOOST_CHECK_EQUAL(rowSet.getCount(), 5u); BOOST_CHECK(rowSet.getMessageLength() > 0); BOOST_CHECK_EQUAL( rowSet.getRawBuffer().size(), static_cast(rowSet.getCount()) * rowSet.getMessageLength()); - // Verify row data using typed Row access. + // Verify row data using typed Row access. The current execute() row is included. for (unsigned i = 0; i < rowSet.getCount(); ++i) { auto row = rowSet.getRow(i); - BOOST_CHECK_EQUAL(row.getInt32(0).value(), static_cast(i + 2)); + BOOST_CHECK_EQUAL(row.getInt32(0).value(), static_cast(i + 1)); } } @@ -92,10 +93,10 @@ BOOST_AUTO_TEST_CASE(fetchFewerRowsThanMaxRows) Statement select{attachment, transaction, "select col from t order by col"}; BOOST_REQUIRE(select.execute(transaction)); - // execute() fetched row 1. Request 100 rows but only 2 remain. + // Request more rows than exist; all 3 rows are returned. RowSet rowSet{select, 100}; - BOOST_CHECK_EQUAL(rowSet.getCount(), 2u); + BOOST_CHECK_EQUAL(rowSet.getCount(), 3u); BOOST_CHECK_EQUAL( rowSet.getRawBuffer().size(), static_cast(rowSet.getCount()) * rowSet.getMessageLength()); } @@ -124,17 +125,18 @@ BOOST_AUTO_TEST_CASE(rowSetIsDisconnectedFromStatement) BOOST_REQUIRE(select.execute(transaction)); RowSet rowSet{select, 10}; - BOOST_CHECK_EQUAL(rowSet.getCount(), 2u); + BOOST_CHECK_EQUAL(rowSet.getCount(), 3u); // Free the statement; the RowSet data is still valid. select.free(); BOOST_CHECK(!rowSet.getRawBuffer().empty()); - BOOST_CHECK_EQUAL(rowSet.getCount(), 2u); + BOOST_CHECK_EQUAL(rowSet.getCount(), 3u); // Typed access still works after the statement is freed. - BOOST_CHECK_EQUAL(rowSet.getRow(0).getInt32(0).value(), 2); - BOOST_CHECK_EQUAL(rowSet.getRow(1).getInt32(0).value(), 3); + BOOST_CHECK_EQUAL(rowSet.getRow(0).getInt32(0).value(), 1); + BOOST_CHECK_EQUAL(rowSet.getRow(1).getInt32(0).value(), 2); + BOOST_CHECK_EQUAL(rowSet.getRow(2).getInt32(0).value(), 3); } BOOST_AUTO_TEST_CASE(moveConstructor) @@ -168,8 +170,9 @@ BOOST_AUTO_TEST_CASE(moveConstructor) BOOST_CHECK_EQUAL(rowSet1.getCount(), 0u); // Typed access works on the moved-to RowSet. - BOOST_CHECK_EQUAL(rowSet2.getRow(0).getInt32(0).value(), 2); - BOOST_CHECK_EQUAL(rowSet2.getRow(1).getInt32(0).value(), 3); + BOOST_CHECK_EQUAL(rowSet2.getRow(0).getInt32(0).value(), 1); + BOOST_CHECK_EQUAL(rowSet2.getRow(1).getInt32(0).value(), 2); + BOOST_CHECK_EQUAL(rowSet2.getRow(2).getInt32(0).value(), 3); } BOOST_AUTO_TEST_CASE(moveAssignment) @@ -195,21 +198,21 @@ BOOST_AUTO_TEST_CASE(moveAssignment) Statement select{attachment, transaction, "select col from t order by col"}; BOOST_REQUIRE(select.execute(transaction)); - // Fetch rows 2-3 into first batch, rows 4-5 into second. + // Fetch rows 1-2 into first batch, rows 3-4 into second. RowSet rowSet1{select, 2}; RowSet rowSet2{select, 2}; BOOST_CHECK_EQUAL(rowSet1.getCount(), 2u); - BOOST_CHECK_EQUAL(rowSet1.getRow(0).getInt32(0).value(), 2); + BOOST_CHECK_EQUAL(rowSet1.getRow(0).getInt32(0).value(), 1); BOOST_CHECK_EQUAL(rowSet2.getCount(), 2u); - BOOST_CHECK_EQUAL(rowSet2.getRow(0).getInt32(0).value(), 4); + BOOST_CHECK_EQUAL(rowSet2.getRow(0).getInt32(0).value(), 3); // Move-assign rowSet2 into rowSet1 (overwrites old data). rowSet1 = std::move(rowSet2); BOOST_CHECK_EQUAL(rowSet1.getCount(), 2u); - BOOST_CHECK_EQUAL(rowSet1.getRow(0).getInt32(0).value(), 4); - BOOST_CHECK_EQUAL(rowSet1.getRow(1).getInt32(0).value(), 5); + BOOST_CHECK_EQUAL(rowSet1.getRow(0).getInt32(0).value(), 3); + BOOST_CHECK_EQUAL(rowSet1.getRow(1).getInt32(0).value(), 4); BOOST_CHECK_EQUAL(rowSet2.getCount(), 0u); } @@ -236,28 +239,102 @@ BOOST_AUTO_TEST_CASE(fetchMultipleBatchesFromSameStatement) Statement select{attachment, transaction, "select col from t order by col"}; BOOST_REQUIRE(select.execute(transaction)); - // execute() fetched row 1. Fetch batches of 3 from the remaining 9 rows. + // Fetch batches of 3, including the current execute() row. RowSet batch1{select, 3}; BOOST_CHECK_EQUAL(batch1.getCount(), 3u); - BOOST_CHECK_EQUAL(batch1.getRow(0).getInt32(0).value(), 2); - BOOST_CHECK_EQUAL(batch1.getRow(1).getInt32(0).value(), 3); - BOOST_CHECK_EQUAL(batch1.getRow(2).getInt32(0).value(), 4); + BOOST_CHECK_EQUAL(batch1.getRow(0).getInt32(0).value(), 1); + BOOST_CHECK_EQUAL(batch1.getRow(1).getInt32(0).value(), 2); + BOOST_CHECK_EQUAL(batch1.getRow(2).getInt32(0).value(), 3); RowSet batch2{select, 3}; BOOST_CHECK_EQUAL(batch2.getCount(), 3u); - BOOST_CHECK_EQUAL(batch2.getRow(0).getInt32(0).value(), 5); - BOOST_CHECK_EQUAL(batch2.getRow(1).getInt32(0).value(), 6); - BOOST_CHECK_EQUAL(batch2.getRow(2).getInt32(0).value(), 7); + BOOST_CHECK_EQUAL(batch2.getRow(0).getInt32(0).value(), 4); + BOOST_CHECK_EQUAL(batch2.getRow(1).getInt32(0).value(), 5); + BOOST_CHECK_EQUAL(batch2.getRow(2).getInt32(0).value(), 6); RowSet batch3{select, 3}; BOOST_CHECK_EQUAL(batch3.getCount(), 3u); - BOOST_CHECK_EQUAL(batch3.getRow(0).getInt32(0).value(), 8); - BOOST_CHECK_EQUAL(batch3.getRow(1).getInt32(0).value(), 9); - BOOST_CHECK_EQUAL(batch3.getRow(2).getInt32(0).value(), 10); + BOOST_CHECK_EQUAL(batch3.getRow(0).getInt32(0).value(), 7); + BOOST_CHECK_EQUAL(batch3.getRow(1).getInt32(0).value(), 8); + BOOST_CHECK_EQUAL(batch3.getRow(2).getInt32(0).value(), 9); - // No more rows; the next batch should be empty. RowSet batch4{select, 3}; - BOOST_CHECK_EQUAL(batch4.getCount(), 0u); + BOOST_CHECK_EQUAL(batch4.getCount(), 1u); + BOOST_CHECK_EQUAL(batch4.getRow(0).getInt32(0).value(), 10); + + // No more rows; the next batch should be empty. + RowSet batch5{select, 3}; + BOOST_CHECK_EQUAL(batch5.getCount(), 0u); +} + +BOOST_AUTO_TEST_CASE(includesCurrentRowAndDoesNotDuplicateAcrossUses) +{ + const auto database = getTempFile("RowSet-includesCurrentRowAndDoesNotDuplicate.fdb"); + + Attachment attachment{getClient(), database, AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + + Statement ddl{attachment, transaction, "create table t (col integer)"}; + ddl.execute(transaction); + transaction.commitRetaining(); + + Statement insert{attachment, transaction, "insert into t (col) values (?)"}; + for (int i = 1; i <= 4; ++i) + { + insert.setInt32(0, i); + insert.execute(transaction); + } + + Statement emptySelect{attachment, transaction, "select col from t where col = 0"}; + BOOST_CHECK(!emptySelect.execute(transaction)); + BOOST_CHECK(!emptySelect.hasCurrentRow()); + + RowSet emptyRowSet{emptySelect, 10}; + BOOST_CHECK_EQUAL(emptyRowSet.getCount(), 0u); + BOOST_CHECK(emptyRowSet.getRawBuffer().empty()); + + Statement select{attachment, transaction, "select col from t order by col"}; + BOOST_REQUIRE(select.execute(transaction)); + BOOST_CHECK(select.hasCurrentRow()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); + + BOOST_REQUIRE(select.fetchNext()); + BOOST_CHECK(select.hasCurrentRow()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 2); + + // After fetchNext(), RowSet starts at the new current row and does not go back to row 1. + RowSet fromCurrent{select, 10}; + BOOST_CHECK(!select.hasCurrentRow()); + BOOST_REQUIRE_EQUAL(fromCurrent.getCount(), 3u); + BOOST_CHECK_EQUAL(fromCurrent.getRow(0).getInt32(0).value(), 2); + BOOST_CHECK_EQUAL(fromCurrent.getRow(1).getInt32(0).value(), 3); + BOOST_CHECK_EQUAL(fromCurrent.getRow(2).getInt32(0).value(), 4); + + BOOST_REQUIRE(select.execute(transaction)); + RowSet firstBatch{select, 2}; + RowSet secondBatch{select, 2}; + BOOST_REQUIRE_EQUAL(firstBatch.getCount(), 2u); + BOOST_CHECK_EQUAL(firstBatch.getRow(0).getInt32(0).value(), 1); + BOOST_CHECK_EQUAL(firstBatch.getRow(1).getInt32(0).value(), 2); + BOOST_REQUIRE_EQUAL(secondBatch.getCount(), 2u); + BOOST_CHECK_EQUAL(secondBatch.getRow(0).getInt32(0).value(), 3); + BOOST_CHECK_EQUAL(secondBatch.getRow(1).getInt32(0).value(), 4); + + Statement procedureDdl{ + attachment, transaction, "create procedure p returns (col integer) as begin col = 42; suspend; end"}; + procedureDdl.execute(transaction); + transaction.commitRetaining(); + + Statement procedure{attachment, transaction, "execute procedure p"}; + BOOST_REQUIRE(procedure.execute(transaction)); + BOOST_CHECK(procedure.hasCurrentRow()); + + RowSet procedureRowSet{procedure, 10}; + BOOST_CHECK(!procedure.hasCurrentRow()); + BOOST_REQUIRE_EQUAL(procedureRowSet.getCount(), 1u); + BOOST_CHECK_EQUAL(procedureRowSet.getRow(0).getInt32(0).value(), 42); } BOOST_AUTO_TEST_SUITE_END()