Skip to content

SQLColumns returns CHAR_OCTET_LENGTH -1/-4 instead of NULL; since 18.00.0003 MSDASQL column schema rowsets fail with 22003 #228

Open

Description

@Joungsoo-Yoon

What happens

Since 18.00.0003, reading column metadata through the Microsoft OLE DB Provider for ODBC Drivers (MSDASQL) fails for every table that has a non-character column. ADO OpenSchema(adSchemaColumns) fails with "Object or provider is not capable of performing requested operation.", and an ATL CColumns rowset (IDBSchemaRowset, DBSCHEMA_COLUMNS) fails with E_FAIL and the error record 22003 Numeric value out of range. Tables with only character columns work, and 18.00.0002 and earlier work.

It is the same mechanism as #215, in SQLColumns instead of SQLGetTypeInfo: MSDASQL binds column 16 (CHAR_OCTET_LENGTH) as SQL_C_ULONG, the driver puts a negative value there, and the range check from #210 now rejects it.

Where it comes from

The ODBC reference for SQLColumns says CHAR_OCTET_LENGTH is "The maximum length in bytes of a character or binary data type column. For all other data types, this column returns a NULL."

PGAPI_Columns in info.c sets it with

set_tuplefield_int4(&tuple[COLUMNS_CHAR_OCTET_LENGTH], PGTYPE_ATTR_TRANSFER_OCTET_LENGTH(conn, field_type, mod_length));

pgtype_attr_transfer_octet_length() in pgtypes.c ends with return -1; for every type outside its switch (integer, numeric, timestamp and so on). For bytea it returns the column size, which is SQL_NO_TOTAL (-4). Before 18.00.0003 these values wrapped to 4294967295 and 4294967292 when read as SQL_C_ULONG, so the wrong value is older than #210; #210 turned it into an error.

With BoolsAsChar=1, boolean is reported as SQL_VARCHAR and also gets CHAR_OCTET_LENGTH = -1.

Environment

  • Fails: psqlODBC 18.00.0003, 18.00.0004 and 18.00.0005 x64, Unicode (podbc35w.dll); the 18.00.0005 ANSI driver (podbc30a.dll) fails the same way
  • Works: 18.00.0000, 18.00.0001, 18.00.0002 and 17.00.0010
  • Windows 11, Windows ODBC Driver Manager, MSDASQL
  • PostgreSQL 16.3
  • Default DSN settings. UseDeclareFetch=1, Parse=1, UnknownSizes=2, TextAsLongVarchar=0, LFConversion=0 and BI=1 don't change the result.

Standalone reproduction

CREATE TABLE public.t_octet (i integer, v varchar(10), b bytea);
/* Shows CHAR_OCTET_LENGTH from SQLColumns for each column of one table:
 * pass 1 reads it with SQLGetData(SQL_C_SLONG) and prints the value and the
 * indicator; pass 2 binds it as SQL_C_ULONG, the way MSDASQL does, and
 * prints the SQLFetch result.
 *   octet_repro.exe "<connection string>" <schema> <table>
 */
#include <windows.h>
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

static void diag(SQLHSTMT stmt)
{
    SQLCHAR state[6], msg[512];
    SQLINTEGER native;
    SQLSMALLINT len;
    SQLSMALLINT i;
    for (i = 1; SQLGetDiagRec(SQL_HANDLE_STMT, stmt, i, state, &native, msg, sizeof(msg), &len) == SQL_SUCCESS; i++)
        printf("    SQLSTATE=%s native=%ld %s\n", state, (long)native, msg);
}

int main(int argc, char **argv)
{
    SQLHENV env;
    SQLHDBC dbc;
    SQLHSTMT stmt;
    SQLRETURN rc;
    SQLCHAR name[129];
    SQLLEN nameInd, ind;
    SQLINTEGER slong;
    SQLUINTEGER ulong;

    if (argc < 4) {
        printf("usage: octet_repro \"<connection string>\" <schema> <table>\n");
        return 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);
    rc = SQLDriverConnect(dbc, NULL, (SQLCHAR *)argv[1], SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT);
    if (!SQL_SUCCEEDED(rc)) {
        printf("connect failed\n");
        return 1;
    }
    {
        SQLCHAR ver[64];
        SQLSMALLINT len;
        SQLGetInfo(dbc, SQL_DRIVER_VER, ver, sizeof(ver), &len);
        printf("driver %s\n", ver);
    }

    printf("pass 1: SQLGetData(16, SQL_C_SLONG)\n");
    SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);
    SQLColumns(stmt, NULL, 0, (SQLCHAR *)argv[2], SQL_NTS, (SQLCHAR *)argv[3], SQL_NTS, NULL, 0);
    while (SQL_SUCCEEDED(SQLFetch(stmt))) {
        SQLSMALLINT type;
        SQLGetData(stmt, 4, SQL_C_CHAR, name, sizeof(name), &nameInd);
        SQLGetData(stmt, 5, SQL_C_SSHORT, &type, 0, NULL);
        rc = SQLGetData(stmt, 16, SQL_C_SLONG, &slong, 0, &ind);
        if (!SQL_SUCCEEDED(rc)) {
            printf("  %-6s DATA_TYPE=%4d SQLGetData failed rc=%d\n", name, type, rc);
            diag(stmt);
        } else if (ind == SQL_NULL_DATA)
            printf("  %-6s DATA_TYPE=%4d CHAR_OCTET_LENGTH=NULL\n", name, type);
        else
            printf("  %-6s DATA_TYPE=%4d CHAR_OCTET_LENGTH=%ld\n", name, type, (long)slong);
    }
    SQLFreeHandle(SQL_HANDLE_STMT, stmt);

    printf("pass 2: SQLBindCol(16, SQL_C_ULONG) + SQLFetch\n");
    SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);
    SQLColumns(stmt, NULL, 0, (SQLCHAR *)argv[2], SQL_NTS, (SQLCHAR *)argv[3], SQL_NTS, NULL, 0);
    SQLBindCol(stmt, 4, SQL_C_CHAR, name, sizeof(name), &nameInd);
    SQLBindCol(stmt, 16, SQL_C_ULONG, &ulong, 0, &ind);
    for (;;) {
        rc = SQLFetch(stmt);
        if (rc == SQL_NO_DATA)
            break;
        if (!SQL_SUCCEEDED(rc)) {
            printf("  %-6s SQLFetch failed rc=%d\n", name, rc);
            diag(stmt);
            continue;
        }
        if (ind == SQL_NULL_DATA)
            printf("  %-6s CHAR_OCTET_LENGTH=NULL\n", name);
        else
            printf("  %-6s CHAR_OCTET_LENGTH=%lu\n", name, (unsigned long)ulong);
    }
    SQLFreeHandle(SQL_HANDLE_STMT, stmt);
    SQLDisconnect(dbc);
    SQLFreeHandle(SQL_HANDLE_DBC, dbc);
    SQLFreeHandle(SQL_HANDLE_ENV, env);
    return 0;
}

Build in an x64 Native Tools prompt and run:

cl /W3 octet_repro.c /link odbc32.lib
octet_repro.exe "DSN=<psqlODBC DSN>;UID=<user>;PWD=<password>" public t_octet

Output with 18.00.0005:

driver 18.00.0005
pass 1: SQLGetData(16, SQL_C_SLONG)
  i      DATA_TYPE=   4 CHAR_OCTET_LENGTH=-1
  v      DATA_TYPE=  12 CHAR_OCTET_LENGTH=20
  b      DATA_TYPE=  -4 CHAR_OCTET_LENGTH=-4
pass 2: SQLBindCol(16, SQL_C_ULONG) + SQLFetch
  i      SQLFetch failed rc=-1
    SQLSTATE=22003 native=23 Numeric value out of range
  v      CHAR_OCTET_LENGTH=20
  b      SQLFetch failed rc=-1
    SQLSTATE=22003 native=23 Numeric value out of range

Output with 18.00.0002:

driver 18.00.0002
pass 1: SQLGetData(16, SQL_C_SLONG)
  i      DATA_TYPE=   4 CHAR_OCTET_LENGTH=-1
  v      DATA_TYPE=  12 CHAR_OCTET_LENGTH=20
  b      DATA_TYPE=  -4 CHAR_OCTET_LENGTH=-4
pass 2: SQLBindCol(16, SQL_C_ULONG) + SQLFetch
  i      CHAR_OCTET_LENGTH=4294967295
  v      CHAR_OCTET_LENGTH=20
  b      CHAR_OCTET_LENGTH=4294967292

The same table through ADO and MSDASQL, in 64-bit PowerShell:

$c = New-Object -ComObject ADODB.Connection
$c.Open("Provider=MSDASQL;DSN=<psqlODBC DSN>;UID=<user>;PWD=<password>")
$rs = $c.OpenSchema(4, [object[]]@($null, "public", "t_octet"))   # 4 = adSchemaColumns

With 18.00.0005, OpenSchema throws "Object or provider is not capable of performing requested operation."; with 18.00.0002 it returns the three columns.

Suggested fix

Return NULL for the types the driver has no octet length for, and INT_MAX for SQL_NO_TOTAL, as #218 does for COLUMN_SIZE:

Int4 octet_length = PGTYPE_ATTR_TRANSFER_OCTET_LENGTH(conn, field_type, mod_length);

if (SQL_NO_TOTAL == octet_length)
    octet_length = INT_MAX;
set_nullfield_int4(&tuple[COLUMNS_CHAR_OCTET_LENGTH], octet_length);

Unlike COLUMN_SIZE in #218, NULL works for this column through MSDASQL: it binds column 16 with an indicator (a non-null pcb in the mylog). With RowVersioning=1, the xmin row that add_tuple_for_oid_or_xmin() adds already has a NULL CHAR_OCTET_LENGTH, and on 18.00.0005 that row passes through MSDASQL and ADO without an error.

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