UNPKG

@itwin/core-backend

Version:
197 lines 9.02 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); exports.ECSqlRowExecutor = void 0; exports.releaseECSqlStatement = releaseECSqlStatement; const core_common_1 = require("@itwin/core-common"); const core_bentley_1 = require("@itwin/core-bentley"); const Symbols_1 = require("./internal/Symbols"); const statementNotPreparedMessage = "Statement is not prepared. Likely cause: the db was closed before step is called or the ECSqlSyncReader is used outside the context of the callback passed to withQueryReader."; function logStatementCleanupError(loggerCategory, message, error) { try { core_bentley_1.Logger.logTrace(loggerCategory, message, () => ({ error: core_bentley_1.BentleyError.getErrorMessage(error) })); } catch { // Cleanup diagnostics must not replace the original query error. } } /** Returns a statement to its cache, falling back to direct disposal without throwing. * @internal */ // eslint-disable-next-line @typescript-eslint/no-deprecated function releaseECSqlStatement(stmt, cache, loggerCategory, canCache) { if (!canCache || !stmt.isPrepared) { try { stmt[Symbol.dispose](); } catch (error) { logStatementCleanupError(loggerCategory, "Failed to dispose an ECSQL statement that could not be cached.", error); } return; } try { cache.addOrDispose(stmt); } catch (error) { try { stmt[Symbol.dispose](); } catch (disposeError) { logStatementCleanupError(loggerCategory, "Failed to dispose an ECSQL statement after it could not be returned to the statement cache.", disposeError); } logStatementCleanupError(loggerCategory, "Failed to return an ECSQL statement to the statement cache; attempted direct disposal instead.", error); } } // -------------------------------------------------------------------------------------------- // ECSqlRowExecutor // -------------------------------------------------------------------------------------------- /** * Executes ECSql queries one row at a time against an IModelDb, maintaining statement state between * successive calls so the caller can page through results via offset-based requests. * @internal */ class ECSqlRowExecutor { _db; _stmt; _loggerCategory; _removeListener; _isDisposed = false; _canCacheStatement = true; /** Whether the statement completed preparation and can be returned to its cache. * @internal */ get canCacheStatement() { return this._canCacheStatement; } // eslint-disable-next-line @typescript-eslint/no-deprecated constructor(_db, _stmt, _loggerCategory) { this._db = _db; this._stmt = _stmt; this._loggerCategory = _loggerCategory; this._removeListener = this._db.onBeforeClose.addListener(() => this.cleanup()); } // -------------------------------------------------------------------------------------------- // Lifecycle // -------------------------------------------------------------------------------------------- /** Disposes the currently held statement without returning it to the cache. * Invoked when the db signals it is closing (the statement cache is cleared on close, so the * checked-out statement must be disposed directly to avoid a use-after-free or double dispose). * @internal */ cleanup() { this._isDisposed = true; try { this._stmt[Symbol.dispose](); } catch (error) { logStatementCleanupError(this._loggerCategory, "Failed to dispose an ECSQL statement while closing its database.", error); } } /** Removes the database-close listener owned by this row executor. * @internal */ [Symbol.dispose]() { this._isDisposed = true; this._removeListener(); } // -------------------------------------------------------------------------------------------- // Core execution // -------------------------------------------------------------------------------------------- /** Prepare the statement and bind parameters in one step. * Call once during reader initialization — avoids the per-row `ensureStatementReady` check. * @param query - The ECSql text to prepare. * @param args - Optional bind parameters. * @throws IModelError on preparation or binding failure. * @internal */ prepareAndBind(query, args) { const prepResult = this.prepareStmt(query); if (!prepResult.isSuccessful) throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, prepResult.message ?? `Failed to prepare statement: ${query}`); if (args) { const bindResult = this.bindValues(args); if (!bindResult.isSuccessful) throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, bindResult.message ?? `Failed to bind values: ${query}`); } } /** Fast-path: step the cursor once and return row data directly. * * Returns the row data array if a row is available. * Returns `undefined` if the result set is exhausted (DONE). * * This avoids all intermediate object allocations (StepResult, RowDataResult, * DbRuntimeStats, DbQueryResponse) that the general `execute()` path creates per row. * * @param options - Native row-adaptor options (should be cached and reused across rows). * @throws IModelError on step failure or row extraction failure. * @internal */ stepNextRow(options) { if (this._isDisposed || !this._stmt.isPrepared) throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, statementNotPreparedMessage); const stepResult = this._stmt.step(); if (stepResult === core_bentley_1.DbResult.BE_SQLITE_ROW) return this._stmt.toRow(options).data; if (stepResult === core_bentley_1.DbResult.BE_SQLITE_DONE) return undefined; throw new core_common_1.IModelError(stepResult, `Step failed with code ${stepResult}`); } /** Get column metadata directly from the prepared statement. * Call once after `prepareAndBind` — the metadata does not change between rows. * @param options - Native row-adaptor options that influence property naming. * @returns Array of column metadata. * @internal */ fetchMetadata(options) { if (this._isDisposed || !this._stmt.isPrepared) throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, statementNotPreparedMessage); return this._stmt.getMetadata(options).properties; } // -------------------------------------------------------------------------------------------- // Execution phases // -------------------------------------------------------------------------------------------- /** Prepares the ECSql statement when the caller did not supply one from its cache. * @param ecsql - The ECSql text to prepare. * @returns An `OperationResult` indicating success or failure. * @internal */ prepareStmt(ecsql) { if (this._stmt.isPrepared) return { isSuccessful: true }; try { this._stmt.prepare(this._db[Symbols_1._nativeDb], ecsql); return { isSuccessful: true }; } catch (error) { this._canCacheStatement = false; try { this._stmt[Symbol.dispose](); } catch (disposeError) { logStatementCleanupError(this._loggerCategory, "Failed to dispose an ECSQL statement after preparation failed.", disposeError); } return { isSuccessful: false, message: error.message }; } } /** Binds the supplied parameter values to the prepared statement. * @param args - The parameter object to bind, or `undefined` when no parameters are needed. * @returns An `OperationResult` indicating success or failure. * @internal */ bindValues(args) { try { if (args === undefined) return { isSuccessful: true }; if (!this._stmt.isPrepared) return { isSuccessful: false, message: statementNotPreparedMessage }; this._stmt.bindParams(args); return { isSuccessful: true }; } catch (error) { return { isSuccessful: false, message: error.message }; } } } exports.ECSqlRowExecutor = ECSqlRowExecutor; //# sourceMappingURL=ECSqlRowExecutor.js.map