1

I just learned the hard way that the Windows ODBC driver API requires an array of SQL_BINARY data, as input parameter, to be terminated with a zero-byte. Even though I didn't find such a statement in the documentation, I found this out by executing a stored procedure using this code:

Minimal Example

// Parameter binding
BYTE data[10] = { 15, 3, 54, 144, 34, 211, 200, 147, 15, 74 };
SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_BINARY, 10, 0, data, 0, NULL);

// Procedure execution
SQLRETURN res = SQLExecDirect(hstmt, (SQLCHAR*)"{call dbo.Update_Data(?)}", SQL_NTS);

It caused SQLExecDirect to fail with SQL_NULL_DATA. Upon querying the Diagnostic Records using SQLGetDiagRec I've received the record:

SQL State = 22001, Error Msg: "[Microsoft][ODBC SQL Server Driver] String or binary data would be truncated"

While this usually indicates that data being inserted or updated into a column is larger than the column itself, this isn't the case here. After 4 hours of trying different parameters and statements I've finally found out the solution is as simple as terminating the byte array with a zero at the last position:

// Parameter binding
BYTE data[11] = { 15, 3, 54, 144, 34, 211, 200, 147, 15, 74, 0 }; // <- 0 termination here
SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_BINARY, 10, 0, data, 0, NULL);

// Procedure execution
SQLRETURN res = SQLExecDirect(hstmt, (SQLCHAR*)"{call dbo.Update_Data(?)}", SQL_NTS);

Now I fail to understand why that is so? The function SQLBindParameter requires the length of the given data (10 as cbColDef or ColumnSize parameter) and still searches for a zero-byte?

To my understanding, zero termination is used where the length of an array is not determined by a length indicating variable but the termination of the array with a zero value. This is usually being done with strings. For binary data, that doesn't make much sense to me, as there could be intended zero bytes inside the array before the actual end (determined by a length indicator) is reached. I could possibly run into this issue, so it would be great if there was some way to avoid zero-terminating the byte array?

Full Example

As requested in the comments here is a full code dump of the unit test:

#include <windows.h>
#include <sql.h>
#include <sqlext.h>

// Settings
#define SIP "127.0.0.1"
#define SPort 1433
#define SUID "User"
#define SPW "PW"
#define SDB "world"

// Global ODBC mem
SQLHENV henv;
SQLHDBC hdbc;
SQLHSTMT hstmt;

// Logs Diagnostic records
void ProcessLogs(SQLSMALLINT plm_handle_type, SQLHANDLE &plm_handle);

// The query being tested
void TestQuery()
{
    int col = 0;
    SQLRETURN res = SQL_NTS;

    // Params
    ULONGLONG id = 44;
    BYTE data[10] = { 15, 3, 54, 144, 34, 211, 200, 147, 15, 74 };

    SQLBindParameter(hstmt, ++col, SQL_PARAM_INPUT, SQL_C_UBIGINT, SQL_BIGINT, 0, 0, &id, 0, NULL);
    SQLBindParameter(hstmt, ++col, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_BINARY, 10, 0, data, 0, NULL);

    // Execution
    res = SQLExecDirect(hstmt, (UCHAR*)"{call dbo.Update_Store_Data(?,?)}", SQL_NTS);
    if (res != SQL_SUCCESS && res != SQL_SUCCESS_WITH_INFO)
    {
        printf("Error during query execution: %hd\n", res);
        ProcessLogs(SQL_HANDLE_STMT, hstmt);
    }
}

// ODBC Driver initialization
bool ODBCInit()
{
    // Init ODBC Handles
    RETCODE res;
    res = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
    res = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, SQL_IS_INTEGER);
    res = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);

    // Connection string
    char connStr[512];
    sprintf_s(connStr
        , sizeof(connStr)
        , "DRIVER={SQL Server};SERVER=%s;ADDRESS=%s,%d;NETWORK=DBMSSOCN;UID=%s;PWD=%s;DATABASE=%s"
        , SIP
        , SIP
        , SPort
        , SUID
        , SPW
        , SDB);

    // Connection
    char outStr[512];
    SQLSMALLINT pcb;
    res = SQLDriverConnect(hdbc, NULL, (SQLCHAR*)connStr, strlen(connStr), (SQLCHAR*)outStr, ARRAYSIZE(outStr), &pcb, SQL_DRIVER_NOPROMPT);
    if (res != SQL_SUCCESS && res != SQL_SUCCESS_WITH_INFO)
    {
        printf("Error during driver connection: %hd\n", res);
        return false;
    }

    // Query handle
    res = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);

    return true;
}

// ODBC Driver handles cleanup
void ODBCClean()
{
    if (hstmt != SQL_NULL_HSTMT)
        SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
    if (hstmt != SQL_NULL_HDBC)
        SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
    if (hstmt != SQL_NULL_HENV)
        SQLFreeHandle(SQL_HANDLE_ENV, henv);
}

int main(int argc, _TCHAR* argv[])
{
    if (ODBCInit())
        TestQuery();
    ODBCClean();

    return 0;
}

The SQL Table definition:

CREATE TABLE dbo.Store
(
    UniqueNumber BIGINT IDENTITY(1,1) NOT NULL,
    ItemID BIGINT NOT NULL,
    AccountUniqueNumber BIGINT NOT NULL,
    StorageType INT NOT NULL,
    Count INT NOT NULL,
    Data BINARY(10) NOT NULL
)

The called procedure:

CREATE PROCEDURE dbo.Update_Store_Data
    @ID     BIGINT,
    @Data   BINARY(10)
AS
BEGIN
    UPDATE dbo.Store
    SET Data = @Data
    WHERE UniqueNumber = @ID
END
13
  • 1
    "I just learned the hard way that the Windows ODBC driver API requires an array of SQL_BINARY data, as input parameter, to be terminated with a zero-byte. " - this certainly is not true in my experience. If you believe it to be the case, post some compilable code that demonstrates it and/or report it to the supplier of your ODBC driver as a bug. Commented May 14, 2017 at 21:28
  • @NeilButterworth You might be lucky and have zero'd memory after binary data that you bind as parameter (happened to me before when it worked). To include the whole ODBC handle allocation and the driver connection would extend this question by at least 50 lines code and would not show a minimal example of the problem anymore imo. I made sure through execution from the management studio directly that the query runs, free of errors. It's definitely a problem with the ODBC driver API. Commented May 14, 2017 at 21:35
  • 1
    You need to specify BufferLength to the function, i.e. SQLBindParameter(..., data, 10, NULL); Commented May 15, 2017 at 0:06
  • 1
    If you set the latest param (StrLen_or_IndPtr) instead of NULL what did you get? It should return the length of binary data, right? Commented May 15, 2017 at 0:44
  • 1
    @Vinzenz: As explained below, you have to set both. Buffer length and StrLen_or_IndPtr. And StrLen_or_IndPtr must not always be the same value as buffer length. Think about a VARBINARY column for example.. Commented May 15, 2017 at 10:41

1 Answer 1

4

It is not true that binary data must be null-terminated (if that would be true, you could not insert any data containing a 0 value, like { 100, 0, 100, 0, 100 }).

  1. You need to set correct values for the buffer length (size of the buffer).
  2. You need to properly setup and initialize StrLen_or_IndPtr argument. For binary buffers, the value of StrLen_or_IndPtr must be the length of the data held in the buffer. Note that this must not be the same as the actual buffer size (but it must be <= buffersize). From the documentation of SQLBindParameter:

The StrLen_or_IndPtr argument points to a buffer that, when SQLExecute or SQLExecDirect is called, contains one of the following [..]:

  • The length of the parameter value stored in *ParameterValuePtr. This is ignored except for character or binary C data.

See below a minimal example that compiles:

#include <windows.h>
#include <tchar.h>
#include <iostream>
#include <sql.h>
#include <sqlext.h>
#include <sqlucode.h>

void printErr(SQLHANDLE handle, SQLSMALLINT handleType)
{
    SQLSMALLINT recNr = 1;
    SQLRETURN ret = SQL_SUCCESS;
    while (ret == SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO)
    {
        SQLWCHAR errMsg[SQL_MAX_MESSAGE_LENGTH + 1];
        SQLWCHAR sqlState[5 + 1];
        errMsg[0] = 0;
        SQLINTEGER nativeError;
        SQLSMALLINT cb = 0;
        ret = SQLGetDiagRec(handleType, handle, recNr, sqlState, &nativeError, errMsg, SQL_MAX_MESSAGE_LENGTH + 1, &cb);
        if (ret == SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO)
        {
            std::wcerr << L"ERROR; native: " << nativeError << L"; state: " << sqlState << L"; msg: " << errMsg << std::endl;
        }
        ++recNr;
    }
}


int _tmain(int argc, _TCHAR* argv[])
{
    // connect to db
    SQLRETURN   nResult = 0;
    SQLHANDLE   handleEnv = 0;

    nResult = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, (SQLHANDLE*)&handleEnv);
    nResult = SQLSetEnvAttr(handleEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3_80, SQL_IS_INTEGER);

    SQLHANDLE   handleDBC = 0;
    nResult = SQLAllocHandle(SQL_HANDLE_DBC, handleEnv, (SQLHANDLE*)&handleDBC);

    SQLWCHAR     strConnect[256] = L"Driver={SQL Server Native Client 11.0};Server=.\\TEST;Database=Foobar;Uid=Secret;Pwd=Secret";
    SQLWCHAR     strConnectOut[1024] = { 0 };
    SQLSMALLINT nNumOut = 0;
    nResult = SQLDriverConnect(handleDBC, NULL, (SQLWCHAR*)strConnect, SQL_NTS, (SQLWCHAR*)strConnectOut, sizeof(strConnectOut), &nNumOut, SQL_DRIVER_NOPROMPT);
    if (!SQL_SUCCEEDED(nResult))
        printErr(handleDBC, SQL_HANDLE_DBC);

    // Allocate a statement
    SQLHSTMT    handleStatement = SQL_NULL_HSTMT;
    nResult = SQLAllocHandle(SQL_HANDLE_STMT, handleDBC, (SQLHANDLE*)&handleStatement);
    if (!SQL_SUCCEEDED(nResult))
        printErr(handleDBC, SQL_HANDLE_DBC);

    int col = 0;
    SQLRETURN res = SQL_NTS;

    // Params
    SQLBIGINT id = 2;
    SQLCHAR data[10] = { 15, 3, 54, 144, 34, 211, 200, 147, 15, 74 };
    SQLLEN cbId = 0;
    SQLLEN cbData = 10;

    res = SQLBindParameter(handleStatement, 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, &id, sizeof(id), &cbId);
    if (!SQL_SUCCEEDED(res))
        printErr(handleStatement, SQL_HANDLE_STMT);

    res = SQLBindParameter(handleStatement, 2, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_BINARY, 10, 0, data, sizeof(data), &cbData);
    if (!SQL_SUCCEEDED(res))
        printErr(handleStatement, SQL_HANDLE_STMT);

    // Execution
    res = SQLExecDirect(handleStatement, (SQLWCHAR*)L"{call dbo.Update_Store_Data(?,?)}", SQL_NTS);
    if (!SQL_SUCCEEDED(res))
    {
        printErr(handleStatement, SQL_HANDLE_STMT);
    }

    return 0;
}
Sign up to request clarification or add additional context in comments.

3 Comments

You're absolutely on spot about the StrLen_or_IndPtr argument! I've just adjusted my unit test to set both the BufferLength and StrLen_or_IndPtr argument to 10 and it worked! Now curious me tested the same thing with leaving BufferLength to 0. And even that worked. So it looks like BufferLength is actually being ignored for SQL_C_BINARY or related to the StrLen_or_IndPtr buffer. Do you get the same result if you leave the BufferLength parameter at 0? If so, can you adjust your answer? Other than that, great answer, thanks for taking the time!!
It could actually be intended as an if-else system. If the StrLen_or_IndPtr parameter is given, it will be interpreted as the length of the binary data. If it is not given (NULL) or set to SQL_NTS it will try to determine the end of the binary data by zero-termination. This is what I get through testing this, at least. Not sure if it's worth another answer though, maybe you can adjust yours a little? I've accepted it in the meantime
How buffer-length and StrLen_or_IndPtr are interpeted depends heavily on the SQL C Type used. See the corresponding comments about those arguments in the ODBC API documentation. The answer would become huge if we would like to include all possible combinations.. I would disadvice to not set buffer length: The information is available "for free" with a simple sizeof(). If the driver does not use it - okay, but you are on the safe side if you set it. It could also be very driver specific what a driver does exactly if only one of the arguments are set (null-padding for example)..

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.