@itwin/core-backend
Version:
iTwin.js backend components
996 lines • 183 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.StandaloneDb = exports.SnapshotDb = exports.BriefcaseDb = exports.IModelDb = exports.BriefcaseLocalValue = void 0;
const fs = require("fs");
const path_1 = require("path");
const touch = require("touch");
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const core_geometry_1 = require("@itwin/core-geometry");
const BackendLoggerCategory_1 = require("./BackendLoggerCategory");
const BriefcaseManager_1 = require("./BriefcaseManager");
const ChannelControl_1 = require("./ChannelControl");
const ChannelAdmin_1 = require("./internal/ChannelAdmin");
const CheckpointManager_1 = require("./CheckpointManager");
const ClassRegistry_1 = require("./ClassRegistry");
const CloudSqlite_1 = require("./CloudSqlite");
const CodeService_1 = require("./CodeService");
const CodeSpecs_1 = require("./CodeSpecs");
const ConcurrentQuery_1 = require("./ConcurrentQuery");
const ECSqlStatement_1 = require("./ECSqlStatement");
const Element_1 = require("./Element");
const ElementGraphics_1 = require("./ElementGraphics");
const Entity_1 = require("./Entity");
const GeoCoordConfig_1 = require("./GeoCoordConfig");
const IModelHost_1 = require("./IModelHost");
const IModelJsFs_1 = require("./IModelJsFs");
const IpcHost_1 = require("./IpcHost");
const Model_1 = require("./Model");
const Relationship_1 = require("./Relationship");
const SchemaSync_1 = require("./SchemaSync");
const ServerBasedLocks_1 = require("./internal/ServerBasedLocks");
const SqliteStatement_1 = require("./SqliteStatement");
const TxnManager_1 = require("./TxnManager");
const ViewDefinition_1 = require("./ViewDefinition");
const ViewStore_1 = require("./ViewStore");
const Settings_1 = require("./workspace/Settings");
const Workspace_1 = require("./workspace/Workspace");
const WorkspaceImpl_1 = require("./internal/workspace/WorkspaceImpl");
const SettingsImpl_1 = require("./internal/workspace/SettingsImpl");
const NativePlatform_1 = require("./internal/NativePlatform");
const NoLocks_1 = require("./internal/NoLocks");
const IModelDbFontsImpl_1 = require("./internal/IModelDbFontsImpl");
const Symbols_1 = require("./internal/Symbols");
const ecschema_metadata_1 = require("@itwin/ecschema-metadata");
const Schema_1 = require("./Schema");
const ElementLRUCache_1 = require("./internal/ElementLRUCache");
// spell:ignore fontid fontmap
const loggerCategory = BackendLoggerCategory_1.BackendLoggerCategory.IModelDb;
/** @internal */
var BriefcaseLocalValue;
(function (BriefcaseLocalValue) {
BriefcaseLocalValue["StandaloneEdit"] = "StandaloneEdit";
BriefcaseLocalValue["NoLocking"] = "NoLocking";
})(BriefcaseLocalValue || (exports.BriefcaseLocalValue = BriefcaseLocalValue = {}));
// function to open an briefcaseDb, perform an operation, and then close it.
const withBriefcaseDb = async (briefcase, fn) => {
const db = await BriefcaseDb.open(briefcase);
try {
return await fn(db);
}
finally {
db.close();
}
};
/**
* Settings for an individual iModel. May only include settings priority for iModel, iTwin and organization.
* @note if there is more than one iModel for an iTwin or organization, they will *each* hold an independent copy of the settings for those priorities.
*/
class IModelSettings extends SettingsImpl_1.SettingsImpl {
verifyPriority(priority) {
if (priority <= Settings_1.SettingsPriority.application)
throw new Error("Use IModelHost.appSettings to access settings of priority 'application' or lower");
}
*getSettingEntries(name) {
yield* super.getSettingEntries(name);
yield* IModelHost_1.IModelHost.appWorkspace.settings.getSettingEntries(name);
}
}
/** An iModel database file. The database file can either be a briefcase or a snapshot.
* @see [Accessing iModels]($docs/learning/backend/AccessingIModels.md)
* @see [About IModelDb]($docs/learning/backend/IModelDb.md)
* @public
*/
class IModelDb extends core_common_1.IModel {
_initialized = false;
/** Keep track of open imodels to support `tryFind` for RPC purposes */
static _openDbs = new Map();
static defaultLimit = 1000; // default limit for batching queries
static maxLimit = 10000; // maximum limit for batching queries
models = new IModelDb.Models(this);
elements = new IModelDb.Elements(this);
views = new IModelDb.Views(this);
tiles = new IModelDb.Tiles(this);
/** @beta */
channels = (0, ChannelAdmin_1.createChannelControl)(this);
_relationships;
// eslint-disable-next-line @typescript-eslint/no-deprecated
_statementCache = new SqliteStatement_1.StatementCache();
_sqliteStatementCache = new SqliteStatement_1.StatementCache();
_codeSpecs;
// eslint-disable-next-line @typescript-eslint/no-deprecated
_classMetaDataRegistry;
_jsClassMap;
_schemaMap;
_schemaContext;
/** @deprecated in 5.0.0 - will not be removed until after 2026-06-13. Use [[fonts]]. */
_fontMap; // eslint-disable-line @typescript-eslint/no-deprecated
_fonts = (0, IModelDbFontsImpl_1.createIModelDbFonts)(this);
_workspace;
_snaps = new Map();
static _shutdownListener; // so we only register listener once
/** @internal */
_locks = (0, NoLocks_1.createNoOpLockControl)();
/** @internal */
_codeService;
/** @alpha */
get codeService() { return this._codeService; }
/** The [[LockControl]] that orchestrates [concurrent editing]($docs/learning/backend/ConcurrencyControl.md) of this iModel. */
get locks() { return this._locks; } // eslint-disable-line @typescript-eslint/no-non-null-assertion
/** Provides methods for interacting with [font-related information]($docs/learning/backend/Fonts.md) stored in this iModel.
* @beta
*/
get fonts() { return this._fonts; }
/**
* Get the [[Workspace]] for this iModel.
* @beta
*/
get workspace() {
if (undefined === this._workspace)
this._workspace = (0, WorkspaceImpl_1.constructWorkspace)(new IModelSettings());
return this._workspace;
}
/**
* get the cloud container for this iModel, if it was opened from one
* @beta
*/
get cloudContainer() {
return this[Symbols_1._nativeDb].cloudContainer;
}
/** Acquire the exclusive schema lock on this iModel.
* @note: To acquire the schema lock, all other briefcases must first release *all* their locks. No other briefcases
* will be able to acquire *any* locks while the schema lock is held.
*/
async acquireSchemaLock() {
return this.locks.acquireLocks({ exclusive: core_common_1.IModel.repositoryModelId });
}
/** determine whether the schema lock is currently held for this iModel. */
get holdsSchemaLock() {
return this.locks.holdsExclusiveLock(core_common_1.IModel.repositoryModelId);
}
/** Event called after a changeset is applied to this IModelDb. */
onChangesetApplied = new core_bentley_1.BeEvent();
/** @internal */
notifyChangesetApplied() {
this.changeset = this[Symbols_1._nativeDb].getCurrentChangeset();
this.onChangesetApplied.raiseEvent();
}
/** @internal */
restartDefaultTxn() {
this[Symbols_1._nativeDb].restartDefaultTxn();
}
/** @deprecated in 5.0.0 - will not be removed until after 2026-06-13. Use [[fonts]]. */
get fontMap() {
return this._fontMap ?? (this._fontMap = new core_common_1.FontMap(this[Symbols_1._nativeDb].readFontMap())); // eslint-disable-line @typescript-eslint/no-deprecated
}
/** @internal */
clearFontMap() {
this._fontMap = undefined; // eslint-disable-line @typescript-eslint/no-deprecated
this[Symbols_1._nativeDb].invalidateFontMap();
}
/** Check if this iModel has been opened read-only or not. */
get isReadonly() { return this.openMode === core_bentley_1.OpenMode.Readonly; }
/** The Guid that identifies this iModel. */
get iModelId() {
(0, core_bentley_1.assert)(undefined !== super.iModelId);
return super.iModelId;
} // GuidString | undefined for the IModel superclass, but required for all IModelDb subclasses
/** @internal*/
[Symbols_1._nativeDb];
/** Get the full path fileName of this iModelDb
* @note this member is only valid while the iModel is opened.
*/
get pathName() { return this[Symbols_1._nativeDb].getFilePath(); }
/** Get the full path to this iModel's "watch file".
* A read-only briefcase opened with `watchForChanges: true` creates this file next to the briefcase file on open, if it doesn't already exist.
* A writable briefcase "touches" this file if it exists whenever it commits changes to the briefcase.
* The read-only briefcase can use a file watcher to react when the writable briefcase makes changes to the briefcase.
* This is more reliable than watching the sqlite WAL file.
* @internal
*/
get watchFilePathName() { return `${this.pathName}-watch`; }
/** @internal */
constructor(args) {
super({ ...args, iTwinId: args.nativeDb.getITwinId(), iModelId: args.nativeDb.getIModelId() });
this[Symbols_1._nativeDb] = args.nativeDb;
// it is illegal to create an IModelDb unless the nativeDb has been opened. Throw otherwise.
if (!this.isOpen)
throw new Error("cannot create an IModelDb unless it has already been opened");
// PR https://github.com/iTwin/imodel-native/pull/558 renamed closeIModel to closeFile because it changed its behavior.
// Ideally, nobody outside of core-backend would be calling it, but somebody important is.
// Make closeIModel available so their code doesn't break.
this[Symbols_1._nativeDb].closeIModel = () => {
if (!this.isReadonly)
this.saveChanges(); // preserve old behavior of closeIModel that was removed when renamed to closeFile
this[Symbols_1._nativeDb].closeFile();
};
this[Symbols_1._nativeDb].setIModelDb(this);
this.loadIModelSettings();
GeoCoordConfig_1.GeoCoordConfig.loadForImodel(this.workspace.settings); // load gcs data specified by iModel's settings dictionaries, must be done before calling initializeIModelDb
this.initializeIModelDb();
IModelDb._openDbs.set(this._fileKey, this);
if (undefined === IModelDb._shutdownListener) { // the first time we create an IModelDb, add a listener to close any orphan files at shutdown.
IModelDb._shutdownListener = IModelHost_1.IModelHost.onBeforeShutdown.addListener(() => {
IModelDb._openDbs.forEach((db) => {
try {
db.abandonChanges();
db.close();
}
catch { }
});
});
}
}
/**
* 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'
*
* *Example:*
* ``` ts
* [[include:IModelDb_attachDb.code]]
* ```
*/
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]]
*
* *Example:*
* ``` ts
* [[include:IModelDb_attachDb.code]]
* ```
*/
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.clearCaches();
this[Symbols_1._nativeDb].detachDb(alias);
}
/** Close this IModel, if it is currently open, and save changes if it was opened in ReadWrite mode. */
close() {
if (!this.isOpen)
return; // don't continue if already closed
this.beforeClose();
IModelDb._openDbs.delete(this._fileKey);
this._workspace?.close();
this.locks[Symbols_1._close]();
this._locks = undefined;
this._codeService?.close();
this._codeService = undefined;
if (!this.isReadonly)
this.saveChanges();
this[Symbols_1._nativeDb].closeFile();
}
/** @internal */
async refreshContainerForRpc(_userAccessToken) { }
/** Event called when the iModel is about to be closed. */
onBeforeClose = new core_bentley_1.BeEvent();
/**
* Called by derived classes before closing the connection
* @internal
*/
beforeClose() {
this.onBeforeClose.raiseEvent();
this.clearCaches();
}
/** @internal */
initializeIModelDb(when) {
const props = this[Symbols_1._nativeDb].getIModelProps(when);
super.initialize(props.rootSubject.name, props);
if (this._initialized)
return;
this._initialized = true;
const db = this.isBriefcaseDb() ? this : undefined;
if (!db || !IpcHost_1.IpcHost.isValid)
return;
db.onNameChanged.addListener(() => IpcHost_1.IpcHost.notifyTxns(db, "notifyIModelNameChanged", db.name));
db.onRootSubjectChanged.addListener(() => IpcHost_1.IpcHost.notifyTxns(db, "notifyRootSubjectChanged", db.rootSubject));
db.onProjectExtentsChanged.addListener(() => IpcHost_1.IpcHost.notifyTxns(db, "notifyProjectExtentsChanged", db.projectExtents.toJSON()));
db.onGlobalOriginChanged.addListener(() => IpcHost_1.IpcHost.notifyTxns(db, "notifyGlobalOriginChanged", db.globalOrigin.toJSON()));
db.onEcefLocationChanged.addListener(() => IpcHost_1.IpcHost.notifyTxns(db, "notifyEcefLocationChanged", db.ecefLocation?.toJSON()));
db.onGeographicCoordinateSystemChanged.addListener(() => IpcHost_1.IpcHost.notifyTxns(db, "notifyGeographicCoordinateSystemChanged", db.geographicCoordinateSystem?.toJSON()));
}
/** Returns true if this is a BriefcaseDb
* @see [[BriefcaseDb.open]]
*/
get isBriefcase() { return false; }
/** Type guard for instanceof [[BriefcaseDb]] */
isBriefcaseDb() { return this.isBriefcase; }
/** Returns true if this is a SnapshotDb
* @see [[SnapshotDb.open]]
*/
get isSnapshot() { return false; }
/** Type guard for instanceof [[SnapshotDb]] */
isSnapshotDb() { return this.isSnapshot; }
/** Returns true if this is a *standalone* iModel
* @see [[StandaloneDb.open]]
* @internal
*/
get isStandalone() { return false; }
/** Type guard for instanceof [[StandaloneDb]]. */
isStandaloneDb() { return this.isStandalone; }
/** Return `true` if the underlying nativeDb is open and valid.
* @internal
*/
get isOpen() { return this[Symbols_1._nativeDb].isOpen(); }
/** Get the briefcase Id of this iModel */
getBriefcaseId() { return this.isOpen ? this[Symbols_1._nativeDb].getBriefcaseId() : core_common_1.BriefcaseIdValue.Illegal; }
/**
* 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]] 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]] 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;
}
}
/** 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[Symbols_1._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);
}
/**
* 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 Determine if errors are logged or not
* @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 Determine if errors are logged or not
* @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 SQL statement to prepare
* @throws [[IModelError]] 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;
}
/**
* queries the BisCore.SubCategory table for entries that are children of used spatial categories and 3D elements.
* @returns array of SubCategoryResultRow
* @internal
*/
async queryAllUsedSpatialSubCategories() {
const result = [];
const parentCategoriesQuery = `SELECT DISTINCT Category.Id AS id FROM BisCore.GeometricElement3d WHERE Category.Id IN (SELECT ECInstanceId FROM BisCore.SpatialCategory)`;
const parentCategories = [];
for await (const row of this.createQueryReader(parentCategoriesQuery)) {
parentCategories.push(row.id);
}
;
const where = [...parentCategories].join(",");
const query = `SELECT ECInstanceId as id, Parent.Id as parentId, Properties as appearance FROM BisCore.SubCategory WHERE Parent.Id IN (${where})`;
try {
for await (const row of this.createQueryReader(query, undefined, { rowFormat: core_common_1.QueryRowFormat.UseJsPropertyNames })) {
result.push(row.toRow());
}
}
catch {
// We can ignore the error here, and just return whatever we were able to query.
}
return result;
}
/**
* queries the BisCore.SubCategory table for the entries that are children of the passed categoryIds.
* @param categoryIds categoryIds to query
* @returns array of SubCategoryResultRow
* @internal
*/
async querySubCategories(categoryIds) {
const result = [];
const where = [...categoryIds].join(",");
const query = `SELECT ECInstanceId as id, Parent.Id as parentId, Properties as appearance FROM BisCore.SubCategory WHERE Parent.Id IN (${where})`;
try {
for await (const row of this.createQueryReader(query, undefined, { rowFormat: core_common_1.QueryRowFormat.UseJsPropertyNames })) {
result.push(row.toRow());
}
}
catch {
// We can ignore the error here, and just return whatever we were able to query.
}
return result;
}
/** Query for a set of entity ids, given an EntityQueryParams
* @param params The query parameters. The `limit` and `offset` members should be used to page results.
* @returns an Id64Set with results of query
* @throws [[IModelError]] if the generated statement is invalid or [IModelDb.maxLimit]($backend) exceeded when collecting ids.
*
* *Example:*
* ``` ts
* [[include:ECSQL-backend-queries.select-element-by-code-value-using-queryEntityIds]]
* ```
*/
queryEntityIds(params) {
let sql = "SELECT ECInstanceId FROM ";
if (params.only)
sql += "ONLY ";
sql += params.from;
if (params.where)
sql += ` WHERE ${params.where}`;
if (params.orderBy)
sql += ` ORDER BY ${params.orderBy}`;
if (typeof params.limit === "number" && params.limit > 0)
sql += ` LIMIT ${params.limit}`;
if (typeof params.offset === "number" && params.offset > 0)
sql += ` OFFSET ${params.offset}`;
const ids = new Set();
// eslint-disable-next-line @typescript-eslint/no-deprecated
this.withPreparedStatement(sql, (stmt) => {
if (params.bindings)
stmt.bindValues(params.bindings);
for (const row of stmt) {
if (row.id !== undefined) {
ids.add(row.id);
if (ids.size > IModelDb.maxLimit) {
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Max LIMIT exceeded in SELECT statement");
}
}
}
});
return ids;
}
/** Clear all in-memory caches held in this IModelDb. */
clearCaches() {
this._statementCache.clear();
this._sqliteStatementCache.clear();
this._classMetaDataRegistry = undefined;
this._jsClassMap = undefined;
this._schemaMap = undefined;
this._schemaContext = undefined;
this.elements[Symbols_1._cache].clear();
this.models[Symbols_1._cache].clear();
this.elements[Symbols_1._instanceKeyCache].clear();
this.models[Symbols_1._instanceKeyCache].clear();
}
/** Update the project extents for this iModel.
* <p><em>Example:</em>
* ``` ts
* [[include:IModelDb.updateProjectExtents]]
* ```
*/
updateProjectExtents(newExtents) {
this.projectExtents = newExtents;
this.updateIModelProps();
}
/** Compute an appropriate project extents for this iModel based on the ranges of all spatial elements.
* Typically, the result is simply the union of the ranges of all spatial elements. However, the algorithm also detects "outlier elements",
* whose placements locate them so far from the rest of the spatial geometry that they are considered statistically insignificant. The
* range of an outlier element does not contribute to the computed extents.
* @param options Specifies the level of detail desired in the return value.
* @returns the computed extents.
* @note This method does not modify the IModel's stored project extents. @see [[updateProjectExtents]].
*/
computeProjectExtents(options) {
const wantFullExtents = true === options?.reportExtentsWithOutliers;
const wantOutliers = true === options?.reportOutliers;
const result = this[Symbols_1._nativeDb].computeProjectExtents(wantFullExtents, wantOutliers);
return {
extents: core_geometry_1.Range3d.fromJSON(result.extents),
extentsWithOutliers: result.fullExtents ? core_geometry_1.Range3d.fromJSON(result.fullExtents) : undefined,
outliers: result.outliers,
};
}
/** Update the [EcefLocation]($docs/learning/glossary#eceflocation) of this iModel. */
updateEcefLocation(ecef) {
this.setEcefLocation(ecef);
this.updateIModelProps();
}
/** Update the IModelProps of this iModel in the database. */
updateIModelProps() {
this[Symbols_1._nativeDb].updateIModelProps(this.toJSON());
}
/** Commit unsaved changes in memory as a Txn to this iModelDb.
* @param description Optional description of the changes
* @throws [[IModelError]] if there is a problem saving changes or if there are pending, un-processed lock or code requests.
* @note This will not push changes to the iModelHub.
* @see [[IModelDb.pushChanges]] to push changes to the iModelHub.
*/
saveChanges(description) {
if (this.openMode === core_bentley_1.OpenMode.Readonly)
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.ReadOnly, "IModelDb was opened read-only");
const stat = this[Symbols_1._nativeDb].saveChanges(description);
if (core_bentley_1.DbResult.BE_SQLITE_OK !== stat)
throw new core_common_1.IModelError(stat, `Could not save changes (${description})`);
}
/** Abandon changes in memory that have not been saved as a Txn to this iModelDb.
* @note This will not delete Txns that have already been saved, even if they have not yet been pushed.
*/
abandonChanges() {
this.clearCaches();
this[Symbols_1._nativeDb].abandonChanges();
}
/**
* Save all changes and perform a [checkpoint](https://www.sqlite.org/c3ref/wal_checkpoint_v2.html) on this IModelDb.
* This ensures that all changes to the database since it was opened are saved to its file and the WAL file is truncated.
* @note Checkpoint automatically happens when IModelDbs are closed. However, the checkpoint
* operation itself can take some time. It may be useful to call this method prior to closing so that the checkpoint "penalty" is paid earlier.
* @note Another use for this function is to permit the file to be copied while it is open for write. iModel files should
* rarely be copied, and even less so while they're opened. But this scenario is sometimes encountered for tests.
*/
performCheckpoint() {
if (!this.isReadonly) {
this.saveChanges();
this.clearCaches();
this[Symbols_1._nativeDb].concurrentQueryShutdown();
this[Symbols_1._nativeDb].clearECDbCache();
this[Symbols_1._nativeDb].performCheckpoint();
}
}
/** @internal
* @deprecated in 4.8 - will not be removed until after 2026-06-13. Use `txns.reverseTxns`.
*/
reverseTxns(numOperations) {
return this[Symbols_1._nativeDb].reverseTxns(numOperations);
}
/** @internal */
reinstateTxn() {
return this[Symbols_1._nativeDb].reinstateTxn();
}
/** @internal */
restartTxnSession() {
return this[Symbols_1._nativeDb].restartTxnSession();
}
/** Import an ECSchema. On success, the schema definition is stored in the iModel.
* This method is asynchronous (must be awaited) because, in the case where this IModelDb is a briefcase, this method first obtains the schema lock from the iModel server.
* You must import a schema into an iModel before you can insert instances of the classes in that schema. See [[Element]]
* @param schemaFileName array of Full paths to ECSchema.xml files to be imported.
* @param {SchemaImportOptions} options - options during schema import.
* @throws [[IModelError]] if the schema lock cannot be obtained or there is a problem importing the schema.
* @note Changes are saved if importSchemas is successful and abandoned if not successful.
* - You can use NativeLoggerCategory to turn on the native logs. You can also control [what exactly is logged by the loggers](https://www.itwinjs.org/learning/common/logging/#controlling-what-is-logged).
* - See [Schema Versioning]($docs/bis/guide/schema-evolution/schema-versioning-and-generations.md) for more information on acceptable changes to schemas.
* @see querySchemaVersion
*/
async importSchemas(schemaFileNames, options) {
if (schemaFileNames.length === 0)
return;
const maybeCustomNativeContext = options?.ecSchemaXmlContext?.nativeContext;
if (this[Symbols_1._nativeDb].schemaSyncEnabled()) {
await SchemaSync_1.SchemaSync.withLockedAccess(this, { openMode: core_bentley_1.OpenMode.Readonly, operationName: "schema sync" }, async (syncAccess) => {
const schemaSyncDbUri = syncAccess.getUri();
this.saveChanges();
try {
this[Symbols_1._nativeDb].importSchemas(schemaFileNames, { schemaLockHeld: false, ecSchemaXmlContext: maybeCustomNativeContext, schemaSyncDbUri });
}
catch (outerErr) {
if (core_bentley_1.DbResult.BE_SQLITE_ERROR_DataTransformRequired === outerErr.errorNumber) {
this.abandonChanges();
if (this[Symbols_1._nativeDb].getITwinId() !== core_bentley_1.Guid.empty)
await this.acquireSchemaLock();
try {
this[Symbols_1._nativeDb].importSchemas(schemaFileNames, { schemaLockHeld: true, ecSchemaXmlContext: maybeCustomNativeContext, schemaSyncDbUri });
}
catch (innerErr) {
throw new core_common_1.IModelError(innerErr.errorNumber, innerErr.message);
}
}
else {
throw new core_common_1.IModelError(outerErr.errorNumber, outerErr.message);
}
}
});
}
else {
const nativeImportOptions = {
schemaLockHeld: true,
ecSchemaXmlContext: maybeCustomNativeContext,
};
if (this[Symbols_1._nativeDb].getITwinId() !== core_bentley_1.Guid.empty) // if this iModel is associated with an iTwin, importing schema requires the schema lock
await this.acquireSchemaLock();
try {
this[Symbols_1._nativeDb].importSchemas(schemaFileNames, nativeImportOptions);
}
catch (err) {
throw new core_common_1.IModelError(err.errorNumber, err.message);
}
}
this.clearCaches();
}
/** Import ECSchema(s) serialized to XML. On success, the schema definition is stored in the iModel.
* This method is asynchronous (must be awaited) because, in the case where this IModelDb is a briefcase, this method first obtains the schema lock from the iModel server.
* You must import a schema into an iModel before you can insert instances of the classes in that schema. See [[Element]]
* @param serializedXmlSchemas The xml string(s) created from a serialized ECSchema.
* @throws [[IModelError]] if the schema lock cannot be obtained or there is a problem importing the schema.
* @note Changes are saved if importSchemaStrings is successful and abandoned if not successful.
* @see querySchemaVersion
* @alpha
*/
async importSchemaStrings(serializedXmlSchemas) {
if (serializedXmlSchemas.length === 0)
return;
if (this[Symbols_1._nativeDb].schemaSyncEnabled()) {
await SchemaSync_1.SchemaSync.withLockedAccess(this, { openMode: core_bentley_1.OpenMode.Readonly, operationName: "schemaSync" }, async (syncAccess) => {
const schemaSyncDbUri = syncAccess.getUri();
this.saveChanges();
try {
this[Symbols_1._nativeDb].importXmlSchemas(serializedXmlSchemas, { schemaLockHeld: false, schemaSyncDbUri });
}
catch (outerErr) {
if (core_bentley_1.DbResult.BE_SQLITE_ERROR_DataTransformRequired === outerErr.errorNumber) {
this.abandonChanges();
if (this[Symbols_1._nativeDb].getITwinId() !== core_bentley_1.Guid.empty)
await this.acquireSchemaLock();
try {
this[Symbols_1._nativeDb].importXmlSchemas(serializedXmlSchemas, { schemaLockHeld: true, schemaSyncDbUri });
}
catch (innerErr) {
throw new core_common_1.IModelError(innerErr.errorNumber, innerErr.message);
}
}
else {
throw new core_common_1.IModelError(outerErr.errorNumber, outerErr.message);
}
}
});
}
else {
if (this.iTwinId && this.iTwinId !== core_bentley_1.Guid.empty) // if this iModel is associated with an iTwin, importing schema requires the schema lock
await this.acquireSchemaLock();
try {
this[Symbols_1._nativeDb].importXmlSchemas(serializedXmlSchemas, { schemaLockHeld: true });
}
catch (err) {
throw new core_common_1.IModelError(err.errorNumber, err.message);
}
}
this.clearCaches();
}
/** Find an opened instance of any subclass of IModelDb, by filename
* @note this method returns an IModelDb if the filename is open for *any* subclass of IModelDb
*/
static findByFilename(fileName) {
for (const entry of this._openDbs) {
// It shouldn't be possible for anything in _openDbs to not be open, but if so just skip them because `pathName` will throw an exception.
if (entry[1].isOpen && entry[1].pathName === fileName)
return entry[1];
}
return undefined;
}
/** Find an open IModelDb by its key.
* @note This method is mainly for use by RPC implementations.
* @throws [[IModelNotFoundResponse]] if an open IModelDb matching the key is not found.
* @see [IModel.key]($common)
*/
static findByKey(key) {
const iModelDb = this.tryFindByKey(key);
if (undefined === iModelDb) {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw new core_common_1.IModelNotFoundResponse(); // a very specific status for the RpcManager
}
return iModelDb;
}
/** Attempt to find an open IModelDb by key.
* @returns The matching IModelDb or `undefined`.
*/
static tryFindByKey(key) {
return this._openDbs.get(key);
}
/** @internal */
static openDgnDb(file, openMode, upgradeOptions, props) {
file.key = file.key ?? core_bentley_1.Guid.createValue();
if (this.tryFindByKey(file.key))
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.AlreadyOpen, `key [${file.key}] for file [${file.path}] is already in use`);
const isUpgradeRequested = upgradeOptions?.domain === core_common_1.DomainOptions.Upgrade || upgradeOptions?.profile === core_common_1.ProfileOptions.Upgrade;
if (isUpgradeRequested && openMode !== core_bentley_1.OpenMode.ReadWrite)
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.UpgradeFailed, "Cannot upgrade a Readonly Db");
try {
const nativeDb = new NativePlatform_1.IModelNative.platform.DgnDb();
const container = props?.container;
if (container) {
// temp files for cloud-based Dbs should be in the profileDir in a subdirectory named for their container
const baseDir = (0, path_1.join)(IModelHost_1.IModelHost.profileDir, "CloudDbTemp", container.containerId);
IModelJsFs_1.IModelJsFs.recursiveMkDirSync(baseDir);
props = { ...props, tempFileBase: (0, path_1.join)(baseDir, file.path) };
}
nativeDb.openIModel(file.path, openMode, upgradeOptions, props, props?.container, props);
return nativeDb;
}
catch (err) {
throw new core_common_1.IModelError(err.errorNumber, `${err.message}, ${file.path}`);
}
}
/**
* Determines if the schemas in the Db must or can be upgraded by comparing them with those included in the
* current version of the software.
* @param filePath Full name of the briefcase including path
* @param forReadWrite Pass true if validating for read-write scenarios - note that the schema version requirements
* for opening the DgnDb read-write is more stringent than when opening the database read-only
* @throws [[IModelError]] If the Db was in an invalid state and that causes a problem with validating schemas
* @see [[BriefcaseDb.upgradeSchemas]] or [[StandaloneDb.upgradeSchemas]]
* @see ($docs/learning/backend/IModelDb.md#upgrading-schemas-in-an-imodel)
*/
static validateSchemas(filePath, forReadWrite) {
const openMode = forReadWrite ? core_bentley_1.OpenMode.ReadWrite : core_bentley_1.OpenMode.Readonly;
const file = { path: filePath };
let result = core_bentley_1.DbResult.BE_SQLITE_OK;
try {
const upgradeOptions = {
domain: core_common_1.DomainOptions.CheckRecommendedUpgrades,
};
const nativeDb = this.openDgnDb(file, openMode, upgradeOptions);
nativeDb.closeFile();
}
catch (err) {
result = err.errorNumber;
}
let schemaState = core_common_1.SchemaState.UpToDate;
switch (result) {
case core_bentley_1.DbResult.BE_SQLITE_OK:
schemaState = core_common_1.SchemaState.UpToDate;
break;
case core_bentley_1.DbResult.BE_SQLITE_ERROR_ProfileTooOld:
case core_bentley_1.DbResult.BE_SQLITE_ERROR_ProfileTooOldForReadWrite:
case core_bentley_1.DbResult.BE_SQLITE_ERROR_SchemaTooOld:
schemaState = core_common_1.SchemaState.TooOld;
break;
case core_bentley_1.DbResult.BE_SQLITE_ERROR_ProfileTooNew:
case core_bentley_1.DbResult.BE_SQLITE_ERROR_ProfileTooNewForReadWrite:
case core_bentley_1.DbResult.BE_SQLITE_ERROR_SchemaTooNew:
schemaState = core_common_1.SchemaState.TooNew;
break;
case core_bentley_1.DbResult.BE_SQLITE_ERROR_SchemaUpgradeRecommended:
schemaState = core_common_1.SchemaState.UpgradeRecommended;
break;
case core_bentley_1.DbResult.BE_SQLITE_ERROR_SchemaUpgradeRequired:
schemaState = core_common_1.SchemaState.UpgradeRequired;
break;
case core_bentley_1.DbResult.BE_SQLITE_ERROR_InvalidProfileVersion:
throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR_InvalidProfileVersion, "The profile of the Db is invalid. Cannot upgrade or open the Db.");
default:
throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, "Error validating schemas. Cannot upgrade or open the Db.");
}
return schemaState;
}
/** The registry of entity metadata for this iModel.
* @internal
* @deprecated in 5.0 - will not be removed until after 2026-06-13. Please use `schemaContext` from the `iModel` instead.
*
* @example
* ```typescript
* // Current usage:
* const classMetaData: EntityMetaData | undefined = iModel.classMetaDataRegistry.find("SchemaName:ClassName");
*
* // Replacement:
* const metaData: EntityClass | undefined = imodel.schemaContext.getSchemaItemSync("SchemaName.ClassName", EntityClass);
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-deprecated
get classMetaDataRegistry() {
if (this._classMetaDataRegistry === undefined)
// eslint-disable-next-line @typescript-eslint/no-deprecated
this._classMetaDataRegistry = new ClassRegistry_1.MetaDataRegistry();
return this._classMetaDataRegistry;
}
/**
* Allows registering js classes mapped to ECClasses
*/
get jsClassMap() {
if (this._jsClassMap === undefined)
this._jsClassMap = new ClassRegistry_1.EntityJsClassMap();
return this._jsClassMap;
}
/**
* Allows locally registering a schema for this imodel, in constrast to [Schemas.registerSchema] which is a global operation
*/
get schemaMap() {
if (this._schemaMap === undefined)
this._schemaMap = new Schema_1.SchemaMap();
return this._schemaMap;
}
/**
* Gets the context that allows accessing the metadata (ecschema-metadata package) of this iModel
* @public @preview
*/
get schemaContext() {
if (this._schemaContext === undefined) {
const context = new ecschema_metadata_1.SchemaContext();
// TODO: We probably need a more optimized locater for here
const locater = new ecschema_metadata_1.SchemaJsonLocater((name) => this.getSchemaProps(name));
context.addLocater(locater);
this._schemaContext = context;
}
return this._schemaContext;
}
/** Get the linkTableRelationships for this IModel */
get relationships() {
return this._relationships || (this._relationships = new Relationship_1.Relationships(this));
}
/** Get the CodeSpecs in this IModel. */
get codeSpecs() {
return (this._codeSpecs !== undefined) ? this._codeSpecs : (this._codeSpecs = new CodeSpecs_1.CodeSpecs(this));
}
/** Prepare an ECSQL statement.
* @param sql The ECSQL statement to prepare
* @param logErrors Determines if error will be logged if statement fail to prepare
* @throws [[IModelError]] if there is a problem preparing the statement.
* @deprecated in 4.11 - will not be removed until after 2026-06-13. Use [IModelDb.createQueryReader]($backend) or [ECDb.createQueryReader]($backend) to query.
*/
// eslint-disable-next-line @typescript-eslint/no-deprecated
prepareStatement(sql, logErrors = true) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const stmt = new ECSqlStatement_1.ECSqlStatement();
stmt.prepare(this[Symbols_1._nativeDb], sql, logErrors);
return stmt;
}
/** Prepare an ECSQL statement.
* @param sql The ECSQL statement to prepare
* @returns `undefined` if there is a problem preparing the statement.
* @deprecated in 4.11 - will not be removed until after 2026-06-13. Use [IModelDb.createQueryReader]($backend) or [ECDb.createQueryReader]($backend) to query.
*/
// eslint-disable-next-line @typescript-eslint/no-deprecated
tryPrepareStatement(sql) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const statement = new ECSqlStatement_1.ECSqlStatement();
const result = statement.tryPrepare(this[Symbols_1._nativeDb], sql);
return core_bentley_1.DbResult.BE_SQLITE_OK === result.status ? statement : undefined;
}
/** Construct an entity (Element or Model) from an iModel.
* @throws [[IModelError]] if the entity cannot be constructed.
*/
constructEntity(props) {
const jsClass = this.getJsClass(props.classFullName);
return Entity_1.Entity.instantiate(jsClass, props, this);
}
/** Get the JavaScript class that handles a given entity class. */
getJsClass(classFullName) {
try {
return ClassRegistry_1.ClassRegistry.getClass(classFullName, this);
}
catch (err) {
if (!ClassRegistry_1.ClassRegistry.isNotFoundError(err)) {
throw err;
}
// eslint-disable-next-line @typescript-eslint/no-deprecated
this.loadMetaData(classFullName);
return ClassRegistry_1.ClassRegistry.getClass(classFullName, this);
}
}
/** Constructs a ResolveInstanceKeyArgs from given parameters
* @throws [[IModelError]] if the combination of supplied parameters is invalid.
* @internal
*/
getInstanceArgs(instanceId, baseClassName, federationGuid, code) {
if (instan