@itwin/core-backend
Version:
iTwin.js backend components
414 lines • 19.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ECDb = exports.ECDbOpenMode = void 0;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module ECDb
*/
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const BackendLoggerCategory_1 = require("./BackendLoggerCategory");
const ConcurrentQuery_1 = require("./ConcurrentQuery");
const ECSqlStatement_1 = require("./ECSqlStatement");
const NativePlatform_1 = require("./internal/NativePlatform");
const SqliteStatement_1 = require("./SqliteStatement");
const Symbols_1 = require("./internal/Symbols");
const loggerCategory = BackendLoggerCategory_1.BackendLoggerCategory.ECDb;
/** Modes for how to open [ECDb]($backend) files.
* @public
*/
var ECDbOpenMode;
(function (ECDbOpenMode) {
ECDbOpenMode[ECDbOpenMode["Readonly"] = 0] = "Readonly";
ECDbOpenMode[ECDbOpenMode["ReadWrite"] = 1] = "ReadWrite";
/** Opens the file read-write and upgrades the file if necessary to the latest file format version. */
ECDbOpenMode[ECDbOpenMode["FileUpgrade"] = 2] = "FileUpgrade";
})(ECDbOpenMode || (exports.ECDbOpenMode = ECDbOpenMode = {}));
/** An ECDb file
* @public
*/
class ECDb {
_nativeDb;
// eslint-disable-next-line @typescript-eslint/no-deprecated
_statementCache = new SqliteStatement_1.StatementCache();
_sqliteStatementCache = new SqliteStatement_1.StatementCache();
/** only for tests
* @internal
*/
resetSqliteCache(size) {
this._sqliteStatementCache.clear();
this._sqliteStatementCache = new SqliteStatement_1.StatementCache(size);
}
constructor() {
this._nativeDb = new NativePlatform_1.IModelNative.platform.ECDb();
}
/** Call this function when finished with this ECDb object. This releases the native resources held by the
* ECDb object.
*/
[Symbol.dispose]() {
if (!this._nativeDb)
return;
this.closeDb();
this._nativeDb.dispose();
this._nativeDb = undefined;
}
/**
* Attach an iModel file to this connection and load and register its schemas.
* @note There are some reserve tablespace names that cannot be used. They are 'main', 'schema_sync_db', 'ecchange' & 'temp'
* @param fileName IModel file name
* @param alias identifier for the attached file. This identifer is used to access schema from the attached file. e.g. if alias is 'abc' then schema can be accessed using 'abc.MySchema.MyClass'
*/
attachDb(fileName, alias) {
if (alias.toLowerCase() === "main" || alias.toLowerCase() === "schema_sync_db" || alias.toLowerCase() === "ecchange" || alias.toLowerCase() === "temp") {
throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, "Reserved tablespace name cannot be used");
}
this[Symbols_1._nativeDb].attachDb(fileName, alias);
}
/**
* Detach the attached file from this connection. The attached file is closed and its schemas are unregistered.
* @note There are some reserve tablespace names that cannot be used. They are 'main', 'schema_sync_db', 'ecchange' & 'temp'
* @param alias identifer that was used in the call to [[attachDb]]
*/
detachDb(alias) {
if (alias.toLowerCase() === "main" || alias.toLowerCase() === "schema_sync_db" || alias.toLowerCase() === "ecchange" || alias.toLowerCase() === "temp") {
throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, "Reserved tablespace name cannot be used");
}
this.clearStatementCache();
this[Symbols_1._nativeDb].detachDb(alias);
}
/** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [Symbol.dispose] instead. */
dispose() {
this[Symbol.dispose]();
}
/** Create an ECDb
* @param pathName The path to the ECDb file to create.
* @throws [IModelError]($common) if the operation failed.
*/
createDb(pathName) {
const status = this[Symbols_1._nativeDb].createDb(pathName);
if (status !== core_bentley_1.DbResult.BE_SQLITE_OK)
throw new core_common_1.IModelError(status, "Failed to created ECDb");
}
/** Open the ECDb.
* @param pathName The path to the ECDb file to open
* @param openMode Open mode
* @throws [IModelError]($common) if the operation failed.
*/
openDb(pathName, openMode = ECDbOpenMode.Readonly) {
const nativeOpenMode = openMode === ECDbOpenMode.Readonly ? core_bentley_1.OpenMode.Readonly : core_bentley_1.OpenMode.ReadWrite;
const tryUpgrade = openMode === ECDbOpenMode.FileUpgrade;
const status = this[Symbols_1._nativeDb].openDb(pathName, nativeOpenMode, tryUpgrade);
if (status !== core_bentley_1.DbResult.BE_SQLITE_OK)
throw new core_common_1.IModelError(status, "Failed to open ECDb");
}
/** Returns true if the ECDb is open */
get isOpen() { return this[Symbols_1._nativeDb].isOpen(); }
/** Close the Db after saving any uncommitted changes.
* @throws [IModelError]($common) if the database is not open.
*/
closeDb() {
this._statementCache.clear();
this._sqliteStatementCache.clear();
this[Symbols_1._nativeDb].closeDb();
}
/** @internal use to test statement caching */
clearStatementCache() {
this._statementCache.clear();
}
/** @internal use to test statement caching */
getCachedStatementCount() {
return this._statementCache.size;
}
/** Commit the outermost transaction, writing changes to the file. Then, restart the transaction.
* @param changesetName The name of the operation that generated these changes.
* @throws [IModelError]($common) if the database is not open or if the operation failed.
*/
saveChanges(changesetName) {
const status = this[Symbols_1._nativeDb].saveChanges(changesetName);
if (status !== core_bentley_1.DbResult.BE_SQLITE_OK)
throw new core_common_1.IModelError(status, "Failed to save changes");
}
/** Abandon (cancel) the outermost transaction, discarding all changes since last save. Then, restart the transaction.
* @throws [IModelError]($common) if the database is not open or if the operation failed.
*/
abandonChanges() {
const status = this[Symbols_1._nativeDb].abandonChanges();
if (status !== core_bentley_1.DbResult.BE_SQLITE_OK)
throw new core_common_1.IModelError(status, "Failed to abandon changes");
}
/** Import a schema.
*
* If the import was successful, the database is automatically saved to disk.
* @param pathName Path to ECSchema XML file to import.
* @throws [IModelError]($common) if the database is not open or if the operation failed.
*/
importSchema(pathName) {
const status = this[Symbols_1._nativeDb].importSchema(pathName);
if (status !== core_bentley_1.DbResult.BE_SQLITE_OK) {
core_bentley_1.Logger.logError(loggerCategory, `Failed to import schema from '${pathName}'.`);
throw new core_common_1.IModelError(status, `Failed to import schema from '${pathName}'.`);
}
}
/**
* Returns the full schema for the input name.
* @param name The name of the schema e.g. 'ECDbMeta'
* @returns The SchemaProps for the requested schema
* @throws if the schema can not be found or loaded.
*/
getSchemaProps(name) {
return this[Symbols_1._nativeDb].getSchemaProps(name);
}
/**
* Use a prepared ECSQL statement, potentially from the statement cache. If the requested statement doesn't exist
* in the statement cache, a new statement is prepared. After the callback completes, the statement is reset and saved
* in the statement cache so it can be reused in the future. Use this method for ECSQL statements that will be
* reused often and are expensive to prepare. The statement cache holds the most recently used statements, discarding
* the oldest statements as it fills. For statements you don't intend to reuse, instead use [[withStatement]].
* @param sql The SQLite SQL statement to execute
* @param callback the callback to invoke on the prepared statement
* @param logErrors Determines if error will be logged if statement fail to prepare
* @returns the value returned by `callback`.
* @see [[withWriteStatement]]
* @beta
*/
withCachedWriteStatement(ecsql, callback, logErrors = true) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const stmt = this._statementCache.findAndRemove(ecsql) ?? this.prepareStatement(ecsql, logErrors);
const release = () => this._statementCache.addOrDispose(stmt);
try {
const val = callback(new ECSqlStatement_1.ECSqlWriteStatement(stmt));
if (val instanceof Promise) {
val.then(release, release);
}
else {
release();
}
return val;
}
catch (err) {
release();
throw err;
}
}
/**
* Prepared and execute a callback on an ECSQL statement. After the callback completes the statement is disposed.
* Use this method for ECSQL statements are either not expected to be reused, or are not expensive to prepare.
* For statements that will be reused often, instead use [[withPreparedStatement]].
* @param sql The SQLite SQL statement to execute
* @param callback the callback to invoke on the prepared statement
* @param logErrors Determines if error will be logged if statement fail to prepare
* @returns the value returned by `callback`.
* @see [[withCachedWriteStatement]]
* @beta
*/
withWriteStatement(ecsql, callback, logErrors = true) {
const stmt = this.prepareWriteStatement(ecsql, logErrors);
const release = () => { };
try {
const val = callback(stmt);
if (val instanceof Promise) {
val.then(release, release);
}
else {
release();
}
return val;
}
catch (err) {
release();
throw err;
}
}
/** Prepare an ECSQL statement.
* @param ecsql The ECSQL statement to prepare
* @param logErrors Determines if error will be logged if statement fail to prepare
* @throws [IModelError]($common) if there is a problem preparing the statement.
* @beta
*/
prepareWriteStatement(ecsql, logErrors = true) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
return new ECSqlStatement_1.ECSqlWriteStatement(this.prepareStatement(ecsql, logErrors));
}
/**
* Use a prepared ECSQL statement, potentially from the statement cache. If the requested statement doesn't exist
* in the statement cache, a new statement is prepared. After the callback completes, the statement is reset and saved
* in the statement cache so it can be reused in the future. Use this method for ECSQL statements that will be
* reused often and are expensive to prepare. The statement cache holds the most recently used statements, discarding
* the oldest statements as it fills. For statements you don't intend to reuse, instead use [[withStatement]].
* @param sql The SQLite SQL statement to execute
* @param callback the callback to invoke on the prepared statement
* @param logErrors Determines if error will be logged if statement fail to prepare
* @returns the value returned by `callback`.
* @see [[withStatement]]
* @public
* @deprecated in 4.11 - will not be removed until after 2026-06-13. Use [[createQueryReader]] for SELECT statements and [[withCachedWriteStatement]] for INSERT/UPDATE/DELETE instead.
*/
// eslint-disable-next-line @typescript-eslint/no-deprecated
withPreparedStatement(ecsql, callback, logErrors = true) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const stmt = this._statementCache.findAndRemove(ecsql) ?? this.prepareStatement(ecsql, logErrors);
const release = () => this._statementCache.addOrDispose(stmt);
try {
const val = callback(stmt);
if (val instanceof Promise) {
val.then(release, release);
}
else {
release();
}
return val;
}
catch (err) {
release();
throw err;
}
}
/**
* Prepared and execute a callback on an ECSQL statement. After the callback completes the statement is disposed.
* Use this method for ECSQL statements are either not expected to be reused, or are not expensive to prepare.
* For statements that will be reused often, instead use [[withPreparedStatement]].
* @param sql The SQLite SQL statement to execute
* @param callback the callback to invoke on the prepared statement
* @param logErrors Determines if error will be logged if statement fail to prepare
* @returns the value returned by `callback`.
* @see [[withPreparedStatement]]
* @public
* @deprecated in 4.11 - will not be removed until after 2026-06-13. Use [[createQueryReader]] for SELECT statements and [[withWriteStatement]] for INSERT/UPDATE/DELETE instead.
*/
// eslint-disable-next-line @typescript-eslint/no-deprecated
withStatement(ecsql, callback, logErrors = true) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const stmt = this.prepareStatement(ecsql, logErrors);
const release = () => stmt[Symbol.dispose]();
try {
const val = callback(stmt);
if (val instanceof Promise) {
val.then(release, release);
}
else {
release();
}
return val;
}
catch (err) {
release();
throw err;
}
}
/** Prepare an ECSQL statement.
* @param ecsql The ECSQL statement to prepare
* @param logErrors Determines if error will be logged if statement fail to prepare
* @throws [IModelError]($common) if there is a problem preparing the statement.
* @deprecated in 4.11 - will not be removed until after 2026-06-13. Use [[prepareWriteStatement]] when preparing an INSERT/UPDATE/DELETE statement or [[createQueryReader]] to execute a SELECT statement.
*/
// eslint-disable-next-line @typescript-eslint/no-deprecated
prepareStatement(ecsql, logErrors = true) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const stmt = new ECSqlStatement_1.ECSqlStatement();
stmt.prepare(this[Symbols_1._nativeDb], ecsql, logErrors);
return stmt;
}
/**
* Use a prepared SQL statement, potentially from the statement cache. If the requested statement doesn't exist
* in the statement cache, a new statement is prepared. After the callback completes, the statement is reset and saved
* in the statement cache so it can be reused in the future. Use this method for SQL statements that will be
* reused often and are expensive to prepare. The statement cache holds the most recently used statements, discarding
* the oldest statements as it fills. For statements you don't intend to reuse, instead use [[withSqliteStatement]].
* @param sql The SQLite SQL statement to execute
* @param callback the callback to invoke on the prepared statement
* @param logErrors Determines if error will be logged if statement fail to prepare
* @returns the value returned by `callback`.
* @see [[withPreparedStatement]]
* @public
*/
withPreparedSqliteStatement(sql, callback, logErrors = true) {
const stmt = this._sqliteStatementCache.findAndRemove(sql) ?? this.prepareSqliteStatement(sql, logErrors);
const release = () => this._sqliteStatementCache.addOrDispose(stmt);
try {
const val = callback(stmt);
if (val instanceof Promise) {
val.then(release, release);
}
else {
release();
}
return val;
}
catch (err) {
release();
throw err;
}
}
/**
* Prepared and execute a callback on a SQL statement. After the callback completes the statement is disposed.
* Use this method for SQL statements are either not expected to be reused, or are not expensive to prepare.
* For statements that will be reused often, instead use [[withPreparedSqliteStatement]].
* @param sql The SQLite SQL statement to execute
* @param callback the callback to invoke on the prepared statement
* @param logErrors Determines if error will be logged if statement fail to prepare
* @returns the value returned by `callback`.
* @public
*/
withSqliteStatement(sql, callback, logErrors = true) {
const stmt = this.prepareSqliteStatement(sql, logErrors);
const release = () => stmt[Symbol.dispose]();
try {
const val = callback(stmt);
if (val instanceof Promise) {
val.then(release, release);
}
else {
release();
}
return val;
}
catch (err) {
release();
throw err;
}
}
/** Prepare an SQL statement.
* @param sql The SQLite SQL statement to prepare
* @param logErrors Determines if error will be logged if statement fail to prepare
* @throws [IModelError]($common) if there is a problem preparing the statement.
* @internal
*/
prepareSqliteStatement(sql, logErrors = true) {
const stmt = new SqliteStatement_1.SqliteStatement(sql);
stmt.prepare(this[Symbols_1._nativeDb], logErrors);
return stmt;
}
/** @internal */
get [Symbols_1._nativeDb]() {
(0, core_bentley_1.assert)(undefined !== this._nativeDb);
return this._nativeDb;
}
/** Allow to execute query and read results along with meta data. The result are streamed.
*
* See also:
* - [ECSQL Overview]($docs/learning/backend/ExecutingECSQL)
* - [Code Examples]($docs/learning/backend/ECSQLCodeExamples)
* - [ECSQL Row Format]($docs/learning/ECSQLRowFormat)
*
* @param params The values to bind to the parameters (if the ECSQL has any).
* @param config Allow to specify certain flags which control how query is executed.
* @returns Returns an [ECSqlReader]($common) which helps iterate over the result set and also give access to metadata.
* @public
* */
createQueryReader(ecsql, params, config) {
if (!this._nativeDb || !this._nativeDb.isOpen()) {
throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, "db not open");
}
const executor = {
execute: async (request) => {
return ConcurrentQuery_1.ConcurrentQuery.executeQueryRequest(this[Symbols_1._nativeDb], request);
},
};
return new core_common_1.ECSqlReader(executor, ecsql, params, config);
}
}
exports.ECDb = ECDb;
//# sourceMappingURL=ECDb.js.map