diff --git a/AGENTS.md b/AGENTS.md index ebac27f..ab752f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,3 +24,6 @@ ### Tests - Run the Boost.Test suite with `ctest --preset default --verbose`. - Boost.Test options can be used with environments variables like `BOOST_TEST_LOG_LEVEL=all`. +- Prefer fewer, comprehensive test cases over many small single-assertion tests. A single test case should verify + all related properties together (e.g., check the full error vector structure in one test instead of separate tests + for each field). diff --git a/src/fb-cpp/Exception.cpp b/src/fb-cpp/Exception.cpp index 6ea022e..d9b23d2 100644 --- a/src/fb-cpp/Exception.cpp +++ b/src/fb-cpp/Exception.cpp @@ -25,6 +25,7 @@ #include "Exception.h" #include "Client.h" #include +#include #include using namespace fbcpp; @@ -84,3 +85,91 @@ std::string DatabaseException::buildMessage(Client& client, const std::intptr_t* return message; } + +void DatabaseException::copyErrorVector(const std::intptr_t* statusVector) +{ + if (!statusVector) + return; + + const auto* p = statusVector; + + while (*p != isc_arg_end) + { + const auto argType = *p++; + + switch (argType) + { + case isc_arg_gds: + case isc_arg_number: + errorVector.push_back(argType); + errorVector.push_back(*p++); + break; + + case isc_arg_string: + case isc_arg_interpreted: + case isc_arg_sql_state: + errorVector.push_back(argType); + errorStrings.emplace_back(reinterpret_cast(*p++)); + errorVector.push_back(0); // placeholder for string pointer + break; + + case isc_arg_cstring: + { + const auto len = static_cast(*p++); + const auto str = reinterpret_cast(*p++); + errorVector.push_back(isc_arg_string); + errorStrings.emplace_back(str, len); + errorVector.push_back(0); // placeholder for string pointer + break; + } + + default: + errorVector.push_back(argType); + errorVector.push_back(*p++); + break; + } + } + + errorVector.push_back(isc_arg_end); + + fixupStringPointers(); +} + +void DatabaseException::fixupStringPointers() +{ + size_t strIdx = 0; + size_t i = 0; + + while (i < errorVector.size() && errorVector[i] != isc_arg_end) + { + const auto argType = errorVector[i]; + + if (argType == isc_arg_string || argType == isc_arg_interpreted || argType == isc_arg_sql_state) + errorVector[i + 1] = reinterpret_cast(errorStrings[strIdx++].c_str()); + + i += 2; + } +} + +std::string DatabaseException::extractSqlState(const std::intptr_t* statusVector) +{ + if (!statusVector) + return {}; + + const auto* p = statusVector; + + while (*p != isc_arg_end) + { + const auto argType = *p++; + + if (argType == isc_arg_sql_state) + return reinterpret_cast(*p); + + if (argType == isc_arg_cstring) + p += 2; + else + p++; + } + + return {}; +} diff --git a/src/fb-cpp/Exception.h b/src/fb-cpp/Exception.h index fb62ddb..9b0b9b7 100644 --- a/src/fb-cpp/Exception.h +++ b/src/fb-cpp/Exception.h @@ -28,6 +28,7 @@ #include "fb-api.h" #include #include +#include #include @@ -199,18 +200,69 @@ namespace fbcpp class DatabaseException final : public FbCppException { public: - using FbCppException::FbCppException; - /// /// Constructs a DatabaseException from a Firebird status vector. /// - explicit DatabaseException(Client& client, const std::intptr_t* status) - : FbCppException{buildMessage(client, status)} + explicit DatabaseException(Client& client, const std::intptr_t* statusVector) + : FbCppException{buildMessage(client, statusVector)}, + sqlState{extractSqlState(statusVector)} + { + copyErrorVector(statusVector); + } + + DatabaseException(const DatabaseException& other) + : FbCppException{static_cast(other)}, + errorVector{other.errorVector}, + errorStrings{other.errorStrings}, + sqlState{other.sqlState} + { + fixupStringPointers(); + } + + DatabaseException(DatabaseException&&) = default; + + DatabaseException& operator=(const DatabaseException&) = delete; + DatabaseException& operator=(DatabaseException&&) = delete; + + /// + /// Returns the Firebird error vector. + /// The vector is terminated by isc_arg_end. + /// + const std::vector& getErrors() const noexcept + { + return errorVector; + } + + /// + /// Returns the primary ISC error code (first isc_arg_gds value), or 0 if none. + /// + std::intptr_t getErrorCode() const noexcept + { + if (errorVector.size() >= 2 && errorVector[0] == isc_arg_gds) + return errorVector[1]; + return 0; + } + + /// + /// Returns the SQL state string (e.g. "42000") if present in the original status vector, + /// or empty otherwise. + /// + const std::string& getSqlState() const noexcept { + return sqlState; } private: - static std::string buildMessage(Client& client, const std::intptr_t* status); + static std::string buildMessage(Client& client, const std::intptr_t* statusVector); + static std::string extractSqlState(const std::intptr_t* statusVector); + + void copyErrorVector(const std::intptr_t* statusVector); + void fixupStringPointers(); + + private: + std::vector errorVector; + std::vector errorStrings; + std::string sqlState; }; } // namespace fbcpp diff --git a/src/test/Exception.cpp b/src/test/Exception.cpp new file mode 100644 index 0000000..745bc4d --- /dev/null +++ b/src/test/Exception.cpp @@ -0,0 +1,70 @@ +/* + * MIT License + * + * 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 + * 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/Exception.h" +#include "fb-cpp/Statement.h" +#include "fb-cpp/Transaction.h" +#include + + +BOOST_AUTO_TEST_SUITE(DatabaseExceptionSuite) + +BOOST_AUTO_TEST_CASE(syntaxErrorExceptionProperties) +{ + const auto database = getTempFile("Exception-syntaxErrorExceptionProperties.fdb"); + + Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)}; + FbDropDatabase attachmentDrop{attachment}; + + Transaction transaction{attachment}; + + try + { + Statement stmt{attachment, transaction, "INVALID SQL STATEMENT !!!"}; + BOOST_FAIL("Expected DatabaseException was not thrown"); + } + catch (const DatabaseException& ex) + { + // what() should contain a formatted error message + std::string message = ex.what(); + BOOST_CHECK(!message.empty()); + + // Error vector should start with isc_arg_gds and end with isc_arg_end + const auto& errors = ex.getErrors(); + BOOST_REQUIRE(errors.size() >= 2); + BOOST_CHECK_EQUAL(errors[0], isc_arg_gds); + BOOST_CHECK_EQUAL(errors.back(), isc_arg_end); + + // getErrorCode() should return the first GDS code + BOOST_CHECK_NE(ex.getErrorCode(), 0); + BOOST_CHECK_EQUAL(ex.getErrorCode(), errors[1]); + + // SQL state, when present, should be a 5-character string + if (!ex.getSqlState().empty()) + BOOST_CHECK_EQUAL(ex.getSqlState().size(), 5u); + } +} + +BOOST_AUTO_TEST_SUITE_END()