// Same as rows_fetched.c, with SQLPrepare: attributes set before SQLPrepare / between SQLPrepare and SQLExecute / after SQLExecute.
#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 const char* Q = "SELECT 1 FROM rdb$database UNION ALL SELECT 2 FROM rdb$database UNION ALL SELECT 3 FROM rdb$database UNION ALL SELECT 4 FROM rdb$database UNION ALL SELECT 5 FROM rdb$database UNION ALL SELECT 6 FROM rdb$database";
static void run(int when) {
SQLHSTMT s; SQLAllocHandle(SQL_HANDLE_STMT, dbc, &s);
enum { A = 4 }; SQLULEN fetched = 777; SQLUSMALLINT status[A] = {9, 9, 9, 9}; SQLINTEGER v[A]; SQLLEN ind[A];
#define SETATTRS() do { SQLSetStmtAttr(s, SQL_ATTR_ROW_ARRAY_SIZE, (SQLPOINTER)(SQLULEN)A, 0); SQLSetStmtAttr(s, SQL_ATTR_ROWS_FETCHED_PTR, &fetched, 0); SQLSetStmtAttr(s, SQL_ATTR_ROW_STATUS_PTR, status, 0); } while (0)
if (when == 0) SETATTRS();
CHECK(SQL_HANDLE_STMT, s, SQLPrepare(s, (SQLCHAR*)Q, SQL_NTS));
if (when == 1) SETATTRS();
CHECK(SQL_HANDLE_STMT, s, SQLExecute(s));
if (when == 2) SETATTRS();
SQLBindCol(s, 1, SQL_C_SLONG, v, 0, ind);
SQLRETURN r; printf("attrs set %s:", when == 0 ? "before SQLPrepare " : when == 1 ? "between Prepare/Execute " : "after SQLExecute ");
while (SQL_SUCCEEDED(r = SQLFetch(s))) { printf(" fetch -> rows_fetched=%lu status=%u,%u,%u,%u values=%d,%d,%d,%d", (unsigned long)fetched, status[0], status[1], status[2], status[3], v[0], v[1], v[2], v[3]); for (int k = 0; k < A; k++) { v[k] = -1; status[k] = 9; } fetched = 777; }
printf("\n"); SQLFreeHandle(SQL_HANDLE_STMT, s);
}
int main(void) { connect_db(); run(0); run(1); run(2); return 0; }
Summary
With
SQL_ATTR_ROW_ARRAY_SIZE> 1, an application relies onSQL_ATTR_ROWS_FETCHED_PTR(andSQL_ATTR_ROW_STATUS_PTR) to know how many of the array's rows aSQLFetchfilled. If those attributes are set beforeSQLPrepare/SQLExecDirect— the usual order, and what generic ODBC consumers do — the driver never writes through them:rows_fetchedkeeps whatever value it had and the status array is untouched, for every fetch including the last partial one. The fetch itself works (SQL_ATTR_ROW_ARRAY_SIZEis honoured, the bound arrays fill), so a caller that trusts the counter processes stale rows at the end of the result set or, if it initialised the counter to 0, sees no rows at all.Set the same two attributes after
SQLPrepare(betweenSQLPrepareandSQLExecute, or afterSQLExecDirect) and both are written correctly. SoSQLPrepareappears to reset the IRD'sSQL_DESC_ROWS_PROCESSED_PTR/SQL_DESC_ARRAY_STATUS_PTRfields. Per the ODBC specification statement attributes persist until the statement is freed or the attribute is set again;SQLPrepareis not supposed to clear them.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
A six-row
SELECT ... UNION ALL ...fetched withSQL_ATTR_ROW_ARRAY_SIZE = 4;rows_fetchedis pre-set to 777 and the status array to 9 before every fetch so an unwritten value is visible. The attributes are set at three points.Output of the program below (verbatim):
The same happens with
SQLExecDirect(attributes set before it: never written; after it: written).Expected
rows_fetched= 4 then 2 and the status array written on every fetch regardless of when the attributes were set.Workaround for applications
Set
SQL_ATTR_ROWS_FETCHED_PTRandSQL_ATTR_ROW_STATUS_PTRafterSQLPrepare/SQLExecDirect.Program
repro_rows_fetched_ptr.c
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.