@itwin/core-common
Version:
iTwin.js components common to frontend and backend
410 lines • 14.7 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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