Summary
On MS SQL Server, reading a uniqueidentifier column into a std::string throws:
22003 (0) - [Microsoft][ODBC Driver 18 for SQL Server]Numeric value out of range
but only when block-prefetch is not active for that result set. The same read succeeds when prefetch is armed, which makes it look intermittent: adding an unrelated column to the SELECT can turn a working query into a throwing one.
Not reproducible on PostgreSQL or SQLite — both return the GUID text fine in every case.
Reproducer
stmt.MigrateDirect([](auto& migration) {
migration.CreateTable("Test")
.Column("Id", SqlColumnTypeDefinitions::Integer {})
.Column("G", SqlColumnTypeDefinitions::Guid {})
.Column("B", SqlColumnTypeDefinitions::VarBinary { 64 });
});
// ... insert one row ...
auto cursor = stmt.ExecuteDirect(sql);
REQUIRE(cursor.FetchRow());
auto const s = cursor.GetColumn<std::string>(1); // column "G"
sql |
MSSQL 2022 |
PostgreSQL 16.4 |
SQLite 3.49 |
SELECT "G" FROM "Test" |
OK |
OK |
OK |
SELECT "G", "Id" FROM "Test" |
OK |
OK |
OK |
SELECT "G", "B" FROM "Test" |
throws 22003 |
OK |
OK |
Mechanism
PrefetchableSqlType() (src/Lightweight/SqlStatement.cpp:661) accepts SQL_GUID and SQL_INTEGER but not SQL_VARBINARY, and a single non-prefetchable column disables prefetch for the whole result set (src/Lightweight/SqlStatement.cpp:810). That is why adding the VARBINARY column flips the outcome:
- Prefetch active — the value is served from the bound
SQL_C_GUID block and converted. Works.
- Prefetch disabled — the read falls through to
SqlDataBinder<std::string>::GetColumn, whose no-Capacity branch (src/Lightweight/DataBinder/BasicStringBinder.hpp:558) starts with a 15-byte buffer and grows it across repeated SQLGetData(SQL_C_CHAR) calls. A uniqueidentifier needs 36 chars + NUL.
The MS SQL Server driver refuses that chunked retrieval for a fixed-length non-character source type: instead of SQL_SUCCESS_WITH_INFO with truncation (which the growth loop is written to handle), it returns 22003. Per the ODBC spec, "string data, right truncated" applies to character/binary source types; for other types a too-small buffer is an out-of-range error.
Confirmed by buffer size, with the VARBINARY column present in both cases:
| Target type |
Retrieval |
MSSQL 2022 |
std::string |
grows from a 15-byte buffer |
throws 22003 |
SqlFixedString<40> |
one SQLGetData, 41-byte buffer |
OK |
So it is the small-buffer growth loop, not the GUID→char conversion itself.
Suggested direction
In the no-Capacity branch of SqlDataBinder<std::string>::GetColumn, size the first SQLGetData from the column's actual width (SQLDescribeCol/SQL_DESC_OCTET_LENGTH) instead of starting at 15, or special-case SQL_GUID to a 37-byte first call. Either removes the dependence on chunked retrieval for fixed-length source types.
Environment
- MS SQL Server 2022 (16.00.4250), ODBC Driver 18 for SQL Server 18.4, Linux
- Compared against PostgreSQL 16.4 (psqlODBC) and SQLite 3.49.1 — neither affected
- Found on branch
fix/596-sqlguid-byte-order (unrelated to that fix; it is pre-existing)
Summary
On MS SQL Server, reading a
uniqueidentifiercolumn into astd::stringthrows:but only when block-prefetch is not active for that result set. The same read succeeds when prefetch is armed, which makes it look intermittent: adding an unrelated column to the
SELECTcan turn a working query into a throwing one.Not reproducible on PostgreSQL or SQLite — both return the GUID text fine in every case.
Reproducer
sqlSELECT "G" FROM "Test"SELECT "G", "Id" FROM "Test"SELECT "G", "B" FROM "Test"Mechanism
PrefetchableSqlType()(src/Lightweight/SqlStatement.cpp:661) acceptsSQL_GUIDandSQL_INTEGERbut notSQL_VARBINARY, and a single non-prefetchable column disables prefetch for the whole result set (src/Lightweight/SqlStatement.cpp:810). That is why adding theVARBINARYcolumn flips the outcome:SQL_C_GUIDblock and converted. Works.SqlDataBinder<std::string>::GetColumn, whose no-Capacitybranch (src/Lightweight/DataBinder/BasicStringBinder.hpp:558) starts with a 15-byte buffer and grows it across repeatedSQLGetData(SQL_C_CHAR)calls. Auniqueidentifierneeds 36 chars + NUL.The MS SQL Server driver refuses that chunked retrieval for a fixed-length non-character source type: instead of
SQL_SUCCESS_WITH_INFOwith truncation (which the growth loop is written to handle), it returns22003. Per the ODBC spec, "string data, right truncated" applies to character/binary source types; for other types a too-small buffer is an out-of-range error.Confirmed by buffer size, with the
VARBINARYcolumn present in both cases:std::stringSqlFixedString<40>SQLGetData, 41-byte bufferSo it is the small-buffer growth loop, not the GUID→char conversion itself.
Suggested direction
In the no-
Capacitybranch ofSqlDataBinder<std::string>::GetColumn, size the firstSQLGetDatafrom the column's actual width (SQLDescribeCol/SQL_DESC_OCTET_LENGTH) instead of starting at 15, or special-caseSQL_GUIDto a 37-byte first call. Either removes the dependence on chunked retrieval for fixed-length source types.Environment
fix/596-sqlguid-byte-order(unrelated to that fix; it is pre-existing)