What happens
Reading an integer that does not fit the requested C type returns SQL_SUCCESS and the low bits: 9223372036854775807::bigint as SQL_C_SLONG is -1, as SQL_C_SSHORT -1; 4294967297::bigint as SQL_C_SLONG is 1; 70000::int4 as SQL_C_SSHORT is 4464. No diagnostic, indicator set to the C type's size. ODBC specifies SQL_ERROR with SQLSTATE 22003 (numeric value out of range) for this conversion. SQL_C_SBIGINT is fine, and values that fit convert correctly.
Where it comes from (convert.c, copy_and_convert_field(), main at 82f041e, ~line 1938):
case SQL_C_SLONG:
case SQL_C_LONG:
len = 4;
if (bind_size > 0)
*((SQLINTEGER *) rgbValueBindRow) = pg_atol(neut_str);
else
*((SQLINTEGER *) rgbValue + bind_row) = pg_atol(neut_str);
break;
pg_atol (and pg_atoi in the SQL_C_SSHORT/SQL_C_USHORT branches, ~line 1921) parse into a wide integer and the assignment stores the low bits; nothing compares the parsed value against the C type's range.
Environment: psqlodbc 16.00.0000 (psqlodbcw.so, Ubuntu 24.04 odbc-postgresql 1:16.00.0000-1), unixODBC 2.3.12, Linux x86_64; PostgreSQL 16.15.
Standalone reproduction (gcc -O1 -Wall int8_narrow.c -lodbc):
/* psqlodbc: SQLGetData of an out-of-range int8 into SQL_C_SLONG returns SQL_SUCCESS with a wrapped value.
* Build: gcc -O1 -Wall int8_narrow.c -lodbc Run: ODBC_CONN='Driver=…/psqlodbcw.so;Server=…;Port=5432;Database=…;Uid=…;Pwd=…;' ./int8_narrow */
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <stdlib.h>
static void diag(SQLSMALLINT t, SQLHANDLE h) {
SQLCHAR st[6], msg[512]; 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 void read_as(SQLHDBC dbc, const char* sql, SQLSMALLINT ctype, const char* name) {
SQLHSTMT h; SQLAllocHandle(SQL_HANDLE_STMT, dbc, &h);
SQLExecDirect(h, (SQLCHAR*)sql, SQL_NTS); SQLFetch(h);
SQLBIGINT big = 0; SQLINTEGER i32 = 0; SQLSMALLINT i16 = 0; SQLLEN ind = -99; SQLRETURN rc;
if (ctype == SQL_C_SBIGINT) rc = SQLGetData(h, 1, ctype, &big, sizeof big, &ind);
else if (ctype == SQL_C_SLONG) rc = SQLGetData(h, 1, ctype, &i32, sizeof i32, &ind);
else rc = SQLGetData(h, 1, ctype, &i16, sizeof i16, &ind);
printf("%-34s as %-13s rc=%d ind=%ld value=%lld\n", sql, name, (int)rc, (long)ind,
ctype == SQL_C_SBIGINT ? (long long)big : ctype == SQL_C_SLONG ? (long long)i32 : (long long)i16);
if (rc != SQL_SUCCESS) diag(SQL_HANDLE_STMT, h);
SQLFreeHandle(SQL_HANDLE_STMT, h);
}
int main(void) {
SQLHENV env; SQLHDBC dbc; 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 ", v);
SQLGetInfo(dbc, SQL_DBMS_VER, v, sizeof v, &n); printf("SQL_DBMS_VER=%s\n", v);
read_as(dbc, "SELECT 9223372036854775807::bigint", SQL_C_SBIGINT, "SQL_C_SBIGINT");
read_as(dbc, "SELECT 9223372036854775807::bigint", SQL_C_SLONG, "SQL_C_SLONG");
read_as(dbc, "SELECT 9223372036854775807::bigint", SQL_C_SSHORT, "SQL_C_SSHORT");
read_as(dbc, "SELECT 4294967297::bigint", SQL_C_SLONG, "SQL_C_SLONG");
read_as(dbc, "SELECT 70000::int4", SQL_C_SSHORT, "SQL_C_SSHORT");
read_as(dbc, "SELECT 2147483647::bigint", SQL_C_SLONG, "SQL_C_SLONG");
SQLDisconnect(dbc); return 0;
}
$ ./int8_narrow
SQL_DRIVER_VER=16.00.0000 SQL_DBMS_VER=16.0.15
SELECT 9223372036854775807::bigint as SQL_C_SBIGINT rc=0 ind=8 value=9223372036854775807
SELECT 9223372036854775807::bigint as SQL_C_SLONG rc=0 ind=4 value=-1
SELECT 9223372036854775807::bigint as SQL_C_SSHORT rc=0 ind=2 value=-1
SELECT 4294967297::bigint as SQL_C_SLONG rc=0 ind=4 value=1
SELECT 70000::int4 as SQL_C_SSHORT rc=0 ind=2 value=4464
SELECT 2147483647::bigint as SQL_C_SLONG rc=0 ind=4 value=2147483647
Expected: rows 2–5 to return SQL_ERROR with 22003, leaving the buffer untouched; row 1 and row 6 as they are.
Suggested fix: parse into a 64-bit value (strtoll, with errno == ERANGE checked for int8 itself), compare against INT32_MIN/MAX (INT16_MIN/MAX for the short branches) before storing, and report 22003 — a new COPY_ result code next to COPY_RESULT_TRUNCATED in convert.h, mapped in the caller the way truncation already maps to 01004. (The SQL_C_NUMERIC path already detects its own overflow through parse_to_numeric_struct(); the integer branches just need the same care.)
What happens
Reading an integer that does not fit the requested C type returns
SQL_SUCCESSand the low bits:9223372036854775807::bigintasSQL_C_SLONGis-1, asSQL_C_SSHORT-1;4294967297::bigintasSQL_C_SLONGis1;70000::int4asSQL_C_SSHORTis4464. No diagnostic, indicator set to the C type's size. ODBC specifiesSQL_ERRORwith SQLSTATE22003(numeric value out of range) for this conversion.SQL_C_SBIGINTis fine, and values that fit convert correctly.Where it comes from (
convert.c,copy_and_convert_field(), main at 82f041e, ~line 1938):pg_atol(andpg_atoiin theSQL_C_SSHORT/SQL_C_USHORTbranches, ~line 1921) parse into a wide integer and the assignment stores the low bits; nothing compares the parsed value against the C type's range.Environment: psqlodbc 16.00.0000 (
psqlodbcw.so, Ubuntu 24.04odbc-postgresql1:16.00.0000-1), unixODBC 2.3.12, Linux x86_64; PostgreSQL 16.15.Standalone reproduction (
gcc -O1 -Wall int8_narrow.c -lodbc):Expected: rows 2–5 to return
SQL_ERRORwith22003, leaving the buffer untouched; row 1 and row 6 as they are.Suggested fix: parse into a 64-bit value (
strtoll, witherrno == ERANGEchecked for int8 itself), compare againstINT32_MIN/MAX(INT16_MIN/MAXfor the short branches) before storing, and report22003— a newCOPY_result code next toCOPY_RESULT_TRUNCATEDinconvert.h, mapped in the caller the way truncation already maps to01004. (TheSQL_C_NUMERICpath already detects its own overflow throughparse_to_numeric_struct(); the integer branches just need the same care.)