The server is untouched in all three cases. Row-wise binding cannot be used as a workaround: SQL_ATTR_PARAM_BIND_TYPE ≠ 0 is refused with HYC00 Only binding by column is currently supported.
/* Column-wise parameter array with SQL_NULL_DATA below row 0, Apache Ignite 2.17 ODBC.
* Build: gcc -O1 -Wall null_below_row0.c -lodbc -o null_below_row0
* Run: ODBC_CONN='Driver=/path/to/libignite-odbc.so;ADDRESS=127.0.0.1:10800;SCHEMA=PUBLIC;' \
* ./null_below_row0 varchar # row 2 stored as '' instead of NULL, rc=0
* ./null_below_row0 binary # segfault inside SQLExecute
* ./null_below_row0 row0 # NULL in row 0 -> all three rows NULL */
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static SQLHENV env; static SQLHDBC dbc;
static void diag(SQLSMALLINT t, SQLHANDLE h) {
SQLCHAR st[6], msg[1024]; SQLINTEGER nat; SQLSMALLINT len, i = 1;
while (SQLGetDiagRec(t, h, i++, st, &nat, msg, sizeof msg, &len) == SQL_SUCCESS)
printf(" %s (%d): %s\n", st, (int)nat, msg);
}
static SQLRETURN exec(const char* sql) {
SQLHSTMT h; SQLAllocHandle(SQL_HANDLE_STMT, dbc, &h);
SQLRETURN rc = SQLExecDirect(h, (SQLCHAR*)sql, SQL_NTS);
if (!SQL_SUCCEEDED(rc)) { printf(" %s -> rc=%d\n", sql, (int)rc); diag(SQL_HANDLE_STMT, h); }
SQLFreeHandle(SQL_HANDLE_STMT, h); return rc;
}
int main(int argc, char** argv) {
const char* mode = argc > 1 ? argv[1] : "varchar";
int binary = !strcmp(mode, "binary"), row0 = !strcmp(mode, "row0");
setvbuf(stdout, NULL, _IONBF, 0);
SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
if (!SQL_SUCCEEDED(SQLDriverConnect(dbc, NULL, (SQLCHAR*)getenv("ODBC_CONN"), SQL_NTS, NULL, 0, NULL,
SQL_DRIVER_NOPROMPT))) { diag(SQL_HANDLE_DBC, dbc); return 1; }
SQLCHAR v[64]; SQLSMALLINT n;
SQLGetInfo(dbc, SQL_DRIVER_VER, v, sizeof v, &n); printf("SQL_DRIVER_VER=%s\n", v);
exec("DROP TABLE IF EXISTS probe_null");
exec(binary ? "CREATE TABLE probe_null (a BIGINT PRIMARY KEY, b BINARY)"
: "CREATE TABLE probe_null (a BIGINT PRIMARY KEY, b VARCHAR)");
SQLHSTMT h; SQLAllocHandle(SQL_HANDLE_STMT, dbc, &h);
SQLBIGINT a[3] = {1, 2, 3}; SQLLEN ia[3] = {0, 0, 0};
char b[3][8] = {"r1", "r2", "r3"}; SQLLEN ib[3];
if (binary) { memset(b, 0xEE, sizeof b); ib[0] = 4; ib[1] = SQL_NULL_DATA; ib[2] = 4; }
else { ib[0] = SQL_NTS; ib[1] = SQL_NULL_DATA; ib[2] = SQL_NTS; }
if (row0) { ib[0] = SQL_NULL_DATA; ib[1] = SQL_NTS; }
SQLULEN processed = 0; SQLUSMALLINT status[3] = {99, 99, 99};
SQLSetStmtAttr(h, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER)(SQLULEN)3, 0);
SQLSetStmtAttr(h, SQL_ATTR_PARAMS_PROCESSED_PTR, &processed, 0);
SQLSetStmtAttr(h, SQL_ATTR_PARAM_STATUS_PTR, status, 0);
SQLPrepare(h, (SQLCHAR*)"INSERT INTO probe_null (a, b) VALUES (?, ?)", SQL_NTS);
SQLBindParameter(h, 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, a, sizeof a[0], ia);
if (binary) SQLBindParameter(h, 2, SQL_PARAM_INPUT, SQL_C_BINARY, SQL_BINARY, 8, 0, b, 8, ib);
else SQLBindParameter(h, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 8, 0, b, 8, ib);
printf("mode=%s indicators = {%ld, %ld, %ld} (SQL_NULL_DATA = %d)\n", mode,
(long)ib[0], (long)ib[1], (long)ib[2], SQL_NULL_DATA);
SQLRETURN rc = SQLExecute(h);
printf("SQLExecute -> rc=%d processed=%lu status=[%d %d %d]\n", (int)rc, (unsigned long)processed,
status[0], status[1], status[2]);
if (!SQL_SUCCEEDED(rc)) diag(SQL_HANDLE_STMT, h);
SQLFreeHandle(SQL_HANDLE_STMT, h);
SQLAllocHandle(SQL_HANDLE_STMT, dbc, &h);
SQLExecDirect(h, (SQLCHAR*)"SELECT a, b IS NULL, LENGTH(b) FROM probe_null ORDER BY a", SQL_NTS);
while (SQLFetch(h) == SQL_SUCCESS) {
SQLBIGINT k = 0; SQLINTEGER isnull = -1, len = -1; SQLLEN l1, l2, l3;
SQLGetData(h, 1, SQL_C_SBIGINT, &k, 0, &l1);
SQLGetData(h, 2, SQL_C_SLONG, &isnull, 0, &l2);
SQLGetData(h, 3, SQL_C_SLONG, &len, 0, &l3);
printf(" row a=%lld b IS NULL=%d LENGTH(b)=%d%s\n", (long long)k, (int)isnull,
l3 == SQL_NULL_DATA ? -1 : (int)len, l3 == SQL_NULL_DATA ? " (NULL)" : "");
}
SQLFreeHandle(SQL_HANDLE_STMT, h);
exec("DROP TABLE probe_null");
SQLDisconnect(dbc); SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env);
return 0;
}
$ ./null_below_row0 varchar
SQL_DRIVER_VER=02.04.0000
mode=varchar indicators = {-3, -1, -3} (SQL_NULL_DATA = -1)
SQLExecute -> rc=0 processed=3 status=[0 0 0]
row a=1 b IS NULL=0 LENGTH(b)=2
row a=2 b IS NULL=0 LENGTH(b)=0
row a=3 b IS NULL=0 LENGTH(b)=2
$ ./null_below_row0 row0
SQL_DRIVER_VER=02.04.0000
mode=row0 indicators = {-1, -3, -3} (SQL_NULL_DATA = -1)
SQLExecute -> rc=0 processed=3 status=[0 0 0]
row a=1 b IS NULL=1 LENGTH(b)=-1 (NULL)
row a=2 b IS NULL=1 LENGTH(b)=-1 (NULL)
row a=3 b IS NULL=1 LENGTH(b)=-1 (NULL)
$ ./null_below_row0 binary
SQL_DRIVER_VER=02.04.0000
mode=binary indicators = {4, -1, 4} (SQL_NULL_DATA = -1)
Segmentation fault (SIGSEGV)
Program received signal SIGSEGV, Segmentation fault.
#0 __memcpy_avx_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:265
#1 0x00007ffff7edf709 in ignite::odbc::query::BatchQuery::MakeRequestExecuteBatch(unsigned long, unsigned long, bool) () from <path>/libignite-odbc.so
#2 0x00007ffff7edfe96 in ignite::odbc::query::BatchQuery::Execute() () from <path>/libignite-odbc.so
#3 0x00007ffff7efd201 in ignite::odbc::Statement::ExecuteSqlQuery() () from <path>/libignite-odbc.so
#4 0x00007ffff7ed0e8c in ignite::SQLExecute(void*) () from <path>/libignite-odbc.so
#5 0x00007ffff7f407dd in SQLExecute () from /lib/x86_64-linux-gnu/libodbc.so.2
#6 0x0000555555555826 in main (argc=<optimized out>, argv=<optimized out>) at null_below_row0.c:54
Component: ODBC driver (
modules/platforms/cpp/odbc), Ignite 2.17.0. Filed here rather than in JIRA since this repository accepts issues; happy to mirror it to IGNITE-* if preferred.Summary
With
SQL_ATTR_PARAMSET_SIZE> 1 and column-wise binding, the ODBC driver decides whether a parameter is NULL by looking at the indicator of row 0 for every row of the array. ASQL_NULL_DATAindicator in any later row is therefore not sent as NULL:VARCHARcolumn the row is stored as a non-NULL empty string, withSQL_SUCCESS,SQL_ATTR_PARAMS_PROCESSED_PTR= 3 and every parameter statusSQL_PARAM_SUCCESS;BINARY/VARBINARYcolumn the client process segfaults insideSQLExecute(memcpywith length -1);SQL_NULL_DATAin row 0 makes every row of the array NULL, whatever the other indicators say.The server is untouched in all three cases. Row-wise binding cannot be used as a workaround:
SQL_ATTR_PARAM_BIND_TYPE≠ 0 is refused withHYC00 Only binding by column is currently supported.Environment
apacheignite/ignite:latest), ODBC driver built from the image'splatforms/cppsources (CMake project version 2.17.0.25077,-DWITH_ODBC=ON -DWITH_CORE=OFF); the driver reportsSQL_DRIVER_VER=SQL_DBMS_VER= 02.04.0000Reproduction (plain ODBC, no other library)
gdb backtrace for the binary case:
Cause (
modules/platforms/cpp/odbc/src/app/parameter.cpp, 2.17)Parameter::Writetestsbuffer.GetInputSize()on the un-offset buffer before it copies the buffer and applies the row's element offset:(
ApplicationDataBuffer::GetInputSizereturns*GetResLen(), andGetResLenapplieselementOffset, which is still 0 onbuffer.) So every row inherits row 0's NULL-ness. What happens next depends on the SQL-type branch that runs on the offset copy:SQL_CHAR/SQL_VARCHAR:buf.GetString(columnSize)reads the row's own indicator (-1) andutility::SqlStringToStringreturns""for any negative length other thanSQL_NTS— the row is written as an empty string.SQL_BINARY/SQL_VARBINARY/SQL_LONGVARBINARY: the branch takes*constRef.GetResLen()— the row's own indicator,SQL_NULL_DATA= -1 — and passes it straight towriter.WriteInt8Array(data, paramLen)as the array length:A length of -1 reaches
memcpyassize_tand the process dies.Suggested fix
Apply the element offset before the NULL test — build
buffirst and testbuf.GetInputSize() == SQL_NULL_DATA— and have the binary branch treat a negative*resLenPtras NULL (orSQL_NTS/SQL_DATA_AT_EXECper the spec) rather than as a length. With that the three cases above become NULL, NULL and NULL.Two smaller things seen on the same path
SQLRowCountafter a successful three-row array execute answers 1, not 3, althoughSQL_PARAM_ARRAY_ROW_COUNTSreportsSQL_PARC_BATCH.SQLGetDataon a zero-length non-NULL character value returnsSQL_NO_DATAand writesSQL_NULL_DATAinto the indicator, so an empty string is indistinguishable from NULL on that path (which is why the program above readsb IS NULLandLENGTH(b)server-side).