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 :2218 — SQL_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.
Summary
When an ODBC application calls
SQLBindParameterwith C typeSQL_C_GUIDand SQL typeSQL_GUIDand 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'sBINARY(16)/CHAR(16) CHARACTER SET OCTETSwire 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
tests/test_guid_and_binary.cppis present, but every test isGTEST_SKIP() << \"Requires Phase 8: SQL_GUID type mapping and FB4+ types (not yet merged)\").::UUIDparameters viaSQLBindParameter(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 asBINARY(16))0306C176-E401-11F0-A1AC-E86A6416A6F530333036-4331-3736-2D45-3430312D3131The 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 asVARCHAR)Expected:
0306C176-E401-11F0-A1AC-E86A6416A6F5Actual (CI run 25007671590):
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
0x00of0x00 0x30 0x00 0x33 ...terminates the string at position 1.Source code analysis (v3.5.0-rc1)
OdbcConvert.cpp:1003— the only placeSQL_C_GUIDappears as a sourceconciseType:So
SQL_C_GUID → SQL_C_CHAR/WCHARis implemented (the output direction — fetching a GUID column as a string), butSQL_C_GUID → SQL_C_BINARYand the parameter-binding direction generally is not implemented.OdbcStatement.cpp:664and:2218—SQL_C_GUIDappears in the accept-list of valid C types inSQLBindCol/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: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())orINSERT ... 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 bySQLExecuteshould send the 16-byte GUID buffer over the wire so Firebird sees:BINARY(16)/CHAR(16) CHARACTER SET OCTETSparameter slot: those exact 16 bytes (in Firebird's native byte order — note thatSQL_GUIDData1/Data2/Data3 are little-endian on x86, whileBINARY(16)is byte-order-agnostic, so the conversion must explicitly arrange the 16 bytes per the canonical UUID layout).VARCHAR/CHARparameter 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 usesSQLBindParameter(SQL_C_GUID, ...)to insert a known UUID (rather thanCHAR_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-testhas the test ready to merge once T5-5 lands.