Skip to content

MSSQL: GetColumn<std::string>() on a uniqueidentifier column fails with 22003 when block-prefetch is off #602

Description

@Yaraslaut

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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions