From 0ff695dcee2934e763ed513b240f1969cceb30cd Mon Sep 17 00:00:00 2001 From: Calvin Buckley Date: Tue, 22 Sep 2026 15:19:19 -0300 Subject: [PATCH 1/3] ext/pdo_odbc: Cleanup nop handlers It's not exhaustive, better to have nop attributes be handled by default --- ext/pdo_odbc/odbc_driver.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ext/pdo_odbc/odbc_driver.c b/ext/pdo_odbc/odbc_driver.c index dbdb0066578b..8f6c49a6aab9 100644 --- a/ext/pdo_odbc/odbc_driver.c +++ b/ext/pdo_odbc/odbc_driver.c @@ -408,16 +408,14 @@ static int odbc_handle_get_attr(pdo_dbh_t *dbh, zend_long attr, zval *val) return pdo_odbc_get_info_string(dbh, SQL_DBMS_VER, val); case PDO_ATTR_SERVER_INFO: return pdo_odbc_get_info_string(dbh, SQL_DBMS_NAME, val); - case PDO_ATTR_PREFETCH: - case PDO_ATTR_TIMEOUT: - case PDO_ATTR_CONNECTION_STATUS: - break; case PDO_ODBC_ATTR_ASSUME_UTF8: ZVAL_BOOL(val, H->assume_utf8); return 1; case PDO_ATTR_AUTOCOMMIT: ZVAL_BOOL(val, dbh->auto_commit); return 1; + default: + break; } return 0; } From e27001b7dc519e2c56e5e36919b8ce8101b708a3 Mon Sep 17 00:00:00 2001 From: Calvin Buckley Date: Wed, 23 Sep 2026 14:23:34 -0300 Subject: [PATCH 2/3] ext/pdo_odbc: timeout support ODBC has both a connection timeout for the connection level and a query timeout at the statement level. Annoyingly, these are somewhat poorly defined. The connection timeout, for instance, can be defined as either a login/connection setup timeout, or as a generic "any action" timeout, and if the latter, the query timeout may not necessarily be implemented. Accordingly, because we have no way to differentiate, we have to set both, and have special handling for the initial connection, as the setAttribute call won't handle early enough if it only handles initial connection timeouts. This means changing it after for a connection isn't relevant for existing statements that have already been constructed; this commit doesn't add a way to change it for a statement after changing it on the connection (be it via setAttribute on the statement, or somehow changing statements from the connection setAttribute). Also annoying is the difference in behaviour between drivers when a timeout happens. For example, SQL Server has a query timeout which just gracefully ends the current query, but Db2i only has a connection timeout which puts the whole connection into an invalid state (but at least it should fail a liveness check for a persistent connection). We don't have much control over this unfortunately. In the future, we should consider clarifying the PDO timeout attribute behaviour's on statements vs. connections, or make them separate attributes, like how PDO_DBLIB has. --- ext/pdo_odbc/odbc_driver.c | 49 ++++++++++++++++++++++++++++++++- ext/pdo_odbc/php_pdo_odbc_int.h | 3 ++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/ext/pdo_odbc/odbc_driver.c b/ext/pdo_odbc/odbc_driver.c index 8f6c49a6aab9..62cfd4ed583f 100644 --- a/ext/pdo_odbc/odbc_driver.c +++ b/ext/pdo_odbc/odbc_driver.c @@ -152,6 +152,7 @@ static bool odbc_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *s S->H = H; S->assume_utf8 = H->assume_utf8; + S->stmt_timeout = H->stmt_timeout; /* before we prepare, we need to peek at the query; if it uses named parameters, * we want PDO to rewrite them for us */ @@ -194,6 +195,16 @@ static bool odbc_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *s } } + rc = SQLSetStmtAttr(S->stmt, SQL_ATTR_QUERY_TIMEOUT, (SQLPOINTER)(uintptr_t) S->stmt_timeout, SQL_IS_INTEGER); + if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) { + pdo_odbc_stmt_error("SQLSetStmtAttr: SQL_ATTR_QUERY_TIMEOUT"); + SQLFreeHandle(SQL_HANDLE_STMT, S->stmt); + if (nsql) { + zend_string_release(nsql); + } + return false; + } + rc = SQLPrepare(S->stmt, (SQLCHAR *) ZSTR_VAL(sql), SQL_NTS); if (nsql) { zend_string_release(nsql); @@ -341,6 +352,7 @@ static bool odbc_handle_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val) { pdo_odbc_db_handle *H = (pdo_odbc_db_handle *)dbh->driver_data; bool bval; + zend_long lval; switch (attr) { case PDO_ODBC_ATTR_ASSUME_UTF8: @@ -373,6 +385,29 @@ static bool odbc_handle_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val) } } return true; + case PDO_ATTR_TIMEOUT: + if (!pdo_get_long_param(&lval, val)) { + return false; + } + /* This will only apply to the current connection and + * any new statements; currently no way to set timeout + * for already existing statements. + */ + H->stmt_timeout = H->conn_timeout = lval; + /* set odbc conn attr in case driver cares */ + RETCODE rc = SQLSetConnectAttr( + H->dbc, + SQL_ATTR_CONNECTION_TIMEOUT, + (SQLPOINTER) lval, + SQL_IS_INTEGER + ); + if (rc != SQL_SUCCESS) { + pdo_odbc_drv_error( + dbh->auto_commit ? "SQLSetConnectAttr AUTOCOMMIT = ON" : "SQLSetConnectAttr AUTOCOMMIT = OFF" + ); + return false; + } + return true; default: strcpy(H->einfo.last_err_msg, "Unknown Attribute"); H->einfo.what = "setAttribute"; @@ -408,6 +443,9 @@ static int odbc_handle_get_attr(pdo_dbh_t *dbh, zend_long attr, zval *val) return pdo_odbc_get_info_string(dbh, SQL_DBMS_VER, val); case PDO_ATTR_SERVER_INFO: return pdo_odbc_get_info_string(dbh, SQL_DBMS_NAME, val); + case PDO_ATTR_TIMEOUT: + ZVAL_LONG(val, H->conn_timeout); + return 1; case PDO_ODBC_ATTR_ASSUME_UTF8: ZVAL_BOOL(val, H->assume_utf8); return 1; @@ -473,7 +511,7 @@ static int pdo_odbc_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{ pdo_odbc_db_handle *H; RETCODE rc; int use_direct = 0; - zend_ulong cursor_lib; + zend_ulong cursor_lib, connection_timeout; H = pecalloc(1, sizeof(*H), dbh->is_persistent); @@ -523,6 +561,15 @@ static int pdo_odbc_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{ goto fail; } + /* setup connection timeout */ + connection_timeout = pdo_attr_lval(driver_options, PDO_ATTR_TIMEOUT, 0); + rc = SQLSetConnectAttr(H->dbc, SQL_ATTR_CONNECTION_TIMEOUT, (void*)connection_timeout, SQL_IS_INTEGER); + if (rc != SQL_SUCCESS) { + pdo_odbc_drv_error("SQLSetConnectAttr SQL_ATTR_CONNECTION_TIMEOUT"); + goto fail; + } + H->stmt_timeout = H->conn_timeout = connection_timeout; + /* a connection string may have = but not ; - i.e. "DSN=PHP" */ if (strchr(dbh->data_source, '=')) { SQLCHAR dsnbuf[1024]; diff --git a/ext/pdo_odbc/php_pdo_odbc_int.h b/ext/pdo_odbc/php_pdo_odbc_int.h index 52d66abfba01..945dee40ea96 100644 --- a/ext/pdo_odbc/php_pdo_odbc_int.h +++ b/ext/pdo_odbc/php_pdo_odbc_int.h @@ -121,6 +121,8 @@ typedef struct { PDO_ODBC_HENV env; PDO_ODBC_HDBC dbc; pdo_odbc_errinfo einfo; + SQLUINTEGER conn_timeout; + SQLUINTEGER stmt_timeout; unsigned assume_utf8:1; unsigned _spare:31; } pdo_odbc_db_handle; @@ -144,6 +146,7 @@ typedef struct { char *convbuf; zend_ulong convbufsize; HashTable *out_buffers; + SQLUINTEGER stmt_timeout; unsigned going_long:1; unsigned assume_utf8:1; unsigned params_dirty:1; From 1bec5994d02ef5833225e076fbc9cf5ad2b1aa16 Mon Sep 17 00:00:00 2001 From: Calvin Buckley Date: Thu, 15 Oct 2026 19:42:41 -0300 Subject: [PATCH 3/3] ext/pdo_odbc: unit test for SQL Server derived from DBLIB --- ext/pdo_odbc/tests/timeout.phpt | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 ext/pdo_odbc/tests/timeout.phpt diff --git a/ext/pdo_odbc/tests/timeout.phpt b/ext/pdo_odbc/tests/timeout.phpt new file mode 100644 index 000000000000..b764b3ba2d55 --- /dev/null +++ b/ext/pdo_odbc/tests/timeout.phpt @@ -0,0 +1,49 @@ +--TEST-- +PDO_DBLIB: Set query timeouts +--EXTENSIONS-- +pdo_dblib +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_TIMEOUT, 1); +$stmt = $db->prepare($sql); +try { + $stmt->execute(); + echo "Shouldn't happen\n"; +} catch (PDOException) { + echo "OK for timeout set after\n"; +} + +// regular timeout attribute will affect query timeout, causing this query to fail +$attributes = [PDO::ATTR_TIMEOUT => 1]; +$db = new PDO($dsn, $user, $password, $attributes); +$stmt = $db->prepare($sql); +try { + $stmt->execute(); + echo "Shouldn't happen\n"; +} catch (PDOException) { + echo "OK for timeout set on ctor\n"; +} + +?> +--EXPECT-- +OK for timeout set after +OK for timeout set on ctor