UNPKG

teradatasql

Version:
919 lines 47.5 kB
"use strict"; // Copyright 2025 by Teradata Corporation. All Rights Reserved. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TeradataConnection = exports.getPackageVersion = void 0; // Get the Teradata Native Binding const koffi = __importStar(require("koffi")); /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-var-requires */ const fs = require("fs"); const path = require("path"); const os = require("os"); /* eslint-enable @typescript-eslint/no-require-imports */ /* eslint-enable @typescript-eslint/no-var-requires */ const teradata_cursor_1 = require("./teradata-cursor"); const teradata_logging_1 = require("./teradata-logging"); const teradata_exceptions_1 = require("./teradata-exceptions"); /* eslint-enable @typescript-eslint/naming-convention */ let g_sVersionNumber = ""; function getPackageVersion() { if (g_sVersionNumber === "") { const sPackageJsonPathName = path.resolve(__dirname, "..", "package.json"); try { const packageJson = fs.readFileSync(sPackageJsonPathName); JSON.parse(packageJson, (sKey, sValue) => { if (sKey === "version") { g_sVersionNumber = sValue; } }); } catch (ex) { throw new teradata_exceptions_1.OperationalError(ex.toString()); } } // end if g_sVersionNumber return g_sVersionNumber; } // end getPackageVersion exports.getPackageVersion = getPackageVersion; function _safeReadFileSync(sPathName) { try { return fs.readFileSync(sPathName, { encoding: "utf-8" }); } catch (error) { return ""; } } // end _safeReadFileSync class TeradataConnection { constructor() { this.sVersionNumber = ""; this.lib = {}; this.bInAsyncConnect = false; /** * Registry of all cursors created from this connection. * * This Set tracks all active TeradataCursor instances associated with this * connection to enable cross-object async operation validation. When any * cursor has an async operation in progress (executeAsync/executemanyAsync), * the connection must block ALL operations (both sync and async). * * Lifecycle: * - Cursor registered: When cursor() is called and TeradataCursor is created * - Cursor unregistered: When cursor.close() is called * * Purpose: Enforces "When an async operation is in progress, ALL other operations * are blocked" rule across object boundaries (connection vs cursor). */ this.activeCursors = new Set(); // Set<TeradataCursor>, using 'any' to avoid circular dependency if (!["aarch64", "arm64", "x64"].includes(process.arch)) { throw new Error("This package requires 64-bit Node.js. 32-bit Node.js is not supported."); } this.sVersionNumber = getPackageVersion(); this.nId = ++TeradataConnection.nInstanceCount; this.poolHandle = null; this.logLevel = 0; this.logger = new teradata_logging_1.TeradataLogging(); const sOSType = process.platform; const bARM = ["aarch64", "arm64"].includes(process.arch); const bFIPS = sOSType === "linux" && _safeReadFileSync("/proc/sys/crypto/fips_enabled").trim() === "1"; let sExtension = ""; if (sOSType === "win32") { sExtension = "dll"; } else if (sOSType === "darwin") { sExtension = "dylib"; } else if (bARM && bFIPS) { sExtension = "arm.fips.so"; } else if (bARM) { sExtension = "arm.so"; } else if (bFIPS) { sExtension = "fips.so"; } else { sExtension = "so"; } const sLibPathName = path.resolve(__dirname, "teradatasql." + sExtension); // Load goside library try { this.gosideLib = koffi.load(`${sLibPathName}`); } catch (error) { throw new teradata_exceptions_1.OperationalError(error.message); } // Load C functions try { this.lib.jsgoCreateConnection = this.gosideLib.func("void jsgoCreateConnection(uint64_t, _In_ char*, _In_ char*, _Out_ str*!, _Inout_ uint64_t*)"); this.lib.jsgoCloseConnection = this.gosideLib.func("void jsgoCloseConnection(uint64_t, uint64_t, _Out_ str*!)"); this.lib.jsgoCancelRequest = this.gosideLib.func("void jsgoCancelRequest(uint64_t, uint64_t, _Out_ str*!)"); this.lib.jsgoCreateRows = this.gosideLib.func("void jsgoCreateRows(uint64_t, uint64_t, _In_ char*, uint64_t, _In_ void*, _Out_ str*!, _Inout_ uint64_t*)"); this.lib.jsgoFetchRow = this.gosideLib.func("void jsgoFetchRow(uint64_t, uint64_t, _Out_ str*!, _Inout_ int64_t*, _Out_ void**!, int64_t)"); this.lib.jsgoResultMetaData = this.gosideLib.func("void jsgoResultMetaData(uint64_t, uint64_t, _Out_ str*!, _Inout_ uint64_t*, _Inout_ int*, _Out_ void**!)"); this.lib.jsgoNextResult = this.gosideLib.func("void jsgoNextResult(uint64_t, uint64_t, _Out_ str*!, _Inout_ char*)"); this.lib.jsgoCloseRows = this.gosideLib.func("void jsgoCloseRows(uint64_t, uint64_t, _Out_ str*!)"); this.lib.jsgoAsyncCreateRows = this.gosideLib.func("void jsgoAsyncCreateRows(uint64_t, uint64_t, _In_ char*, uint64_t, _In_ void*, _Out_ str*!, _Inout_ uint64_t*)"); this.lib.jsgoPollRows = this.gosideLib.func("void jsgoPollRows(uint64_t, uint64_t, _Out_ str*!, _Inout_ uint64_t*)"); this.lib.jsgoAsyncCreateConnection = this.gosideLib.func("void jsgoAsyncCreateConnection(uint64_t, _In_ char*, _In_ char*, _Out_ str*!, _Inout_ uint64_t*)"); this.lib.jsgoAsyncCloseConnection = this.gosideLib.func("void jsgoAsyncCloseConnection(uint64_t, uint64_t, _Out_ str*!)"); this.lib.jsgoPollConnection = this.gosideLib.func("void jsgoPollConnection(uint64_t, uint64_t, _Out_ str*!, _Inout_ char*)"); } catch (error) { throw new teradata_exceptions_1.OperationalError(error.message); } } // All DBAPI 2.0 methods // https://www.python.org/dev/peps/pep-0249/#connection-objects get Id() { return this.nId; } get uLog() { return this.logLevel; } get uPoolHandle() { return this.poolHandle; } get isAsyncConnect() { return this.bInAsyncConnect; } get connected() { return this.poolHandle !== null; } cursor() { // Block cursor creation during connectAsync/closeAsync operations this._stopIfInAsyncConnect(); // Block new cursor creation if an existing cursor has async SQL execution in progress if (this._hasActiveCursorAsyncOp()) { throw new teradata_exceptions_1.ProgrammingError(TeradataConnection.ERROR_802_OTHER_CURSOR_ASYNC_IN_PROGRESS); } // Create new cursor and register it const newCursor = new teradata_cursor_1.TeradataCursor(this, this.lib); this._registerCursor(newCursor); return newCursor; } nativeSQL(sSQL) { this.logger.traceLog("> enter nativeSQL TeradataConnection"); try { this._stopIfInAsyncConnect(); // cursor() checks _hasActiveCursorAsyncOp() const cur = this.cursor(); try { cur.execute("{fn teradata_nativesql}" + sSQL); // teradata_nativesql never produces a fake result set return cur.fetchone()[0]; } finally { cur.close(); } } finally { this.logger.traceLog("< leave nativeSQL TeradataConnection"); } } // end nativeSQL get autocommit() { this.logger.traceLog("> enter autocommit getter"); try { this._stopIfInAsyncConnect(); return this.nativeSQL("{fn teradata_autocommit}") === "true"; } finally { this.logger.traceLog("< leave autocommit getter"); } } // end autocommit getter set autocommit(value) { this.logger.traceLog("> enter autocommit setter"); try { this._stopIfInAsyncConnect(); this.nativeSQL("{fn teradata_autocommit_" + (value ? "on" : "off") + "}"); } finally { this.logger.traceLog("< leave autocommit setter"); } } // end autocommit setter connect(connectParams = {}, sJSON = "{}") { this._stopIfInAsyncConnect(); const combinedConnectParams = this._prepareConnectionParams(connectParams, sJSON); this.logLevel = combinedConnectParams.log ? Number(combinedConnectParams.log) : 0; this.logger.setLogLevel(this.logLevel); this.logger.traceLog("> enter connect TeradataConnection " + JSON.stringify(combinedConnectParams)); try { let outputPtrPtr = [null]; let poolHandlePtr = [0]; this.lib.jsgoCreateConnection(this.uLog, this.sVersionNumber, JSON.stringify(combinedConnectParams), outputPtrPtr, poolHandlePtr); const outputString = outputPtrPtr[0]; if (outputString) { throw new teradata_exceptions_1.OperationalError(outputString); } this.poolHandle = poolHandlePtr[0]; } finally { this.logger.traceLog("< leave connect TeradataConnection"); } } close() { this.logger.traceLog("> enter close TeradataConnection"); try { this._stopIfInAsyncConnect(); let outputPtrPtr = [null]; this.lib.jsgoCloseConnection(this.uLog, this.poolHandle, outputPtrPtr); const outputString = outputPtrPtr[0]; if (outputString) { throw new teradata_exceptions_1.OperationalError(outputString); } // Set poolHandle to null after successful close this.poolHandle = null; } finally { this.logger.traceLog("< leave close TeradataConnection"); } } cancel() { this.logger.traceLog("> enter cancel TeradataConnection"); try { this._stopIfInAsyncConnect(); // Does NOT check _hasActiveCursorAsyncOp() to allow cancellation // during cursor async operations (executeAsync/executemanyAsync) let outputPtrPtr = [null]; this.lib.jsgoCancelRequest(this.uLog, this.poolHandle, outputPtrPtr); const outputString = outputPtrPtr[0]; if (outputString) { throw new teradata_exceptions_1.OperationalError(outputString); } } finally { this.logger.traceLog("< leave cancel TeradataConnection"); } } commit() { this.logger.traceLog("> enter commit TeradataConnection"); try { this._stopIfInAsyncConnect(); // cursor() checks _hasActiveCursorAsyncOp() this.cursor().execute("{fn teradata_commit}"); } finally { this.logger.traceLog("< leave commit TeradataConnection"); } } rollback() { this.logger.traceLog("> enter rollback TeradataConnection"); try { this._stopIfInAsyncConnect(); // cursor() checks _hasActiveCursorAsyncOp() this.cursor().execute("{fn teradata_rollback}"); } finally { this.logger.traceLog("< leave rollback TeradataConnection"); } } /** * Asynchronously connects to the Teradata database server using non-blocking operations. * * @param connectParams - Connection parameters (host, user, password, etc.) * @param sJSON - Additional parameters as JSON string (default: "{}") * @returns Promise resolving when connection is established * @throws {ProgrammingError} Error 803 - Connection async operation in progress * @throws {ProgrammingError} Error 804 - Cursor async operation blocks connection operation * @throws {OperationalError} Connection failed * * @example * ```typescript * await conn.connectAsync({ host: "whomooz", user: "guest", password: "please" }); * ``` * * @remarks * **Mutual Exclusion:** ALL new operations blocked while an async operation in progress. * * **Timeout:** Uses 5-minute timeout if logon_timeout=0 (default), otherwise uses logon_timeout + 5s buffer. * * **Polling:** Checks completion every 100ms until done or timeout. */ async connectAsync(connectParams = {}, sJSON = "{}") { try { // Block if has connection async operation in progress this._stopIfInAsyncConnect(); // Block if any cursor has async operation in progress if (this._hasActiveCursorAsyncOp()) { throw new teradata_exceptions_1.ProgrammingError(TeradataConnection.ERROR_804_CURSOR_ASYNC_BLOCKS_CONNECTION); } // Prepare connection parameters using shared helper const combinedConnectParams = this._prepareConnectionParams(connectParams, sJSON); this.logLevel = combinedConnectParams.log ? Number(combinedConnectParams.log) : 0; this.logger.setLogLevel(this.logLevel); // Trace log moved here after logger is properly initialized with correct log level this.logger.traceLog("> enter connectAsync TeradataConnection"); this.bInAsyncConnect = true; // async connect starts const dStartTime = Date.now(); // Calculate effective client-side timeout based on logon_timeout parameter // Two scenarios (undefined is treated as "0" per driver default behavior): // 1. Not specified OR "0" -> Use 5-minute extended timeout (compromise) // 2. Positive value -> Use specified value + 5s buffer // Parse logon_timeout, treating undefined as 0 (driver default) const logonTimeoutSeconds = combinedConnectParams.logon_timeout ? parseInt(combinedConnectParams.logon_timeout, 10) : 0; // undefined defaults to 0 per README documentation let effectiveTimeoutMs; let timeoutReason; if (logonTimeoutSeconds === 0) { // Scenario 1: logon_timeout not specified or "0" - use 5-minute extended timeout // This is a compromise: sync connect() waits indefinitely with default/"0", // but async operations should always have a safety timeout effectiveTimeoutMs = TeradataConnection.CONNECT_ASYNC_DEFAULT_TIMEOUT_MS; timeoutReason = "extended (logon_timeout=0 default)"; } else { // Scenario 2: logon_timeout > 0 - honor user's timeout + 5s buffer // Buffer ensures server-side timeout triggers first for cleaner errors effectiveTimeoutMs = (logonTimeoutSeconds * 1000) + 5000; timeoutReason = `user-specified (${logonTimeoutSeconds}s + 5s buffer)`; } this.logger.debugLog(`connectAsync timeout: ${effectiveTimeoutMs}ms [${timeoutReason}]`); await this._createConnection(this.logLevel, this.sVersionNumber, JSON.stringify(combinedConnectParams)); let result; let pollAttempts = 0; while (true) { pollAttempts++; let elapsedTime = Date.now() - dStartTime; // Client-side timeout protection: prevent infinite polling // Timeout behavior depends on logon_timeout setting to respect user intent if (elapsedTime > effectiveTimeoutMs) { this.logger.debugLog(`connectAsync timeout after ${pollAttempts} polling attempts (${elapsedTime}ms) - handle ${this.poolHandle}`); // Clear poolHandle on timeout to ensure clean state const previousHandle = this.poolHandle; this.poolHandle = null; this.logger.debugLog(`connectAsync timeout - handle ${previousHandle} cleared`); throw new teradata_exceptions_1.OperationalError(`Connection timeout: Operation took longer than ${effectiveTimeoutMs / 1000} seconds [${timeoutReason}]`); } try { this.logger.traceLog(`_pollConnection TeradataConnection, bInAsyncConnect=` + this.bInAsyncConnect); result = await this._pollConnection(this.logLevel, this.poolHandle); this.logger.debugLog(`connectAsync _pollConnection attempt #${pollAttempts} returned ${result} (elapsed: ${elapsedTime}ms)`); if (typeof result === "number") { if (result !== 0) { // Operation completed successfully this.logger.debugLog(`connectAsync completed successfully with handle ${this.poolHandle} after ${pollAttempts} attempts (${elapsedTime}ms)`); break; } else { this.logger.debugLog(`connectAsync will retry _pollConnection after ${TeradataConnection.POLLING_INTERVAL_MS} ms (attempt #${pollAttempts}, elapsed: ${elapsedTime}ms)`); await new Promise((resolve) => setTimeout(resolve, TeradataConnection.POLLING_INTERVAL_MS)); } } else { // Clear poolHandle on connection failure this.poolHandle = null; this.logger.debugLog(`connectAsync error after ${pollAttempts} attempts (${elapsedTime}ms): ${result}`); throw new teradata_exceptions_1.OperationalError(`connectAsync error: ${result}`); } } catch (error) { // Clear poolHandle on connection failure to ensure clean state const previousHandle = this.poolHandle; this.poolHandle = null; const errorStr = error.toString(); const errorCode = (0, teradata_exceptions_1.extractErrorCode)(errorStr); elapsedTime = Date.now() - dStartTime; // Extract error ID if present for logging const errorMatch = errorStr.match(/^\[ERR-(\d+-\d+)\]/); if (errorMatch) { const errorId = errorMatch[0]; this.logger.debugLog(`connectAsync: operation_failed error_id=${errorId}, total_time=${elapsedTime}ms`); } else { this.logger.debugLog(`connectAsync: operation_failed (no_correlation_id), total_time=${elapsedTime}ms`); } this.logger.debugLog(`connectAsync error [${errorCode}] after ${elapsedTime}ms - handle ${previousHandle} cleared: ${errorStr}`); throw new teradata_exceptions_1.OperationalError(errorStr); } } // end infinite loop this.logger.timingLog("connectAsync took " + (Date.now() - dStartTime) + " ms"); } finally { this.bInAsyncConnect = false; // async connect completes this.logger.traceLog("< leave connectAsync TeradataConnection, bInAsyncConnect=" + this.bInAsyncConnect); } } /** * Asynchronously closes the connection using non-blocking operations. * * @returns Promise resolving when connection is closed * @throws {ProgrammingError} Error 803 - Connection async operation already in progress * @throws {ProgrammingError} Error 804 - Cursor async operation blocks connection operation * @throws {OperationalError} Close operation failed * * @example * ```typescript * await conn.closeAsync(); * ``` * * @remarks * **Mutual Exclusion:** ALL operations blocked while async operation in progress. * * **Timeout:** Fixed 30-second timeout for close operations. * * **Polling:** Checks completion every 100ms until done or timeout. * * **Cleanup:** Sets poolHandle to null after successful close. */ async closeAsync() { this.logger.traceLog("> enter closeAsync TeradataConnection, bInAsyncConnect=" + this.bInAsyncConnect); try { // Block if connection already has async operation in progress this._stopIfInAsyncConnect(); // Block if any cursor has async operation in progress if (this._hasActiveCursorAsyncOp()) { throw new teradata_exceptions_1.ProgrammingError(TeradataConnection.ERROR_804_CURSOR_ASYNC_BLOCKS_CONNECTION); } this.bInAsyncConnect = true; // async close starts const dStartTime = Date.now(); await this._closeConnection(this.logLevel, this.poolHandle); let result; let pollAttempts = 0; while (true) { pollAttempts++; let elapsedTime = Date.now() - dStartTime; // Client-side timeout protection: prevent infinite polling if (elapsedTime > TeradataConnection.CLOSE_ASYNC_TIMEOUT_MS) { this.logger.debugLog(`closeAsync timeout after ${pollAttempts} polling attempts (${elapsedTime}ms) - handle ${this.poolHandle}`); throw new teradata_exceptions_1.OperationalError(`Connection close timeout: Operation took longer than ${TeradataConnection.CLOSE_ASYNC_TIMEOUT_MS / 1000} seconds`); } try { this.logger.traceLog(`_pollConnection TeradataConnection, bInAsyncConnect=` + this.bInAsyncConnect); result = await this._pollConnection(this.logLevel, this.poolHandle); this.logger.debugLog(`closeAsync _pollConnection attempt #${pollAttempts} returned ${result} (elapsed: ${elapsedTime}ms)`); if (typeof result === "number") { if (result !== 0) { // Connection closed successfully const closedHandle = this.poolHandle; this.poolHandle = null; this.logger.debugLog(`closeAsync completed successfully - handle ${closedHandle} closed after ${pollAttempts} attempts (${elapsedTime}ms)`); break; } else { this.logger.debugLog(`closeAsync will retry _pollConnection after ${TeradataConnection.POLLING_INTERVAL_MS} ms (attempt #${pollAttempts}, elapsed: ${elapsedTime}ms)`); await new Promise((resolve) => setTimeout(resolve, TeradataConnection.POLLING_INTERVAL_MS)); } } else { const errorHandle = this.poolHandle; this.logger.debugLog(`closeAsync error after ${pollAttempts} attempts (${elapsedTime}ms) - handle ${errorHandle}: ${result}`); throw new teradata_exceptions_1.OperationalError(`closeAsync error: ${result}`); } } catch (error) { const errorStr = error.toString(); const errorCode = (0, teradata_exceptions_1.extractErrorCode)(errorStr); elapsedTime = Date.now() - dStartTime; this.logger.debugLog(`closeAsync error [${errorCode}] after ${elapsedTime}ms, attempt #${pollAttempts} - handle ${this.poolHandle}: ${errorStr}`); throw new teradata_exceptions_1.OperationalError(errorStr); } } // end infinite loop this.logger.timingLog("closeAsync took " + (Date.now() - dStartTime) + " ms"); } finally { this.bInAsyncConnect = false; // async close completes const closedHandle = this.poolHandle; this.logger.traceLog("< leave closeAsync TeradataConnection, bInAsyncConnect=" + this.bInAsyncConnect + ", handle=" + closedHandle); } } /** * Prepares connection parameters by merging user params with system metadata. * * This helper method consolidates common parameter preparation logic shared by * both connect() and connectAsync(). It handles: * - JSON kwargs parsing and boolean-to-string conversion * - Stack trace extraction and formatting * - Client metadata injection (kind, VM, OS, stack, extras) * * **Metadata Added:** * - `client_kind`: "S" for Node.js * - `client_vmname`: Node.js version * - `client_osname`: OS version and architecture * - `client_stack`: Formatted call stack * - `client_extra`: Node.js version and timezone * * @param connectParams - User-provided connection parameters * @param sJSON - Additional parameters as JSON string * @returns Merged connection parameters with system metadata * @private */ _prepareConnectionParams(connectParams, sJSON) { const kwargs = { ...JSON.parse(sJSON) }; Object.keys(kwargs).forEach((key) => { if (kwargs[key] === true) { kwargs[key] = "true"; } else if (kwargs[key] === false) { kwargs[key] = "false"; } }); const traceback = Error().stack; let sFrame; let sFrames = []; let listFrames = []; if (traceback) { traceback.replace(path.sep, "/"); sFrames = traceback.split("\n at").slice(1); for (const fr of sFrames) { sFrame = fr.split("/").pop(); if (sFrame) { sFrame = sFrame.split(":")[0]; if (!listFrames.includes(sFrame)) { listFrames.push(sFrame); } } } } if (process.platform === "win32") { // Windows listFrames = listFrames.map((frame) => frame.split(" (C")[0].trim()); } kwargs.client_kind = "S"; // G = Go, P = Python, R = R, S = Node.js kwargs.client_vmname = "Node.js " + process.version; kwargs.client_osname = os.version().split(":")[0] + " " + os.arch(); kwargs.client_stack = listFrames.join(" "); kwargs.client_extra = "NODEJS=" + process.version + ";TZ=" + Intl.DateTimeFormat().resolvedOptions().timeZone + ";"; // must be semicolon-terminated return { ...connectParams, ...kwargs }; } /** * Validates if an async connect/close operation is currently in progress. * * This method prevents race conditions by ensuring only one async operation * can be active at a time. It's called at the beginning of synchronous and * asynchronous methods to enforce mutual exclusion. * * * @throws {ProgrammingError} Error 803 - If bInAsyncConnect is true, indicating * an async connection operation is already in progress * * @private */ _stopIfInAsyncConnect() { if (this.bInAsyncConnect) { this.logger.debugLog(`_stopIfInAsyncConnect: async operation already in progress - rejecting new operation (handle: ${this.poolHandle})`); throw new teradata_exceptions_1.ProgrammingError(TeradataConnection.ERROR_803_CONNECTION_ASYNC_IN_PROGRESS); } this.logger.debugLog(`_stopIfInAsyncConnect: no async operation in progress - allowing new operation (handle: ${this.poolHandle})`); } /** * Registers a cursor in the active cursor registry. * * This method adds a TeradataCursor to the activeCursors Set, enabling * cross-object async operation validation. Once registered, the connection * can check if any cursor has an async operation in progress. * * **Lifecycle Integration:** * - Called by: cursor() method when creating new TeradataCursor * - Paired with: _unregisterCursor() called by cursor.close() * * **Purpose:** * - Enables _hasActiveCursorAsyncOp() to detect cursor async operations * - Enforces "When async operation is in progress, ALL other operations are blocked" rule * * @param cursor - The TeradataCursor instance to register * @private */ _registerCursor(cursor) { this.activeCursors.add(cursor); this.logger.debugLog(`_registerCursor: registered cursor (total active: ${this.activeCursors.size})`); } /** * Unregisters a cursor from the active cursor registry. * * This method removes a TeradataCursor from the activeCursors Set when * the cursor is closed. This cleanup ensures accurate async operation * validation state. * * **Lifecycle Integration:** * - Called by: TeradataCursor.close() method * - Paired with: _registerCursor() called by cursor() * * **Purpose:** * - Maintains accurate cursor registry state * - Prevents memory leaks from closed cursors * * @param cursor - The TeradataCursor instance to unregister * @internal - Public for TeradataCursor access, but not part of public API */ _unregisterCursor(cursor) { this.activeCursors.delete(cursor); this.logger.debugLog(`_unregisterCursor: unregistered cursor (total active: ${this.activeCursors.size})`); } /** * Checks if any active cursor has an async operation in progress. * * This method provides cross-object async operation validation by checking * if any registered cursor is currently executing an async SQL operation * (executeAsync/executemanyAsync). * * **Validation Logic:** * - Iterates through all cursors in activeCursors Set * - Optionally excludes a specific cursor from the check (used by that cursor to check others) * - Checks each cursor's bInAsyncExecute flag * - Returns true if ANY cursor (except excluded one) has async operation in progress * * **Usage:** * - Called by: connectAsync() and closeAsync() before starting operation * - Called by: cursor() to check if any cursor has async operation before creating new cursor * - Purpose: Enforces "When async operation is in progress, ALL other operations are blocked" * * @param excludeCursor - Optional cursor to exclude from the check (typically the caller) * @returns True if any cursor (except excluded) has async operation in progress, false otherwise * @private */ _hasActiveCursorAsyncOp(excludeCursor) { for (const cursor of this.activeCursors) { // Skip the excluded cursor if provided if (excludeCursor && cursor === excludeCursor) { continue; } if (cursor.isAsyncExec) { this.logger.debugLog(`_hasActiveCursorAsyncOp: found cursor ${cursor.Id} with async operation in progress`); return true; } } return false; } /** * Initiates asynchronous connection creation on the Go side. * * This method starts the async connection process by calling the Go-side * jsgoAsyncCreateConnection function. It handles immediate errors but does * not wait for connection completion - that's handled by polling. * * **Go-side Integration:** * - Calls jsgoAsyncCreateConnection with log level, version, and params * - Handles immediate validation errors from Go side * - Does not block waiting for connection establishment * * **Error Handling:** * - Throws OperationalError for immediate Go-side failures * - Provides timing logs for performance monitoring * - Preserves original Go error messages * * @param nLog - Logging level for Go-side operations * @param sVersionNumber - Driver version number * @param sConnectParams - JSON-serialized connection parameters * @throws {OperationalError} For immediate connection creation failures * * @private */ async _createConnection(nLog, sVersionNumber, sConnectParams) { this.logger.traceLog("> enter _createConnection TeradataConnection, bInAsyncConnect=" + this.bInAsyncConnect); this.logger.debugLog(`_createConnection called with logLevel=${nLog}, version=${sVersionNumber}, paramsLength=${sConnectParams.length}`); let dStartTimeBeginning = Date.now(); let outputPtrPtr = [null]; let poolHandlePtr = [0]; this.logger.debugLog(`_createConnection calling Go-side jsgoAsyncCreateConnection`); this.lib.jsgoAsyncCreateConnection(nLog, sVersionNumber, sConnectParams, outputPtrPtr, poolHandlePtr); const outputString = outputPtrPtr[0]; if (outputString) { throw new teradata_exceptions_1.OperationalError(outputString); } // Set the initial connection handle for polling (matches synchronous connect behavior) this.poolHandle = poolHandlePtr[0]; this.logger.debugLog(`_createConnection received initial handle: ${this.poolHandle}`); this.logger.timingLog("_createConnection took " + (Date.now() - dStartTimeBeginning) + " ms"); this.logger.traceLog("> leave _createConnection TeradataConnection"); } /** * Initiates asynchronous connection closure on the Go side. * * This method starts the async connection close process by calling the Go-side * jsgoAsyncCloseConnection function. It validates the handle first and handles * immediate errors, but does not wait for close completion. * * **Pre-conditions:** * - Connection handle should be valid (Go-side will validate) * - No other async operations should be in progress * * **Go-side Integration:** * - Calls jsgoAsyncCloseConnection with log level and pool handle * - Go-side validates handle and provides proper error messages * - Does not block waiting for connection closure completion * * **Error Handling:** * - Go-side performs comprehensive handle validation * - Throws OperationalError for immediate Go-side failures * - Provides timing logs for performance monitoring * - Preserves original Go error messages * * @param nLog - Logging level for Go-side operations * @param nPoolHandle - Connection pool handle to close (Go-side validates) * @throws {OperationalError} For immediate close failures from Go-side validation * * @private */ async _closeConnection(nLog, nPoolHandle) { this.logger.traceLog("> enter _closeConnection TeradataConnection, bInAsyncConnect=" + this.bInAsyncConnect); this.logger.debugLog(`_closeConnection called with logLevel=${nLog}, handle=${nPoolHandle}`); let dStartTimeBeginning = Date.now(); let outputPtrPtr = [null]; this.logger.debugLog(`_closeConnection calling Go-side jsgoAsyncCloseConnection for handle=${nPoolHandle}`); this.lib.jsgoAsyncCloseConnection(nLog, nPoolHandle, outputPtrPtr); const outputString = outputPtrPtr[0]; if (outputString) { throw new teradata_exceptions_1.OperationalError(outputString); } this.logger.timingLog("_closeConnection took " + (Date.now() - dStartTimeBeginning) + " ms"); this.logger.traceLog("> leave _closeConnection TeradataConnection"); } /** * Polls the Go side for async connection operation status. * * This method directly calls the Go-side jsgoPollConnection function to check * if an async operation (connect or close) has completed. It implements the * compatibility fix for the new Go pbReady flag implementation. * * **Go-side Compatibility:** * - Updated for new Go implementation using *pbReady = C.char(1) for completion * - Changed from string-based status to numeric flag (0 = executing, 1 = complete) * - Maintains compatibility with enhanced Go-side error handling * * **Return Values:** * - `number`: 0 = operation still executing, non-zero = operation complete * - `string`: Error message from Go side for immediate failure * * **Error Handling:** * - Go-side validates connection handle with proper error codes * - Returns error string for Go-side failures * - Provides comprehensive timing logs for performance analysis * - Preserves original Go error messages for debugging * * **Performance Monitoring:** * - Logs timing for each poll operation * - Tracks polling frequency and duration * - Enables performance optimization analysis * * @param nLog - Logging level for Go-side operations * @param nPoolHandle - Connection pool handle to poll (Go-side validates) * @returns Operation status as number (0 = executing, non-zero = complete) * or error string for immediate failures * * @private */ _jsgoPollConnection(nLog, nPoolHandle) { this.logger.traceLog("> enter _jsgoPollConnection TeradataConnection, bInAsyncConnect=" + this.bInAsyncConnect); this.logger.debugLog(`_jsgoPollConnection polling handle=${nPoolHandle}`); let dStartTime = Date.now(); let outputPtrPtr = [null]; let statusPtr = [0]; // Initialize as number array for pbReady flag this.lib.jsgoPollConnection(nLog, nPoolHandle, outputPtrPtr, statusPtr); const outputString = outputPtrPtr[0]; if (outputString) { // Detect and log error correlation ID const errorMatch = outputString.match(/^\[ERR-(\d+-\d+)\]/); if (errorMatch) { const errorId = errorMatch[0]; // Full [ERR-xxx-yyy] this.logger.debugLog(`_jsgoPollConnection: received_error error_id=${errorId} from Go layer for handle=${nPoolHandle}`); } this.logger.debugLog("_jsgoPollConnection output=" + outputString); this.logger.timingLog("_jsgoPollConnection took " + (Date.now() - dStartTime) + " ms"); return outputString; } this.logger.timingLog("_jsgoPollConnection took " + (Date.now() - dStartTime) + " ms"); const readyFlag = statusPtr[0]; this.logger.debugLog(`_jsgoPollConnection received readyFlag=${readyFlag} for handle=${nPoolHandle}`); if (readyFlag !== 0) { // 1 means operation complete, 0 means still executing this.logger.traceLog(`> leave _jsgoPollConnection TeradataConnection, connection operation complete`); this.logger.debugLog(`_jsgoPollConnection operation completed for handle=${nPoolHandle}`); return nPoolHandle || 1; } this.logger.traceLog("> leave _jsgoPollConnection TeradataConnection, the connection operation is still executing."); this.logger.debugLog(`_jsgoPollConnection operation still executing for handle=${nPoolHandle}`); return 0; // Connection operation is still executing } /** * Promise wrapper for async connection polling operations. * * This method wraps the synchronous _jsgoPollConnection call in a Promise * to integrate seamlessly with the async/await pattern used in connectAsync() * and closeAsync(). It provides clean separation between sync Go calls and * async JavaScript patterns. * * **Promise Resolution:** * - **Resolves** with numeric result: 0 = operation not complete, non-zero = complete * - **Rejects** with string result: error message from Go side * * **Integration Pattern:** * - Used in while loops within async methods for non-blocking polling * - Enables proper error propagation through Promise rejection * - Maintains clean async/await syntax in calling methods * * **Design Benefits:** * - Clean separation of sync Go calls from async JavaScript patterns * - Consistent error handling through Promise rejection * - Enables timeout handling in calling methods * - Provides foundation for future polling enhancements * * @param nLog - Logging level for Go-side operations * @param nPoolHandle - Connection pool handle to poll * @returns Promise<number> - 0 for operation not complete, non-zero for complete * @rejects {string} Error message from Go side * * @private */ _pollConnection(nLog, nPoolHandle) { return new Promise((resolve, reject) => { this.logger.traceLog("_pollConnection TeradataConnection, bInAsyncConnect=" + this.bInAsyncConnect); const result = this._jsgoPollConnection(nLog, nPoolHandle); if (typeof result === "number") { resolve(result); // 0: operation not complete, non-zero: operation complete } else { reject(result); // error text } }); } } exports.TeradataConnection = TeradataConnection; TeradataConnection.nInstanceCount = 0; /** * Constants for async operation timing and control. * * These constants centralize timing parameters and control flags used across * all async connection operations, ensuring consistency and maintainability. */ /** * Default timeout duration (in milliseconds) for connectAsync() when logon_timeout is "0" or not specified. * * This constant is used exclusively by connectAsync() as a safety timeout when the logon_timeout * parameter is "0" (default) or explicitly set to "0". While synchronous connect() waits indefinitely * with logon_timeout="0", async operations should always have a safety timeout. The 5-minute duration * provides a reasonable compromise between: * - Honoring user intent of "long wait acceptable" (implied by logon_timeout="0") * - Preventing infinite hangs in async operations * - Allowing slow connections to succeed * * Note: When logon_timeout is set to a positive value, connectAsync() uses that value plus a 5-second * buffer instead of this constant. */ TeradataConnection.CONNECT_ASYNC_DEFAULT_TIMEOUT_MS = 300000; // 5 minutes for connectAsync() with logon_timeout="0" /** * Timeout duration (in milliseconds) for closeAsync() operations. * * This constant is used exclusively by closeAsync() to provide client-side timeout protection. * Connection close operations typically complete quickly, so 30 seconds is generous enough * to handle network delays while preventing indefinite hangs. Note: connectAsync() uses * a different timeout strategy based on the logon_timeout parameter. */ TeradataConnection.CLOSE_ASYNC_TIMEOUT_MS = 30000; // 30 seconds timeout for closeAsync() /** * Polling interval (in milliseconds) for connectAsync() and closeAsync() operations. * * This constant controls how frequently the client checks Go-side operation status * during async connection operations. The 100ms interval provides a good balance between: * - Responsiveness: Quick detection of operation completion * - CPU efficiency: Avoiding excessive polling overhead * - Network efficiency: Not overwhelming the Go-side with status requests */ TeradataConnection.POLLING_INTERVAL_MS = 100; // 100ms polling interval for connection async operations /** * Error message constants for async operation enforcement. * * These constants centralize error messages to ensure consistency across methods * and simplify maintenance. They enforce mutual exclusion during async operations. * Made public to allow teradata-cursor.ts to reference them. */ /** * Error message when attempting cursor operations while this cursor's own async operation is in progress. * Used by: TeradataCursor._stopIfInAsyncExecute() */ TeradataConnection.ERROR_801_CURSOR_SELF_ASYNC_IN_PROGRESS = "[Error 801] [SQLState HY000] Cannot perform operation while cursor async operation (executeAsync/executemanyAsync) is in progress on this cursor."; /** * Error message when attempting operations while another cursor's async operation is in progress. * Used by: cursor(), TeradataCursor._stopIfInAsyncExecute() */ TeradataConnection.ERROR_802_OTHER_CURSOR_ASYNC_IN_PROGRESS = "[Error 802] [SQLState HY000] Cannot perform operation while cursor async operation (executeAsync/executemanyAsync) is in progress on another cursor on this connection"; /** * Error message when attempting new operations while connection async operation is in progress. * Used by: _stopIfInAsyncConnect(), TeradataCursor._stopIfInAsyncExecute() */ TeradataConnection.ERROR_803_CONNECTION_ASYNC_IN_PROGRESS = "[Error 803] [SQLState HY000] Cannot perform new operation while connection async operation (connectAsync/closeAsync) is in progress."; /** * Error message when attempting connection async operations while cursor async operation is in progress. * Used by: connectAsync(), closeAsync() */ TeradataConnection.ERROR_804_CURSOR_ASYNC_BLOCKS_CONNECTION = "[Error 804] [SQLState HY000] Cannot start new connection async operation (connectAsync/closeAsync) while cursor async operation (executeAsync/executemanyAsync) is in progress."; //# sourceMappingURL=teradata-connection.js.map