From 778bb9978004fa2b2f63b70521526f29de5a487b Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Tue, 17 Feb 2026 19:30:34 -0300 Subject: [PATCH 1/2] Add scrollable cursor support to StatementOptions Add CursorType enum (FORWARD_ONLY, SCROLLABLE) and expose it through StatementOptions::setCursorType(). When SCROLLABLE is set, openCursor() passes IStatement::CURSOR_TYPE_SCROLLABLE so that fetchPrior, fetchFirst, fetchLast, fetchAbsolute and fetchRelative work correctly. The default remains FORWARD_ONLY, preserving existing behavior. Fixes https://github.com/asfernandes/fb-cpp/issues/25 --- src/fb-cpp/Statement.cpp | 5 +- src/fb-cpp/Statement.h | 40 +++++- src/test/ScrollableCursor.cpp | 230 ++++++++++++++++++++++++++++++++++ 3 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 src/test/ScrollableCursor.cpp diff --git a/src/fb-cpp/Statement.cpp b/src/fb-cpp/Statement.cpp index 2cd830f..a42a5b1 100644 --- a/src/fb-cpp/Statement.cpp +++ b/src/fb-cpp/Statement.cpp @@ -56,6 +56,9 @@ Statement::Statement( if (options.getCursorName().has_value()) statementHandle->setCursorName(&statusWrapper, options.getCursorName()->c_str()); + if (options.getCursorType() == CursorType::SCROLLABLE) + cursorFlags = fb::IStatement::CURSOR_TYPE_SCROLLABLE; + type = static_cast(statementHandle->getType(&statusWrapper)); switch (type) @@ -223,7 +226,7 @@ bool Statement::execute(Transaction& transaction) case StatementType::SELECT: case StatementType::SELECT_FOR_UPDATE: resultSetHandle.reset(statementHandle->openCursor(&statusWrapper, transaction.getHandle().get(), - inMetadata.get(), inMessage.data(), outMetadata.get(), 0)); + inMetadata.get(), inMessage.data(), outMetadata.get(), cursorFlags)); return resultSetHandle->fetchNext(&statusWrapper, outMessageData) == fb::IStatus::RESULT_OK; default: diff --git a/src/fb-cpp/Statement.h b/src/fb-cpp/Statement.h index 93e81cf..3d1e30c 100644 --- a/src/fb-cpp/Statement.h +++ b/src/fb-cpp/Statement.h @@ -66,6 +66,22 @@ namespace fbcpp { class Transaction; + /// + /// @brief Selects the cursor type for a SELECT statement. + /// + enum class CursorType + { + /// + /// Forward-only traversal (default, more efficient for streaming). + /// + FORWARD_ONLY, + + /// + /// Allows bidirectional traversal and absolute/relative positioning. + /// + SCROLLABLE, + }; + /// /// Represents options used when preparing a Statement. /// @@ -129,10 +145,30 @@ namespace fbcpp return *this; } + /// + /// @brief Returns the cursor type to be used when opening a result set. + /// + CursorType getCursorType() const + { + return cursorType; + } + + /// + /// @brief Sets the cursor type used when opening a result set. + /// @param value `FORWARD_ONLY` for streaming access, `SCROLLABLE` for bidirectional navigation. + /// @return Reference to this instance for fluent configuration. + /// + StatementOptions& setCursorType(CursorType value) + { + cursorType = value; + return *this; + } + private: bool prefetchLegacyPlan = false; bool prefetchPlan = false; std::optional cursorName; + CursorType cursorType = CursorType::FORWARD_ONLY; }; /// @@ -231,7 +267,8 @@ namespace fbcpp outMetadata{std::move(o.outMetadata)}, outDescriptors{std::move(o.outDescriptors)}, outMessage{std::move(o.outMessage)}, - type{o.type} + type{o.type}, + cursorFlags{o.cursorFlags} { } @@ -2736,6 +2773,7 @@ namespace fbcpp std::vector outDescriptors; std::vector outMessage; StatementType type; + unsigned cursorFlags = 0; }; /// diff --git a/src/test/ScrollableCursor.cpp b/src/test/ScrollableCursor.cpp new file mode 100644 index 0000000..24fc2f0 --- /dev/null +++ b/src/test/ScrollableCursor.cpp @@ -0,0 +1,230 @@ +/* + * MIT License + * + * Copyright (c) 2025 Adriano dos Santos Fernandes + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "TestUtil.h" +#include "fb-cpp/Statement.h" +#include "fb-cpp/Transaction.h" + + +BOOST_AUTO_TEST_SUITE(ScrollableCursorSuite) + +BOOST_AUTO_TEST_CASE(defaultCursorTypeIsForwardOnly) +{ + StatementOptions options; + BOOST_CHECK(options.getCursorType() == CursorType::FORWARD_ONLY); +} + +BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchFirst) +{ + const auto database = getTempFile("ScrollableCursor-fetchFirst.fdb"); + + Attachment attachment{CLIENT, 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 <= 3; ++i) + { + insert.setInt32(0, i); + insert.execute(transaction); + } + + Statement select{attachment, transaction, "select col from t order by col", + StatementOptions().setCursorType(CursorType::SCROLLABLE)}; + BOOST_REQUIRE(select.execute(transaction)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); + + BOOST_REQUIRE(select.fetchNext()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 2); + + BOOST_REQUIRE(select.fetchFirst()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); +} + +BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchLast) +{ + const auto database = getTempFile("ScrollableCursor-fetchLast.fdb"); + + Attachment attachment{CLIENT, 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 <= 3; ++i) + { + insert.setInt32(0, i); + insert.execute(transaction); + } + + Statement select{attachment, transaction, "select col from t order by col", + StatementOptions().setCursorType(CursorType::SCROLLABLE)}; + BOOST_REQUIRE(select.execute(transaction)); + + BOOST_REQUIRE(select.fetchLast()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 3); +} + +BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchPrior) +{ + const auto database = getTempFile("ScrollableCursor-fetchPrior.fdb"); + + Attachment attachment{CLIENT, 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 <= 3; ++i) + { + insert.setInt32(0, i); + insert.execute(transaction); + } + + Statement select{attachment, transaction, "select col from t order by col", + StatementOptions().setCursorType(CursorType::SCROLLABLE)}; + BOOST_REQUIRE(select.execute(transaction)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); + + BOOST_REQUIRE(select.fetchNext()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 2); + + BOOST_REQUIRE(select.fetchPrior()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); +} + +BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchAbsolute) +{ + const auto database = getTempFile("ScrollableCursor-fetchAbsolute.fdb"); + + Attachment attachment{CLIENT, 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 <= 5; ++i) + { + insert.setInt32(0, i); + insert.execute(transaction); + } + + Statement select{attachment, transaction, "select col from t order by col", + StatementOptions().setCursorType(CursorType::SCROLLABLE)}; + BOOST_REQUIRE(select.execute(transaction)); + + BOOST_REQUIRE(select.fetchAbsolute(3)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 3); + + BOOST_REQUIRE(select.fetchAbsolute(1)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); + + BOOST_REQUIRE(select.fetchAbsolute(5)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 5); +} + +BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchRelative) +{ + const auto database = getTempFile("ScrollableCursor-fetchRelative.fdb"); + + Attachment attachment{CLIENT, 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 <= 5; ++i) + { + insert.setInt32(0, i); + insert.execute(transaction); + } + + Statement select{attachment, transaction, "select col from t order by col", + StatementOptions().setCursorType(CursorType::SCROLLABLE)}; + BOOST_REQUIRE(select.execute(transaction)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); + + BOOST_REQUIRE(select.fetchRelative(2)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 3); + + BOOST_REQUIRE(select.fetchRelative(-1)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 2); +} + +BOOST_AUTO_TEST_CASE(forwardOnlyIsDefault) +{ + const auto database = getTempFile("ScrollableCursor-forwardOnlyIsDefault.fdb"); + + Attachment attachment{CLIENT, 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 <= 3; ++i) + { + insert.setInt32(0, i); + insert.execute(transaction); + } + + // Default options — forward-only cursor + Statement select{attachment, transaction, "select col from t order by col"}; + BOOST_REQUIRE(select.execute(transaction)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); + + BOOST_REQUIRE(select.fetchNext()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 2); + + BOOST_REQUIRE(select.fetchNext()); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 3); + + BOOST_CHECK_EQUAL(select.fetchNext(), false); +} + +BOOST_AUTO_TEST_SUITE_END() From 85003687b41d9bbb363b23ea53d0e69ac7c9bce5 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Wed, 18 Feb 2026 18:51:59 -0300 Subject: [PATCH 2/2] Update ScrollableCursor tests to support multiple fetch methods in a single test. --- src/test/ScrollableCursor.cpp | 167 +++------------------------------- 1 file changed, 14 insertions(+), 153 deletions(-) diff --git a/src/test/ScrollableCursor.cpp b/src/test/ScrollableCursor.cpp index 24fc2f0..b08eb7b 100644 --- a/src/test/ScrollableCursor.cpp +++ b/src/test/ScrollableCursor.cpp @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2025 Adriano dos Santos Fernandes + * Copyright (c) 2026 F.D.Castel * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -35,9 +35,9 @@ BOOST_AUTO_TEST_CASE(defaultCursorTypeIsForwardOnly) BOOST_CHECK(options.getCursorType() == CursorType::FORWARD_ONLY); } -BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchFirst) +BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchMethods) { - const auto database = getTempFile("ScrollableCursor-fetchFirst.fdb"); + const auto database = getTempFile("ScrollableCursor-fetchMethods.fdb"); Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)}; FbDropDatabase attachmentDrop{attachment}; @@ -49,7 +49,7 @@ BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchFirst) transaction.commitRetaining(); Statement insert{attachment, transaction, "insert into t (col) values (?)"}; - for (int i = 1; i <= 3; ++i) + for (int i = 1; i <= 5; ++i) { insert.setInt32(0, i); insert.execute(transaction); @@ -60,171 +60,32 @@ BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchFirst) BOOST_REQUIRE(select.execute(transaction)); BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); + // fetchNext BOOST_REQUIRE(select.fetchNext()); BOOST_CHECK_EQUAL(select.getInt32(0).value(), 2); + // fetchFirst BOOST_REQUIRE(select.fetchFirst()); BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); -} - -BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchLast) -{ - const auto database = getTempFile("ScrollableCursor-fetchLast.fdb"); - - Attachment attachment{CLIENT, 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 <= 3; ++i) - { - insert.setInt32(0, i); - insert.execute(transaction); - } - - Statement select{attachment, transaction, "select col from t order by col", - StatementOptions().setCursorType(CursorType::SCROLLABLE)}; - BOOST_REQUIRE(select.execute(transaction)); + // fetchLast BOOST_REQUIRE(select.fetchLast()); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 3); -} - -BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchPrior) -{ - const auto database = getTempFile("ScrollableCursor-fetchPrior.fdb"); - - Attachment attachment{CLIENT, 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 <= 3; ++i) - { - insert.setInt32(0, i); - insert.execute(transaction); - } - - Statement select{attachment, transaction, "select col from t order by col", - StatementOptions().setCursorType(CursorType::SCROLLABLE)}; - BOOST_REQUIRE(select.execute(transaction)); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); - - BOOST_REQUIRE(select.fetchNext()); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 2); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 5); + // fetchPrior BOOST_REQUIRE(select.fetchPrior()); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); -} - -BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchAbsolute) -{ - const auto database = getTempFile("ScrollableCursor-fetchAbsolute.fdb"); - - Attachment attachment{CLIENT, 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 <= 5; ++i) - { - insert.setInt32(0, i); - insert.execute(transaction); - } - - Statement select{attachment, transaction, "select col from t order by col", - StatementOptions().setCursorType(CursorType::SCROLLABLE)}; - BOOST_REQUIRE(select.execute(transaction)); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 4); + // fetchAbsolute BOOST_REQUIRE(select.fetchAbsolute(3)); BOOST_CHECK_EQUAL(select.getInt32(0).value(), 3); - BOOST_REQUIRE(select.fetchAbsolute(1)); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); - - BOOST_REQUIRE(select.fetchAbsolute(5)); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 5); -} - -BOOST_AUTO_TEST_CASE(scrollableCursorSupportsFetchRelative) -{ - const auto database = getTempFile("ScrollableCursor-fetchRelative.fdb"); - - Attachment attachment{CLIENT, 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 <= 5; ++i) - { - insert.setInt32(0, i); - insert.execute(transaction); - } - - Statement select{attachment, transaction, "select col from t order by col", - StatementOptions().setCursorType(CursorType::SCROLLABLE)}; - BOOST_REQUIRE(select.execute(transaction)); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); - + // fetchRelative BOOST_REQUIRE(select.fetchRelative(2)); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 3); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 5); BOOST_REQUIRE(select.fetchRelative(-1)); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 2); -} - -BOOST_AUTO_TEST_CASE(forwardOnlyIsDefault) -{ - const auto database = getTempFile("ScrollableCursor-forwardOnlyIsDefault.fdb"); - - Attachment attachment{CLIENT, 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 <= 3; ++i) - { - insert.setInt32(0, i); - insert.execute(transaction); - } - - // Default options — forward-only cursor - Statement select{attachment, transaction, "select col from t order by col"}; - BOOST_REQUIRE(select.execute(transaction)); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 1); - - BOOST_REQUIRE(select.fetchNext()); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 2); - - BOOST_REQUIRE(select.fetchNext()); - BOOST_CHECK_EQUAL(select.getInt32(0).value(), 3); - - BOOST_CHECK_EQUAL(select.fetchNext(), false); + BOOST_CHECK_EQUAL(select.getInt32(0).value(), 4); } BOOST_AUTO_TEST_SUITE_END()