UNPKG

@itwin/core-backend

Version:
188 lines 7.42 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module ECSQL */ import { DbValueFormat, ECSqlReaderBase, IModelError, PropertyMetaDataMap, QueryBinder, QueryOptionsBuilder, QueryRowFormat, } from "@itwin/core-common"; import { DbResult } from "@itwin/core-bentley"; /** * Execute ECSQL statements synchronously and read the results one row at a time. * * This is the synchronous counterpart of [[ECSqlReader]] from `@itwin/core-common`. * It uses [[ECSqlRowExecutor]] directly — each call to [[step]] fetches exactly one row * with no internal caching, paging, or offset tracking. * * The query results are returned one row at a time. The format of the row is dictated by the * `rowFormat` specified in the `options` parameter of the constructed ECSqlSyncReader object. * Defaults to [[QueryRowFormat.UseECSqlPropertyIndexes]] when no `rowFormat` is defined. * * There are three primary ways to interact with and read the results: * - Iterate using ECSqlSyncReader as a synchronous iterator. * - Step manually using [[ECSqlSyncReader.step]]. * - Capture all results at once using [[ECSqlSyncReader.toArray]]. * * @note When iterating over the results, the current row is a [[QueryRowProxy]] object. * To get the row as a basic JavaScript object, call [[QueryRowProxy.toRow]] on it. * @beta */ export class ECSqlSyncReader extends ECSqlReaderBase { _executor; query; _currentRow; _options; /** Cached native row-adaptor options — built once and reused for every row. */ _cachedRowOptions; /** * @internal */ constructor(_executor, query, param, options) { const resolvedOptions = options ?? new QueryOptionsBuilder().getOptions(); if (typeof resolvedOptions.rowFormat === "undefined") resolvedOptions.rowFormat = QueryRowFormat.UseECSqlPropertyIndexes; super(resolvedOptions.rowFormat); this._executor = _executor; this.query = query; if (query.trim().length === 0) { throw new IModelError(DbResult.BE_SQLITE_ERROR, "expecting non-empty ecsql statement"); } this._options = resolvedOptions; const serializedParam = param ? param.serialize() : new QueryBinder().serialize(); // Prepare the statement and bind parameters once — avoids per-row ensureStatementReady overhead. const args = Object.keys(serializedParam).length > 0 ? serializedParam : undefined; this._executor.prepareAndBind(this.query, args); // Build and cache the native row-adaptor options — reused for every step call. // eslint-disable-next-line @typescript-eslint/no-deprecated const valueFormat = this._options.rowFormat === QueryRowFormat.UseJsPropertyNames ? DbValueFormat.JsNames : DbValueFormat.ECSqlNames; this._cachedRowOptions = { abbreviateBlobs: this._options.abbreviateBlobs ?? false, // eslint-disable-next-line @typescript-eslint/no-deprecated classIdsToClassNames: this._options.convertClassIdsToClassNames ?? false, useJsName: valueFormat === DbValueFormat.JsNames, doNotConvertClassIdsToClassNamesWhenAliased: true, }; this._currentRow = undefined; } /** * @internal */ getRowInternal() { if (!this._currentRow) throw new IModelError(DbResult.BE_SQLITE_ERROR, "no current row"); return this._currentRow; } /** * Reads a single row from the executor. Returns the row data or `undefined` when the * result set is exhausted. * * Uses the fast-path `stepNextRow` method on the executor, which calls `stmt.step()` * and `stmt.toRow()` directly — no intermediate request/response objects are allocated per row. * @internal */ readRow() { if (this._done) { return undefined; } // Fetch metadata once on the first call. if (this._props.length === 0) { this._props = new PropertyMetaDataMap(this._executor.fetchMetadata(this._cachedRowOptions)); } return this.stepWithRetry(); } /** * Steps the cursor with retry logic for transient busy/interrupt conditions. * Returns the row data array, or `undefined` when the result set is exhausted. * @internal */ stepWithRetry() { const result = this._executor.stepNextRow(this._cachedRowOptions); if (result) { ECSqlSyncReader.replaceBase64WithUint8Array(result); return result; } return undefined; // undefined (done) → undefined } /** * Get the metadata for each column in the query result. * * If metadata has not been retrieved yet, fetches it directly from the prepared * statement. If a step has not yet occurred, executes a single step to ensure * the statement cursor is positioned, preserving the row for the next [[step]] call. * * @returns An array of [[QueryPropertyMetaData]]. * @beta */ getMetaData() { if (this._props.length === 0) { this._props = new PropertyMetaDataMap(this._executor.fetchMetadata(this._cachedRowOptions)); } return this._props.properties; } /** * Step to the next row of the query result. * * Each call executes exactly one step on the underlying statement — there is no * internal row caching or offset tracking. * * @returns `true` if a row can be read from `current`.<br/> * `false` if there are no more rows; i.e., all rows have been stepped through already. * @beta */ step() { if (this._done) { return false; } this._currentRow = this.readRow(); if (!this._currentRow) { this._done = true; return false; } return true; } /** * Get all remaining rows from the query result. * * @returns An array of all remaining rows from the query result. * @beta */ toArray() { const rows = []; while (this.step()) { rows.push(this.formatCurrentRow()); } return rows; } /** * Accessor for using ECSqlSyncReader as a synchronous iterator. * * @returns A synchronous iterator over the rows returned by the executed ECSQL query. * @beta */ [Symbol.iterator]() { return this; } /** * Calls step when called as an iterator. * * Returns the row alongside a `done` boolean to indicate if there are any more rows for an iterator to step to. * * @returns An object with the keys: `value` which contains the row and `done` which contains a boolean. * @beta */ next() { if (this.step()) { return { done: false, value: this.current, }; } else { return { done: true, value: this.current, }; } } } //# sourceMappingURL=ECSqlSyncReader.js.map