From 1298ccc62a10663839af782ee8db06eb6878a9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Rend=C3=B3n=20V=C3=A9lez?= <22383219+rendonvelez@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:24:52 -0500 Subject: [PATCH 01/11] feat: add connection.cancel() and statement.cancel() via SQLCancel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #434. Add the ability to cancel in-flight operations: - connection.cancel(): ODBCConnection keeps a registry of statement handles for in-flight operations (std::set guarded by a uv_mutex_t). QueryAsyncWorker and CallProcedureAsyncWorker register their handle right after allocating it and deregister it in their destructors. cancel() walks the registry and calls SQLCancel on each handle. - statement.cancel(): calls SQLCancel directly on the statement's handle, which lives for the whole lifetime of the Statement object. The cancelled operations return with SQLSTATE HY008 ("Operation canceled"), so their promises reject (or their callbacks receive an error) and callers can distinguish cancellation from other failures. SQLCancel is invoked synchronously on the main thread rather than through an AsyncWorker: cancellation is most needed precisely when the libuv thread pool is saturated by the operations being cancelled, so queueing the cancel behind them could delay it indefinitely. Calling SQLCancel from a different thread than the one running the statement is the documented multithreaded-cancel use of the function. StatementData.hstmt is now initialized to SQL_NULL_HANDLE so that cleanup paths that run before the handle is allocated read a defined value. Signed-off-by: Gustavo Rendón Vélez <22383219+rendonvelez@users.noreply.github.com> --- README.md | 124 ++++++++++++++++++++++++++++++++++++++++ lib/Connection.js | 38 ++++++++++++ lib/Statement.js | 35 +++++++++++- lib/odbc.d.ts | 8 +++ src/odbc.h | 2 +- src/odbc_connection.cpp | 92 +++++++++++++++++++++++++++++ src/odbc_connection.h | 11 ++++ src/odbc_statement.cpp | 57 ++++++++++++++++++ src/odbc_statement.h | 1 + 9 files changed, 366 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7559fa1e..de87ab8a 100644 --- a/README.md +++ b/README.md @@ -640,6 +640,64 @@ odbc.connect(`${process.env.CONNECTION_STRING}`, (error, connection) => { --- +### `.cancel(callback?)` + +Cancels all operations currently running on the connection (queries and procedure calls) by calling `SQLCancel` on their statement handles. The cancelled operations return with SQLSTATE HY008 ("Operation canceled"), so their promises reject (or their callbacks are called with an error). If no operations are running, `.cancel` is a no-op. + +#### Parameters: +* **callback?**: The function called when `.cancel` has finished execution. If no callback function is given, `.cancel` will return a native JavaScript `Promise`. Callback signature is: + * error: The error that occured in execution, or `null` if no error + +#### Examples: + +**Promises** + +```javascript +const odbc = require('odbc'); + +// can only use await keyword in an async function +async function cancelExample() { + const connection = await odbc.connect(`${process.env.CONNECTION_STRING}`); + + // cancel the query if it is still running after 10 seconds + const timer = setTimeout(() => { + connection.cancel(); + }, 10000); + + try { + const result = await connection.query('SELECT * FROM HUGE_TABLE'); + console.log(result); + } catch (error) { + // if cancelled, error contains an odbcError with state 'HY008' + } finally { + clearTimeout(timer); + } +} + +cancelExample(); +``` + +**Callbacks** + +```javascript +const odbc = require('odbc'); + +odbc.connect(`${process.env.CONNECTION_STRING}`, (error, connection) => { + const timer = setTimeout(() => { + connection.cancel((cancelError) => { + if (cancelError) { return; } // handle + }); + }, 10000); + + connection.query('SELECT * FROM HUGE_TABLE', (queryError, result) => { + clearTimeout(timer); + // if cancelled, queryError contains an odbcError with state 'HY008' + }); +}); +``` + +--- + ### `.close(callback?)` Closes an open connection. Any transactions on the connection that have not been ended will be rolledback. @@ -1027,6 +1085,72 @@ odbc.connect(`${process.env.CONNECTION_STRING}`, (error, connection) => { --- +### `.cancel(callback?)` + +Cancels any operation currently running on the Statement (e.g. a long `.execute()`) by calling `SQLCancel` on its handle. The cancelled operation returns with SQLSTATE HY008 ("Operation canceled"). + +#### Parameters: +* **callback?**: The function called when `.cancel` has finished execution. If no callback function is given, `.cancel` will return a native JavaScript `Promise`. Callback signature is: + * error: The error that occured in execution, or `null` if no error + +#### Examples: + +**Promises** + +```javascript +const odbc = require('odbc'); + +// can only use await keyword in an async function +async function cancelExample() { + const connection = await odbc.connect(`${process.env.CONNECTION_STRING}`); + const statement = await connection.createStatement(); + await statement.prepare('SELECT * FROM HUGE_TABLE WHERE FIELD_1 = ?'); + await statement.bind([1]); + + // cancel the execution if it is still running after 10 seconds + const timer = setTimeout(() => { + statement.cancel(); + }, 10000); + + try { + const result = await statement.execute(); + console.log(result); + } catch (error) { + // if cancelled, error contains an odbcError with state 'HY008' + } finally { + clearTimeout(timer); + } +} + +cancelExample(); +``` + +**Callbacks** + +```javascript +const odbc = require('odbc'); + +odbc.connect(`${process.env.CONNECTION_STRING}`, (error, connection) => { + connection.createStatement((error1, statement) => { + if (error1) { return; } // handle + statement.prepare('SELECT * FROM HUGE_TABLE', (error2) => { + if (error2) { return; } // handle + const timer = setTimeout(() => { + statement.cancel((cancelError) => { + if (cancelError) { return; } // handle + }); + }, 10000); + statement.execute((error3, result) => { + clearTimeout(timer); + // if cancelled, error3 contains an odbcError with state 'HY008' + }); + }); + }); +}); +``` + +--- + ### `.close(callback?)` Closes the Statement, freeing the statement handle. Running functions on the statement after closing will result in an error. diff --git a/lib/Connection.js b/lib/Connection.js index a9efd3a5..735849c6 100644 --- a/lib/Connection.js +++ b/lib/Connection.js @@ -295,6 +295,44 @@ class Connection { }); } + /** + * Cancels all operations currently running on this connection (queries and + * procedure calls) by calling SQLCancel on their statement handles. The + * cancelled operations return with SQLSTATE HY008 ("Operation canceled"). + * Asynchronous, can be used either with a callback function or a Promise. + * @param {function} [callback] - Callback function. If not passed, a Promise will be returned. + */ + cancel(callback = undefined) { + // type-checking + if (typeof callback !== 'function' && typeof callback !== 'undefined') { + throw new TypeError('[node-odbc]: Incorrect function signature for call to connection.cancel({function}[optional]).'); + } + + // promise... + if (callback === undefined) { + if (!this.odbcConnection) + { + throw new Error(Connection.CONNECTION_CLOSED_ERROR); + } + return new Promise((resolve, reject) => { + this.odbcConnection.cancel((error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + } + + // ...or callback + if (!this.odbcConnection) + { + return callback(new Error(Connection.CONNECTION_CLOSED_ERROR)); + } + return this.odbcConnection.cancel((error) => callback(error)); + } + // TODO: Documentation primaryKeys(catalog, schema, table, callback = undefined) { // promise... diff --git a/lib/Statement.js b/lib/Statement.js index 34ea9ad3..053ef9de 100644 --- a/lib/Statement.js +++ b/lib/Statement.js @@ -168,10 +168,43 @@ class Statement { } } + /** + * Cancels any operation currently running on this statement (e.g. a long + * execute()) by calling SQLCancel on its handle. The cancelled operation + * returns with SQLSTATE HY008 ("Operation canceled"). + * @param {function} [callback] - The callback function that returns the result. If omitted, uses a Promise. + */ + cancel(callback = undefined) { + if (typeof callback !== 'function' && typeof callback !== 'undefined') { + throw new TypeError('[node-odbc]: Incorrect function signature for call to statement.cancel({function}[optional]).'); + } + + if (typeof callback === 'undefined') { + if (!this.odbcStatement) { + throw new Error(Statement.STATEMENT_CLOSED_ERROR); + } + return new Promise((resolve, reject) => { + this.odbcStatement.cancel((error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + } + + // ...or callback + if (!this.odbcStatement) { + return callback(new Error(Statement.STATEMENT_CLOSED_ERROR)); + } + return this.odbcStatement.cancel((error) => callback(error)); + } + /** * Closes the statement, deleting the prepared statement and freeing the handle, making further * calls on the object invalid. - * @param {function} [callback] - The callback function that returns the result. If omitted, uses a Promise. + * @param {function} [callback] - The callback function that returns the result. If omitted, uses a Promise. */ close(callback = undefined) { if (typeof callback !== 'function' && typeof callback !== 'undefined') { diff --git a/lib/odbc.d.ts b/lib/odbc.d.ts index 3fab1a01..f058fd45 100644 --- a/lib/odbc.d.ts +++ b/lib/odbc.d.ts @@ -39,6 +39,8 @@ declare namespace odbc { execute(callback: (error: NodeOdbcError, result: Result) => undefined): undefined; + cancel(callback: (error: NodeOdbcError) => undefined): undefined; + close(callback: (error: NodeOdbcError) => undefined): undefined; //////////////////////////////////////////////////////////////////////////// @@ -51,6 +53,8 @@ declare namespace odbc { execute(): Promise>; + cancel(): Promise; + close(): Promise; } @@ -112,6 +116,8 @@ declare namespace odbc { rollback(callback: (error: NodeOdbcError) => undefined): undefined; + cancel(callback: (error: NodeOdbcError) => undefined): undefined; + close(callback: (error: NodeOdbcError) => undefined): undefined; //////////////////////////////////////////////////////////////////////////// @@ -142,6 +148,8 @@ declare namespace odbc { rollback(): Promise; + cancel(): Promise; + close(): Promise; connected(): boolean; diff --git a/src/odbc.h b/src/odbc.h index 4d5e7c58..3c0a3a5f 100644 --- a/src/odbc.h +++ b/src/odbc.h @@ -190,7 +190,7 @@ typedef struct StatementData { SQLHENV henv; SQLHDBC hdbc; - SQLHSTMT hstmt; + SQLHSTMT hstmt = SQL_NULL_HANDLE; QueryOptions query_options; diff --git a/src/odbc_connection.cpp b/src/odbc_connection.cpp index 4a6e2e81..178fed47 100644 --- a/src/odbc_connection.cpp +++ b/src/odbc_connection.cpp @@ -47,6 +47,7 @@ Napi::Object ODBCConnection::Init(Napi::Env env, Napi::Object exports) { InstanceMethod("close", &ODBCConnection::Close), InstanceMethod("createStatement", &ODBCConnection::CreateStatement), InstanceMethod("query", &ODBCConnection::Query), + InstanceMethod("cancel", &ODBCConnection::Cancel), InstanceMethod("beginTransaction", &ODBCConnection::BeginTransaction), InstanceMethod("commit", &ODBCConnection::Commit), InstanceMethod("rollback", &ODBCConnection::Rollback), @@ -167,12 +168,27 @@ ODBCConnection::ODBCConnection(const Napi::CallbackInfo& info) : Napi::ObjectWra this->hDBC = *(info[1].As>().Data()); this->connectionOptions = *(info[2].As>().Data()); this->getInfoResults = *(info[3].As>().Data()); + + uv_mutex_init(&this->activeStatementsMutex); } ODBCConnection::~ODBCConnection() { this->Free(); + uv_mutex_destroy(&this->activeStatementsMutex); +} + +void ODBCConnection::RegisterActiveStatement(SQLHSTMT hstmt) { + uv_mutex_lock(&this->activeStatementsMutex); + this->activeStatements.insert(hstmt); + uv_mutex_unlock(&this->activeStatementsMutex); +} + +void ODBCConnection::UnregisterActiveStatement(SQLHSTMT hstmt) { + uv_mutex_lock(&this->activeStatementsMutex); + this->activeStatements.erase(hstmt); + uv_mutex_unlock(&this->activeStatementsMutex); } SQLRETURN ODBCConnection::Free() { @@ -743,6 +759,10 @@ class QueryAsyncWorker : public ODBCAsyncWorker { return; } + // make the handle reachable by connection.cancel() while this worker + // is executing + odbcConnectionObject->RegisterActiveStatement(data->hstmt); + // set SQL_ATTR_QUERY_TIMEOUT if (data->query_options.timeout > 0) { return_code = @@ -992,6 +1012,9 @@ class QueryAsyncWorker : public ODBCAsyncWorker { } ~QueryAsyncWorker() { + if (data != NULL) { + odbcConnectionObject->UnregisterActiveStatement(data->hstmt); + } if (!data->query_options.use_cursor) { uv_mutex_lock(&ODBC::g_odbcMutex); @@ -1151,6 +1174,68 @@ Napi::Value ODBCConnection::Query(const Napi::CallbackInfo& info) { return env.Undefined(); } +/* + * ODBCConnection::Cancel + * + * Description: Calls SQLCancel on the statement handles of all in-flight + * operations on this connection (queries and procedure + * calls). The cancelled operations will return with SQLSTATE + * HY008 ("Operation canceled"), rejecting their promises or + * calling their callbacks with an error. + * + * SQLCancel is called synchronously on the main thread + * instead of through an AsyncWorker: cancel() is most needed + * precisely when the libuv thread pool is saturated by the + * operations being cancelled, so queueing the cancel behind + * them could delay it indefinitely. Calling SQLCancel from a + * different thread than the one running the statement is the + * documented multithreaded-cancel use of the function, and it + * returns quickly. + * + * Parameters: + * const Napi::CallbackInfo& info: + * The information passed from the JavaSript environment, including the + * function arguments for 'cancel'. + * + * info[0]: Function: callback function: + * function(error) + * error: An error object if one or more statements could not be + * cancelled, or null if the operation was successful. + * + * Return: + * Napi::Value: + * Undefined (results returned in callback) + */ +Napi::Value ODBCConnection::Cancel(const Napi::CallbackInfo& info) { + + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + + Napi::Function callback = info[0].As(); + + size_t failure_count = 0; + + uv_mutex_lock(&this->activeStatementsMutex); + for (SQLHSTMT hstmt : this->activeStatements) { + SQLRETURN return_code = SQLCancel(hstmt); + if (!SQL_SUCCEEDED(return_code)) { + failure_count++; + } + } + uv_mutex_unlock(&this->activeStatementsMutex); + + std::vector callbackArguments; + if (failure_count > 0) { + Napi::Error error = Napi::Error::New(env, "[odbc] Error canceling one or more active statements"); + callbackArguments.push_back(error.Value()); + } else { + callbackArguments.push_back(env.Null()); + } + callback.Call(callbackArguments); + + return env.Undefined(); +} + // If we have a parameter with input/output params (e.g. calling a procedure), // then we need to take the Parameter structures of the StatementData and create // a Napi::Array from those that were overwritten. @@ -1282,6 +1367,10 @@ class CallProcedureAsyncWorker : public ODBCAsyncWorker { return; } + // make the handle reachable by connection.cancel() while this worker + // is executing + odbcConnectionObject->RegisterActiveStatement(data->hstmt); + return_code = set_fetch_size ( @@ -1924,6 +2013,9 @@ class CallProcedureAsyncWorker : public ODBCAsyncWorker { } ~CallProcedureAsyncWorker() { + if (data != NULL) { + odbcConnectionObject->UnregisterActiveStatement(data->hstmt); + } delete[] overwriteParams; delete data; data = NULL; diff --git a/src/odbc_connection.h b/src/odbc_connection.h index afd74428..b5688329 100644 --- a/src/odbc_connection.h +++ b/src/odbc_connection.h @@ -20,6 +20,7 @@ #define _SRC_ODBC_CONNECTION_H #include +#include class ODBCConnection : public Napi::ObjectWrap { @@ -63,6 +64,7 @@ class ODBCConnection : public Napi::ObjectWrap { Napi::Value Close(const Napi::CallbackInfo& info); Napi::Value CreateStatement(const Napi::CallbackInfo& info); Napi::Value Query(const Napi::CallbackInfo& info); + Napi::Value Cancel(const Napi::CallbackInfo& info); Napi::Value CallProcedure(const Napi::CallbackInfo& info); Napi::Value BeginTransaction(const Napi::CallbackInfo& info); @@ -99,6 +101,15 @@ class ODBCConnection : public Napi::ObjectWrap { SQLHENV hENV; SQLHDBC hDBC; + // Statement handles for in-flight operations, registered by the + // AsyncWorkers so that cancel() can call SQLCancel on them from the main + // thread while a worker thread is blocked on the ODBC call. + uv_mutex_t activeStatementsMutex; + std::set activeStatements; + + void RegisterActiveStatement(SQLHSTMT hstmt); + void UnregisterActiveStatement(SQLHSTMT hstmt); + ConnectionOptions connectionOptions; GetInfoResults getInfoResults; diff --git a/src/odbc_statement.cpp b/src/odbc_statement.cpp index b1802446..e7b79f9d 100644 --- a/src/odbc_statement.cpp +++ b/src/odbc_statement.cpp @@ -34,6 +34,7 @@ Napi::Object ODBCStatement::Init(Napi::Env env, Napi::Object exports) { InstanceMethod("prepare", &ODBCStatement::Prepare), InstanceMethod("bind", &ODBCStatement::Bind), InstanceMethod("execute", &ODBCStatement::Execute), + InstanceMethod("cancel", &ODBCStatement::Cancel), InstanceMethod("close", &ODBCStatement::Close), }); @@ -546,6 +547,62 @@ Napi::Value ODBCStatement::Execute(const Napi::CallbackInfo& info) { return env.Undefined(); } +/****************************************************************************** + ********************************** CANCEL ************************************ + *****************************************************************************/ + +/* + * ODBCStatement::Cancel + * + * Description: Calls SQLCancel on this statement's handle, aborting any + * operation currently running on it (e.g. a long execute()). + * The cancelled operation will return with SQLSTATE HY008 + * ("Operation canceled"). + * + * SQLCancel is called synchronously on the main thread + * instead of through an AsyncWorker: cancel() is most needed + * precisely when the libuv thread pool is saturated by the + * operations being cancelled, so queueing the cancel behind + * them could delay it indefinitely. Calling SQLCancel from a + * different thread than the one running the statement is the + * documented multithreaded-cancel use of the function, and it + * returns quickly. + * + * Parameters: + * const Napi::CallbackInfo& info: + * The information passed from the JavaSript environment, including the + * function arguments for 'cancel'. + * + * info[0]: Function: callback function: + * function(error) + * error: An error object if the statement could not be + * cancelled, or null if the operation was successful. + * + * Return: + * Napi::Value: + * Undefined (results returned in callback) + */ +Napi::Value ODBCStatement::Cancel(const Napi::CallbackInfo& info) { + + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + + Napi::Function callback = info[0].As(); + + SQLRETURN return_code = SQLCancel(this->data->hstmt); + + std::vector callbackArguments; + if (!SQL_SUCCEEDED(return_code)) { + Napi::Error error = Napi::Error::New(env, "[odbc] Error canceling the statement"); + callbackArguments.push_back(error.Value()); + } else { + callbackArguments.push_back(env.Null()); + } + callback.Call(callbackArguments); + + return env.Undefined(); +} + /****************************************************************************** ********************************** CLOSE ************************************* *****************************************************************************/ diff --git a/src/odbc_statement.h b/src/odbc_statement.h index 6f9325a1..c23e30f8 100644 --- a/src/odbc_statement.h +++ b/src/odbc_statement.h @@ -38,6 +38,7 @@ class ODBCStatement : public Napi::ObjectWrap { Napi::Value Prepare(const Napi::CallbackInfo& info); Napi::Value Bind(const Napi::CallbackInfo& info); Napi::Value Execute(const Napi::CallbackInfo& info); + Napi::Value Cancel(const Napi::CallbackInfo& info); Napi::Value Close(const Napi::CallbackInfo& info); }; #endif From 1fec882e7c343371d3d828ab13db2c19686b6d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Rend=C3=B3n=20V=C3=A9lez?= <22383219+rendonvelez@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:06:06 -0500 Subject: [PATCH 02/11] fix: bind SQL_TINYINT columns as SQL_C_SHORT to support signed values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQL_TINYINT columns and procedure parameters were bound as SQL_C_UTINYINT. When a driver returns a signed TINYINT with a negative value (e.g. MySQL, Impala, Hive), the conversion to an unsigned C type fails with SQLSTATE 22003 (numeric value out of range), so any SELECT over such a column errors out. Bind TINYINT to SQL_C_SHORT/SQL_C_SSHORT instead, the same path already used for SQL_SMALLINT. SQLSMALLINT covers the full range of both signed (-128..127) and unsigned (0..255) TINYINT, so drivers that expose TINYINT as unsigned (e.g. SQL Server) keep returning values above 127 correctly. This also fixes TINYINT output parameters in callProcedure, which had no SQL_C_UTINYINT case in the result conversion and fell through to the SQL_C_CHAR default. Signed-off-by: Gustavo Rendón Vélez <22383219+rendonvelez@users.noreply.github.com> --- src/odbc.h | 2 -- src/odbc_connection.cpp | 29 +++++------------------------ 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/odbc.h b/src/odbc.h index 4d5e7c58..4605adbd 100644 --- a/src/odbc.h +++ b/src/odbc.h @@ -129,7 +129,6 @@ typedef struct ColumnData { SQLCHAR *char_data; SQLWCHAR *wchar_data; SQLDOUBLE double_data; - SQLCHAR tinyint_data; SQLUSMALLINT usmallint_data; SQLSMALLINT smallint_data; SQLINTEGER integer_data; @@ -279,7 +278,6 @@ typedef struct StatementData { for (int i = 0; i < this->column_count; i++) { switch (this->columns[i]->bind_type) { case SQL_C_CHAR: - case SQL_C_UTINYINT: case SQL_C_BINARY: delete[] (SQLCHAR *)this->bound_columns[i].buffer; break; diff --git a/src/odbc_connection.cpp b/src/odbc_connection.cpp index 4a6e2e81..14a905eb 100644 --- a/src/odbc_connection.cpp +++ b/src/odbc_connection.cpp @@ -1749,12 +1749,6 @@ class CallProcedureAsyncWorker : public ODBCAsyncWorker { break; case SQL_TINYINT: - data->parameters[i]->ValueType = SQL_C_UTINYINT; - data->parameters[i]->ParameterValuePtr = new SQLCHAR(); - data->parameters[i]->BufferLength = sizeof(SQLCHAR); - data->parameters[i]->DecimalDigits = data->storedRows[i][SQLPROCEDURECOLUMNS_DECIMAL_DIGITS_INDEX].smallint_data; - break; - case SQL_SMALLINT: data->parameters[i]->ValueType = SQL_C_SSHORT; data->parameters[i]->ParameterValuePtr = new SQLSMALLINT(); @@ -3399,15 +3393,12 @@ bind_buffers break; } + // Bind SQL_TINYINT to SQL_C_SHORT: SQLSMALLINT can hold the full range + // of TINYINT columns, which could be signed (-128 to 127) or unsigned + // (0 to 255) depending on the DBMS. This ensures negative values don't + // fail conversion with SQLSTATE 22003 and unsigned values above 127 are + // preserved. case SQL_TINYINT: - { - column->buffer_size = sizeof(SQLCHAR); - column->bind_type = SQL_C_UTINYINT; - data->bound_columns[i].buffer = - new SQLCHAR[data->fetch_size](); - break; - } - case SQL_SMALLINT: { column->buffer_size = sizeof(SQLSMALLINT); @@ -3829,11 +3820,6 @@ fetch_and_store ((SQLDOUBLE *)(data->bound_columns[column_index].buffer))[row_index]; break; - case SQL_C_UTINYINT: - row[column_index].tinyint_data = - ((SQLCHAR *)(data->bound_columns[column_index].buffer))[row_index]; - break; - case SQL_C_SSHORT: case SQL_C_SHORT: row[column_index].smallint_data = @@ -4172,11 +4158,6 @@ Napi::Array process_data_for_napi(Napi::Env env, StatementData *data, Napi::Arra case SQL_SMALLINT: case SQL_INTEGER: switch(columns[j]->bind_type) { - case SQL_C_TINYINT: - case SQL_C_UTINYINT: - case SQL_C_STINYINT: - value = Napi::Number::New(env, storedRow[j].tinyint_data); - break; case SQL_C_SHORT: case SQL_C_USHORT: case SQL_C_SSHORT: From 6354a7d6fb1e822a02bc02eac155875fed300703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Rend=C3=B3n=20V=C3=A9lez?= <22383219+rendonvelez@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:33:53 -0500 Subject: [PATCH 03/11] ci: re-run checks after windows runner fix landed on main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustavo Rendón Vélez <22383219+rendonvelez@users.noreply.github.com> From 061d35207a654d1f5cb8771e7644430d1b8d1ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Rend=C3=B3n=20V=C3=A9lez?= <22383219+rendonvelez@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:40:51 -0500 Subject: [PATCH 04/11] refactor: remove redundant NULL checks in async worker destructors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The data pointer is always valid: the only constructors of QueryAsyncWorker and CallProcedureAsyncWorker receive it as a required argument, so checking it for NULL before unregistering while dereferencing it unconditionally right after was inconsistent. Addresses review feedback. Signed-off-by: Gustavo Rendón Vélez <22383219+rendonvelez@users.noreply.github.com> --- src/odbc_connection.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/odbc_connection.cpp b/src/odbc_connection.cpp index 178fed47..f20f8aa4 100644 --- a/src/odbc_connection.cpp +++ b/src/odbc_connection.cpp @@ -1012,9 +1012,7 @@ class QueryAsyncWorker : public ODBCAsyncWorker { } ~QueryAsyncWorker() { - if (data != NULL) { - odbcConnectionObject->UnregisterActiveStatement(data->hstmt); - } + odbcConnectionObject->UnregisterActiveStatement(data->hstmt); if (!data->query_options.use_cursor) { uv_mutex_lock(&ODBC::g_odbcMutex); @@ -2013,9 +2011,7 @@ class CallProcedureAsyncWorker : public ODBCAsyncWorker { } ~CallProcedureAsyncWorker() { - if (data != NULL) { - odbcConnectionObject->UnregisterActiveStatement(data->hstmt); - } + odbcConnectionObject->UnregisterActiveStatement(data->hstmt); delete[] overwriteParams; delete data; data = NULL; From 4931ccfe4f3f4940e1f9e5a77d8414dd343cea67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Rend=C3=B3n=20V=C3=A9lez?= <22383219+rendonvelez@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:48:29 -0500 Subject: [PATCH 05/11] fix: merge INOUT TINYINT parameter sizing with SMALLINT and clean up comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For SQL_PARAM_INPUT_OUTPUT parameters, the TINYINT case overrode BufferLength to sizeof(SQLCHAR) without changing the value type that ODBC::StoreBindValues had already chosen (SQL_C_SBIGINT/SQL_C_DOUBLE for JS numbers). Route it through the same path as SQL_SMALLINT, which handles the numeric binding and the character-buffer resize. Also remove the stale SQL_C_STINYINT/SQL_C_TINYINT entries (listed twice) from the unhandled-C-types comment, since TINYINT columns no longer bind to those types. Addresses review feedback. Signed-off-by: Gustavo Rendón Vélez <22383219+rendonvelez@users.noreply.github.com> --- src/odbc_connection.cpp | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/odbc_connection.cpp b/src/odbc_connection.cpp index 14a905eb..9598573e 100644 --- a/src/odbc_connection.cpp +++ b/src/odbc_connection.cpp @@ -1491,6 +1491,7 @@ class CallProcedureAsyncWorker : public ODBCAsyncWorker { } break; } + case SQL_TINYINT: case SQL_SMALLINT: { switch(parameter->ValueType) { @@ -1513,18 +1514,6 @@ class CallProcedureAsyncWorker : public ODBCAsyncWorker { break; } - case SQL_TINYINT: { - switch(parameter->ValueType) - { - case SQL_C_CHAR: - default: { - parameter->BufferLength = sizeof(SQLCHAR); - break; - } - } - break; - } - case SQL_DECIMAL: case SQL_NUMERIC: { switch(parameter->ValueType) @@ -3888,14 +3877,10 @@ fetch_and_store // TODO: Unhandled C types: // SQL_C_SSHORT // SQL_C_SHORT - // SQL_C_STINYINT - // SQL_C_TINYINT // SQL_C_ULONG // SQL_C_LONG // SQL_C_FLOAT // SQL_C_BIT - // SQL_C_STINYINT - // SQL_C_TINYINT // SQL_C_SBIGINT // SQL_C_BOOKMARK // SQL_C_VARBOOKMARK From 25e61bd6be35da2e0cef4817f2cb4f4dbe3d858a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Rend=C3=B3n=20V=C3=A9lez?= <22383219+rendonvelez@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:54:01 -0500 Subject: [PATCH 06/11] docs: document cancellation-checkpoint caveat for cancel() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQLCancel only requests cancellation; when the operation actually aborts depends on the engine reaching a cancellation checkpoint. Note the known cases: Impala's sleep() completes its full wait, and stored procedure calls on Db2 for IBM i are not cancelable. Signed-off-by: Gustavo Rendón Vélez <22383219+rendonvelez@users.noreply.github.com> --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index de87ab8a..dd33ca16 100644 --- a/README.md +++ b/README.md @@ -644,6 +644,8 @@ odbc.connect(`${process.env.CONNECTION_STRING}`, (error, connection) => { Cancels all operations currently running on the connection (queries and procedure calls) by calling `SQLCancel` on their statement handles. The cancelled operations return with SQLSTATE HY008 ("Operation canceled"), so their promises reject (or their callbacks are called with an error). If no operations are running, `.cancel` is a no-op. +**Note:** `SQLCancel` only _requests_ cancellation — when the operation actually aborts depends on the driver and on the database engine reaching a cancellation checkpoint. Row-producing operations (scans, fetches) usually abort promptly, but some operations never check for cancellation and only fail with HY008 once they finish on their own. Known examples: Impala's `sleep()` function completes its full wait before honoring the cancel, and calls to stored procedures on Db2 for IBM i are not cancelable at all. + #### Parameters: * **callback?**: The function called when `.cancel` has finished execution. If no callback function is given, `.cancel` will return a native JavaScript `Promise`. Callback signature is: * error: The error that occured in execution, or `null` if no error @@ -1089,6 +1091,8 @@ odbc.connect(`${process.env.CONNECTION_STRING}`, (error, connection) => { Cancels any operation currently running on the Statement (e.g. a long `.execute()`) by calling `SQLCancel` on its handle. The cancelled operation returns with SQLSTATE HY008 ("Operation canceled"). +**Note:** the cancellation-checkpoint caveat described in [connection.cancel()](#cancelcallback) applies here as well. + #### Parameters: * **callback?**: The function called when `.cancel` has finished execution. If no callback function is given, `.cancel` will return a native JavaScript `Promise`. Callback signature is: * error: The error that occured in execution, or `null` if no error From 6a48986e88694fee250b6db898d4f32f44e010f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Rend=C3=B3n=20V=C3=A9lez?= <22383219+rendonvelez@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:03:30 -0500 Subject: [PATCH 07/11] build: reserve mach-o header padding on darwin builds Link darwin binaries with -headerpad_max_install_names so that install_name_tool can later add LC_RPATH entries to the prebuilt without overflowing into the __text section. The official v2.5.0 darwin prebuilts shipped with only 64 bytes of padding, which corrupts the binary when consumers patch rpaths. --- binding.gyp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/binding.gyp b/binding.gyp index a19ac7e0..c42fd88b 100644 --- a/binding.gyp +++ b/binding.gyp @@ -45,6 +45,11 @@ ], }], ], + # Reserve header padding so install_name_tool can add LC_RPATH + # entries to the prebuilt binaries without corrupting them. + 'xcode_settings': { + 'OTHER_LDFLAGS': [ '-Wl,-headerpad_max_install_names' ] + }, 'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ] }], [ 'OS == "freebsd"', { From 8ea1eaddd5c16074c34f4eb039a2448ee7a2ceb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Rend=C3=B3n=20V=C3=A9lez?= <22383219+rendonvelez@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:03:30 -0500 Subject: [PATCH 08/11] ci: adapt release workflow for fork-hosted prebuilts - Drop the ppc64le job: that runner is not available on this fork and would block the release job forever. - Mark releases as prerelease and remove the npm publish step: this fork only distributes prebuilt binaries via GitHub Releases. --- .github/workflows/create-release.yml | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 25a2571e..1ce32278 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -14,15 +14,12 @@ jobs: matrix: node-version: - 20.x + # ppc64le removed: that runner is not available on this fork. os: - windows-2022 - ubuntu-22.04 - macos-15 - macos-15-intel - - ubuntu-24.04-ppc64le - include: - - os: ubuntu-24.04-ppc64le - arch: ppc64le runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 @@ -65,11 +62,6 @@ jobs: - name: get-npm-version id: package-version run: jq -r '"version=" + .version' package.json >> "$GITHUB_OUTPUT" - - name: get-npm-tag - id: package-tag - run: .github/npm-tag.sh $PACKAGE_VERSION >> "$GITHUB_OUTPUT" - env: - PACKAGE_VERSION: ${{ steps.package-version.outputs.version }} # Download all of the various binaries that were created - name: Download built binaries from workflow uses: actions/download-artifact@v5 @@ -77,22 +69,16 @@ jobs: path: binaries pattern: binaries-* merge-multiple: true - - name: Create Release + - name: Create Release id: create_release uses: softprops/action-gh-release@v2 with: draft: false - prerelease: false + prerelease: true name: ${{ steps.package-version.outputs.version }} generate_release_notes: true fail_on_unmatched_files: true files: ./binaries/odbc-*.tar.gz - - name: Publish to NPM - uses: actions/setup-node@v4 - with: - node-version: '24' - registry-url: 'https://registry.npmjs.org' - - run: npm publish --tag $PACKAGE_TAG - env: - PACKAGE_TAG: ${{ steps.package-tag.outputs.tag }} + # npm publish removed: this fork only distributes prebuilt binaries + # through GitHub Releases; the package is never published to npm. From e28617b9c61ea711fb6482d14407bed25b8ad2fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Rend=C3=B3n=20V=C3=A9lez?= <22383219+rendonvelez@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:03:30 -0500 Subject: [PATCH 09/11] chore: version 2.5.0-saturno.1 with fork-hosted binaries Bump to a distinguishable prerelease version and point binary.host to this fork's GitHub Releases so node-pre-gyp downloads the patched prebuilts instead of the upstream ones. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 050113f2..6d2e92fb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "odbc", "description": "unixodbc bindings for node", - "version": "2.5.0", + "version": "2.5.0-saturno.1", "homepage": "http://github.com/IBM/node-odbc/", "main": "lib/odbc.js", "types": "lib/odbc.d.ts", @@ -60,7 +60,7 @@ "binary": { "module_name": "odbc", "module_path": "./lib/bindings/napi-v{napi_build_version}", - "host": "https://github.com/IBM/node-odbc/releases/download/v{version}", + "host": "https://github.com/rendonvelez/node-odbc/releases/download/v{version}", "package_name": "{name}-v{version}-{platform}-{arch}-napi-v{napi_build_version}.tar.gz", "napi_versions": [ 8 From 0a0151785db84ee21768851cc3d9aaf7e35227c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Rend=C3=B3n=20V=C3=A9lez?= <22383219+rendonvelez@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:26:20 -0500 Subject: [PATCH 10/11] ci: pin windows-2022 and upgrade npm in build workflow The node-gyp bundled with npm on Node 20 fails to locate Visual Studio on windows-latest (ERR_CHILD_PROCESS_STDIO_MAXBUFFER in 'find VS'). Mirror the create-release workflow: pin windows-2022 and upgrade npm before building. --- .github/workflows/build.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f37b654..60040e6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,10 @@ jobs: node-version: - 20.x os: - - windows-latest + # windows-2022 pinned: the node-gyp bundled with npm on Node 20 + # fails to locate Visual Studio on newer windows images + # (ERR_CHILD_PROCESS_STDIO_MAXBUFFER in "find VS"). + - windows-2022 - ubuntu-latest - macos-latest runs-on: ${{ matrix.os }} @@ -25,5 +28,7 @@ jobs: - name: Install dependencies if: ${{ startsWith(matrix.os, 'macos') }} run: brew install unixodbc + - name: Upgrade NPM + run: npm install -g npm - name: Build package run: npm ci --production --build-from-source From ecea4a6494d201e80d35bc405c8b31f3d37e326c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Rend=C3=B3n=20V=C3=A9lez?= <22383219+rendonvelez@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:31:38 -0500 Subject: [PATCH 11/11] chore: rename fork prerelease line to 2.5.0-eva.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d2e92fb..7daaa384 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "odbc", "description": "unixodbc bindings for node", - "version": "2.5.0-saturno.1", + "version": "2.5.0-eva.1", "homepage": "http://github.com/IBM/node-odbc/", "main": "lib/odbc.js", "types": "lib/odbc.d.ts",