diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f37b65..60040e6 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 diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 25a2571..1ce3227 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. diff --git a/README.md b/README.md index 7559fa1..dd33ca1 100644 --- a/README.md +++ b/README.md @@ -640,6 +640,66 @@ 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. + +**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 + +#### 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 +1087,74 @@ 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"). + +**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 + +#### 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/binding.gyp b/binding.gyp index a19ac7e..c42fd88 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"', { diff --git a/lib/Connection.js b/lib/Connection.js index a9efd3a..735849c 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 34ea9ad..053ef9d 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 3fab1a0..f058fd4 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/package.json b/package.json index 050113f..7daaa38 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-eva.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 diff --git a/src/odbc.h b/src/odbc.h index 4d5e7c5..98bd66e 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; @@ -190,7 +189,7 @@ typedef struct StatementData { SQLHENV henv; SQLHDBC hdbc; - SQLHSTMT hstmt; + SQLHSTMT hstmt = SQL_NULL_HANDLE; QueryOptions query_options; @@ -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 4a6e2e8..da490d1 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,7 @@ class QueryAsyncWorker : public ODBCAsyncWorker { } ~QueryAsyncWorker() { + odbcConnectionObject->UnregisterActiveStatement(data->hstmt); if (!data->query_options.use_cursor) { uv_mutex_lock(&ODBC::g_odbcMutex); @@ -1151,6 +1172,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 +1365,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 ( @@ -1491,6 +1578,7 @@ class CallProcedureAsyncWorker : public ODBCAsyncWorker { } break; } + case SQL_TINYINT: case SQL_SMALLINT: { switch(parameter->ValueType) { @@ -1513,18 +1601,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) @@ -1749,12 +1825,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(); @@ -1924,6 +1994,7 @@ class CallProcedureAsyncWorker : public ODBCAsyncWorker { } ~CallProcedureAsyncWorker() { + odbcConnectionObject->UnregisterActiveStatement(data->hstmt); delete[] overwriteParams; delete data; data = NULL; @@ -3399,15 +3470,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 +3897,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 = @@ -3902,14 +3965,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 @@ -4172,11 +4231,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: diff --git a/src/odbc_connection.h b/src/odbc_connection.h index afd7442..b568832 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 b180244..e7b79f9 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 6f9325a..c23e30f 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