Skip to content

SQL_C_GUID parameter binding sends corrupted data on the wire (Phase 8 / T5-5) #295

@fdcastel

Description

@fdcastel

Summary

When an ODBC application calls SQLBindParameter with C type SQL_C_GUID and SQL type SQL_GUID and provides a 16-byte UUID buffer, the driver accepts the call without error but does not convert the C-side 16-byte GUID into Firebird's BINARY(16) / CHAR(16) CHARACTER SET OCTETS wire format. The data observed on the Firebird side is corrupted (and the corruption pattern depends on the inferred SQL parameter type).

This is captured in #287 Tier 5 / T5-5 ("SQL_GUID type mapping") as ❌ OPEN. Filing this as a concrete reproducer with empirical observations so the eventual T5-5 fix has a clear acceptance test.

Versions tested

  • Driver: firebird-odbc 3.0.1.21 (current Chocolatey release) and v3.5.0-rc1 sources (the new Phase 8 GUID test file tests/test_guid_and_binary.cpp is present, but every test is GTEST_SKIP() << \"Requires Phase 8: SQL_GUID type mapping and FB4+ types (not yet merged)\").
  • Firebird: 5.0.3.1 (Windows Server 2025).
  • Application: duckdb/odbc-scanner, which binds DuckDB ::UUID parameters via SQLBindParameter(SQL_C_GUID, SQL_GUID, len=16, ptr=16-byte-GUID, ...) (see src/types/uuid_type.cpp lines 33-44).

Reproducer

The failing CI run is on a paused branch on my fork: fdcastel/odbc-scanner:firebird-uuid-test. Two CI runs document the behavior:

Reproducer 1 — UUID_TO_CHAR(?) (parameter inferred by Firebird as BINARY(16))

SELECT UUID_TO_CHAR(?) FROM rdb$database
-- bound parameter: SQL_C_GUID, 16 bytes, value = UUID '0306C176-E401-11F0-A1AC-E86A6416A6F5'
  • Expected (clean round-trip): 0306C176-E401-11F0-A1AC-E86A6416A6F5
  • Actual (CI run 25007127097, attempt 2): 30333036-4331-3736-2D45-3430312D3131

The actual 16 bytes Firebird stored are 30 33 30 36 43 31 37 36 2D 45 34 30 31 2D 31 31 — i.e., the ASCII characters of the first 16 chars of the canonical-form UUID string. The driver appears to convert the SQL_C_GUID buffer to a 36-char canonical string and then truncate to 16 bytes when binding to a BINARY(16) parameter slot.

Reproducer 2 — CHAR_TO_UUID(?) (parameter inferred by Firebird as VARCHAR)

SELECT UUID_TO_CHAR(CHAR_TO_UUID(?)) FROM rdb$database
-- same SQL_C_GUID parameter as above
  • Expected: 0306C176-E401-11F0-A1AC-E86A6416A6F5

  • Actual (CI run 25007671590):

    HY000(-833): [ODBC Firebird Driver][Firebird]expression evaluation not supported
    -Human readable UUID argument for CHAR_TO_UUID must have hex digit at position 2 instead of \"\"
    

    CHAR_TO_UUID receives an essentially empty string. Pattern is consistent with the SQL_C_GUID buffer being interpreted as a wide-char (UTF-16) C string by a narrow-char path — the leading 0x00 of 0x00 0x30 0x00 0x33 ... terminates the string at position 1.

Source code analysis (v3.5.0-rc1)

OdbcConvert.cpp:1003 — the only place SQL_C_GUID appears as a source conciseType:

case SQL_C_GUID:
    switch(to->conciseType)
    {
    case SQL_C_CHAR:
        return &OdbcConvert::convGuidToString;
    case SQL_C_WCHAR:
        return &OdbcConvert::convGuidToStringW;
    default:
        return &OdbcConvert::notYetImplemented;
    }
    break;

So SQL_C_GUID → SQL_C_CHAR/WCHAR is implemented (the output direction — fetching a GUID column as a string), but SQL_C_GUID → SQL_C_BINARY and the parameter-binding direction generally is not implemented.

OdbcStatement.cpp:664 and :2218SQL_C_GUID appears in the accept-list of valid C types in SQLBindCol / SQLBindParameter (case SQL_C_GUID: break;), so the driver does not reject the call. But there is no corresponding wire-format conversion when the parameter is later sent.

tests/test_guid_and_binary.cpp — every one of the eight SQL_GUID tests (InsertAndRetrieveUuidBinary, UuidToCharReturnsValidFormat, CharToUuidRoundtrip, RetrieveAsSqlGuidStruct, etc.) starts with:

GTEST_SKIP() << \"Requires Phase 8: SQL_GUID type mapping and FB4+ types (not yet merged)\";

confirming the maintainers consider full SQL_GUID support a future (Phase 8) item. None of those tests currently cover the parameter-binding direction specifically — they all use INSERT ... VALUES (GEN_UUID()) or INSERT ... VALUES (CHAR_TO_UUID('literal')) to populate columns server-side, then fetch.

Expected behavior

SQLBindParameter(stmt, n, SQL_PARAM_INPUT, SQL_C_GUID, SQL_GUID, 16, 0, ptr, 16, &len) followed by SQLExecute should send the 16-byte GUID buffer over the wire so Firebird sees:

  • For a BINARY(16) / CHAR(16) CHARACTER SET OCTETS parameter slot: those exact 16 bytes (in Firebird's native byte order — note that SQL_GUID Data1/Data2/Data3 are little-endian on x86, while BINARY(16) is byte-order-agnostic, so the conversion must explicitly arrange the 16 bytes per the canonical UUID layout).
  • For a VARCHAR/CHAR parameter slot: the 36-char canonical-form UUID string, narrow-char encoded.

The fix maps to #287 T5-5 ("add GUID conversion methods (GUID↔string, GUID↔binary)"). Suggested acceptance test: un-skip CharToUuidRoundtrip, InsertAndRetrieveUuidBinary, and add a parallel test that uses SQLBindParameter(SQL_C_GUID, ...) to insert a known UUID (rather than CHAR_TO_UUID('literal')), then fetches it back and compares.

Downstream

A duckdb/odbc-scanner PR #169 follow-up adding a Firebird UUID round-trip test is paused on this issue. The branch fdcastel/odbc-scanner:firebird-uuid-test has the test ready to merge once T5-5 lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions