Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions src/Lightweight/DataBinder/BasicStringBinder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,18 @@ struct SqlDataBinder<AnsiStringType>
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<N> 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<SQLLEN>(AnsiStringType::Capacity) + 1;
else
return static_cast<SQLLEN>(StringTraits::Size(result));
}();

return SQLBindCol(stmt, column, SQL_C_CHAR, (SQLPOINTER) StringTraits::Data(result), bufferLength, indicator);
}
Comment on lines +289 to 301
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if -else lambda construction does not strike me as very elegant.

this is just what was generated by Claude. Feel free to replace if you find a solution less offensive to the eye that still passes the test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the pattern we use in the code to make variables const


static void PostProcessOutputColumn(SQLHSTMT stmt, SQLUSMALLINT column, AnsiStringType* result, SQLLEN* indicator)
Expand Down Expand Up @@ -338,8 +344,10 @@ struct SqlDataBinder<AnsiStringType>
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)
Expand Down
4 changes: 2 additions & 2 deletions src/Lightweight/DataBinder/SqlFixedString.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -471,9 +471,9 @@ struct SqlBasicStringOperations<SqlFixedString<N, T, Mode>>
LIGHTWEIGHT_FORCE_INLINE static void TrimRight(ValueType* boundOutputString, SQLLEN indicator) noexcept
{
#if defined(_WIN32)
size_t n = (std::min) (static_cast<size_t>(indicator) / sizeof(CharType), N - 1);
size_t n = (std::min) (static_cast<size_t>(indicator) / sizeof(CharType), N);
#else
size_t n = std::min(static_cast<size_t>(indicator), N - 1);
size_t n = std::min(static_cast<size_t>(indicator), N);
#endif
// Strip trailing whitespace and null characters
while (n > 0 && (std::isspace((*boundOutputString)[n - 1]) || (*boundOutputString)[n - 1] == '\0'))
Expand Down
28 changes: 28 additions & 0 deletions src/tests/DataBinderTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint64_t, PrimaryKey::ServerSideAutoIncrement> id {};
Field<SqlTrimmedFixedString<3>> code {};
};

TEST_CASE_METHOD(SqlTestFixture,
"Trimmed fixed string preserves a value that fills its capacity",
"[DataMapper][SqlFixedString][regression]")
{
auto dm = DataMapper {};
dm.CreateTable<FullCapacityFixedRow>();

FullCapacityFixedRow row {};
row.code = "EUR";
dm.Create(row);

auto const result = dm.QuerySingle<FullCapacityFixedRow>(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-
Expand Down
Loading