UNPKG

@itwin/core-backend

Version:
615 lines • 25.6 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 SQLite */ import { assert, BentleyError, DbResult, LRUMap } from "@itwin/core-bentley"; import { ECJsNames, IModelError } from "@itwin/core-common"; import { IModelNative } from "./internal/NativePlatform"; function checkBind(stat) { if (stat !== DbResult.BE_SQLITE_OK) throw new IModelError(stat, "SQLite Bind error"); } /** Executes SQLite SQL statements. * * A statement must be prepared before it can be executed, and it must be released when no longer needed. * See [IModelDb.withPreparedSqliteStatement]($backend) or * [ECDb.withPreparedSqliteStatement]($backend) for a convenient and * reliable way to prepare, execute, and then release a statement. * * A statement may contain parameters that must be filled in before use by calling [SqliteStatement.bindValue]($backend) * or [SqliteStatement.bindValues]($backend). * * Once prepared (and parameters are bound, if any), the statement is executed by calling [SqliteStatement.step]($backend). * In case of an **SQL SELECT** statement, the current row can be retrieved with [SqliteStatement.getRow]($backend) as * a whole, or with [SqliteStatement.getValue]($backend) when individual values are needed. * Alternatively, query results of an **SQL SELECT** statement can be stepped through by using * standard iteration syntax, such as `for of`. * * > Preparing a statement can be time-consuming. The best way to reduce the effect of this overhead is to cache and reuse prepared * > statements. A cached prepared statement may be used in different places in an app, as long as the statement is general enough. * > The key to making this strategy work is to phrase a statement in a general way and use placeholders to represent parameters that will vary on each use. * @public */ export class SqliteStatement { _sql; _stmt; _db; constructor(_sql) { this._sql = _sql; } get stmt() { return this._stmt; } get sql() { return this._sql; } /** Check if this statement has been prepared successfully or not */ get isPrepared() { return undefined !== this._stmt; } /** Prepare this statement prior to first use. * @param db The DgnDb or ECDb to prepare the statement against * @param sql The SQL statement string to prepare * @param logErrors Determine if errors are logged or not * @throws if the SQL statement cannot be prepared. Normally, prepare fails due to SQL syntax errors or references to tables or properties that do not exist. * The error.message property will provide details. */ prepare(db, logErrors = true) { if (this.isPrepared) throw new Error("SqliteStatement is already prepared"); this._db = db; this._stmt = new IModelNative.platform.SqliteStatement(); this._stmt.prepare(db, this._sql, logErrors); } /** Indicates whether the prepared statement makes no **direct* changes to the content of the file * or not. See [SQLite docs](https://www.sqlite.org/c3ref/stmt_readonly.html) for details. */ get isReadonly() { return this.stmt.isReadonly(); } /** Reset this statement so that the next call to step will return the first row, if any. */ reset() { this.stmt.reset(); } /** Call this function when finished with this statement. This releases the native resources held by the statement. */ [Symbol.dispose]() { if (this._stmt) { this._stmt.dispose(); // free native statement this._stmt = undefined; this._db = undefined; } } /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [Symbol.dispose] instead. */ dispose() { this[Symbol.dispose](); } /** * Call `step` on this statement and determine whether a new row is available. * Use this method only when this statement has been prepared with a SELECT statement. * @return true if a new row is available, false otherwise. * @throws if `step` returns anything other than BE_SQLITE_ROW or BE_SQLITE_DONE. */ nextRow() { const rc = this.step(); switch (rc) { case DbResult.BE_SQLITE_ROW: return true; case DbResult.BE_SQLITE_DONE: return false; } this.throwSqlError(rc); return false; // unreachable } throwSqlError(rc) { throw new SqliteStatement.DbError(rc === DbResult.BE_SQLITE_CONSTRAINT_FOREIGNKEY ? "ValueIsInUse" : rc === DbResult.BE_SQLITE_CONSTRAINT_UNIQUE ? "DuplicateValue" : "SqlLogicError", rc, `SQL error: ${this._db.getLastError()}`); } stepForWrite() { const rc = this.step(); if (rc !== DbResult.BE_SQLITE_DONE) this.throwSqlError(rc); } /** Binds a value to the specified SQL parameter. * The value must be of one of these types: * JavaScript Type | SQLite Type * --- | --- * undefined | NULL * boolean | INTEGER with true being bound as 1 and false as 0 * number | INTEGER if number is integral or REAL if number is not integral * string | TEXT * Uint8Array or ArrayBuffer | BLOB * [StringParam]($backend) where member **id** is set | INTEGER * [StringParam]($backend) where member **guid** is set | BLOB * * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param value Value to bind. * @throws [IModelError]($common) if the value is of an unsupported type or in * case of other binding errors. */ bindValue(parameter, value) { let stat; if (value === undefined || value === null) { stat = this.stmt.bindNull(parameter); } else if (typeof (value) === "number") { if (Number.isInteger(value)) stat = this.stmt.bindInteger(parameter, value); else stat = this.stmt.bindDouble(parameter, value); } else if (typeof (value) === "boolean") { stat = this.stmt.bindInteger(parameter, value ? 1 : 0); } else if (typeof (value) === "string") { stat = this.stmt.bindString(parameter, value); } else if (!!value.id) { stat = this.stmt.bindId(parameter, value.id); } else if (!!value.guid) { stat = this.stmt.bindGuid(parameter, value.guid); } else if (value instanceof Uint8Array) { stat = this.stmt.bindBlob(parameter, value); } else throw new IModelError(DbResult.BE_SQLITE_ERROR, `Parameter value ${value} is of an unsupported data type.`); if (stat !== DbResult.BE_SQLITE_OK) throw new IModelError(stat, "Error in bindValue"); } /** Bind an integer parameter * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val integer to bind. */ bindInteger(parameter, val) { checkBind(this.stmt.bindInteger(parameter, val)); } /** Bind an integer parameter if it is defined. Otherwise do nothing. * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val integer to bind. */ maybeBindInteger(parameter, val) { if (val !== undefined) this.bindInteger(parameter, val); } /** Bind a boolean parameter. * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val boolean to bind. */ bindBoolean(parameter, val) { this.bindInteger(parameter, val ? 1 : 0); } /** Bind a boolean parameter if it is defined. Otherwise do nothing. * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val boolean to bind. */ maybeBindBoolean(parameter, val) { if (val !== undefined) this.bindBoolean(parameter, val); } /** JSON.stringify a property value and bind the JSON string. * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val object to bind. * @internal */ bindProps(colIndex, val) { this.bindString(colIndex, JSON.stringify(val)); } /** JSON.stringify a property value if it is defined, and bind the JSON string. Otherwise do nothing. * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val object to bind. * @internal */ maybeBindProps(colIndex, val) { if (val !== undefined) this.bindProps(colIndex, val); } /** Bind a double parameter * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val double to bind. */ bindDouble(parameter, val) { checkBind(this.stmt.bindDouble(parameter, val)); } /** Bind a double parameter if it is defined. Otherwise do nothing. * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val double to bind. */ maybeBindDouble(parameter, val) { if (val !== undefined) this.bindDouble(parameter, val); } /** Bind a string parameter * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val string to bind. */ bindString(parameter, val) { checkBind(this.stmt.bindString(parameter, val)); } /** Bind a string parameter if it is defined. Otherwise do nothing. * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val string to bind. */ maybeBindString(parameter, val) { if (val !== undefined) this.bindString(parameter, val); } /** Bind an Id64String parameter as a 64-bit integer * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val Id to bind. */ bindId(parameter, id) { checkBind(this.stmt.bindId(parameter, id)); } /** Bind a Guid parameter * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val Guid to bind. */ bindGuid(parameter, guid) { checkBind(this.stmt.bindGuid(parameter, guid)); } /** Bind a blob parameter * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val blob to bind. */ bindBlob(parameter, blob) { checkBind(this.stmt.bindBlob(parameter, blob)); } /** Bind a blob parameter if it is defined. Otherwise do nothing. * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') * @param val blob to bind. */ maybeBindBlob(parameter, val) { if (val !== undefined) this.bindBlob(parameter, val); } /** Bind null to a parameter * @param parameter Index (1-based) or name of the parameter (including the initial ':', '@' or '$') */ bindNull(parameter) { checkBind(this.stmt.bindNull(parameter)); } /** Bind values to all parameters in the statement. * @param values The values to bind to the parameters. * Pass an *array* of values if the parameters are *positional*. * Pass an *object of the values keyed on the parameter name* for *named parameters*. * The values in either the array or object must match the respective types of the parameter. * See [[SqliteStatement.bindValue]] for details on the supported types. */ bindValues(values) { if (Array.isArray(values)) { for (let i = 0; i < values.length; i++) { const paramIndex = i + 1; const paramValue = values[i]; if (paramValue === undefined || paramValue === null) continue; this.bindValue(paramIndex, paramValue); } return; } for (const entry of Object.entries(values)) { const paramName = entry[0]; const paramValue = entry[1]; if (paramValue === undefined || paramValue === null) continue; this.bindValue(paramName, paramValue); } } /** Clear any bindings that were previously set on this statement. * @throws [IModelError]($common) in case of errors */ clearBindings() { const stat = this.stmt.clearBindings(); if (stat !== DbResult.BE_SQLITE_OK) throw new IModelError(stat, "Error in clearBindings"); } /** Step this statement to the next row. * * For **SQL SELECT** statements the method returns * - [DbResult.BE_SQLITE_ROW]($core-bentley) if the statement now points successfully to the next row. * - [DbResult.BE_SQLITE_DONE]($core-bentley) if the statement has no more rows. * - Error status in case of errors. * * For **SQL INSERT, UPDATE, DELETE** statements the method returns * - [DbResult.BE_SQLITE_DONE]($core-bentley) if the statement has been executed successfully. * - Error status in case of errors. */ step() { return this.stmt.step(); } /** Get the query result's column count (only for SQL SELECT statements). */ getColumnCount() { return this.stmt.getColumnCount(); } /** Get the value for the column at the given index in the query result. * @param columnIx Index of SQL column in query result (0-based) */ getValue(columnIx) { return new SqliteValue(this.stmt, columnIx); } /** Determine whether the value of the specified column is null * @param colIndex Index of SQL column in query result (0-based) */ isValueNull(colIndex) { return this.stmt.isValueNull(colIndex); } /** Get a size in bytes of a blob or text column * @param colIndex Index of SQL column in query result (0-based) */ getColumnBytes(colIndex) { return this.stmt.getColumnBytes(colIndex); } /** Get a value as a blob * @param colIndex Index of SQL column in query result (0-based) */ getValueBlob(colIndex) { return this.stmt.getValueBlob(colIndex); } /** Get the value as a blob, or undefined if it is null. * @param colIndex Index of SQL column in query result (0-based) */ getValueBlobMaybe(colIndex) { return this.isValueNull(colIndex) ? undefined : this.getValueBlob(colIndex); } /** Get a value as a double * @param colIndex Index of SQL column in query result (0-based) */ getValueDouble(colIndex) { return this.stmt.getValueDouble(colIndex); } /** Get the value as an double, or undefined if it is null. * @param colIndex Index of SQL column in query result (0-based) */ getValueDoubleMaybe(colIndex) { return this.isValueNull(colIndex) ? undefined : this.getValueDouble(colIndex); } /** Get a value as a integer * @param colIndex Index of SQL column in query result (0-based) */ getValueInteger(colIndex) { return this.stmt.getValueInteger(colIndex); } /** Get the value as an integer, or undefined if it is null. * @param colIndex Index of SQL column in query result (0-based) */ getValueIntegerMaybe(colIndex) { return this.isValueNull(colIndex) ? undefined : this.getValueInteger(colIndex); } /** Get a value as a string * @param colIndex Index of SQL column in query result (0-based) */ getValueString(colIndex) { return this.stmt.getValueString(colIndex); } /** Get the value as a string, or undefined if it is null. * @param colIndex Index of SQL column in query result (0-based) */ getValueStringMaybe(colIndex) { return this.isValueNull(colIndex) ? undefined : this.getValueString(colIndex); } /** Get a value as an Id * @param colIndex Index of SQL column in query result (0-based) */ getValueId(colIndex) { return this.stmt.getValueId(colIndex); } /** Get a value as a Guid * @param colIndex Index of SQL column in query result (0-based) */ getValueGuid(colIndex) { return this.stmt.getValueGuid(colIndex); } /** Get the value as a boolean. Returns `false` if the column is null. * @param colIndex Index of SQL column in query result (0-based) */ getValueBoolean(colIndex) { return this.isValueNull(colIndex) ? false : 0 !== this.getValueInteger(colIndex); } /** Get the value of a [julianday](https://www.sqlite.org/lang_datefunc.html) column as a JavaScript `Date`. * @param colIndex Index of SQL column in query result (0-based) * @beta */ getValueDate(colIndex) { return new Date((this.stmt.getValueDouble(colIndex) - 2440587.5) * 86400000); // conversion from julian day ms to unix epoch ms } /** Get the value as a "props" JSON string, then parse it and return the object * @param colIndex Index of SQL column in query result (0-based) * @internal */ getProps(colIndex) { return JSON.parse(this.getValueString(colIndex)); } /** Get the value as a "props" JSON string, then parse it and return the object. * If the column is null, return undefined. * @param colIndex Index of SQL column in query result (0-based) * @internal */ getPropsMaybe(colIndex) { return this.isValueNull(colIndex) ? undefined : this.getProps(colIndex); } /** Get the current row. * The returned row is formatted as JavaScript object where every SELECT clause item becomes a property in the JavaScript object. * * The SQL select clause item's name becomes the member name of the JavaScript object, **with the first character lowered**. * * SQLite Type | JavaScript Type * --- | --- * [SqliteValueType.Null]($backend) | undefined * [SqliteValueType.Integer]($backend) | number * [SqliteValueType.Double]($backend) | number * [SqliteValueType.String]($backend) | string * [SqliteValueType.Blob]($backend) | Uint8Array */ getRow() { const colCount = this.getColumnCount(); const row = {}; const duplicatePropNames = new Map(); for (let i = 0; i < colCount; i++) { const sqliteValue = this.getValue(i); if (!sqliteValue.isNull) { const propName = SqliteStatement.determineResultRowPropertyName(duplicatePropNames, sqliteValue); let val; switch (sqliteValue.type) { case SqliteValueType.Blob: val = sqliteValue.getBlob(); break; case SqliteValueType.Double: val = sqliteValue.getDouble(); break; case SqliteValueType.Integer: val = sqliteValue.getInteger(); break; case SqliteValueType.String: val = sqliteValue.getString(); break; default: throw new Error("Unsupported SqliteValueType"); } Object.defineProperty(row, propName, { enumerable: true, configurable: true, writable: true, value: val }); } } return row; } static determineResultRowPropertyName(duplicatePropNames, sqliteValue) { let jsName = ECJsNames.toJsName(sqliteValue.columnName); // now check duplicates. If there are, append a numeric suffix to the duplicates let suffix = duplicatePropNames.get(jsName); if (suffix === undefined) duplicatePropNames.set(jsName, 0); else { suffix++; duplicatePropNames.set(jsName, suffix); jsName += `_${suffix}`; } return jsName; } /** Calls step when called as an iterator. */ next() { return DbResult.BE_SQLITE_ROW === this.step() ? { done: false, value: this.getRow() } : { done: true, value: undefined }; } /** The iterator that will step through the results of this statement. */ [Symbol.iterator]() { return this; } } /** Data type of a value in in an SQLite SQL query result. * See also: * - [SqliteValue]($backend) * - [SqliteStatement]($backend) * - [SqliteStatement.getValue]($backend) * @public */ export var SqliteValueType; (function (SqliteValueType) { // do not change the values of that enum. It must correspond to the respective // enum DbValueType in the native BeSQLite API. SqliteValueType[SqliteValueType["Integer"] = 1] = "Integer"; SqliteValueType[SqliteValueType["Double"] = 2] = "Double"; SqliteValueType[SqliteValueType["String"] = 3] = "String"; SqliteValueType[SqliteValueType["Blob"] = 4] = "Blob"; SqliteValueType[SqliteValueType["Null"] = 5] = "Null"; })(SqliteValueType || (SqliteValueType = {})); /** Value of a column in a row of an SQLite SQL query result. * See also: * - [SqliteStatement]($backend) * - [SqliteStatement.getValue]($backend) * @public */ export class SqliteValue { _stmt; _colIndex; constructor(stmt, colIndex) { this._stmt = stmt; this._colIndex = colIndex; } /** Indicates whether the value is NULL or not. */ get isNull() { return this._stmt.isValueNull(this._colIndex); } /** Gets the data type of the value. */ get type() { return this._stmt.getColumnType(this._colIndex); } /** Gets the name of the column of the value. */ get columnName() { return this._stmt.getColumnName(this._colIndex); } /** Gets the SqlValue as JavaScript value. * * SQLite Type | JavaScript Type * --- | --- * [SqliteValueType.Null]($backend) | undefined * [SqliteValueType.Integer]($backend) | number * [SqliteValueType.Double]($backend) | number * [SqliteValueType.String]($backend) | string * [SqliteValueType.Blob]($backend) | Uint8Array */ get value() { switch (this.type) { case SqliteValueType.Null: return undefined; case SqliteValueType.Blob: return this.getBlob(); case SqliteValueType.Double: return this.getDouble(); case SqliteValueType.Integer: return this.getInteger(); case SqliteValueType.String: return this.getString(); default: throw new Error("Unhandled SqliteValueType"); } } /** Get the value as Blob */ getBlob() { return this._stmt.getValueBlob(this._colIndex); } /** Get the value as a double value */ getDouble() { return this._stmt.getValueDouble(this._colIndex); } /** Get the value as a integer value */ getInteger() { return this._stmt.getValueInteger(this._colIndex); } /** Get the value as a string value */ getString() { return this._stmt.getValueString(this._colIndex); } /** Get the value as an Id value */ getId() { return this._stmt.getValueId(this._colIndex); } /** Get the value as a Guid value */ getGuid() { return this._stmt.getValueGuid(this._colIndex); } } /** A cache for previously prepared SqliteStatements. * It only holds Statements after they are no longer in use, resetting and clearing their bindings before saving them. * When a request to use a statement from the cache is made, it is first removed from the cache. * @internal */ export class StatementCache { _cache; constructor(maxCount = 40) { this._cache = new LRUMap(maxCount); } get size() { return this._cache.size; } addOrDispose(stmt) { assert(stmt.isPrepared); const existing = this._cache.get(stmt.sql); if (existing !== undefined) { stmt.dispose(); // we already have a statement with this sql cached, we can't save another one so just dispose it return; } if (this._cache.size >= this._cache.limit) { const oldest = this._cache.shift(); oldest[1].dispose(); } stmt.reset(); stmt.clearBindings(); this._cache.set(stmt.sql, stmt); } findAndRemove(sql) { return this._cache.delete(sql); } clear() { this._cache.forEach((stmt) => stmt.dispose()); this._cache.clear(); } } /** @public */ (function (SqliteStatement) { class DbError extends BentleyError { /** A string that indicates the type of problem that caused the exception. */ errorId; /** @internal */ constructor(errorId, errNum, message) { super(errNum, message); this.errorId = errorId; } } SqliteStatement.DbError = DbError; })(SqliteStatement || (SqliteStatement = {})); //# sourceMappingURL=SqliteStatement.js.map