diff --git a/README.md b/README.md index 2febe70..b8f6979 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,12 @@ Statement statement{attachment, transaction, "select id, name from users where i // Set parameters statement.setInt32(0, 42); -/* Or +/* +// Or: statement.set(0, 42); + +// Or: +statement.set(SomeStructOrTuple{42}); */ // Execute and get results @@ -56,9 +60,13 @@ if (statement.execute(transaction)) const std::optional id = statement.getInt32(0); const std::optional name = statement.getString(1); - /* Or + /* + // Or: const auto id = statement.get(0); const auto name = statement.get(1); + + // Or: + const auto [id, name] = statement.get(); */ } while (statement.fetchNext()); } diff --git a/src/fb-cpp/Statement.h b/src/fb-cpp/Statement.h index 6651b1c..40a296d 100644 --- a/src/fb-cpp/Statement.h +++ b/src/fb-cpp/Statement.h @@ -36,6 +36,7 @@ #include "Descriptor.h" #include "SmartPtrs.h" #include "Exception.h" +#include "StructBinding.h" #include #include #include @@ -2042,6 +2043,51 @@ namespace fbcpp template T get(unsigned index); + /// + /// @brief Retrieves all output columns into a user-defined aggregate struct. + /// @tparam T An aggregate type whose fields match the output column count and types. + /// @return The populated struct with values from the current row. + /// @throws FbCppException if field count mismatches output column count. + /// @throws FbCppException if a NULL value is encountered for a non-optional field. + /// + template + T get() + { + using namespace impl::reflection; + + constexpr std::size_t N = fieldCountV; + + if (N != outDescriptors.size()) + { + throw FbCppException("Struct field count (" + std::to_string(N) + + ") does not match output column count (" + std::to_string(outDescriptors.size()) + ")"); + } + + return getStruct(std::make_index_sequence{}); + } + + /// + /// @brief Sets all input parameters from fields of a user-defined aggregate struct. + /// @tparam T An aggregate type whose fields match the input parameter count. + /// @param value The struct containing parameter values. + /// @throws FbCppException if field count mismatches input parameter count. + /// + template + void set(const T& value) + { + using namespace impl::reflection; + + constexpr std::size_t N = fieldCountV; + + if (N != inDescriptors.size()) + { + throw FbCppException("Struct field count (" + std::to_string(N) + + ") does not match input parameter count (" + std::to_string(inDescriptors.size()) + ")"); + } + + setStruct(value, std::make_index_sequence{}); + } + private: /// /// @brief Validates and returns the descriptor for the given input parameter index. @@ -2065,6 +2111,53 @@ namespace fbcpp return outDescriptors[index]; } + /// + /// @brief Helper to retrieve all output columns into a struct. + /// + template + T getStruct(std::index_sequence) + { + using namespace impl::reflection; + + return T{getStructField>(static_cast(Is))...}; + } + + /// + /// @brief Helper to get a single field value, throwing if NULL for non-optional fields. + /// + template + auto getStructField(unsigned index) + { + using namespace impl::reflection; + + if constexpr (isOptionalV) + return get(index); + else + { + auto opt = get>(index); + + if (!opt.has_value()) + { + throw FbCppException( + "Null value encountered for non-optional field at index " + std::to_string(index)); + } + + return std::move(opt.value()); + } + } + + /// + /// @brief Helper to set all input parameters from a struct. + /// + template + void setStruct(const T& value, std::index_sequence) + { + using namespace impl::reflection; + + const auto tuple = toTupleRef(value); + (set(static_cast(Is), std::get(tuple)), ...); + } + /// /// @brief Converts and writes numeric parameter values following descriptor rules. /// diff --git a/src/fb-cpp/StructBinding.h b/src/fb-cpp/StructBinding.h new file mode 100644 index 0000000..7fb3c10 --- /dev/null +++ b/src/fb-cpp/StructBinding.h @@ -0,0 +1,332 @@ +/* + * 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. + */ + +#ifndef FBCPP_STRUCT_BINDING_H +#define FBCPP_STRUCT_BINDING_H + +#include +#include +#include +#include +#include + + +namespace fbcpp +{ + /// + /// Concept constraining types to aggregates suitable for struct binding. + /// + template + concept Aggregate = std::is_aggregate_v && !std::is_array_v && !std::is_union_v; +} // namespace fbcpp + +namespace fbcpp::impl::reflection +{ + /// Maximum number of struct fields supported. + inline constexpr std::size_t maxFieldCount = 32; + + /// Helper to detect std::optional + template + struct IsOptional : std::false_type + { + }; + + template + struct IsOptional> : std::true_type + { + }; + + template + inline constexpr bool isOptionalV = IsOptional::value; + + /// Universal converter for aggregate initialization detection. + struct UniversalType + { + template + constexpr operator T() const noexcept + { + if constexpr (isOptionalV) + return T{std::nullopt}; + else + return T{}; + } + }; + + // Field count detection via SFINAE + template + struct IsBraceConstructibleImpl : std::false_type + { + }; + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wconversion" +#endif + + template + struct IsBraceConstructibleImpl, + std::void_t> : std::true_type + { + }; + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif + + template + inline constexpr bool isBraceConstructibleV = IsBraceConstructibleImpl>::value; + + // Binary search for field count + template + constexpr std::size_t detectFieldCount() + { + if constexpr (Lo == Hi) + return Lo; + else + { + constexpr std::size_t Mid = Lo + (Hi - Lo + 1) / 2; + if constexpr (isBraceConstructibleV) + return detectFieldCount(); + else + return detectFieldCount(); + } + } + + /// Number of fields in aggregate type T. + template + inline constexpr std::size_t fieldCountV = detectFieldCount(); + + // toTupleRef implementation for each field count (0-32) + template + auto toTupleRef(T&& obj) + { + constexpr std::size_t N = fieldCountV>; + + if constexpr (N == 0) + return std::tuple<>{}; + else if constexpr (N == 1) + { + auto&& [v1] = std::forward(obj); + return std::forward_as_tuple(v1); + } + else if constexpr (N == 2) + { + auto&& [v1, v2] = std::forward(obj); + return std::forward_as_tuple(v1, v2); + } + else if constexpr (N == 3) + { + auto&& [v1, v2, v3] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3); + } + else if constexpr (N == 4) + { + auto&& [v1, v2, v3, v4] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4); + } + else if constexpr (N == 5) + { + auto&& [v1, v2, v3, v4, v5] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5); + } + else if constexpr (N == 6) + { + auto&& [v1, v2, v3, v4, v5, v6] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6); + } + else if constexpr (N == 7) + { + auto&& [v1, v2, v3, v4, v5, v6, v7] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7); + } + else if constexpr (N == 8) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8); + } + else if constexpr (N == 9) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9); + } + else if constexpr (N == 10) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10); + } + else if constexpr (N == 11) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11); + } + else if constexpr (N == 12) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12); + } + else if constexpr (N == 13) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13); + } + else if constexpr (N == 14) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14); + } + else if constexpr (N == 15) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15); + } + else if constexpr (N == 16) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16); + } + else if constexpr (N == 17) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17); + } + else if constexpr (N == 18) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18] = + std::forward(obj); + return std::forward_as_tuple( + v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18); + } + else if constexpr (N == 19) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19] = + std::forward(obj); + return std::forward_as_tuple( + v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19); + } + else if constexpr (N == 20) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20] = + std::forward(obj); + return std::forward_as_tuple( + v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20); + } + else if constexpr (N == 21) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21] = + std::forward(obj); + return std::forward_as_tuple( + v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21); + } + else if constexpr (N == 22) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, + v22] = std::forward(obj); + return std::forward_as_tuple( + v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22); + } + else if constexpr (N == 23) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, + v23] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, + v18, v19, v20, v21, v22, v23); + } + else if constexpr (N == 24) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, + v23, v24] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, + v18, v19, v20, v21, v22, v23, v24); + } + else if constexpr (N == 25) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, + v23, v24, v25] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, + v18, v19, v20, v21, v22, v23, v24, v25); + } + else if constexpr (N == 26) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, + v23, v24, v25, v26] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, + v18, v19, v20, v21, v22, v23, v24, v25, v26); + } + else if constexpr (N == 27) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, + v23, v24, v25, v26, v27] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, + v18, v19, v20, v21, v22, v23, v24, v25, v26, v27); + } + else if constexpr (N == 28) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, + v23, v24, v25, v26, v27, v28] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, + v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28); + } + else if constexpr (N == 29) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, + v23, v24, v25, v26, v27, v28, v29] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, + v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29); + } + else if constexpr (N == 30) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, + v23, v24, v25, v26, v27, v28, v29, v30] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, + v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30); + } + else if constexpr (N == 31) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, + v23, v24, v25, v26, v27, v28, v29, v30, v31] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, + v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31); + } + else if constexpr (N == 32) + { + auto&& [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, + v23, v24, v25, v26, v27, v28, v29, v30, v31, v32] = std::forward(obj); + return std::forward_as_tuple(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, + v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32); + } + else + static_assert(N <= maxFieldCount, "Struct has too many fields for struct binding (max 32)"); + } + + /// Type of the tuple representing T's fields. + template + using TupleType = decltype(toTupleRef(std::declval())); + + /// Type of the I-th field of aggregate T. + template + using FieldType = std::remove_reference_t>>; +} // namespace fbcpp::impl::reflection + + +#endif // FBCPP_STRUCT_BINDING_H diff --git a/src/test/Statement.cpp b/src/test/Statement.cpp index 59e2352..61d32d4 100644 --- a/src/test/Statement.cpp +++ b/src/test/Statement.cpp @@ -2521,3 +2521,185 @@ BOOST_AUTO_TEST_CASE(opaqueDecFloat34NullHandling) } BOOST_AUTO_TEST_SUITE_END() + + +BOOST_AUTO_TEST_SUITE(StructBindingSuite) + +BOOST_AUTO_TEST_CASE(getStructRetrievesAllColumns) +{ + struct Result + { + std::optional col1; + std::optional col2; + std::optional col3; + }; + + const auto database = getTempFile("Statement-getStructRetrievesAllColumns.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + Statement stmt{attachment, transaction, "select 42, 'hello', 3.14e0 from rdb$database"}; + BOOST_REQUIRE(stmt.execute(transaction)); + + const auto result = stmt.get(); + BOOST_CHECK(result.col1.has_value()); + BOOST_CHECK_EQUAL(result.col1.value(), 42); + BOOST_CHECK(result.col2.has_value()); + BOOST_CHECK_EQUAL(result.col2.value(), "hello"); + BOOST_CHECK(result.col3.has_value()); + BOOST_CHECK_CLOSE(result.col3.value(), 3.14, 0.001); +} + +BOOST_AUTO_TEST_CASE(setStructSetsAllParameters) +{ + struct Params + { + std::int32_t val1; + std::string_view val2; + }; + + const auto database = getTempFile("Statement-setStructSetsAllParameters.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + Statement stmt{attachment, transaction, "select cast(? as integer), cast(? as varchar(50)) from rdb$database"}; + + Params params{123, "test"}; + stmt.set(params); + BOOST_REQUIRE(stmt.execute(transaction)); + + BOOST_CHECK_EQUAL(stmt.getInt32(0).value(), 123); + BOOST_CHECK_EQUAL(stmt.getString(1).value(), "test"); +} + +BOOST_AUTO_TEST_CASE(getStructFieldCountMismatchThrows) +{ + struct WrongSize + { + std::optional col1; + std::optional col2; + }; + + const auto database = getTempFile("Statement-getStructFieldCountMismatchThrows.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + Statement stmt{attachment, transaction, "select 1, 2, 3 from rdb$database"}; + BOOST_REQUIRE(stmt.execute(transaction)); + + BOOST_CHECK_THROW(stmt.get(), FbCppException); +} + +BOOST_AUTO_TEST_CASE(setStructFieldCountMismatchThrows) +{ + struct WrongSize + { + std::int32_t val1; + std::int32_t val2; + std::int32_t val3; + }; + + const auto database = getTempFile("Statement-setStructFieldCountMismatchThrows.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + Statement stmt{attachment, transaction, "select cast(? as integer) from rdb$database"}; + + WrongSize params{1, 2, 3}; + BOOST_CHECK_THROW(stmt.set(params), FbCppException); +} + +BOOST_AUTO_TEST_CASE(nullForNonOptionalFieldThrows) +{ + struct NonOptional + { + std::int32_t value; + }; + + const auto database = getTempFile("Statement-nullForNonOptionalFieldThrows.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + Statement stmt{attachment, transaction, "select cast(null as integer) from rdb$database"}; + BOOST_REQUIRE(stmt.execute(transaction)); + + BOOST_CHECK_THROW(stmt.get(), FbCppException); +} + +BOOST_AUTO_TEST_CASE(mixedOptionalAndNonOptionalFields) +{ + struct Mixed + { + std::int32_t required; + std::optional optional; + }; + + const auto database = getTempFile("Statement-mixedOptionalAndNonOptionalFields.fdb"); + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + Statement stmt{attachment, transaction, "select 42, cast(null as varchar(10)) from rdb$database"}; + BOOST_REQUIRE(stmt.execute(transaction)); + + const auto result = stmt.get(); + BOOST_CHECK_EQUAL(result.required, 42); + BOOST_CHECK(!result.optional.has_value()); +} + +BOOST_AUTO_TEST_CASE(structWithDateTimeFields) +{ + struct DateTimeResult + { + std::optional dateCol; + std::optional