UNPKG

@itwin/core-common

Version:

iTwin.js components common to frontend and backend

410 lines • 14.7 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 iModels */ import { Base64EncodedString } from "./Base64EncodedString"; import { DbQueryError, DbRequestKind, DbResponseStatus, DbValueFormat, QueryBinder, QueryOptionsBuilder, QueryRowFormat, } from "./ConcurrentQuery"; /** @public */ export class PropertyMetaDataMap { properties; _byPropName = new Map(); _byJsonName = new Map(); _byNoCase = new Map(); constructor(properties) { this.properties = properties; for (const property of this.properties) { property.extendType = property.extendedType !== undefined ? property.extendedType : ""; // eslint-disable-line @typescript-eslint/no-deprecated property.extendedType = property.extendedType === "" ? undefined : property.extendedType; this._byPropName.set(property.name, property.index); this._byJsonName.set(property.jsonName, property.index); this._byNoCase.set(property.name.toLowerCase(), property.index); this._byNoCase.set(property.jsonName.toLowerCase(), property.index); } } get length() { return this.properties.length; } [Symbol.iterator]() { return this.properties[Symbol.iterator](); } findByName(name) { const index = this._byPropName.get(name); if (typeof index === "number") { return this.properties[index]; } return undefined; } findByJsonName(name) { const index = this._byJsonName.get(name); if (typeof index === "number") { return this.properties[index]; } return undefined; } findByNoCase(name) { const index = this._byNoCase.get(name.toLowerCase()); if (typeof index === "number") { return this.properties[index]; } return undefined; } } /** * Execute ECSQL statements and read the results. * * The query results are returned one row at a time. The format of the row is dictated by the * [[QueryOptions.rowFormat]] specified in the `options` parameter of the constructed ECSqlReader object. Defaults to * [[QueryRowFormat.UseECSqlPropertyIndexes]] when no `rowFormat` is defined. * * There are three primary ways to interact with and read the results: * - Stream them using ECSqlReader as an asynchronous iterator. * - Iterator over them manually using [[ECSqlReader.step]]. * - Capture all of the results at once in an array using [[QueryRowProxy.toArray]]. * * @see * - [ECSQL Overview]($docs/learning/backend/ExecutingECSQL) * - [ECSQL Row Formats]($docs/learning/ECSQLRowFormat) for more details on how rows are formatted. * - [ECSQL Code Examples]($docs/learning/ECSQLCodeExamples#iterating-over-query-results) for examples of each * of the above ways of interacting with ECSqlReader. * * @note When iterating over the results, the current row will be a [[QueryRowProxy]] object. To get the row as a basic * JavaScript object, call [[QueryRowProxy.toRow]] on it. * @public */ export class ECSqlReader { _executor; query; static _maxRetryCount = 10; _localRows = []; _localOffset = 0; _globalOffset = -1; _globalCount = -1; _done = false; _globalDone = false; _props = new PropertyMetaDataMap([]); _param = new QueryBinder().serialize(); _lockArgs = false; _stats = { backendCpuTime: 0, backendTotalTime: 0, backendMemUsed: 0, backendRowsReturned: 0, totalTime: 0, retryCount: 0, prepareTime: 0 }; _options = new QueryOptionsBuilder().getOptions(); _rowProxy = new Proxy(this, { get: (target, key) => { if (typeof key === "string") { const idx = Number.parseInt(key, 10); if (!Number.isNaN(idx)) { return target.getRowInternal()[idx]; } const prop = target._props.findByNoCase(key); if (prop) { return target.getRowInternal()[prop.index]; } if (key === "getMetaData") { return () => target._props.properties; } if (key === "toRow") { return () => target.formatCurrentRow(true); } if (key === "toArray") { return () => this.getRowInternal(); } } return undefined; }, has: (target, p) => { return !target._props.findByNoCase(p); }, ownKeys: (target) => { const keys = []; for (const prop of target._props) { keys.push(prop.name); } return keys; }, }); /** * @internal */ constructor(_executor, query, param, options) { this._executor = _executor; this.query = query; if (query.trim().length === 0) { throw new Error("expecting non-empty ecsql statement"); } if (param) { this._param = param.serialize(); } this.reset(options); } static replaceBase64WithUint8Array(row) { for (const key of Object.keys(row)) { const val = row[key]; if (typeof val === "string") { if (Base64EncodedString.hasPrefix(val)) { row[key] = Base64EncodedString.toUint8Array(val); } } else if (typeof val === "object" && val !== null) { this.replaceBase64WithUint8Array(val); } } } setParams(param) { if (this._lockArgs) { throw new Error("call resetBindings() before setting or changing parameters"); } this._param = param.serialize(); } reset(options) { if (options) { this._options = options; } this._props = new PropertyMetaDataMap([]); this._localRows = []; this._globalDone = false; this._globalOffset = 0; this._globalCount = -1; if (typeof this._options.rowFormat === "undefined") this._options.rowFormat = QueryRowFormat.UseECSqlPropertyIndexes; if (this._options.limit) { if (typeof this._options.limit.offset === "number" && this._options.limit.offset > 0) this._globalOffset = this._options.limit.offset; if (typeof this._options.limit.count === "number" && this._options.limit.count > 0) this._globalCount = this._options.limit.count; } this._done = false; } /** * Get the current row from the query result. The current row is the one most recently stepped-to * (by step() or during iteration). * * Each value from the row can be accessed by index or by name. * * The format of the row is dictated by the [[QueryOptions.rowFormat]] specified in the `options` parameter of the * constructed ECSqlReader object. * * @see * - [[QueryRowFormat]] * - [ECSQL Row Formats]($docs/learning/ECSQLRowFormat) * * @note The current row is be a [[QueryRowProxy]] object. To get the row as a basic JavaScript object, call * [[QueryRowProxy.toRow]] on it. * * @example * ```ts * const reader = iModel.createQueryReader("SELECT ECInstanceId FROM bis.Element"); * while (await reader.step()) { * // Both lines below print the same value * console.log(reader.current[0]); * console.log(reader.current.ecinstanceid); * } * ``` * * @return The current row as a [[QueryRowProxy]]. */ get current() { return this._rowProxy; } /** * Clear all bindings. */ resetBindings() { this._param = new QueryBinder().serialize(); this._lockArgs = false; } /** * Returns if there are more rows available. * * @returns `true` if all rows have been stepped through already.<br/> * `false` if there are any yet unaccessed rows. */ get done() { return this._done; } /** * @internal */ getRowInternal() { if (this._localRows.length <= this._localOffset) throw new Error("no current row"); return this._localRows[this._localOffset]; } /** * Get performance-related statistics for the current query. */ get stats() { return this._stats; } /** * */ async readRows() { if (this._globalDone) { return []; } this._lockArgs = true; this._globalOffset += this._localRows.length; this._globalCount -= this._localRows.length; if (this._globalCount === 0) { return []; } const valueFormat = this._options.rowFormat === QueryRowFormat.UseJsPropertyNames ? DbValueFormat.JsNames : DbValueFormat.ECSqlNames; const request = { ...this._options, kind: DbRequestKind.ECSql, valueFormat, query: this.query, args: this._param, }; request.includeMetaData = this._props.length > 0 ? false : true; request.limit = { offset: this._globalOffset, count: this._globalCount < 1 ? -1 : this._globalCount }; const resp = await this.runWithRetry(request); this._globalDone = resp.status === DbResponseStatus.Done; if (this._props.length === 0 && resp.meta.length > 0) { this._props = new PropertyMetaDataMap(resp.meta); } for (const row of resp.data) { ECSqlReader.replaceBase64WithUint8Array(row); } return resp.data; } /** * @internal */ async runWithRetry(request) { const needRetry = (rs) => (rs.status === DbResponseStatus.Partial || rs.status === DbResponseStatus.QueueFull || rs.status === DbResponseStatus.Timeout || rs.status === DbResponseStatus.ShuttingDown) && (rs.data === undefined || rs.data.length === 0); const updateStats = (rs) => { this._stats.backendCpuTime += rs.stats.cpuTime; this._stats.backendTotalTime += rs.stats.totalTime; this._stats.backendMemUsed += rs.stats.memUsed; this._stats.prepareTime += rs.stats.prepareTime; this._stats.backendRowsReturned += (rs.data === undefined) ? 0 : rs.data.length; }; const execQuery = async (req) => { const startTime = Date.now(); const rs = await this._executor.execute(req); this.stats.totalTime += (Date.now() - startTime); return rs; }; let retry = ECSqlReader._maxRetryCount; let resp = await execQuery(request); DbQueryError.throwIfError(resp, request); while (--retry > 0 && needRetry(resp)) { resp = await execQuery(request); this._stats.retryCount += 1; if (needRetry(resp)) { updateStats(resp); } } if (retry === 0 && needRetry(resp)) { throw new Error("query too long to execute or server is too busy"); } updateStats(resp); return resp; } /** * @internal */ formatCurrentRow(onlyReturnObject = false) { if (!onlyReturnObject && this._options.rowFormat === QueryRowFormat.UseECSqlPropertyIndexes) { return this.getRowInternal(); } const formattedRow = {}; for (const prop of this._props) { const propName = this._options.rowFormat === QueryRowFormat.UseJsPropertyNames ? prop.jsonName : prop.name; const val = this.getRowInternal()[prop.index]; if (typeof val !== "undefined" && val !== null) { Object.defineProperty(formattedRow, propName, { value: val, enumerable: true, }); } } return formattedRow; } /** * Get the metadata for each column in the query result. * * @returns An array of [[QueryPropertyMetaData]]. */ async getMetaData() { if (this._props.length === 0) { await this.fetchRows(); } return this._props.properties; } /** * */ async fetchRows() { this._localOffset = -1; this._localRows = await this.readRows(); if (this._localRows.length === 0) { this._done = true; } } /** * Step to the next row of the query result. * * @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. */ async step() { if (this._done) { return false; } const cachedRows = this._localRows.length; if (this._localOffset < cachedRows - 1) { ++this._localOffset; } else { await this.fetchRows(); this._localOffset = 0; return !this._done; } return true; } /** * Get all remaining rows from the query result. * * @returns An array of all remaining rows from the query result. */ async toArray() { const rows = []; while (await this.step()) { rows.push(this.formatCurrentRow()); } return rows; } /** * Accessor for using ECSqlReader as an asynchronous iterator. * * @returns An asynchronous iterator over the rows returned by the executed ECSQL query. */ [Symbol.asyncIterator]() { 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. */ async next() { if (await this.step()) { return { done: false, value: this.current, }; } else { return { done: true, value: this.current, }; } } } //# sourceMappingURL=ECSqlReader.js.map