diff --git a/IscDbc/Connection.h b/IscDbc/Connection.h index 8c980fb2..52d78c94 100644 --- a/IscDbc/Connection.h +++ b/IscDbc/Connection.h @@ -525,6 +525,12 @@ class HeadSqlVar virtual void setSqlLen ( short len ) = 0; virtual short getSqlMultiple () = 0; + // True for Firebird BINARY(n) / VARBINARY(n), i.e. CHAR(n) / VARCHAR(n) + // CHARACTER SET OCTETS at the wire layer (the FB4+ standard-compliant + // aliases). Implementation in IscHeadSqlVar.h. + virtual bool isBinary () = 0; + virtual bool isVarBinary () = 0; + virtual char * getSqlData() = 0; virtual short * getSqlInd() = 0; virtual void setSqlData( char *data ) = 0; diff --git a/IscDbc/IscHeadSqlVar.h b/IscDbc/IscHeadSqlVar.h index 38fb53b8..215d5b28 100644 --- a/IscDbc/IscHeadSqlVar.h +++ b/IscDbc/IscHeadSqlVar.h @@ -103,6 +103,31 @@ class IscHeadSqlVar : public HeadSqlVar inline char * getSqlData() { return sqlvar->sqldata; } inline short * getSqlInd() { return sqlvar->sqlind; } + // Charset id for CHARACTER SET OCTETS, used to identify Firebird's + // FB4+ BINARY / VARBINARY types at the wire layer. SqlProperties + // carries the charset id in a dedicated `sqlcharset` field (separate + // from `sqlsubtype`, which is documented as "BLOBs & Text types only"; + // see IscDbc/Sqlda.h). Charset id 1 = OCTETS per the driver's charset + // table at IscDbc/MultibyteConvert.cpp and per Firebird's + // RDB$CHARACTER_SETS system table. + static constexpr unsigned CHARSET_OCTETS = 1; + + // True iff the slot describes a Firebird BINARY(n) — the FB4+ alias for + // CHAR(n) CHARACTER SET OCTETS. + inline bool isBinary() + { + return sqlvar->sqltype == SQL_TEXT + && sqlvar->sqlcharset == CHARSET_OCTETS; + } + + // True iff the slot describes a Firebird VARBINARY(n) — the FB4+ alias + // for VARCHAR(n) CHARACTER SET OCTETS. + inline bool isVarBinary() + { + return sqlvar->sqltype == SQL_VARYING + && sqlvar->sqlcharset == CHARSET_OCTETS; + } + // not used //void setSqlInd( short *ind ) { sqlvar->sqlind = ind; } //void setSqlType ( short type ) { sqlvar->sqltype = type; } diff --git a/OdbcConvert.cpp b/OdbcConvert.cpp index 1f5a8594..61273731 100644 --- a/OdbcConvert.cpp +++ b/OdbcConvert.cpp @@ -1004,9 +1004,29 @@ ADRESS_FUNCTION OdbcConvert::getAdressFunction(DescRecord * from, DescRecord * t switch(to->conciseType) { case SQL_C_CHAR: - return &OdbcConvert::convGuidToString; case SQL_C_WCHAR: - return &OdbcConvert::convGuidToStringW; + if ( to->isIndicatorSqlDa ) + { + // Input parameter. Firebird describes BINARY(n) / VARBINARY(n) + // as (VAR)CHAR(n) CHARACTER SET OCTETS and the driver maps both + // to SQL_C_CHAR, so raw bytes vs. text is decided from the + // sqlvar, not from conciseType. setTypeText() turns a + // SQL_VARYING slot into SQL_TEXT so the converters write at + // offset 0 with no length prefix (a no-op for SQL_TEXT; the + // charset is kept either way). + const bool octets = to->headSqlVarPtr->isBinary() + || to->headSqlVarPtr->isVarBinary(); + to->headSqlVarPtr->setTypeText(); + if ( octets ) + return &OdbcConvert::convGuidToBinary; + return &OdbcConvert::convGuidToString; + } + // Fetching a column described as SQL_GUID into the application + // buffer. Unreachable until the column-side SQL_GUID mapping + // lands (T5-5, #287). + if ( to->conciseType == SQL_C_WCHAR ) + return &OdbcConvert::convGuidToStringW; + return &OdbcConvert::convGuidToString; default: return &OdbcConvert::notYetImplemented; } @@ -1638,29 +1658,90 @@ int OdbcConvert::notYetImplemented(DescRecord * from, DescRecord * to) // Guid //////////////////////////////////////////////////////////////////////// +// Format a SQLGUID as the 36-char canonical UUID string (8-4-4-4-12 hex, +// upper-case, no NUL counted). `out` must hold at least GUID_STRING_LEN + 1 +// bytes. Returns snprintf's character count — always GUID_STRING_LEN for a +// well-formed call, or negative on a (practically impossible) encoding error. +// A GUID never formats to any other length, so callers treat +// "result != GUID_STRING_LEN" as a hard error rather than clamping. +static int formatGuidCanonical(char* out, size_t cap, const SQLGUID* g) +{ + return snprintf(out, cap, + "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", + (unsigned int) g->Data1, g->Data2, g->Data3, + g->Data4[0], g->Data4[1], g->Data4[2], g->Data4[3], + g->Data4[4], g->Data4[5], g->Data4[6], g->Data4[7]); +} + +// Write SQLGUID as the 36-char canonical UUID string. +// +// Firebird side (to->isIndicatorSqlDa): the text is staged in +// DescRecord::localDataPtr and sqldata is pointed at it, the idiom +// transferStringToAllowedType uses, instead of being written into the wire +// buffer directly. That buffer is whatever Firebird described for the slot: +// it may be SQL_VARYING (2-byte length prefix), shorter than 36 bytes +// (CHAR(1), VARCHAR(10)), or a UTF8 / UNICODE_FSS text that the driver maps +// to SQL_C_WCHAR (a CHAR(36) UTF8 slot is 144 bytes). The dispatch has +// already called setTypeText(); Sqlda::checkAndRebuild() then rebuilds the +// message from the new type and length, and Firebird itself reports the +// truncation when the slot cannot hold 36 chars. +// +// Application side: copy into the bound buffer honoring its size, report +// the full length via the indicator and 01004 when it does not fit. int OdbcConvert::convGuidToString(DescRecord * from, DescRecord * to) { char* pointer = (char*)getAdressBindDataTo((char*)to->dataPtr); SQLLEN * indicatorTo = getAdressBindIndTo((char*)to->indicatorPtr); SQLLEN * indicatorFrom = getAdressBindIndFrom((char*)from->indicatorPtr); - ODBCCONVERT_CHECKNULL( pointer ); + if ( to->isIndicatorSqlDa ) + { + ODBCCONVERT_CHECKNULL_SQLDA; + } + else + { + ODBCCONVERT_CHECKNULL( pointer ); + } SQLGUID *g = (SQLGUID*)getAdressBindDataFrom((char*)from->dataPtr); - int len, outlen = to->length; - len = snprintf(pointer, outlen, "%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", - (unsigned int) g->Data1, g->Data2, g->Data3, g->Data4[0], g->Data4[1], g->Data4[2], g->Data4[3], g->Data4[4], g->Data4[5], g->Data4[6], g->Data4[7]); + char buf[GUID_STRING_LEN + 1]; + if ( formatGuidCanonical(buf, sizeof(buf), g) != GUID_STRING_LEN ) + { + if ( parentStmt ) + parentStmt->postError( new OdbcError( 0, "HY000", "Internal error formatting GUID" ) ); + return SQL_ERROR; + } - if ( len == -1 ) len = outlen; + if ( to->isIndicatorSqlDa ) + { + // allocateLocalDataPtr() frees any previous buffer first, so the + // staging area is always exactly GUID_STRING_LEN + 1 bytes. + to->allocateLocalDataPtr( GUID_STRING_LEN + 1 ); + memcpy( to->localDataPtr, buf, GUID_STRING_LEN ); + to->headSqlVarPtr->setSqlLen( (short)GUID_STRING_LEN ); + to->headSqlVarPtr->setSqlData( to->localDataPtr ); + return SQL_SUCCESS; + } + + int outlen = to->length; + SQLRETURN ret = SQL_SUCCESS; + int copy = GUID_STRING_LEN; + if ( outlen <= GUID_STRING_LEN ) + { + copy = outlen > 0 ? outlen - 1 : 0; + if ( parentStmt ) + parentStmt->postError( new OdbcError( 0, "01004", "Data truncated" ) ); + ret = SQL_SUCCESS_WITH_INFO; + } + if ( copy > 0 ) + memcpy( pointer, buf, copy ); + pointer[copy] = 0; - if ( to->isIndicatorSqlDa ) { - to->headSqlVarPtr->setSqlLen(len); - } else if ( indicatorTo ) - setIndicatorPtr(indicatorTo, len, to); + setIndicatorPtr( indicatorTo, GUID_STRING_LEN, to ); - return SQL_SUCCESS; + return ret; } int OdbcConvert::convGuidToStringW(DescRecord * from, DescRecord * to) @@ -1688,6 +1769,49 @@ int OdbcConvert::convGuidToStringW(DescRecord * from, DescRecord * to) return SQL_SUCCESS; } +// Write SQLGUID as 16 bytes in canonical UUID byte order (Data1/2/3 +// big-endian, Data4 unchanged) into a Firebird (VAR)CHAR(n) CHARACTER SET +// OCTETS parameter, i.e. BINARY(n) / VARBINARY(n). Input parameters only; +// the dispatch has already called setTypeText(), so the write goes at +// offset 0 of the wire buffer and sqllen reports the 16 bytes. +int OdbcConvert::convGuidToBinary(DescRecord * from, DescRecord * to) +{ + char* pointer = (char*)getAdressBindDataTo((char*)to->dataPtr); + SQLLEN * indicatorTo = getAdressBindIndTo((char*)to->indicatorPtr); + SQLLEN * indicatorFrom = getAdressBindIndFrom((char*)from->indicatorPtr); + + ODBCCONVERT_CHECKNULL_SQLDA; + + SQLGUID *g = (SQLGUID*)getAdressBindDataFrom((char*)from->dataPtr); + + unsigned char buf[GUID_BINARY_LEN]; + buf[0] = (unsigned char)((g->Data1 >> 24) & 0xFF); + buf[1] = (unsigned char)((g->Data1 >> 16) & 0xFF); + buf[2] = (unsigned char)((g->Data1 >> 8) & 0xFF); + buf[3] = (unsigned char)( g->Data1 & 0xFF); + buf[4] = (unsigned char)((g->Data2 >> 8) & 0xFF); + buf[5] = (unsigned char)( g->Data2 & 0xFF); + buf[6] = (unsigned char)((g->Data3 >> 8) & 0xFF); + buf[7] = (unsigned char)( g->Data3 & 0xFF); + memcpy(buf + 8, g->Data4, 8); + + // A slot shorter than 16 bytes cannot hold a GUID, and a truncated GUID + // is corrupt rather than merely short, so this is a hard error. ODBC + // SQLSTATE 22001 "String data, right truncation". + if ( (int)to->length < GUID_BINARY_LEN ) + { + if ( parentStmt ) + parentStmt->postError( new OdbcError( 0, "22001", + "String data, right truncation - GUID requires 16 bytes" ) ); + return SQL_ERROR; + } + + memcpy( pointer, buf, GUID_BINARY_LEN ); + to->headSqlVarPtr->setSqlLen( GUID_BINARY_LEN ); + + return SQL_SUCCESS; +} + //////////////////////////////////////////////////////////////////////// // TinyInt diff --git a/OdbcConvert.h b/OdbcConvert.h index 16618b83..77d27c10 100644 --- a/OdbcConvert.h +++ b/OdbcConvert.h @@ -48,6 +48,13 @@ class DescRecord; class OdbcConvert; class OdbcStatement; +// Canonical SQLGUID wire sizes. +// - GUID_BINARY_LEN: 16 raw bytes, the BINARY(16) / VARBINARY(16) wire form. +// - GUID_STRING_LEN: 36 chars (8-4-4-4-12 hex+dashes, no NUL terminator), +// the canonical text form written into VARCHAR/CHAR slots. +static constexpr int GUID_BINARY_LEN = 16; +static constexpr int GUID_STRING_LEN = 36; + typedef int (OdbcConvert::*ADRESS_FUNCTION)(DescRecord * from, DescRecord * to); class OdbcConvert @@ -94,6 +101,7 @@ class OdbcConvert // Guid int convGuidToString(DescRecord * from, DescRecord * to); int convGuidToStringW(DescRecord * from, DescRecord * to); + int convGuidToBinary(DescRecord * from, DescRecord * to); // TinyInt int convTinyIntToBoolean(DescRecord * from, DescRecord * to); diff --git a/tests/test_guid_and_binary.cpp b/tests/test_guid_and_binary.cpp index 16f39035..7e09e6c8 100644 --- a/tests/test_guid_and_binary.cpp +++ b/tests/test_guid_and_binary.cpp @@ -467,6 +467,339 @@ TEST_F(Fb4PlusTest, Binary16MapsToGuid) { EXPECT_EQ(ind, 16); } +// ============================================================ +// SQL_C_GUID parameter binding direction (issue #295) +// +// These tests cover SQLBindParameter(SQL_C_GUID, ...) — the input direction. +// They do NOT depend on SQL_GUID column type mapping (T5-5), so they run on +// all Firebird versions that support the underlying SQL types. +// ============================================================ + +class GuidParamBindingTest : public OdbcConnectedTest { +protected: + // The known UUID used by every test — canonical text form and matching + // SQLGUID struct + canonical 16-byte representation. + static constexpr const char* kCanonicalText = + "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"; + + static SQLGUID makeKnownGuid() { + SQLGUID g{}; + g.Data1 = 0xA0EEBC99; + g.Data2 = 0x9C0B; + g.Data3 = 0x4EF8; + const unsigned char d4[8] = {0xBB, 0x6D, 0x6B, 0xB9, 0xBD, 0x38, 0x0A, 0x11}; + memcpy(g.Data4, d4, 8); + return g; + } + + int GetServerMajor() { + return GetServerMajorVersion(hDbc); + } +}; + +// Bind SQL_C_GUID to a CHAR(16) CHARACTER SET OCTETS parameter and verify the +// 16 bytes Firebird stores match the canonical UUID byte order. +TEST_F(GuidParamBindingTest, BindGuidToCharOctets16) { + REQUIRE_FIREBIRD_CONNECTION(); + + TempTable table(this, "TEST_PB_GUID_OCT", + "ID INTEGER NOT NULL PRIMARY KEY, " + "VAL CHAR(16) CHARACTER SET OCTETS"); + + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = sizeof(guid); + SQLINTEGER id = 1; + SQLLEN idInd = sizeof(id); + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_SLONG, SQL_INTEGER, 0, 0, &id, sizeof(id), &idInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLExecDirect(hStmt, + (SQLCHAR*)"INSERT INTO TEST_PB_GUID_OCT (ID, VAL) VALUES (?, ?)", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + Commit(); + ReallocStmt(); + + // Read back as canonical text via UUID_TO_CHAR — Firebird's UUID_TO_CHAR + // round-trips an OCTETS-string in canonical UUID byte order. + ExecDirect("SELECT UUID_TO_CHAR(VAL) FROM TEST_PB_GUID_OCT WHERE ID = 1"); + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + SQLCHAR buf[64] = {}; + SQLLEN ind = 0; + ret = SQLGetData(hStmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + std::string text((char*)buf); + while (!text.empty() && text.back() == ' ') text.pop_back(); + EXPECT_EQ(text, kCanonicalText) + << "BINARY(16) GUID round-trip failed: stored bytes do not decode to " + << "the canonical UUID. Got: '" << text << "'"; +} + +// Bind SQL_C_GUID to a text parameter. Firebird describes the `?` inside +// CHAR_TO_UUID(?) as CHAR(36) in the connection charset. With CHARSET=UTF8 +// that slot is 144 bytes and the driver maps it to SQL_C_WCHAR; before the +// fix the wide-char writer put UTF-16 on the wire, which is the "hex digit +// at position 2" failure from issue #295. With CHARSET=NONE the slot is +// CHAR(36) ASCII and the old code passed on Windows by accident (_snprintf +// fits exactly 36 chars), so this test guards the regression only under +// UTF8 or on Linux. The driver must send the 36-char canonical UUID string. +TEST_F(GuidParamBindingTest, BindGuidToVarcharViaCharToUuid) { + REQUIRE_FIREBIRD_CONNECTION(); + + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = sizeof(guid); + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + // Round-trip through CHAR_TO_UUID then UUID_TO_CHAR — exercises the + // SQL_C_GUID → VARCHAR wire path on the way in. + ret = SQLExecDirect(hStmt, + (SQLCHAR*)"SELECT UUID_TO_CHAR(CHAR_TO_UUID(?)) FROM rdb$database", + SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "Exec: " << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << "Fetch: " << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + SQLCHAR buf[64] = {}; + SQLLEN ind = 0; + ret = SQLGetData(hStmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + std::string text((char*)buf); + while (!text.empty() && text.back() == ' ') text.pop_back(); + EXPECT_EQ(text, kCanonicalText) + << "SQL_C_GUID → VARCHAR round-trip failed. Got: '" << text << "'"; +} + +// Bind SQL_C_GUID to a literal `UUID_TO_CHAR(?)` SELECT — Firebird infers +// BINARY(16) for the parameter slot. The wire payload must be the 16 raw +// bytes of the canonical UUID, not a stringified form. +TEST_F(GuidParamBindingTest, BindGuidToUuidToCharRoundtrip) { + REQUIRE_FIREBIRD_CONNECTION(); + + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = sizeof(guid); + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLExecDirect(hStmt, + (SQLCHAR*)"SELECT UUID_TO_CHAR(?) FROM rdb$database", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + SQLCHAR buf[64] = {}; + SQLLEN ind = 0; + ret = SQLGetData(hStmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + std::string text((char*)buf); + while (!text.empty() && text.back() == ' ') text.pop_back(); + EXPECT_EQ(text, kCanonicalText) + << "SQL_C_GUID → BINARY(16) (UUID_TO_CHAR(?)) round-trip failed. " + << "Got: '" << text << "' — this is the corruption pattern from issue #295."; +} + +// Bind SQL_C_GUID to a VARCHAR(16) CHARACTER SET OCTETS parameter — the +// wire form of a native FB4+ VARBINARY(16). Unlike CHAR(16) OCTETS (a +// fixed SQL_TEXT slot, covered by BindGuidToCharOctets16), this is a +// SQL_VARYING slot, so it exercises convGuidToBinary writing into a +// length-prefixed wire buffer. The dispatch calls setTypeText() to convert +// the varying wire to SQL_TEXT before writing 16 raw bytes; without that the +// first GUID bytes are read as a VARYING length prefix and the buffer is +// over-read (SEGFAULT on the FB6 snapshot, silent on FB5). +// +// Skipped on FB6: even with the fix, the current FB6 master snapshot aborts +// this parameterized OCTETS-VARYING insert with a server-side "Stack +// overflow" — the same parameterized-query incompatibility already guarded +// across the suite (see SKIP_ON_FIREBIRD6 usages). Exercised on FB 3/4/5. +TEST_F(GuidParamBindingTest, BindGuidToVarcharOctets16) { + REQUIRE_FIREBIRD_CONNECTION(); + SKIP_ON_FIREBIRD6(); + + TempTable table(this, "TEST_PB_GUID_VOCT", + "ID INTEGER NOT NULL PRIMARY KEY, " + "VAL VARCHAR(16) CHARACTER SET OCTETS"); + + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = sizeof(guid); + SQLINTEGER id = 1; + SQLLEN idInd = sizeof(id); + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_SLONG, SQL_INTEGER, 0, 0, &id, sizeof(id), &idInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLExecDirect(hStmt, + (SQLCHAR*)"INSERT INTO TEST_PB_GUID_VOCT (ID, VAL) VALUES (?, ?)", SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + Commit(); + ReallocStmt(); + + ExecDirect("SELECT UUID_TO_CHAR(VAL) FROM TEST_PB_GUID_VOCT WHERE ID = 1"); + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + SQLCHAR buf[64] = {}; + SQLLEN ind = 0; + ret = SQLGetData(hStmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + std::string text((char*)buf); + while (!text.empty() && text.back() == ' ') text.pop_back(); + EXPECT_EQ(text, kCanonicalText) + << "VARBINARY(16) (VARCHAR(16) OCTETS) GUID round-trip failed. " + << "Got: '" << text << "'"; +} + +// ODBC defines no GUID conversion to numeric or date/time SQL types (see +// "C to SQL: GUID"), so the driver must reject the bind with 07006 instead +// of sending a string and letting Firebird fail on the conversion. +TEST_F(GuidParamBindingTest, BindGuidToIntegerParamIsRejected) { + REQUIRE_FIREBIRD_CONNECTION(); + + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = sizeof(guid); + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLExecDirect(hStmt, + (SQLCHAR*)"SELECT CAST(? AS INTEGER) FROM rdb$database", SQL_NTS); + EXPECT_EQ(ret, SQL_ERROR); + EXPECT_EQ(GetSqlState(SQL_HANDLE_STMT, hStmt), "07006") + << GetOdbcError(SQL_HANDLE_STMT, hStmt); +} + +// An OCTETS slot shorter than 16 bytes cannot hold a GUID. A truncated GUID +// is corrupt rather than merely short, so the driver reports 22001 instead +// of writing a partial value. +TEST_F(GuidParamBindingTest, BindGuidToUndersizedOctetsIsRejected) { + REQUIRE_FIREBIRD_CONNECTION(); + + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = sizeof(guid); + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLExecDirect(hStmt, + (SQLCHAR*)"SELECT CAST(? AS CHAR(8) CHARACTER SET OCTETS) FROM rdb$database", + SQL_NTS); + EXPECT_EQ(ret, SQL_ERROR); + EXPECT_EQ(GetSqlState(SQL_HANDLE_STMT, hStmt), "22001") + << GetOdbcError(SQL_HANDLE_STMT, hStmt); +} + +// Any OCTETS slot of 16 bytes or more receives the 16 raw bytes, not the +// 36-char text: a VARBINARY(32) parameter carries a 16-byte value that +// UUID_TO_CHAR decodes back to the canonical form. +// +// Skipped on FB6 for the same OCTETS-varying parameter limitation noted on +// BindGuidToVarcharOctets16. +TEST_F(GuidParamBindingTest, BindGuidToOversizedOctetsStoresRawBytes) { + REQUIRE_FIREBIRD_CONNECTION(); + SKIP_ON_FIREBIRD6(); + + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = sizeof(guid); + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLExecDirect(hStmt, + (SQLCHAR*)"SELECT UUID_TO_CHAR(CAST(? AS VARCHAR(32) CHARACTER SET OCTETS)) " + "FROM rdb$database", + SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + SQLCHAR buf[64] = {}; + SQLLEN ind = 0; + ret = SQLGetData(hStmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + std::string text((char*)buf); + while (!text.empty() && text.back() == ' ') text.pop_back(); + EXPECT_EQ(text, kCanonicalText) + << "VARBINARY(32) GUID round-trip failed. Got: '" << text << "'"; +} + +// Executing a prepared statement with a NULL GUID and then with a value must +// clear the null flag on the wire, so the second execution sends the value. +// Covers both the OCTETS and the text parameter paths. +TEST_F(GuidParamBindingTest, BindGuidValueAfterNull) { + REQUIRE_FIREBIRD_CONNECTION(); + + const char* statements[] = { + "SELECT UUID_TO_CHAR(?) FROM rdb$database", + "SELECT UUID_TO_CHAR(CHAR_TO_UUID(?)) FROM rdb$database", + }; + + for (const char* sql : statements) { + SQLGUID guid = makeKnownGuid(); + SQLLEN guidInd = SQL_NULL_DATA; + + SQLRETURN ret = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, + SQL_C_GUID, SQL_GUID, 16, 0, &guid, sizeof(guid), &guidInd); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + ret = SQLPrepare(hStmt, (SQLCHAR*)sql, SQL_NTS); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << sql << ": " << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + // NULL in, NULL out. + ret = SQLExecute(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << sql << ": " << GetOdbcError(SQL_HANDLE_STMT, hStmt); + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << sql << ": " << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + SQLCHAR buf[64] = {}; + SQLLEN ind = 0; + ret = SQLGetData(hStmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + EXPECT_EQ(ind, SQL_NULL_DATA) << sql; + SQLCloseCursor(hStmt); + + // Value in, value out. + guidInd = sizeof(guid); + ret = SQLExecute(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << sql << ": " << GetOdbcError(SQL_HANDLE_STMT, hStmt); + ret = SQLFetch(hStmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)) << sql << ": " << GetOdbcError(SQL_HANDLE_STMT, hStmt); + + memset(buf, 0, sizeof(buf)); + ret = SQLGetData(hStmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + std::string text((char*)buf); + while (!text.empty() && text.back() == ' ') text.pop_back(); + EXPECT_EQ(text, kCanonicalText) << sql << ": value after NULL was lost"; + + ReallocStmt(); + } +} + // Test: DECFLOAT column insertion and retrieval on Firebird 4+ TEST_F(Fb4PlusTest, DecfloatInsertAndRetrieve) { GTEST_SKIP() << "Requires Phase 8: SQL_GUID type mapping and FB4+ types (not yet merged)";