From 486b840c1f4f4191bd442279e66f4ea4bc244775 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Thu, 12 Feb 2026 15:53:21 -0300 Subject: [PATCH 1/4] Rec 1: Support SQL_ATTR_CURSOR_SCROLLABLE setter Implement SQL_ATTR_CURSOR_SCROLLABLE as a writable attribute in PGAPI_SetStmtAttr. Setting SQL_SCROLLABLE maps to SQL_CURSOR_STATIC, and SQL_NONSCROLLABLE maps to SQL_CURSOR_FORWARD_ONLY. Previously the driver unconditionally rejected this attribute with SQL_ERROR, even though it fully supports scrollable cursors via SQL_ATTR_CURSOR_TYPE. Add cursor-scrollable regression test to verify the behavior. --- pgapi30.c | 7 +- test/expected/cursor-scrollable.out | 14 +++ test/src/cursor-scrollable-test.c | 132 ++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 test/expected/cursor-scrollable.out create mode 100644 test/src/cursor-scrollable-test.c diff --git a/pgapi30.c b/pgapi30.c index 7f047555..2585b839 100644 --- a/pgapi30.c +++ b/pgapi30.c @@ -2401,12 +2401,17 @@ PGAPI_SetStmtAttr(HSTMT StatementHandle, /* Whether automatic population of IPD is supported */ if (SQL_FALSE == Value) break; - case SQL_ATTR_CURSOR_SCROLLABLE: /* -1 */ case SQL_ATTR_CURSOR_SENSITIVITY: /* -2 */ case SQL_ATTR_AUTO_IPD: /* 10001 */ /* Unsupported attributes */ SC_set_error(stmt, DESC_OPTION_NOT_FOR_THE_DRIVER, "Unsupported statement option (Set)", func); return SQL_ERROR; + case SQL_ATTR_CURSOR_SCROLLABLE: /* -1 */ + if ((SQLUINTEGER)(SQLULEN)Value == SQL_SCROLLABLE) + ret = PGAPI_SetStmtOption(StatementHandle, SQL_CURSOR_TYPE, SQL_CURSOR_STATIC); + else + ret = PGAPI_SetStmtOption(StatementHandle, SQL_CURSOR_TYPE, SQL_CURSOR_FORWARD_ONLY); + return ret; /* case SQL_ATTR_ROW_BIND_TYPE: ** == SQL_BIND_TYPE(ODBC2.0) */ case SQL_ATTR_IMP_ROW_DESC: /* 10012 (read-only) */ case SQL_ATTR_IMP_PARAM_DESC: /* 10013 (read-only) */ diff --git a/test/expected/cursor-scrollable.out b/test/expected/cursor-scrollable.out new file mode 100644 index 00000000..ec6bc688 --- /dev/null +++ b/test/expected/cursor-scrollable.out @@ -0,0 +1,14 @@ +connected +Setting SQL_ATTR_CURSOR_SCROLLABLE = SQL_SCROLLABLE +ok +cursor_type after SQL_SCROLLABLE: SQL_CURSOR_STATIC +scrollable: SQL_SCROLLABLE +Setting SQL_ATTR_CURSOR_SCROLLABLE = SQL_NONSCROLLABLE +ok +cursor_type after SQL_NONSCROLLABLE: SQL_CURSOR_FORWARD_ONLY +scrollable: SQL_NONSCROLLABLE +Testing scrollable cursor with SQL_FETCH_FIRST +row: row1 +row: row2 +after SQL_FETCH_FIRST: row1 +disconnecting diff --git a/test/src/cursor-scrollable-test.c b/test/src/cursor-scrollable-test.c new file mode 100644 index 00000000..0934a512 --- /dev/null +++ b/test/src/cursor-scrollable-test.c @@ -0,0 +1,132 @@ +/* + * Test SQL_ATTR_CURSOR_SCROLLABLE setter support. + * + * Verifies that setting SQL_ATTR_CURSOR_SCROLLABLE to SQL_SCROLLABLE + * or SQL_NONSCROLLABLE properly sets the cursor type and allows + * scrollable operations. + */ +#include +#include +#include + +#include "common.h" + +int +main(int argc, char **argv) +{ + SQLRETURN rc; + HSTMT hstmt = SQL_NULL_HSTMT; + SQLUINTEGER scrollable; + SQLUINTEGER cursor_type; + + test_connect(); + + rc = SQLAllocHandle(SQL_HANDLE_STMT, conn, &hstmt); + if (!SQL_SUCCEEDED(rc)) + { + print_diag("failed to allocate stmt handle", SQL_HANDLE_DBC, conn); + exit(1); + } + + /* + * Test 1: Set SQL_ATTR_CURSOR_SCROLLABLE to SQL_SCROLLABLE + */ + printf("Setting SQL_ATTR_CURSOR_SCROLLABLE = SQL_SCROLLABLE\n"); + rc = SQLSetStmtAttr(hstmt, SQL_ATTR_CURSOR_SCROLLABLE, + (SQLPOINTER) SQL_SCROLLABLE, SQL_IS_UINTEGER); + CHECK_STMT_RESULT(rc, "SQLSetStmtAttr SQL_SCROLLABLE failed", hstmt); + printf("ok\n"); + + /* Verify cursor type is now SQL_CURSOR_STATIC */ + rc = SQLGetStmtAttr(hstmt, SQL_ATTR_CURSOR_TYPE, + &cursor_type, SQL_IS_UINTEGER, NULL); + CHECK_STMT_RESULT(rc, "SQLGetStmtAttr SQL_ATTR_CURSOR_TYPE failed", hstmt); + printf("cursor_type after SQL_SCROLLABLE: %s\n", + cursor_type == SQL_CURSOR_STATIC ? "SQL_CURSOR_STATIC" : + cursor_type == SQL_CURSOR_FORWARD_ONLY ? "SQL_CURSOR_FORWARD_ONLY" : + "other"); + + /* Verify SQL_ATTR_CURSOR_SCROLLABLE reads back correctly */ + rc = SQLGetStmtAttr(hstmt, SQL_ATTR_CURSOR_SCROLLABLE, + &scrollable, SQL_IS_UINTEGER, NULL); + CHECK_STMT_RESULT(rc, "SQLGetStmtAttr SQL_ATTR_CURSOR_SCROLLABLE failed", hstmt); + printf("scrollable: %s\n", + scrollable == SQL_SCROLLABLE ? "SQL_SCROLLABLE" : + scrollable == SQL_NONSCROLLABLE ? "SQL_NONSCROLLABLE" : + "other"); + + /* + * Test 2: Set SQL_ATTR_CURSOR_SCROLLABLE to SQL_NONSCROLLABLE + */ + printf("Setting SQL_ATTR_CURSOR_SCROLLABLE = SQL_NONSCROLLABLE\n"); + rc = SQLSetStmtAttr(hstmt, SQL_ATTR_CURSOR_SCROLLABLE, + (SQLPOINTER) SQL_NONSCROLLABLE, SQL_IS_UINTEGER); + CHECK_STMT_RESULT(rc, "SQLSetStmtAttr SQL_NONSCROLLABLE failed", hstmt); + printf("ok\n"); + + /* Verify cursor type is now SQL_CURSOR_FORWARD_ONLY */ + rc = SQLGetStmtAttr(hstmt, SQL_ATTR_CURSOR_TYPE, + &cursor_type, SQL_IS_UINTEGER, NULL); + CHECK_STMT_RESULT(rc, "SQLGetStmtAttr SQL_ATTR_CURSOR_TYPE failed", hstmt); + printf("cursor_type after SQL_NONSCROLLABLE: %s\n", + cursor_type == SQL_CURSOR_STATIC ? "SQL_CURSOR_STATIC" : + cursor_type == SQL_CURSOR_FORWARD_ONLY ? "SQL_CURSOR_FORWARD_ONLY" : + "other"); + + /* Verify SQL_ATTR_CURSOR_SCROLLABLE reads back correctly */ + rc = SQLGetStmtAttr(hstmt, SQL_ATTR_CURSOR_SCROLLABLE, + &scrollable, SQL_IS_UINTEGER, NULL); + CHECK_STMT_RESULT(rc, "SQLGetStmtAttr SQL_ATTR_CURSOR_SCROLLABLE failed", hstmt); + printf("scrollable: %s\n", + scrollable == SQL_SCROLLABLE ? "SQL_SCROLLABLE" : + scrollable == SQL_NONSCROLLABLE ? "SQL_NONSCROLLABLE" : + "other"); + + /* + * Test 3: Verify scrollable cursor actually works by doing a + * SQL_FETCH_FIRST after some fetches + */ + printf("Testing scrollable cursor with SQL_FETCH_FIRST\n"); + rc = SQLSetStmtAttr(hstmt, SQL_ATTR_CURSOR_SCROLLABLE, + (SQLPOINTER) SQL_SCROLLABLE, SQL_IS_UINTEGER); + CHECK_STMT_RESULT(rc, "SQLSetStmtAttr SQL_SCROLLABLE failed", hstmt); + + rc = SQLExecDirect(hstmt, (SQLCHAR *) "SELECT 'row' || g FROM generate_series(1, 5) g", SQL_NTS); + CHECK_STMT_RESULT(rc, "SQLExecDirect failed", hstmt); + + /* Fetch first two rows */ + { + char buf[40]; + SQLLEN ind; + + rc = SQLFetch(hstmt); + CHECK_STMT_RESULT(rc, "SQLFetch failed", hstmt); + rc = SQLGetData(hstmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + CHECK_STMT_RESULT(rc, "SQLGetData failed", hstmt); + printf("row: %s\n", buf); + + rc = SQLFetch(hstmt); + CHECK_STMT_RESULT(rc, "SQLFetch failed", hstmt); + rc = SQLGetData(hstmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + CHECK_STMT_RESULT(rc, "SQLGetData failed", hstmt); + printf("row: %s\n", buf); + + /* Now scroll back to first */ + rc = SQLFetchScroll(hstmt, SQL_FETCH_FIRST, 0); + CHECK_STMT_RESULT(rc, "SQLFetchScroll SQL_FETCH_FIRST failed", hstmt); + rc = SQLGetData(hstmt, 1, SQL_C_CHAR, buf, sizeof(buf), &ind); + CHECK_STMT_RESULT(rc, "SQLGetData failed", hstmt); + printf("after SQL_FETCH_FIRST: %s\n", buf); + } + + rc = SQLFreeStmt(hstmt, SQL_CLOSE); + CHECK_STMT_RESULT(rc, "SQLFreeStmt failed", hstmt); + + rc = SQLFreeHandle(SQL_HANDLE_STMT, hstmt); + CHECK_STMT_RESULT(rc, "SQLFreeHandle failed", hstmt); + + /* Clean up */ + test_disconnect(); + + return 0; +} From 3fa97f024729e1ec823580cccd5345e03394275f Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Thu, 12 Feb 2026 15:53:27 -0300 Subject: [PATCH 2/4] Rec 2: Reject SQL_ATTR_ASYNC_ENABLE = SQL_ASYNC_ENABLE_ON Return SQL_ERROR with SQLSTATE HYC00 when an application attempts to enable asynchronous execution, which the driver does not support. Previously the driver silently ignored the request, returning SQL_SUCCESS, which misled applications into believing async was active. Setting SQL_ASYNC_ENABLE_OFF remains a successful no-op. Add async-enable regression test to verify the behavior. --- options.c | 10 ++++- test/expected/async-enable.out | 8 ++++ test/src/async-enable-test.c | 69 ++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 test/expected/async-enable.out create mode 100644 test/src/async-enable-test.c diff --git a/options.c b/options.c index 5818cdb6..b12b1955 100644 --- a/options.c +++ b/options.c @@ -41,8 +41,14 @@ set_statement_option(ConnectionClass *conn, ci = &(SC_get_conn(stmt)->connInfo); switch (fOption) { - case SQL_ASYNC_ENABLE: /* ignored */ - break; + case SQL_ASYNC_ENABLE: + if (vParam == SQL_ASYNC_ENABLE_ON) { + if (stmt) + SC_set_error(stmt, STMT_OPTION_NOT_FOR_THE_DRIVER, + "Asynchronous execution is not supported", func); + return SQL_ERROR; + } + break; /* SQL_ASYNC_ENABLE_OFF is a no-op, which is correct */ case SQL_BIND_TYPE: /* now support multi-column and multi-row binding */ diff --git a/test/expected/async-enable.out b/test/expected/async-enable.out new file mode 100644 index 00000000..64a52ecc --- /dev/null +++ b/test/expected/async-enable.out @@ -0,0 +1,8 @@ +connected +Setting SQL_ATTR_ASYNC_ENABLE = SQL_ASYNC_ENABLE_ON +ok, got SQL_ERROR as expected +Setting SQL_ATTR_ASYNC_ENABLE = SQL_ASYNC_ENABLE_OFF +ok +Getting SQL_ATTR_ASYNC_ENABLE +async_enable: SQL_ASYNC_ENABLE_OFF +disconnecting diff --git a/test/src/async-enable-test.c b/test/src/async-enable-test.c new file mode 100644 index 00000000..c8f346d5 --- /dev/null +++ b/test/src/async-enable-test.c @@ -0,0 +1,69 @@ +/* + * Test SQL_ATTR_ASYNC_ENABLE handling. + * + * Verifies that setting SQL_ATTR_ASYNC_ENABLE to SQL_ASYNC_ENABLE_ON + * returns SQL_ERROR (since async is not supported), and that setting + * SQL_ASYNC_ENABLE_OFF succeeds. + */ +#include +#include +#include + +#include "common.h" + +int +main(int argc, char **argv) +{ + SQLRETURN rc; + HSTMT hstmt = SQL_NULL_HSTMT; + SQLINTEGER async_enable; + + test_connect(); + + rc = SQLAllocHandle(SQL_HANDLE_STMT, conn, &hstmt); + if (!SQL_SUCCEEDED(rc)) + { + print_diag("failed to allocate stmt handle", SQL_HANDLE_DBC, conn); + exit(1); + } + + /* + * Test 1: Setting SQL_ASYNC_ENABLE_ON should fail with SQL_ERROR + */ + printf("Setting SQL_ATTR_ASYNC_ENABLE = SQL_ASYNC_ENABLE_ON\n"); + rc = SQLSetStmtAttr(hstmt, SQL_ATTR_ASYNC_ENABLE, + (SQLPOINTER) SQL_ASYNC_ENABLE_ON, SQL_IS_UINTEGER); + if (rc == SQL_ERROR) + printf("ok, got SQL_ERROR as expected\n"); + else + printf("unexpected result: %d\n", rc); + + /* + * Test 2: Setting SQL_ASYNC_ENABLE_OFF should succeed + */ + printf("Setting SQL_ATTR_ASYNC_ENABLE = SQL_ASYNC_ENABLE_OFF\n"); + rc = SQLSetStmtAttr(hstmt, SQL_ATTR_ASYNC_ENABLE, + (SQLPOINTER) SQL_ASYNC_ENABLE_OFF, SQL_IS_UINTEGER); + CHECK_STMT_RESULT(rc, "SQLSetStmtAttr SQL_ASYNC_ENABLE_OFF failed", hstmt); + printf("ok\n"); + + /* + * Test 3: Getting SQL_ATTR_ASYNC_ENABLE should return SQL_ASYNC_ENABLE_OFF + */ + printf("Getting SQL_ATTR_ASYNC_ENABLE\n"); + rc = SQLGetStmtAttr(hstmt, SQL_ATTR_ASYNC_ENABLE, + &async_enable, SQL_IS_UINTEGER, NULL); + CHECK_STMT_RESULT(rc, "SQLGetStmtAttr SQL_ATTR_ASYNC_ENABLE failed", hstmt); + printf("async_enable: %s\n", + async_enable == SQL_ASYNC_ENABLE_OFF ? "SQL_ASYNC_ENABLE_OFF" : + async_enable == SQL_ASYNC_ENABLE_ON ? "SQL_ASYNC_ENABLE_ON" : + "other"); + + rc = SQLFreeHandle(SQL_HANDLE_STMT, hstmt); + CHECK_STMT_RESULT(rc, "SQLFreeHandle failed", hstmt); + + /* Clean up */ + test_disconnect(); + + return 0; +} From 952494f877bb9a398eec70988e3a01f8ff701a83 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Thu, 12 Feb 2026 15:53:35 -0300 Subject: [PATCH 3/4] Rec 3+4: Fix SQLSTATE codes for invalid info types and conn attrs Rec 3: Return SQLSTATE HY096 for invalid SQLGetInfo info types instead of HYC00. Add CONN_INVALID_INFO_TYPE error code (215) mapped to HY096. Use it in PGAPI_GetInfo for unrecognized info type values. Rec 4: Return SQLSTATE HY092 for invalid SQLSetConnectAttr attributes instead of HY000. Add explicit SQLSTATE mapping for the existing CONN_OPTION_NOT_FOR_THE_DRIVER error code to HY092. Previously this code had no mapping and fell through to the generic HY000 default. --- connection.h | 2 +- environ.c | 6 ++++++ info.c | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/connection.h b/connection.h index 2e2d61c8..16467350 100644 --- a/connection.h +++ b/connection.h @@ -68,7 +68,7 @@ typedef enum #define CONN_UNABLE_TO_LOAD_DLL 212 #define CONN_ILLEGAL_TRANSACT_STATE 213 #define CONN_VALUE_OUT_OF_RANGE 214 - +#define CONN_INVALID_INFO_TYPE 215 #define CONN_OPTION_NOT_FOR_THE_DRIVER 216 #define CONN_EXEC_ERROR 217 diff --git a/environ.c b/environ.c index f075fbf2..4cb8a27d 100644 --- a/environ.c +++ b/environ.c @@ -375,6 +375,12 @@ PGAPI_ConnectError(HDBC hdbc, case CONN_VALUE_OUT_OF_RANGE: pg_sqlstate_set(env, szSqlState, "HY019", "22003"); break; + case CONN_INVALID_INFO_TYPE: + pg_sqlstate_set(env, szSqlState, "HY096", "S1096"); + break; + case CONN_OPTION_NOT_FOR_THE_DRIVER: + pg_sqlstate_set(env, szSqlState, "HY092", "S1092"); + break; case CONNECTION_COULD_NOT_SEND: case CONNECTION_COULD_NOT_RECEIVE: case CONNECTION_COMMUNICATION_ERROR: diff --git a/info.c b/info.c index d5cc3e93..755baed7 100644 --- a/info.c +++ b/info.c @@ -1069,7 +1069,7 @@ MYLOG(0, "CONVERT_FUNCTIONS=" FORMAT_ULEN "\n", value); default: /* unrecognized key */ - CC_set_error(conn, CONN_NOT_IMPLEMENTED_ERROR, "Unrecognized key passed to PGAPI_GetInfo.", NULL); + CC_set_error(conn, CONN_INVALID_INFO_TYPE, "Unrecognized key passed to PGAPI_GetInfo.", NULL); goto cleanup; } From d059849878111f9c8891402816cddb0077c80b65 Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Thu, 12 Feb 2026 15:53:40 -0300 Subject: [PATCH 4/4] Add regression tests for SQLSTATE conformance (Rec 3+4) Add odbc-conformance test that verifies: - SQLGetInfo with invalid info type returns SQLSTATE HY096 - SQLSetConnectAttr with invalid attribute returns SQLSTATE HY092 Also register all new tests (async-enable, cursor-scrollable, odbc-conformance) in the test/tests list. --- test/expected/odbc-conformance.out | 6 +++ test/src/odbc-conformance-test.c | 59 ++++++++++++++++++++++++++++++ test/tests | 3 ++ 3 files changed, 68 insertions(+) create mode 100644 test/expected/odbc-conformance.out create mode 100644 test/src/odbc-conformance-test.c diff --git a/test/expected/odbc-conformance.out b/test/expected/odbc-conformance.out new file mode 100644 index 00000000..611638b3 --- /dev/null +++ b/test/expected/odbc-conformance.out @@ -0,0 +1,6 @@ +connected +Testing SQLGetInfo with invalid info type 99999 +SQLSTATE: HY096 +Testing SQLSetConnectAttr with invalid attribute 99999 +SQLSTATE: HY092 +disconnecting diff --git a/test/src/odbc-conformance-test.c b/test/src/odbc-conformance-test.c new file mode 100644 index 00000000..b9c114cb --- /dev/null +++ b/test/src/odbc-conformance-test.c @@ -0,0 +1,59 @@ +/* + * Test that SQLGetInfo returns SQLSTATE HY096 for invalid info types, + * and that SQLSetConnectAttr returns SQLSTATE HY092 for invalid attributes. + */ +#include +#include +#include + +#include "common.h" + +int +main(int argc, char **argv) +{ + SQLRETURN rc; + char buf[256]; + SQLSMALLINT len; + char sqlstate[6]; + SQLINTEGER nativeerror; + SQLSMALLINT textlen; + + test_connect(); + + /* + * Test 1: SQLGetInfo with invalid info type should return HY096 + */ + printf("Testing SQLGetInfo with invalid info type 99999\n"); + rc = SQLGetInfo(conn, (SQLUSMALLINT) 99999, buf, sizeof(buf), &len); + if (rc == SQL_ERROR) + { + rc = SQLGetDiagRec(SQL_HANDLE_DBC, conn, 1, (SQLCHAR *) sqlstate, + &nativeerror, NULL, 0, &textlen); + printf("SQLSTATE: %s\n", sqlstate); + } + else + { + printf("unexpected result: %d\n", rc); + } + + /* + * Test 2: SQLSetConnectAttr with invalid attribute should return HY092 + */ + printf("Testing SQLSetConnectAttr with invalid attribute 99999\n"); + rc = SQLSetConnectAttr(conn, (SQLINTEGER) 99999, (SQLPOINTER) 0, SQL_IS_UINTEGER); + if (rc == SQL_ERROR) + { + rc = SQLGetDiagRec(SQL_HANDLE_DBC, conn, 1, (SQLCHAR *) sqlstate, + &nativeerror, NULL, 0, &textlen); + printf("SQLSTATE: %s\n", sqlstate); + } + else + { + printf("unexpected result: %d\n", rc); + } + + /* Clean up */ + test_disconnect(); + + return 0; +} diff --git a/test/tests b/test/tests index 4db4b81d..ead21ea6 100644 --- a/test/tests +++ b/test/tests @@ -29,9 +29,11 @@ TESTBINS = exe/connect-test \ exe/boolsaschar-test \ exe/cvtnulldate-test \ exe/alter-test \ + exe/async-enable-test \ exe/quotes-test \ exe/cursors-test \ exe/cursor-movement-test \ + exe/cursor-scrollable-test \ exe/cursor-commit-test \ exe/cursor-name-test \ exe/cursor-block-delete-test \ @@ -51,6 +53,7 @@ TESTBINS = exe/connect-test \ exe/large-object-test \ exe/large-object-data-at-exec-test \ exe/odbc-escapes-test \ + exe/odbc-conformance-test \ exe/wchar-char-test \ exe/params-batch-exec-test \ exe/fetch-refcursors-test \