@itwin/core-backend
Version:
iTwin.js backend components
615 lines • 25.6 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 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