// Variants of bigint_null.c: NULL first; INTEGER column with SQL_C_DEFAULT NULL (control); BIGINT with NULL bound SQL_C_CHAR explicitly.
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
static void diag(SQLSMALLINT ht, SQLHANDLE h, const char* where) {
SQLCHAR st[6], msg[1024]; SQLINTEGER ne; SQLSMALLINT len; SQLSMALLINT i = 1;
while (SQLGetDiagRec(ht, h, i++, st, &ne, msg, sizeof msg, &len) == SQL_SUCCESS)
printf(" [%s] %s (%d) at %s\n", st, msg, (int)ne, where);
}
#define CHECK(ht, h, call) do { SQLRETURN _r = (call); if (!SQL_SUCCEEDED(_r)) { printf("FAILED rc=%d: %s\n", (int)_r, #call); diag(ht, h, #call); exit(1);} } while (0)
static SQLHENV env; static SQLHDBC dbc;
static void connect_db(void) {
const char* cs = getenv("FB_CONN");
if (!cs) { fprintf(stderr, "set FB_CONN=Driver=...;DBNAME=...;UID=...;PWD=...;CHARSET=UTF8;\n"); exit(2); }
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);
CHECK(SQL_HANDLE_DBC, dbc, SQLDriverConnect(dbc, NULL, (SQLCHAR*)cs, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT));
SQLCHAR name[64], ver[64]; SQLSMALLINT l;
SQLGetInfo(dbc, SQL_DRIVER_NAME, name, sizeof name, &l); SQLGetInfo(dbc, SQL_DRIVER_VER, ver, sizeof ver, &l);
printf("driver %s %s, ", name, ver);
SQLGetInfo(dbc, SQL_DBMS_NAME, name, sizeof name, &l); SQLGetInfo(dbc, SQL_DBMS_VER, ver, sizeof ver, &l);
printf("server %s %s\n", name, ver);
}
static void exec_ignore(const char* sql) { SQLHSTMT s; SQLAllocHandle(SQL_HANDLE_STMT, dbc, &s); SQLExecDirect(s, (SQLCHAR*)sql, SQL_NTS); SQLFreeHandle(SQL_HANDLE_STMT, s); }
static void exec_ok(const char* sql) { SQLHSTMT s; SQLAllocHandle(SQL_HANDLE_STMT, dbc, &s); CHECK(SQL_HANDLE_STMT, s, SQLExecDirect(s, (SQLCHAR*)sql, SQL_NTS)); SQLFreeHandle(SQL_HANDLE_STMT, s); }
static void run(const char* label, const char* coltype, SQLSMALLINT sqltype, SQLSMALLINT valctype, SQLSMALLINT nullctype, const long long* vals, int n) {
exec_ignore("DROP TABLE t_bn2"); char sql[128]; snprintf(sql, sizeof sql, "CREATE TABLE t_bn2 (id INTEGER, v %s)", coltype); exec_ok(sql);
SQLHSTMT s; SQLAllocHandle(SQL_HANDLE_STMT, dbc, &s);
CHECK(SQL_HANDLE_STMT, s, SQLPrepare(s, (SQLCHAR*)"INSERT INTO t_bn2 (id, v) VALUES (?, ?)", SQL_NTS));
SQLINTEGER id; SQLBIGINT v8; SQLINTEGER v4; SQLLEN id_ind = 0, v_ind;
for (int i = 0; i < n; i++) { id = i + 1; int is_null = vals[i] == -1; v8 = vals[i]; v4 = (SQLINTEGER)vals[i]; v_ind = is_null ? SQL_NULL_DATA : 0;
CHECK(SQL_HANDLE_STMT, s, SQLBindParameter(s, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &id, 0, &id_ind));
CHECK(SQL_HANDLE_STMT, s, SQLBindParameter(s, 2, SQL_PARAM_INPUT, is_null ? nullctype : valctype, sqltype, 0, 0, valctype == SQL_C_SBIGINT ? (void*)&v8 : (void*)&v4, 0, &v_ind));
CHECK(SQL_HANDLE_STMT, s, SQLExecute(s)); }
SQLFreeHandle(SQL_HANDLE_STMT, s); SQLAllocHandle(SQL_HANDLE_STMT, dbc, &s);
CHECK(SQL_HANDLE_STMT, s, SQLExecDirect(s, (SQLCHAR*)"SELECT v FROM t_bn2 ORDER BY id", SQL_NTS));
printf("%-58s got v =", label);
while (SQLFetch(s) == SQL_SUCCESS) { SQLLEN ind; SQLBIGINT g; SQLGetData(s, 1, SQL_C_SBIGINT, &g, 0, &ind); if (ind == SQL_NULL_DATA) printf(" NULL"); else printf(" %lld", (long long)g); }
printf("\n"); SQLFreeHandle(SQL_HANDLE_STMT, s);
}
int main(void) { connect_db(); SQLSetConnectAttr(dbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);
const long long a[] = {1, -1, 3, 4}, b[] = {-1, 2, 3, 4}, c[] = {1, 2, 3, 4};
run("BIGINT, NULL@2 as SQL_C_DEFAULT, values SQL_C_SBIGINT (1,NULL,3,4)", "BIGINT", SQL_BIGINT, SQL_C_SBIGINT, SQL_C_DEFAULT, a, 4);
run("BIGINT, NULL@1 as SQL_C_DEFAULT, values SQL_C_SBIGINT (NULL,2,3,4)", "BIGINT", SQL_BIGINT, SQL_C_SBIGINT, SQL_C_DEFAULT, b, 4);
run("BIGINT, NULL@2 as SQL_C_CHAR explicitly, values SQL_C_SBIGINT", "BIGINT", SQL_BIGINT, SQL_C_SBIGINT, SQL_C_CHAR, a, 4);
run("BIGINT, no NULL at all, values SQL_C_SBIGINT (control)", "BIGINT", SQL_BIGINT, SQL_C_SBIGINT, SQL_C_SBIGINT, c, 4);
run("INTEGER, NULL@2 as SQL_C_DEFAULT, values SQL_C_SLONG (control)", "INTEGER", SQL_INTEGER, SQL_C_SLONG, SQL_C_DEFAULT, a, 4);
return 0; }
Summary
Observed: a prepared
INSERT ... VALUES (?, ?)whose second parameter isSQL_BIGINT, executed once per row withSQLBindParametercalled before each execute. When one row's NULL is bound with a character C type —SQL_C_DEFAULT(whose default forSQL_BIGINTisSQL_C_CHARper the ODBC specification) orSQL_C_CHARexplicitly — with indicatorSQL_NULL_DATA, every following row on that parameter is written as NULL, although each of those rows is rebound withSQL_C_SBIGINTand a real value, and every call returnsSQL_SUCCESS. If the NULL row comes first, the whole column is NULL.Binding the NULL row with
SQL_C_SBIGINTgives the correct result, and the same pattern on anINTEGERparameter (SQL_C_DEFAULTNULL, thenSQL_C_SLONGvalues) is also correct — so it is specific to a character-typed bind on aSQL_BIGINTparameter, and the later rebinds toSQL_C_SBIGINTare not taking effect. I have not looked at the driver source for the cause.Environment
libOdbcFb.so, 2026-04-11);SQL_DRIVER_NAME=OdbcFb,SQL_DRIVER_VER=00.00.000. Same result with v3.0.1 (linux_libs.zip,Release_x86_64/libOdbcFb.so).firebirdsql/firebird:5container), database character set UTF8, clientlibfbclient.so.5.0.4from the same imagegcc repro.c -lodbc; connection stringDriver=/path/libOdbcFb.so;DBNAME=inet://host:3050//path/db.fdb;UID=...;PWD=...;CHARSET=UTF8;Reproduction
INSERT INTO t (id INTEGER, v BIGINT) VALUES (?, ?)prepared once, executed four times, parameter 2 rebound before each execute. Output of the two programs below (verbatim, v3.5.0-rc1; v3.0.1 prints the same):Expected
1, NULL, 3, 4(andNULL, 2, 3, 4) in every variant: a NULL indicator carries no data whatever C type is named, and the nextSQLBindParameternamesSQL_C_SBIGINTexplicitly.Workaround for applications
Bind NULLs for
SQL_BIGINTparameters withSQL_C_SBIGINTrather thanSQL_C_DEFAULT.Program
repro_bigint_default_null.c (variants A/B)
repro_bigint_null_variants.c (NULL first, explicit SQL_C_CHAR, INTEGER control)
Found while running Firebird through adbcBridge (an ADBC-over-ODBC driver; its Firebird entry and the workaround it uses are in docs/COMPATIBILITY.md). The reproduction above is pure ODBC and does not involve it. Happy to test a fix branch against the same setup.