Skip to content

SQLPrepare discards SQL_ATTR_ROWS_FETCHED_PTR and SQL_ATTR_ROW_STATUS_PTR set before it, so block cursors never learn how many rows a fetch returned #301

Description

@singhpratech

Summary

With SQL_ATTR_ROW_ARRAY_SIZE > 1, an application relies on SQL_ATTR_ROWS_FETCHED_PTR (and SQL_ATTR_ROW_STATUS_PTR) to know how many of the array's rows a SQLFetch filled. If those attributes are set before SQLPrepare / SQLExecDirect — the usual order, and what generic ODBC consumers do — the driver never writes through them: rows_fetched keeps 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_SIZE is 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 (between SQLPrepare and SQLExecute, or after SQLExecDirect) and both are written correctly. So SQLPrepare appears to reset the IRD's SQL_DESC_ROWS_PROCESSED_PTR / SQL_DESC_ARRAY_STATUS_PTR fields. Per the ODBC specification statement attributes persist until the statement is freed or the attribute is set again; SQLPrepare is not supposed to clear them.

Environment

  • Driver: firebird-odbc-driver v3.5.0-rc1, Linux x64 release tarball (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).
  • Driver manager: unixODBC 2.3.12 (Ubuntu), narrow (ANSI) entry points
  • Server: Firebird 5.0.4 (firebirdsql/firebird:5 container), database character set UTF8, client libfbclient.so.5.0.4 from the same image
  • Client: the C programs below, built with gcc repro.c -lodbc; connection string Driver=/path/libOdbcFb.so;DBNAME=inet://host:3050//path/db.fdb;UID=...;PWD=...;CHARSET=UTF8;

Reproduction

A six-row SELECT ... UNION ALL ... fetched with SQL_ATTR_ROW_ARRAY_SIZE = 4; rows_fetched is 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):

attrs set before SQLPrepare        :  fetch -> rows_fetched=777 status=9,9,9,9 values=1,2,3,4  fetch -> rows_fetched=777 status=9,9,9,9 values=5,6,-1,-1
attrs set between Prepare/Execute  :  fetch -> rows_fetched=4 status=0,0,0,0 values=1,2,3,4  fetch -> rows_fetched=2 status=0,0,3,3 values=5,6,-1,-1
attrs set after SQLExecute         :  fetch -> rows_fetched=4 status=0,0,0,0 values=1,2,3,4  fetch -> rows_fetched=2 status=0,0,3,3 values=5,6,-1,-1

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_PTR and SQL_ATTR_ROW_STATUS_PTR after SQLPrepare / SQLExecDirect.

Program

repro_rows_fetched_ptr.c
// 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; }

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions