Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions IscDbc/Connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,8 @@ class HeadSqlVar
//virtual void setSqlSubType ( short subtype ) = 0;
virtual void setSqlLen ( short len ) = 0;
virtual short getSqlMultiple () = 0;
virtual short getSqlSubtype () = 0;
Comment thread
fdcastel marked this conversation as resolved.
Outdated
virtual short getSqlLen () = 0;

virtual char * getSqlData() = 0;
virtual short * getSqlInd() = 0;
Expand Down
2 changes: 2 additions & 0 deletions IscDbc/IscHeadSqlVar.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ class IscHeadSqlVar : public HeadSqlVar
inline void setSqlData ( char* data ) { sqlvar->sqldata = data; }

inline short getSqlMultiple () { return sqlMultiple; }
inline short getSqlSubtype () { return sqlvar->sqlsubtype; }
Comment thread
fdcastel marked this conversation as resolved.
Outdated
inline short getSqlLen () { return sqlvar->sqllen; }
inline char * getSqlData() { return sqlvar->sqldata; }
inline short * getSqlInd() { return sqlvar->sqlind; }

Expand Down
105 changes: 105 additions & 0 deletions OdbcConvert.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,37 @@ ADRESS_FUNCTION OdbcConvert::getAdressFunction(DescRecord * from, DescRecord * t
}
break;
case SQL_C_GUID:
// Wire-side (input parameter binding) — Firebird's IPD describes the
// slot as either (VAR)BINARY(16) / (VAR)CHAR(16) CHARACTER SET OCTETS
// (subtype 1, sqllen 16) or a text VARCHAR/CHAR. Route to dedicated
// wire-only conversions; leave the pre-existing convGuidToString /
// convGuidToStringW untouched for the app-side fetch path below.
if ( to->isIndicatorSqlDa )
{
// BINARY(16) / VARBINARY(16) — i.e. (VAR)CHAR(16) CHARACTER SET
// OCTETS, which is how Firebird represents native FB4+ BINARY types
// on the wire (subtype 1, sqllen 16). Send 16 raw canonical bytes.
// setTypeText() converts a SQL_VARYING wire (VARBINARY) to SQL_TEXT
// so convGuidToBinary can write at offset 0 with no length prefix;
// without it Firebird reads the first GUID bytes as a VARYING length
// prefix and over-reads the buffer (SEGFAULT on FB6). A fixed
// CHAR(16) wire is already SQL_TEXT, so this is a no-op there.
if ( to->headSqlVarPtr->getSqlSubtype() == 1
Comment thread
fdcastel marked this conversation as resolved.
Outdated
Comment thread
fdcastel marked this conversation as resolved.
Outdated
&& to->headSqlVarPtr->getSqlLen() == 16 )
{
to->headSqlVarPtr->setTypeText();
return &OdbcConvert::convGuidToBinary;
}
// Text wire (VARCHAR/CHAR, any charset) — 36-char canonical UUID.
// setTypeText() converts SQL_VARYING to SQL_TEXT so convGuidToVarString
// can write the bytes without a length prefix.
to->headSqlVarPtr->setTypeText();
Comment thread
fdcastel marked this conversation as resolved.
return &OdbcConvert::convGuidToVarString;
Comment thread
fdcastel marked this conversation as resolved.
Outdated
}
Comment thread
fdcastel marked this conversation as resolved.
// App-side output (fetching a column described as SQL_GUID into the
// application's bound buffer). Currently unreachable because the column
// side of SQL_GUID mapping is task T5-5 (open), but kept intact for when
// that lands.
switch(to->conciseType)
{
case SQL_C_CHAR:
Expand Down Expand Up @@ -1688,6 +1719,80 @@ 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).
// Used when the target is BINARY(16) / CHAR(16) CHARACTER SET OCTETS or SQL_C_BINARY.
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( pointer );

SQLGUID *g = (SQLGUID*)getAdressBindDataFrom((char*)from->dataPtr);

unsigned char buf[16];
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);

int outlen = (int)to->length;
int len = outlen < 16 ? outlen : 16;
Comment thread
fdcastel marked this conversation as resolved.
Outdated

if ( len > 0 )
memcpy(pointer, buf, len);

if ( to->isIndicatorSqlDa ) {
to->headSqlVarPtr->setSqlLen(len);
} else
if ( indicatorTo )
setIndicatorPtr(indicatorTo, len, to);

return SQL_SUCCESS;
}

// Write SQLGUID as the 36-char canonical UUID string into a Firebird text
// parameter slot (VARCHAR/CHAR of any text charset). The wire-side SQLDA
// buffer for an untyped `?` placeholder is sized for VARCHAR(0) — too small
// for 36 bytes — so we stage the string in the DescRecord's local buffer
// and redirect Firebird to read from there via setSqlData(), mirroring the
// idiom transferStringToAllowedType already uses for app→wire string moves.
// The dispatch in getAdressFunction has already called setTypeText() to
// convert SQL_VARYING wires to SQL_TEXT (no length prefix), so we just
// write 36 raw bytes and set sqllen.
int OdbcConvert::convGuidToVarString(DescRecord * from, DescRecord * to)
{
SQLLEN * indicatorFrom = getAdressBindIndFrom((char*)from->indicatorPtr);
SQLLEN * indicatorTo = getAdressBindIndTo((char*)to->indicatorPtr);

ODBCCONVERT_CHECKNULL_SQLDA;

SQLGUID *g = (SQLGUID*)getAdressBindDataFrom((char*)from->dataPtr);

char tmp[37];
Comment thread
fdcastel marked this conversation as resolved.
Outdated
int srcLen = snprintf(tmp, sizeof(tmp),
"%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]);
if ( srcLen < 0 || srcLen > 36 ) srcLen = 36;

if ( !to->isLocalDataPtr )
to->allocateLocalDataPtr( srcLen + 1 );

memcpy(to->localDataPtr, tmp, srcLen);
Comment thread
fdcastel marked this conversation as resolved.
Outdated
to->headSqlVarPtr->setSqlLen((short)srcLen);
to->headSqlVarPtr->setSqlData(to->localDataPtr);

return SQL_SUCCESS;
}


////////////////////////////////////////////////////////////////////////
// TinyInt
Expand Down
2 changes: 2 additions & 0 deletions OdbcConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ class OdbcConvert
// Guid
int convGuidToString(DescRecord * from, DescRecord * to);
int convGuidToStringW(DescRecord * from, DescRecord * to);
int convGuidToBinary(DescRecord * from, DescRecord * to);
int convGuidToVarString(DescRecord * from, DescRecord * to);

// TinyInt
int convTinyIntToBoolean(DescRecord * from, DescRecord * to);
Expand Down
196 changes: 196 additions & 0 deletions tests/test_guid_and_binary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,202 @@ 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 VARCHAR parameter (Firebird infers VARCHAR for an
// untyped `?` slot inside CHAR_TO_UUID(?)). 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 << "'";
}

// 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)";
Expand Down