diff --git a/src/Lightweight/DataBinder/BasicStringBinder.hpp b/src/Lightweight/DataBinder/BasicStringBinder.hpp index 7daf6287..f978c3a4 100644 --- a/src/Lightweight/DataBinder/BasicStringBinder.hpp +++ b/src/Lightweight/DataBinder/BasicStringBinder.hpp @@ -286,12 +286,18 @@ struct SqlDataBinder cb.PlanPostProcessOutputColumn( [stmt, column, indicator, result]() { PostProcessOutputColumn(stmt, column, result, indicator); }); - return SQLBindCol(stmt, - column, - SQL_C_CHAR, - (SQLPOINTER) StringTraits::Data(result), - (SQLLEN) StringTraits::Size(result), - indicator); + // Per ODBC spec, BufferLength for SQL_C_CHAR must include space for the + // null terminator. SqlFixedString backs its data with _data[N+1] for + // exactly this reason; pass Capacity+1 to let the driver write a full + // N-char value plus null. Dynamic strings keep the current behaviour. + SQLLEN const bufferLength = [&]() -> SQLLEN { + if constexpr (requires { AnsiStringType::Capacity; }) + return static_cast(AnsiStringType::Capacity) + 1; + else + return static_cast(StringTraits::Size(result)); + }(); + + return SQLBindCol(stmt, column, SQL_C_CHAR, (SQLPOINTER) StringTraits::Data(result), bufferLength, indicator); } static void PostProcessOutputColumn(SQLHSTMT stmt, SQLUSMALLINT column, AnsiStringType* result, SQLLEN* indicator) @@ -338,8 +344,10 @@ struct SqlDataBinder if constexpr (requires { AnsiStringType::Capacity; }) { StringTraits::Resize(result, AnsiStringType::Capacity); + // BufferLength for SQL_C_CHAR must include space for the null terminator; + // _data is sized N+1 to accommodate it. SQLRETURN const rv = - SQLGetData(stmt, column, SQL_C_CHAR, StringTraits::Data(result), AnsiStringType::Capacity, indicator); + SQLGetData(stmt, column, SQL_C_CHAR, StringTraits::Data(result), AnsiStringType::Capacity + 1, indicator); if (rv == SQL_SUCCESS || rv == SQL_NO_DATA) { if (*indicator == SQL_NULL_DATA) diff --git a/src/Lightweight/DataBinder/SqlFixedString.hpp b/src/Lightweight/DataBinder/SqlFixedString.hpp index 4c59aaca..219ee09e 100644 --- a/src/Lightweight/DataBinder/SqlFixedString.hpp +++ b/src/Lightweight/DataBinder/SqlFixedString.hpp @@ -471,9 +471,9 @@ struct SqlBasicStringOperations> LIGHTWEIGHT_FORCE_INLINE static void TrimRight(ValueType* boundOutputString, SQLLEN indicator) noexcept { #if defined(_WIN32) - size_t n = (std::min) (static_cast(indicator) / sizeof(CharType), N - 1); + size_t n = (std::min) (static_cast(indicator) / sizeof(CharType), N); #else - size_t n = std::min(static_cast(indicator), N - 1); + size_t n = std::min(static_cast(indicator), N); #endif // Strip trailing whitespace and null characters while (n > 0 && (std::isspace((*boundOutputString)[n - 1]) || (*boundOutputString)[n - 1] == '\0')) diff --git a/src/tests/DataBinderTests.cpp b/src/tests/DataBinderTests.cpp index e472ff22..e5263cb0 100644 --- a/src/tests/DataBinderTests.cpp +++ b/src/tests/DataBinderTests.cpp @@ -1450,6 +1450,34 @@ TEST_CASE_METHOD(SqlTestFixture, "Trimmed fixed strings strip trailing padding t CHECK(result->stringWide == SqlTrimmedWideFixedString<32> { L"Hellö" }); } +// Regression for #485: BasicStringBinder::OutputColumn passes BufferLength=N +// (not N+1) to SQLBindCol, so ODBC truncates any value of length N to N-1 +// data chars + null. The bug only surfaces when the stored value fills the +// full capacity with no trailing whitespace -- e.g. a 3-letter currency code +// stored in a CHAR(3) column. +struct FullCapacityFixedRow +{ + Field id {}; + Field> code {}; +}; + +TEST_CASE_METHOD(SqlTestFixture, + "Trimmed fixed string preserves a value that fills its capacity", + "[DataMapper][SqlFixedString][regression]") +{ + auto dm = DataMapper {}; + dm.CreateTable(); + + FullCapacityFixedRow row {}; + row.code = "EUR"; + dm.Create(row); + + auto const result = dm.QuerySingle(row.id); + REQUIRE(result.has_value()); + CHECK(result->code.Value().size() == 3); + CHECK(result->code.Value().str() == "EUR"); +} + // Coverage for the SQL_C_WCHAR truncation arithmetic in // detail::GetRawColumnArrayData (BasicStringBinder.hpp). The function has two // re-fetch branches: (a) the driver reports total length up front and we re-