From 35778e3d613e156920c38a60673049a63f42ccf4 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Tue, 16 Jun 2026 22:02:06 -0300 Subject: [PATCH 1/6] Add Attachment::execute methods --- src/fb-cpp/Attachment.cpp | 13 +++++++++++++ src/fb-cpp/Attachment.h | 13 +++++++++++++ src/test/Attachment.cpp | 17 +++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/src/fb-cpp/Attachment.cpp b/src/fb-cpp/Attachment.cpp index 02527e4..13037a4 100644 --- a/src/fb-cpp/Attachment.cpp +++ b/src/fb-cpp/Attachment.cpp @@ -25,6 +25,8 @@ #include "Attachment.h" #include "Client.h" #include "Exception.h" +#include "Statement.h" +#include "Transaction.h" using namespace fbcpp; using namespace fbcpp::impl; @@ -93,3 +95,14 @@ void Attachment::dropDatabase() { disconnectOrDrop(true); } + +bool Attachment::execute(Transaction& transaction, std::string_view sql) +{ + return execute(transaction, sql, StatementOptions{}); +} + +bool Attachment::execute(Transaction& transaction, std::string_view sql, const StatementOptions& options) +{ + Statement statement{*this, transaction, sql, options}; + return statement.execute(transaction); +} diff --git a/src/fb-cpp/Attachment.h b/src/fb-cpp/Attachment.h index 0f98a61..9532b8c 100644 --- a/src/fb-cpp/Attachment.h +++ b/src/fb-cpp/Attachment.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -41,6 +42,8 @@ namespace fbcpp { class Client; + class StatementOptions; + class Transaction; /// /// Represents options used when creating an Attachment object. @@ -301,6 +304,16 @@ namespace fbcpp /// void dropDatabase(); + /// + /// Prepares and executes an SQL statement using the supplied transaction. + /// + bool execute(Transaction& transaction, std::string_view sql); + + /// + /// Prepares and executes an SQL statement using the supplied transaction and statement options. + /// + bool execute(Transaction& transaction, std::string_view sql, const StatementOptions& options); + private: void disconnectOrDrop(bool drop); diff --git a/src/test/Attachment.cpp b/src/test/Attachment.cpp index 7003e60..3e4863b 100644 --- a/src/test/Attachment.cpp +++ b/src/test/Attachment.cpp @@ -92,6 +92,23 @@ BOOST_AUTO_TEST_CASE(createDatabaseWithForcedWritesOff) transaction.commit(); } +BOOST_AUTO_TEST_CASE(executePreparesAndExecutesStatement) +{ + const auto database = getTempFile("Attachment-executePreparesAndExecutesStatement.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_CHECK(attachment.execute(transaction, "create table t (id integer not null primary key)")); + transaction.commitRetaining(); + + BOOST_CHECK(attachment.execute(transaction, "insert into t (id) values (1)")); + BOOST_CHECK(attachment.execute(transaction, "select id from t")); + BOOST_CHECK(!attachment.execute(transaction, "select id from t where id = 2")); + + transaction.commit(); +} + BOOST_AUTO_TEST_CASE(isNotValidAfterMove) { const auto database = getTempFile("Attachment-isNotValidAfterMove.fdb"); From 48e47d7c3fc6bcd2f67e69a7a6cd90fd65f3ed89 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Tue, 16 Jun 2026 22:02:06 -0300 Subject: [PATCH 2/6] Add Attachment::queryRowSet methods --- src/fb-cpp/Attachment.cpp | 31 +++++++++ src/fb-cpp/Attachment.h | 13 ++++ src/fb-cpp/RowSet.cpp | 25 ++++++- src/fb-cpp/RowSet.h | 8 +++ src/test/Attachment.cpp | 135 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 2 deletions(-) diff --git a/src/fb-cpp/Attachment.cpp b/src/fb-cpp/Attachment.cpp index 13037a4..5face5e 100644 --- a/src/fb-cpp/Attachment.cpp +++ b/src/fb-cpp/Attachment.cpp @@ -25,6 +25,7 @@ #include "Attachment.h" #include "Client.h" #include "Exception.h" +#include "RowSet.h" #include "Statement.h" #include "Transaction.h" @@ -106,3 +107,33 @@ bool Attachment::execute(Transaction& transaction, std::string_view sql, const S Statement statement{*this, transaction, sql, options}; return statement.execute(transaction); } + +RowSet Attachment::queryRowSet(Transaction& transaction, std::string_view sql, unsigned maxRows) +{ + return queryRowSet(transaction, sql, maxRows, StatementOptions{}); +} + +RowSet Attachment::queryRowSet( + Transaction& transaction, std::string_view sql, unsigned maxRows, const StatementOptions& options) +{ + Statement statement{*this, transaction, sql, options}; + + switch (statement.getType()) + { + case StatementType::SELECT: + case StatementType::SELECT_FOR_UPDATE: + break; + + case StatementType::EXEC_PROCEDURE: + if (!statement.getOutputDescriptors().empty()) + break; + + throw FbCppException("Cannot use procedure without output columns with Attachment::queryRowSet"); + + default: + throw FbCppException("Cannot use non-query SQL with Attachment::queryRowSet"); + } + + const auto hasRow = statement.execute(transaction); + return RowSet{statement, hasRow ? maxRows : 0u, hasRow}; +} diff --git a/src/fb-cpp/Attachment.h b/src/fb-cpp/Attachment.h index 9532b8c..a9c8feb 100644 --- a/src/fb-cpp/Attachment.h +++ b/src/fb-cpp/Attachment.h @@ -42,6 +42,7 @@ namespace fbcpp { class Client; + class RowSet; class StatementOptions; class Transaction; @@ -314,6 +315,18 @@ namespace fbcpp /// bool execute(Transaction& transaction, std::string_view sql, const StatementOptions& options); + /// + /// Prepares and executes a query using the supplied transaction and returns up to maxRows rows. + /// + RowSet queryRowSet(Transaction& transaction, std::string_view sql, unsigned maxRows); + + /// + /// Prepares and executes a query using the supplied transaction and statement options and returns up to maxRows + /// rows. + /// + RowSet queryRowSet( + Transaction& transaction, std::string_view sql, unsigned maxRows, const StatementOptions& options); + private: void disconnectOrDrop(bool drop); diff --git a/src/fb-cpp/RowSet.cpp b/src/fb-cpp/RowSet.cpp index de3dce4..7cf56a6 100644 --- a/src/fb-cpp/RowSet.cpp +++ b/src/fb-cpp/RowSet.cpp @@ -25,19 +25,25 @@ #include "RowSet.h" #include "Client.h" #include "Statement.h" +#include using namespace fbcpp; 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(statement.getResultSetHandle()); + assert(includeCurrentRow || statement.getResultSetHandle()); descriptors = statement.getOutputDescriptors(); @@ -49,7 +55,22 @@ RowSet::RowSet(Statement& statement, unsigned maxRows) auto resultSet = statement.getResultSetHandle(); auto* dest = buffer.data(); - for (unsigned i = 0; i < maxRows; ++i) + if (includeCurrentRow && maxRows > 0u) + { + auto& currentRow = statement.getOutputMessage(); + assert(currentRow.size() == messageLength); + std::copy(currentRow.begin(), currentRow.end(), dest); + dest += messageLength; + ++count; + } + + if (!resultSet) + { + buffer.resize(static_cast(dest - buffer.data())); + return; + } + + for (unsigned i = count; i < maxRows; ++i) { if (resultSet->fetchNext(&statusWrapper, dest) != fb::IStatus::RESULT_OK) break; diff --git a/src/fb-cpp/RowSet.h b/src/fb-cpp/RowSet.h index 1d39752..be96cdd 100644 --- a/src/fb-cpp/RowSet.h +++ b/src/fb-cpp/RowSet.h @@ -69,6 +69,14 @@ namespace fbcpp /// 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/test/Attachment.cpp b/src/test/Attachment.cpp index 3e4863b..b655b57 100644 --- a/src/test/Attachment.cpp +++ b/src/test/Attachment.cpp @@ -25,6 +25,7 @@ #include "TestUtil.h" #include "fb-cpp/Attachment.h" #include "fb-cpp/Exception.h" +#include "fb-cpp/RowSet.h" #include "fb-cpp/Statement.h" #include "fb-cpp/Transaction.h" #include @@ -109,6 +110,140 @@ BOOST_AUTO_TEST_CASE(executePreparesAndExecutesStatement) transaction.commit(); } +BOOST_AUTO_TEST_CASE(queryReturnsRowsIncludingFirstRow) +{ + const auto database = getTempFile("Attachment-queryReturnsRowsIncludingFirstRow.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE(attachment.execute(transaction, "create table t (id integer not null primary key)")); + transaction.commitRetaining(); + + for (int i = 1; i <= 3; ++i) + { + Statement insert{attachment, transaction, "insert into t (id) values (?)"}; + insert.setInt32(0, i); + BOOST_REQUIRE(insert.execute(transaction)); + } + + auto rowSet = attachment.queryRowSet(transaction, "select id from t order by id", 10u); + + BOOST_REQUIRE_EQUAL(rowSet.getCount(), 3u); + 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); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryHonorsMaxRows) +{ + const auto database = getTempFile("Attachment-queryHonorsMaxRows.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE(attachment.execute(transaction, "create table t (id integer not null primary key)")); + transaction.commitRetaining(); + + for (int i = 1; i <= 5; ++i) + { + Statement insert{attachment, transaction, "insert into t (id) values (?)"}; + insert.setInt32(0, i); + BOOST_REQUIRE(insert.execute(transaction)); + } + + auto rowSet = attachment.queryRowSet(transaction, "select id from t order by id", 2u); + + BOOST_REQUIRE_EQUAL(rowSet.getCount(), 2u); + BOOST_CHECK_EQUAL(rowSet.getRow(0).getInt32(0).value(), 1); + BOOST_CHECK_EQUAL(rowSet.getRow(1).getInt32(0).value(), 2); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryReturnsEmptyRowSetForNoRows) +{ + const auto database = getTempFile("Attachment-queryReturnsEmptyRowSetForNoRows.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE(attachment.execute(transaction, "create table t (id integer not null primary key)")); + transaction.commitRetaining(); + + auto rowSet = attachment.queryRowSet(transaction, "select id from t", 10u); + + BOOST_CHECK_EQUAL(rowSet.getCount(), 0u); + BOOST_CHECK(rowSet.getRawBuffer().empty()); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(querySupportsStatementOptions) +{ + const auto database = getTempFile("Attachment-querySupportsStatementOptions.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + auto rowSet = + attachment.queryRowSet(transaction, "select 1 from rdb$database", 1u, StatementOptions().setDialect(3u)); + + BOOST_REQUIRE_EQUAL(rowSet.getCount(), 1u); + BOOST_CHECK_EQUAL(rowSet.getRow(0).getInt32(0).value(), 1); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryThrowsForNonQueryStatement) +{ + const auto database = getTempFile("Attachment-queryThrowsForNonQueryStatement.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_CHECK_THROW(attachment.queryRowSet(transaction, "create table t (id integer)", 10u), FbCppException); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryRowSetSupportsProcedureWithOutputColumns) +{ + const auto database = getTempFile("Attachment-queryRowSetSupportsProcedureWithOutputColumns.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE(attachment.execute(transaction, + "create procedure p returns (id integer, name varchar(20)) as begin id = 42; name = 'answer'; suspend; end")); + transaction.commitRetaining(); + + auto rowSet = attachment.queryRowSet(transaction, "execute procedure p", 10u); + + BOOST_REQUIRE_EQUAL(rowSet.getCount(), 1u); + BOOST_CHECK_EQUAL(rowSet.getRow(0).getInt32(0).value(), 42); + BOOST_CHECK_EQUAL(rowSet.getRow(0).getString(1).value(), "answer"); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryRowSetRejectsProcedureWithoutOutputColumns) +{ + const auto database = getTempFile("Attachment-queryRowSetRejectsProcedureWithoutOutputColumns.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE(attachment.execute(transaction, "create procedure p as begin end")); + transaction.commitRetaining(); + + BOOST_CHECK_THROW(attachment.queryRowSet(transaction, "execute procedure p", 10u), FbCppException); + + transaction.commit(); +} + BOOST_AUTO_TEST_CASE(isNotValidAfterMove) { const auto database = getTempFile("Attachment-isNotValidAfterMove.fdb"); From 13748f6c035db507bed6fb0186423f99c991c1eb Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Tue, 16 Jun 2026 22:02:06 -0300 Subject: [PATCH 3/6] Add Attachment::queryScalar methods --- src/fb-cpp/Attachment.h | 38 +++++++++++++++- src/test/Attachment.cpp | 98 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/src/fb-cpp/Attachment.h b/src/fb-cpp/Attachment.h index a9c8feb..8503d01 100644 --- a/src/fb-cpp/Attachment.h +++ b/src/fb-cpp/Attachment.h @@ -27,6 +27,7 @@ #include "fb-api.h" #include "SmartPtrs.h" +#include "RowSet.h" #include #include #include @@ -42,7 +43,6 @@ namespace fbcpp { class Client; - class RowSet; class StatementOptions; class Transaction; @@ -327,6 +327,19 @@ namespace fbcpp RowSet queryRowSet( Transaction& transaction, std::string_view sql, unsigned maxRows, const StatementOptions& options); + /// + /// Prepares and executes a query using the supplied transaction and returns the first column of the first row. + /// + template + std::optional queryScalar(Transaction& transaction, std::string_view sql); + + /// + /// Prepares and executes a query using the supplied transaction and statement options and returns the first + /// column of the first row. + /// + template + std::optional queryScalar(Transaction& transaction, std::string_view sql, const StatementOptions& options); + private: void disconnectOrDrop(bool drop); @@ -334,6 +347,29 @@ namespace fbcpp Client* client; FbRef handle; }; + + template + std::optional Attachment::queryScalar(Transaction& transaction, std::string_view sql) + { + auto rowSet = queryRowSet(transaction, sql, 1u); + + if (rowSet.getCount() == 0u) + return std::nullopt; + + return rowSet.getRow(0).get>(0); + } + + template + std::optional Attachment::queryScalar( + Transaction& transaction, std::string_view sql, const StatementOptions& options) + { + auto rowSet = queryRowSet(transaction, sql, 1u, options); + + if (rowSet.getCount() == 0u) + return std::nullopt; + + return rowSet.getRow(0).get>(0); + } } // namespace fbcpp diff --git a/src/test/Attachment.cpp b/src/test/Attachment.cpp index b655b57..d0e356b 100644 --- a/src/test/Attachment.cpp +++ b/src/test/Attachment.cpp @@ -244,6 +244,104 @@ BOOST_AUTO_TEST_CASE(queryRowSetRejectsProcedureWithoutOutputColumns) transaction.commit(); } +BOOST_AUTO_TEST_CASE(queryScalarReturnsFirstColumnOfFirstRow) +{ + const auto database = getTempFile("Attachment-queryScalarReturnsFirstColumnOfFirstRow.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE( + attachment.execute(transaction, "create table t (id integer not null primary key, name varchar(20))")); + transaction.commitRetaining(); + + BOOST_REQUIRE(attachment.execute(transaction, "insert into t (id, name) values (1, 'one')")); + BOOST_REQUIRE(attachment.execute(transaction, "insert into t (id, name) values (2, 'two')")); + + const auto value = attachment.queryScalar(transaction, "select name, id from t order by id"); + + BOOST_REQUIRE(value.has_value()); + BOOST_CHECK_EQUAL(*value, "one"); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryScalarReturnsNulloptForNoRows) +{ + const auto database = getTempFile("Attachment-queryScalarReturnsNulloptForNoRows.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + const auto value = attachment.queryScalar(transaction, "select 1 from rdb$database where 1 = 0"); + + BOOST_CHECK(!value.has_value()); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryScalarReturnsNulloptForNullColumn) +{ + const auto database = getTempFile("Attachment-queryScalarReturnsNulloptForNullColumn.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + const auto value = + attachment.queryScalar(transaction, "select cast(null as integer) from rdb$database"); + + BOOST_CHECK(!value.has_value()); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryScalarSupportsStatementOptions) +{ + const auto database = getTempFile("Attachment-queryScalarSupportsStatementOptions.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + const auto value = attachment.queryScalar( + transaction, "select 1 from rdb$database", StatementOptions().setDialect(3u)); + + BOOST_REQUIRE(value.has_value()); + BOOST_CHECK_EQUAL(*value, 1); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryScalarSupportsProcedureWithOutputColumns) +{ + const auto database = getTempFile("Attachment-queryScalarSupportsProcedureWithOutputColumns.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE(attachment.execute(transaction, + "create procedure p returns (id integer, name varchar(20)) as begin id = 42; name = 'answer'; suspend; end")); + transaction.commitRetaining(); + + const auto value = attachment.queryScalar(transaction, "execute procedure p"); + + BOOST_REQUIRE(value.has_value()); + BOOST_CHECK_EQUAL(*value, 42); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryScalarThrowsForNonQueryStatement) +{ + const auto database = getTempFile("Attachment-queryScalarThrowsForNonQueryStatement.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_CHECK_THROW(attachment.queryScalar(transaction, "create table t (id integer)"), FbCppException); + + transaction.commit(); +} + BOOST_AUTO_TEST_CASE(isNotValidAfterMove) { const auto database = getTempFile("Attachment-isNotValidAfterMove.fdb"); From c2538c3a5b7ed596089873ff964d7d6268b5efcd Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Tue, 16 Jun 2026 22:02:06 -0300 Subject: [PATCH 4/6] Add Attachment::queryFirstRowAs methods --- src/fb-cpp/Attachment.h | 37 +++++++++ src/test/Attachment.cpp | 165 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/src/fb-cpp/Attachment.h b/src/fb-cpp/Attachment.h index 8503d01..52c1f7b 100644 --- a/src/fb-cpp/Attachment.h +++ b/src/fb-cpp/Attachment.h @@ -340,6 +340,20 @@ namespace fbcpp template std::optional queryScalar(Transaction& transaction, std::string_view sql, const StatementOptions& options); + /// + /// Prepares and executes a query using the supplied transaction and returns the first row mapped as T. + /// + template + std::optional queryFirstRowAs(Transaction& transaction, std::string_view sql); + + /// + /// Prepares and executes a query using the supplied transaction and statement options and returns the first row + /// mapped as T. + /// + template + std::optional queryFirstRowAs( + Transaction& transaction, std::string_view sql, const StatementOptions& options); + private: void disconnectOrDrop(bool drop); @@ -370,6 +384,29 @@ namespace fbcpp return rowSet.getRow(0).get>(0); } + + template + std::optional Attachment::queryFirstRowAs(Transaction& transaction, std::string_view sql) + { + auto rowSet = queryRowSet(transaction, sql, 1u); + + if (rowSet.getCount() == 0u) + return std::nullopt; + + return rowSet.getRow(0).get(); + } + + template + std::optional Attachment::queryFirstRowAs( + Transaction& transaction, std::string_view sql, const StatementOptions& options) + { + auto rowSet = queryRowSet(transaction, sql, 1u, options); + + if (rowSet.getCount() == 0u) + return std::nullopt; + + return rowSet.getRow(0).get(); + } } // namespace fbcpp diff --git a/src/test/Attachment.cpp b/src/test/Attachment.cpp index d0e356b..b5b38a7 100644 --- a/src/test/Attachment.cpp +++ b/src/test/Attachment.cpp @@ -342,6 +342,171 @@ BOOST_AUTO_TEST_CASE(queryScalarThrowsForNonQueryStatement) transaction.commit(); } +BOOST_AUTO_TEST_CASE(queryFirstRowAsReturnsStructFromFirstRow) +{ + struct Result + { + std::int32_t id; + std::optional name; + }; + + const auto database = getTempFile("Attachment-queryFirstRowAsReturnsStructFromFirstRow.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE( + attachment.execute(transaction, "create table t (id integer not null primary key, name varchar(20))")); + transaction.commitRetaining(); + + BOOST_REQUIRE(attachment.execute(transaction, "insert into t (id, name) values (1, 'one')")); + BOOST_REQUIRE(attachment.execute(transaction, "insert into t (id, name) values (2, 'two')")); + + const auto value = attachment.queryFirstRowAs(transaction, "select id, name from t order by id"); + + BOOST_REQUIRE(value.has_value()); + BOOST_CHECK_EQUAL(value->id, 1); + BOOST_REQUIRE(value->name.has_value()); + BOOST_CHECK_EQUAL(*value->name, "one"); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryFirstRowAsReturnsTupleFromFirstRow) +{ + using Result = std::tuple>; + + const auto database = getTempFile("Attachment-queryFirstRowAsReturnsTupleFromFirstRow.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + const auto value = attachment.queryFirstRowAs(transaction, "select 42, 'answer' from rdb$database"); + + BOOST_REQUIRE(value.has_value()); + BOOST_CHECK_EQUAL(std::get<0>(*value), 42); + BOOST_REQUIRE(std::get<1>(*value).has_value()); + BOOST_CHECK_EQUAL(*std::get<1>(*value), "answer"); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryFirstRowAsReturnsNulloptForNoRows) +{ + struct Result + { + std::int32_t id; + }; + + const auto database = getTempFile("Attachment-queryFirstRowAsReturnsNulloptForNoRows.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + const auto value = attachment.queryFirstRowAs(transaction, "select 1 from rdb$database where 1 = 0"); + + BOOST_CHECK(!value.has_value()); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryFirstRowAsSupportsStatementOptions) +{ + using Result = std::tuple; + + const auto database = getTempFile("Attachment-queryFirstRowAsSupportsStatementOptions.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + const auto value = attachment.queryFirstRowAs( + transaction, "select 1 from rdb$database", StatementOptions().setDialect(3u)); + + BOOST_REQUIRE(value.has_value()); + BOOST_CHECK_EQUAL(std::get<0>(*value), 1); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryFirstRowAsSupportsProcedureWithOutputColumns) +{ + struct Result + { + std::int32_t id; + std::optional name; + }; + + const auto database = getTempFile("Attachment-queryFirstRowAsSupportsProcedureWithOutputColumns.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE(attachment.execute(transaction, + "create procedure p returns (id integer, name varchar(20)) as begin id = 42; name = 'answer'; suspend; end")); + transaction.commitRetaining(); + + const auto value = attachment.queryFirstRowAs(transaction, "execute procedure p"); + + BOOST_REQUIRE(value.has_value()); + BOOST_CHECK_EQUAL(value->id, 42); + BOOST_REQUIRE(value->name.has_value()); + BOOST_CHECK_EQUAL(*value->name, "answer"); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryFirstRowAsThrowsForFieldCountMismatch) +{ + struct Result + { + std::int32_t id; + }; + + const auto database = getTempFile("Attachment-queryFirstRowAsThrowsForFieldCountMismatch.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_CHECK_THROW(attachment.queryFirstRowAs(transaction, "select 1, 2 from rdb$database"), FbCppException); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryFirstRowAsThrowsForNullIntoNonOptionalField) +{ + struct Result + { + std::int32_t id; + }; + + const auto database = getTempFile("Attachment-queryFirstRowAsThrowsForNullIntoNonOptionalField.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_CHECK_THROW(attachment.queryFirstRowAs(transaction, "select cast(null as integer) from rdb$database"), + FbCppException); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryFirstRowAsThrowsForNonQueryStatement) +{ + struct Result + { + std::int32_t id; + }; + + const auto database = getTempFile("Attachment-queryFirstRowAsThrowsForNonQueryStatement.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_CHECK_THROW(attachment.queryFirstRowAs(transaction, "create table t (id integer)"), FbCppException); + + transaction.commit(); +} + BOOST_AUTO_TEST_CASE(isNotValidAfterMove) { const auto database = getTempFile("Attachment-isNotValidAfterMove.fdb"); From 4b988eba43969069180bb71c7e5ccef2138b1009 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Tue, 16 Jun 2026 22:02:06 -0300 Subject: [PATCH 5/6] Refactor --- src/fb-cpp/Attachment.cpp | 14 +-- src/fb-cpp/Attachment.h | 160 ++++++++++++++++++++++++------ src/fb-cpp/RowSet.cpp | 1 + src/fb-cpp/Statement.cpp | 51 ++++++++++ src/fb-cpp/Statement.h | 179 ++-------------------------------- src/fb-cpp/StatementOptions.h | 163 +++++++++++++++++++++++++++++++ src/test/Attachment.cpp | 166 +++++++++++++++++++++++++++++++ 7 files changed, 521 insertions(+), 213 deletions(-) create mode 100644 src/fb-cpp/StatementOptions.h diff --git a/src/fb-cpp/Attachment.cpp b/src/fb-cpp/Attachment.cpp index 5face5e..2292b99 100644 --- a/src/fb-cpp/Attachment.cpp +++ b/src/fb-cpp/Attachment.cpp @@ -97,27 +97,21 @@ void Attachment::dropDatabase() disconnectOrDrop(true); } -bool Attachment::execute(Transaction& transaction, std::string_view sql) -{ - return execute(transaction, sql, StatementOptions{}); -} - bool Attachment::execute(Transaction& transaction, std::string_view sql, const StatementOptions& options) { Statement statement{*this, transaction, sql, options}; return statement.execute(transaction); } -RowSet Attachment::queryRowSet(Transaction& transaction, std::string_view sql, unsigned maxRows) -{ - return queryRowSet(transaction, sql, maxRows, StatementOptions{}); -} - RowSet Attachment::queryRowSet( Transaction& transaction, std::string_view sql, unsigned maxRows, const StatementOptions& options) { Statement statement{*this, transaction, sql, options}; + return queryPreparedRowSet(statement, transaction, maxRows); +} +RowSet Attachment::queryPreparedRowSet(Statement& statement, Transaction& transaction, unsigned maxRows) +{ switch (statement.getType()) { case StatementType::SELECT: diff --git a/src/fb-cpp/Attachment.h b/src/fb-cpp/Attachment.h index 52c1f7b..368af38 100644 --- a/src/fb-cpp/Attachment.h +++ b/src/fb-cpp/Attachment.h @@ -26,15 +26,16 @@ #define FBCPP_ATTACHMENT_H #include "fb-api.h" -#include "SmartPtrs.h" #include "RowSet.h" +#include "SmartPtrs.h" +#include "StatementOptions.h" +#include #include #include #include #include #include #include -#include /// @@ -43,7 +44,7 @@ namespace fbcpp { class Client; - class StatementOptions; + class Statement; class Transaction; /// @@ -308,54 +309,90 @@ namespace fbcpp /// /// Prepares and executes an SQL statement using the supplied transaction. /// - bool execute(Transaction& transaction, std::string_view sql); + bool execute(Transaction& transaction, std::string_view sql, const StatementOptions& options = {}); /// - /// Prepares and executes an SQL statement using the supplied transaction and statement options. + /// Prepares, binds parameters, and executes an SQL statement using the supplied transaction. /// - bool execute(Transaction& transaction, std::string_view sql, const StatementOptions& options); + template + bool execute(Transaction& transaction, std::string_view sql, const Params& params); + + /// + /// Prepares, binds parameters, and executes an SQL statement using the supplied transaction and statement + /// options. + /// + template + bool execute( + Transaction& transaction, std::string_view sql, const StatementOptions& options, const Params& params); /// /// Prepares and executes a query using the supplied transaction and returns up to maxRows rows. /// - RowSet queryRowSet(Transaction& transaction, std::string_view sql, unsigned maxRows); + RowSet queryRowSet( + Transaction& transaction, std::string_view sql, unsigned maxRows, const StatementOptions& options = {}); /// - /// Prepares and executes a query using the supplied transaction and statement options and returns up to maxRows + /// Prepares, binds parameters, and executes a query using the supplied transaction and returns up to maxRows /// rows. /// - RowSet queryRowSet( - Transaction& transaction, std::string_view sql, unsigned maxRows, const StatementOptions& options); + template + RowSet queryRowSet(Transaction& transaction, std::string_view sql, unsigned maxRows, const Params& params); + + /// + /// Prepares, binds parameters, and executes a query using the supplied transaction and statement options and + /// returns up to maxRows rows. + /// + template + RowSet queryRowSet(Transaction& transaction, std::string_view sql, unsigned maxRows, + const StatementOptions& options, const Params& params); /// /// Prepares and executes a query using the supplied transaction and returns the first column of the first row. /// template - std::optional queryScalar(Transaction& transaction, std::string_view sql); + std::optional queryScalar( + Transaction& transaction, std::string_view sql, const StatementOptions& options = {}); /// - /// Prepares and executes a query using the supplied transaction and statement options and returns the first - /// column of the first row. + /// Prepares, binds parameters, and executes a query using the supplied transaction and returns the first column + /// of the first row. /// - template - std::optional queryScalar(Transaction& transaction, std::string_view sql, const StatementOptions& options); + template + std::optional queryScalar(Transaction& transaction, std::string_view sql, const Params& params); + + /// + /// Prepares, binds parameters, and executes a query using the supplied transaction and statement options and + /// returns the first column of the first row. + /// + template + std::optional queryScalar( + Transaction& transaction, std::string_view sql, const StatementOptions& options, const Params& params); /// /// Prepares and executes a query using the supplied transaction and returns the first row mapped as T. /// template - std::optional queryFirstRowAs(Transaction& transaction, std::string_view sql); + std::optional queryFirstRowAs( + Transaction& transaction, std::string_view sql, const StatementOptions& options = {}); /// - /// Prepares and executes a query using the supplied transaction and statement options and returns the first row + /// Prepares, binds parameters, and executes a query using the supplied transaction and returns the first row /// mapped as T. /// - template + template + std::optional queryFirstRowAs(Transaction& transaction, std::string_view sql, const Params& params); + + /// + /// Prepares, binds parameters, and executes a query using the supplied transaction and statement options and + /// returns the first row mapped as T. + /// + template std::optional queryFirstRowAs( - Transaction& transaction, std::string_view sql, const StatementOptions& options); + Transaction& transaction, std::string_view sql, const StatementOptions& options, const Params& params); private: void disconnectOrDrop(bool drop); + RowSet queryPreparedRowSet(Statement& statement, Transaction& transaction, unsigned maxRows); private: Client* client; @@ -363,9 +400,10 @@ namespace fbcpp }; template - std::optional Attachment::queryScalar(Transaction& transaction, std::string_view sql) + std::optional Attachment::queryScalar( + Transaction& transaction, std::string_view sql, const StatementOptions& options) { - auto rowSet = queryRowSet(transaction, sql, 1u); + auto rowSet = queryRowSet(transaction, sql, 1u, options); if (rowSet.getCount() == 0u) return std::nullopt; @@ -374,7 +412,7 @@ namespace fbcpp } template - std::optional Attachment::queryScalar( + std::optional Attachment::queryFirstRowAs( Transaction& transaction, std::string_view sql, const StatementOptions& options) { auto rowSet = queryRowSet(transaction, sql, 1u, options); @@ -382,32 +420,90 @@ namespace fbcpp if (rowSet.getCount() == 0u) return std::nullopt; - return rowSet.getRow(0).get>(0); + return rowSet.getRow(0).get(); } +} // namespace fbcpp - template - std::optional Attachment::queryFirstRowAs(Transaction& transaction, std::string_view sql) +#include "Statement.h" + +namespace fbcpp +{ + template + bool Attachment::execute(Transaction& transaction, std::string_view sql, const Params& params) { - auto rowSet = queryRowSet(transaction, sql, 1u); + return execute(transaction, sql, StatementOptions{}, params); + } + + template + bool Attachment::execute( + Transaction& transaction, std::string_view sql, const StatementOptions& options, const Params& params) + { + Statement statement{*this, transaction, sql, options}; + statement.set(params); + return statement.execute(transaction); + } + + template + RowSet Attachment::queryRowSet( + Transaction& transaction, std::string_view sql, unsigned maxRows, const Params& params) + { + return queryRowSet(transaction, sql, maxRows, StatementOptions{}, params); + } + + template + RowSet Attachment::queryRowSet(Transaction& transaction, std::string_view sql, unsigned maxRows, + const StatementOptions& options, const Params& params) + { + Statement statement{*this, transaction, sql, options}; + statement.set(params); + return queryPreparedRowSet(statement, transaction, maxRows); + } + + template + std::optional Attachment::queryScalar(Transaction& transaction, std::string_view sql, const Params& params) + { + auto rowSet = queryRowSet(transaction, sql, 1u, params); if (rowSet.getCount() == 0u) return std::nullopt; - return rowSet.getRow(0).get(); + return rowSet.getRow(0).template get>(0); } - template + template + std::optional Attachment::queryScalar( + Transaction& transaction, std::string_view sql, const StatementOptions& options, const Params& params) + { + auto rowSet = queryRowSet(transaction, sql, 1u, options, params); + + if (rowSet.getCount() == 0u) + return std::nullopt; + + return rowSet.getRow(0).template get>(0); + } + + template + std::optional Attachment::queryFirstRowAs(Transaction& transaction, std::string_view sql, const Params& params) + { + auto rowSet = queryRowSet(transaction, sql, 1u, params); + + if (rowSet.getCount() == 0u) + return std::nullopt; + + return rowSet.getRow(0).template get(); + } + + template std::optional Attachment::queryFirstRowAs( - Transaction& transaction, std::string_view sql, const StatementOptions& options) + Transaction& transaction, std::string_view sql, const StatementOptions& options, const Params& params) { - auto rowSet = queryRowSet(transaction, sql, 1u, options); + auto rowSet = queryRowSet(transaction, sql, 1u, options, params); if (rowSet.getCount() == 0u) return std::nullopt; - return rowSet.getRow(0).get(); + return rowSet.getRow(0).template get(); } } // namespace fbcpp - #endif // FBCPP_ATTACHMENT_H diff --git a/src/fb-cpp/RowSet.cpp b/src/fb-cpp/RowSet.cpp index 7cf56a6..64bdb02 100644 --- a/src/fb-cpp/RowSet.cpp +++ b/src/fb-cpp/RowSet.cpp @@ -23,6 +23,7 @@ */ #include "RowSet.h" +#include "Attachment.h" #include "Client.h" #include "Statement.h" #include diff --git a/src/fb-cpp/Statement.cpp b/src/fb-cpp/Statement.cpp index 28564b1..6c9ac0c 100644 --- a/src/fb-cpp/Statement.cpp +++ b/src/fb-cpp/Statement.cpp @@ -181,6 +181,52 @@ Statement::Statement( outRow = std::make_unique(attachment.getClient(), outDescriptors, std::span{outMessage}); } +Statement::Statement(Statement&& o) noexcept + : attachment{o.attachment}, + statusWrapper{std::move(o.statusWrapper)}, + calendarConverter{std::move(o.calendarConverter)}, + numericConverter{std::move(o.numericConverter)}, + statementHandle{std::move(o.statementHandle)}, + resultSetHandle{std::move(o.resultSetHandle)}, + inMetadata{std::move(o.inMetadata)}, + inDescriptors{std::move(o.inDescriptors)}, + inMessage{std::move(o.inMessage)}, + outMetadata{std::move(o.outMetadata)}, + outDescriptors{std::move(o.outDescriptors)}, + outMessage{std::move(o.outMessage)}, + outRow{std::make_unique(attachment->getClient(), outDescriptors, std::span{outMessage})}, + type{o.type}, + cursorFlags{o.cursorFlags} +{ + o.outRow.reset(); +} + +Statement& Statement::operator=(Statement&& o) noexcept +{ + if (this != &o) + { + attachment = o.attachment; + statusWrapper = std::move(o.statusWrapper); + calendarConverter = std::move(o.calendarConverter); + numericConverter = std::move(o.numericConverter); + statementHandle = std::move(o.statementHandle); + resultSetHandle = std::move(o.resultSetHandle); + inMetadata = std::move(o.inMetadata); + inDescriptors = std::move(o.inDescriptors); + inMessage = std::move(o.inMessage); + outMetadata = std::move(o.outMetadata); + outDescriptors = std::move(o.outDescriptors); + outMessage = std::move(o.outMessage); + outRow = std::make_unique(attachment->getClient(), outDescriptors, std::span{outMessage}); + type = o.type; + cursorFlags = o.cursorFlags; + + o.outRow.reset(); + } + + return *this; +} + void Statement::free() { assert(isValid()); @@ -243,6 +289,11 @@ bool Statement::execute(Transaction& transaction) } } +Client& Statement::getClient() noexcept +{ + return attachment->getClient(); +} + bool Statement::fetchNext() { assert(isValid()); diff --git a/src/fb-cpp/Statement.h b/src/fb-cpp/Statement.h index 7ae32eb..05faa8a 100644 --- a/src/fb-cpp/Statement.h +++ b/src/fb-cpp/Statement.h @@ -29,9 +29,9 @@ #include "fb-api.h" #include "types.h" #include "Blob.h" -#include "Attachment.h" #include "Client.h" #include "Row.h" +#include "StatementOptions.h" #include "NumericConverter.h" #include "CalendarConverter.h" #include "Descriptor.h" @@ -65,133 +65,9 @@ /// namespace fbcpp { + class Attachment; 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. - /// - class StatementOptions final - { - public: - /// - /// @brief Reports whether the legacy textual plan should be prefetched during prepare. - /// - bool getPrefetchLegacyPlan() const - { - return prefetchLegacyPlan; - } - - /// - /// @brief Enables or disables prefetching of the legacy textual plan at prepare time. - /// @param value `true` to prefetch the legacy plan, `false` to skip it. - /// @return Reference to this instance for fluent configuration. - /// - StatementOptions& setPrefetchLegacyPlan(bool value) - { - prefetchLegacyPlan = value; - return *this; - } - - /// - /// @brief Reports whether the structured plan should be prefetched during prepare. - /// - bool getPrefetchPlan() const - { - return prefetchPlan; - } - - /// - /// @brief Enables or disables prefetching of the structured plan at prepare time. - /// @param value `true` to prefetch the optimized plan, `false` to skip it. - /// @return Reference to this instance for fluent configuration. - /// - StatementOptions& setPrefetchPlan(bool value) - { - prefetchPlan = value; - return *this; - } - - /// - /// @brief Returns the cursor name to be set for the statement. - /// - const std::optional& getCursorName() const - { - return cursorName; - } - - /// - /// @brief Sets the cursor name for the statement. - /// @param value The name of the cursor. - /// @return Reference to this instance for fluent configuration. - /// - StatementOptions& setCursorName(const std::string& value) - { - cursorName = value; - 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; - } - - /// - /// @brief Returns the SQL dialect used when preparing the statement. - /// - unsigned getDialect() const - { - return dialect; - } - - /// - /// @brief Sets the SQL dialect used when preparing the statement. - /// @param value SQL dialect number (1 for InterBase compatibility, 3 for current). - /// @return Reference to this instance for fluent configuration. - /// - StatementOptions& setDialect(unsigned value) - { - dialect = value; - return *this; - } - - private: - bool prefetchLegacyPlan = false; - bool prefetchPlan = false; - std::optional cursorName; - CursorType cursorType = CursorType::FORWARD_ONLY; - unsigned dialect = SQL_DIALECT_CURRENT; - }; - /// /// @brief Distinguishes the semantic category of the prepared SQL statement. /// @@ -274,25 +150,7 @@ namespace fbcpp /// /// @brief Transfers ownership of an existing prepared statement. /// - Statement(Statement&& o) noexcept - : attachment{o.attachment}, - statusWrapper{std::move(o.statusWrapper)}, - calendarConverter{std::move(o.calendarConverter)}, - numericConverter{std::move(o.numericConverter)}, - statementHandle{std::move(o.statementHandle)}, - resultSetHandle{std::move(o.resultSetHandle)}, - inMetadata{std::move(o.inMetadata)}, - inDescriptors{std::move(o.inDescriptors)}, - inMessage{std::move(o.inMessage)}, - outMetadata{std::move(o.outMetadata)}, - outDescriptors{std::move(o.outDescriptors)}, - outMessage{std::move(o.outMessage)}, - outRow{std::make_unique(attachment->getClient(), outDescriptors, std::span{outMessage})}, - type{o.type}, - cursorFlags{o.cursorFlags} - { - o.outRow.reset(); - } + Statement(Statement&& o) noexcept; /// /// @brief Transfers ownership of another prepared statement into this one. @@ -300,31 +158,7 @@ namespace fbcpp /// The old handles are released via `FbRef::operator=(FbRef&&)`. /// After the assignment, `this` is valid (with `o`'s state) and `o` is invalid. /// - Statement& operator=(Statement&& o) noexcept - { - if (this != &o) - { - attachment = o.attachment; - statusWrapper = std::move(o.statusWrapper); - calendarConverter = std::move(o.calendarConverter); - numericConverter = std::move(o.numericConverter); - statementHandle = std::move(o.statementHandle); - resultSetHandle = std::move(o.resultSetHandle); - inMetadata = std::move(o.inMetadata); - inDescriptors = std::move(o.inDescriptors); - inMessage = std::move(o.inMessage); - outMetadata = std::move(o.outMetadata); - outDescriptors = std::move(o.outDescriptors); - outMessage = std::move(o.outMessage); - outRow = std::make_unique(attachment->getClient(), outDescriptors, std::span{outMessage}); - type = o.type; - cursorFlags = o.cursorFlags; - - o.outRow.reset(); - } - - return *this; - } + Statement& operator=(Statement&& o) noexcept; Statement(const Statement&) = delete; Statement& operator=(const Statement&) = delete; @@ -1167,7 +1001,7 @@ namespace fbcpp assert(isValid()); - auto& client = attachment->getClient(); + auto& client = getClient(); const auto value = optValue.value(); const auto& descriptor = getInDescriptor(index); const auto message = inMessage.data(); @@ -2191,6 +2025,9 @@ namespace fbcpp } } + private: + Client& getClient() noexcept; + private: Attachment* attachment; impl::StatusWrapper statusWrapper; diff --git a/src/fb-cpp/StatementOptions.h b/src/fb-cpp/StatementOptions.h new file mode 100644 index 0000000..b943767 --- /dev/null +++ b/src/fb-cpp/StatementOptions.h @@ -0,0 +1,163 @@ +/* + * MIT License + * + * Copyright (c) 2026 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. + */ + +#ifndef FBCPP_STATEMENT_OPTIONS_H +#define FBCPP_STATEMENT_OPTIONS_H + +#include "fb-api.h" +#include +#include + +/// +/// fb-cpp namespace. +/// +namespace fbcpp +{ + /// + /// @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. + /// + class StatementOptions final + { + public: + /// + /// @brief Reports whether the legacy textual plan should be prefetched during prepare. + /// + bool getPrefetchLegacyPlan() const + { + return prefetchLegacyPlan; + } + + /// + /// @brief Enables or disables prefetching of the legacy textual plan at prepare time. + /// @param value `true` to prefetch the legacy plan, `false` to skip it. + /// @return Reference to this instance for fluent configuration. + /// + StatementOptions& setPrefetchLegacyPlan(bool value) + { + prefetchLegacyPlan = value; + return *this; + } + + /// + /// @brief Reports whether the structured plan should be prefetched during prepare. + /// + bool getPrefetchPlan() const + { + return prefetchPlan; + } + + /// + /// @brief Enables or disables prefetching of the structured plan at prepare time. + /// @param value `true` to prefetch the optimized plan, `false` to skip it. + /// @return Reference to this instance for fluent configuration. + /// + StatementOptions& setPrefetchPlan(bool value) + { + prefetchPlan = value; + return *this; + } + + /// + /// @brief Returns the cursor name to be set for the statement. + /// + const std::optional& getCursorName() const + { + return cursorName; + } + + /// + /// @brief Sets the cursor name for the statement. + /// @param value The name of the cursor. + /// @return Reference to this instance for fluent configuration. + /// + StatementOptions& setCursorName(const std::string& value) + { + cursorName = value; + 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; + } + + /// + /// @brief Returns the SQL dialect used when preparing the statement. + /// + unsigned getDialect() const + { + return dialect; + } + + /// + /// @brief Sets the SQL dialect used when preparing the statement. + /// @param value SQL dialect number (1 for InterBase compatibility, 3 for current). + /// @return Reference to this instance for fluent configuration. + /// + StatementOptions& setDialect(unsigned value) + { + dialect = value; + return *this; + } + + private: + bool prefetchLegacyPlan = false; + bool prefetchPlan = false; + std::optional cursorName; + CursorType cursorType = CursorType::FORWARD_ONLY; + unsigned dialect = SQL_DIALECT_CURRENT; + }; +} // namespace fbcpp + +#endif // FBCPP_STATEMENT_OPTIONS_H diff --git a/src/test/Attachment.cpp b/src/test/Attachment.cpp index b5b38a7..03ffd8f 100644 --- a/src/test/Attachment.cpp +++ b/src/test/Attachment.cpp @@ -507,6 +507,172 @@ BOOST_AUTO_TEST_CASE(queryFirstRowAsThrowsForNonQueryStatement) transaction.commit(); } +BOOST_AUTO_TEST_CASE(executeBindsTupleParameters) +{ + const auto database = getTempFile("Attachment-executeBindsTupleParameters.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE( + attachment.execute(transaction, "create table t (id integer not null primary key, name varchar(20))")); + transaction.commitRetaining(); + + BOOST_REQUIRE(attachment.execute( + transaction, "insert into t (id, name) values (?, ?)", std::tuple{1, std::string_view{"one"}})); + BOOST_REQUIRE(attachment.execute(transaction, "insert into t (id, name) values (?, ?)", + StatementOptions().setDialect(3u), std::tuple{2, std::string_view{"two"}})); + + const auto count = attachment.queryScalar(transaction, "select count(*) from t"); + BOOST_REQUIRE(count.has_value()); + BOOST_CHECK_EQUAL(*count, 2); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(executeBindsStructParameters) +{ + struct Params + { + std::int32_t id; + std::string_view name; + }; + + const auto database = getTempFile("Attachment-executeBindsStructParameters.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE( + attachment.execute(transaction, "create table t (id integer not null primary key, name varchar(20))")); + transaction.commitRetaining(); + + BOOST_REQUIRE(attachment.execute(transaction, "insert into t (id, name) values (?, ?)", Params{1, "one"})); + + const auto name = attachment.queryScalar(transaction, "select name from t where id = 1"); + BOOST_REQUIRE(name.has_value()); + BOOST_CHECK_EQUAL(*name, "one"); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryRowSetBindsTupleParameters) +{ + const auto database = getTempFile("Attachment-queryRowSetBindsTupleParameters.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE(attachment.execute(transaction, "create table t (id integer not null primary key)")); + transaction.commitRetaining(); + + BOOST_REQUIRE(attachment.execute(transaction, "insert into t (id) values (?)", std::tuple{1})); + BOOST_REQUIRE(attachment.execute(transaction, "insert into t (id) values (?)", std::tuple{2})); + BOOST_REQUIRE(attachment.execute(transaction, "insert into t (id) values (?)", std::tuple{3})); + + auto rowSet = attachment.queryRowSet(transaction, "select id from t where id > ? order by id", 10u, std::tuple{1}); + + BOOST_REQUIRE_EQUAL(rowSet.getCount(), 2u); + BOOST_CHECK_EQUAL(rowSet.getRow(0).getInt32(0).value(), 2); + BOOST_CHECK_EQUAL(rowSet.getRow(1).getInt32(0).value(), 3); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryRowSetBindsOptionsAndStructParameters) +{ + struct Params + { + std::int32_t minId; + }; + + const auto database = getTempFile("Attachment-queryRowSetBindsOptionsAndStructParameters.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE(attachment.execute(transaction, "create table t (id integer not null primary key)")); + transaction.commitRetaining(); + + BOOST_REQUIRE(attachment.execute(transaction, "insert into t (id) values (?)", std::tuple{1})); + BOOST_REQUIRE(attachment.execute(transaction, "insert into t (id) values (?)", std::tuple{2})); + + auto rowSet = attachment.queryRowSet( + transaction, "select id from t where id > ? order by id", 10u, StatementOptions().setDialect(3u), Params{1}); + + BOOST_REQUIRE_EQUAL(rowSet.getCount(), 1u); + BOOST_CHECK_EQUAL(rowSet.getRow(0).getInt32(0).value(), 2); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryScalarBindsTupleParameters) +{ + const auto database = getTempFile("Attachment-queryScalarBindsTupleParameters.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE( + attachment.execute(transaction, "create table t (id integer not null primary key, name varchar(20))")); + transaction.commitRetaining(); + + BOOST_REQUIRE(attachment.execute( + transaction, "insert into t (id, name) values (?, ?)", std::tuple{1, std::string_view{"one"}})); + + const auto name = + attachment.queryScalar(transaction, "select name from t where id = ?", std::tuple{1}); + const auto nameWithOptions = attachment.queryScalar( + transaction, "select name from t where id = ?", StatementOptions().setDialect(3u), std::tuple{1}); + + BOOST_REQUIRE(name.has_value()); + BOOST_CHECK_EQUAL(*name, "one"); + BOOST_REQUIRE(nameWithOptions.has_value()); + BOOST_CHECK_EQUAL(*nameWithOptions, "one"); + + transaction.commit(); +} + +BOOST_AUTO_TEST_CASE(queryFirstRowAsBindsStructParameters) +{ + struct Params + { + std::int32_t id; + }; + + struct Result + { + std::int32_t id; + std::optional name; + }; + + const auto database = getTempFile("Attachment-queryFirstRowAsBindsStructParameters.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true).setForcedWrites(false)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + BOOST_REQUIRE( + attachment.execute(transaction, "create table t (id integer not null primary key, name varchar(20))")); + transaction.commitRetaining(); + + BOOST_REQUIRE(attachment.execute( + transaction, "insert into t (id, name) values (?, ?)", std::tuple{1, std::string_view{"one"}})); + + const auto value = + attachment.queryFirstRowAs(transaction, "select id, name from t where id = ?", Params{1}); + const auto valueWithOptions = attachment.queryFirstRowAs( + transaction, "select id, name from t where id = ?", StatementOptions().setDialect(3u), Params{1}); + + BOOST_REQUIRE(value.has_value()); + BOOST_CHECK_EQUAL(value->id, 1); + BOOST_REQUIRE(value->name.has_value()); + BOOST_CHECK_EQUAL(*value->name, "one"); + BOOST_REQUIRE(valueWithOptions.has_value()); + BOOST_CHECK_EQUAL(valueWithOptions->id, 1); + + transaction.commit(); +} + BOOST_AUTO_TEST_CASE(isNotValidAfterMove) { const auto database = getTempFile("Attachment-isNotValidAfterMove.fdb"); From a9c18f4d7696d425a2bde117c938c574934b1660 Mon Sep 17 00:00:00 2001 From: Adriano dos Santos Fernandes Date: Tue, 16 Jun 2026 22:02:06 -0300 Subject: [PATCH 6/6] EXECUTE PROCEDURE do not return more than 1 row --- src/fb-cpp/Attachment.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fb-cpp/Attachment.cpp b/src/fb-cpp/Attachment.cpp index 2292b99..53aabb1 100644 --- a/src/fb-cpp/Attachment.cpp +++ b/src/fb-cpp/Attachment.cpp @@ -129,5 +129,6 @@ RowSet Attachment::queryPreparedRowSet(Statement& statement, Transaction& transa } const auto hasRow = statement.execute(transaction); - return RowSet{statement, hasRow ? maxRows : 0u, hasRow}; + const auto effectiveMaxRows = statement.getType() == StatementType::EXEC_PROCEDURE ? 1u : maxRows; + return RowSet{statement, hasRow ? effectiveMaxRows : 0u, hasRow}; }