@itwin/core-backend
Version:
iTwin.js backend components
1,019 lines • 184 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module iModels
*/
import * as fs from "fs";
import { join } from "path";
import * as touch from "touch";
import { assert, BeEvent, BentleyStatus, ChangeSetStatus, DbChangeStage, DbConflictCause, DbConflictResolution, DbResult, Guid, Id64, IModelStatus, JsonUtils, Logger, LogLevel, LRUMap, OpenMode } from "@itwin/core-bentley";
import { BriefcaseIdValue, Code, DomainOptions, ECJsNames, ECSqlReader, EntityMetaData, FontMap, IModel, IModelError, IModelNotFoundResponse, ProfileOptions, QueryRowFormat, SchemaState, ViewStoreError, ViewStoreRpc } from "@itwin/core-common";
import { Range2d, Range3d } from "@itwin/core-geometry";
import { BackendLoggerCategory } from "./BackendLoggerCategory";
import { BriefcaseManager } from "./BriefcaseManager";
import { ChannelControl } from "./ChannelControl";
import { createChannelControl } from "./internal/ChannelAdmin";
import { CheckpointManager, V2CheckpointManager } from "./CheckpointManager";
import { ClassRegistry, EntityJsClassMap, MetaDataRegistry } from "./ClassRegistry";
import { CloudSqlite } from "./CloudSqlite";
import { CodeService } from "./CodeService";
import { CodeSpecs } from "./CodeSpecs";
import { ConcurrentQuery } from "./ConcurrentQuery";
import { ECSqlStatement } from "./ECSqlStatement";
import { Element } from "./Element";
import { generateElementGraphics } from "./ElementGraphics";
import { Entity } from "./Entity";
import { GeoCoordConfig } from "./GeoCoordConfig";
import { IModelHost } from "./IModelHost";
import { IModelJsFs } from "./IModelJsFs";
import { IpcHost } from "./IpcHost";
import { Model } from "./Model";
import { Relationships } from "./Relationship";
import { SchemaSync } from "./SchemaSync";
import { createServerBasedLocks } from "./internal/ServerBasedLocks";
import { SqliteStatement, StatementCache } from "./SqliteStatement";
import { TxnManager } from "./TxnManager";
import { DrawingViewDefinition, SheetViewDefinition, ViewDefinition } from "./ViewDefinition";
import { ViewStore } from "./ViewStore";
import { SettingsPriority } from "./workspace/Settings";
import { Workspace, WorkspaceSettingNames } from "./workspace/Workspace";
import { constructWorkspace, throwWorkspaceDbLoadErrors } from "./internal/workspace/WorkspaceImpl";
import { SettingsImpl } from "./internal/workspace/SettingsImpl";
import { IModelNative } from "./internal/NativePlatform";
import { createNoOpLockControl } from "./internal/NoLocks";
import { createIModelDbFonts } from "./internal/IModelDbFontsImpl";
import { _cache, _close, _hubAccess, _instanceKeyCache, _nativeDb, _releaseAllLocks, _resetIModelDb } from "./internal/Symbols";
import { ECVersion, SchemaContext, SchemaJsonLocater } from "@itwin/ecschema-metadata";
import { SchemaMap } from "./Schema";
import { ElementLRUCache, InstanceKeyLRUCache } from "./internal/ElementLRUCache";
import { IModelIncrementalSchemaLocater } from "./IModelIncrementalSchemaLocater";
// spell:ignore fontid fontmap
const loggerCategory = BackendLoggerCategory.IModelDb;
/** @internal */
export var BriefcaseLocalValue;
(function (BriefcaseLocalValue) {
BriefcaseLocalValue["StandaloneEdit"] = "StandaloneEdit";
BriefcaseLocalValue["NoLocking"] = "NoLocking";
})(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 {
verifyPriority(priority) {
if (priority <= SettingsPriority.application)
throw new Error("Use IModelHost.appSettings to access settings of priority 'application' or lower");
}
*getSettingEntries(name) {
yield* super.getSettingEntries(name);
yield* 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
*/
export class IModelDb extends 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 = createChannelControl(this);
_relationships;
// eslint-disable-next-line @typescript-eslint/no-deprecated
_statementCache = new StatementCache();
_sqliteStatementCache = new 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 = createIModelDbFonts(this);
_workspace;
_snaps = new Map();
static _shutdownListener; // so we only register listener once
/** @internal */
_locks = 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 = constructWorkspace(new IModelSettings());
return this._workspace;
}
/**
* get the cloud container for this iModel, if it was opened from one
* @beta
*/
get cloudContainer() {
return this[_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: IModel.repositoryModelId });
}
/** determine whether the schema lock is currently held for this iModel. */
get holdsSchemaLock() {
return this.locks.holdsExclusiveLock(IModel.repositoryModelId);
}
/** Event called after a changeset is applied to this IModelDb. */
onChangesetApplied = new BeEvent();
/** @internal */
notifyChangesetApplied() {
this.changeset = this[_nativeDb].getCurrentChangeset();
this.onChangesetApplied.raiseEvent();
}
/** @internal */
restartDefaultTxn() {
this[_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 FontMap(this[_nativeDb].readFontMap())); // eslint-disable-line @typescript-eslint/no-deprecated
}
/** @internal */
clearFontMap() {
this._fontMap = undefined; // eslint-disable-line @typescript-eslint/no-deprecated
this[_nativeDb].invalidateFontMap();
}
/** Check if this iModel has been opened read-only or not. */
get isReadonly() { return this.openMode === OpenMode.Readonly; }
/** The Guid that identifies this iModel. */
get iModelId() {
assert(undefined !== super.iModelId);
return super.iModelId;
} // GuidString | undefined for the IModel superclass, but required for all IModelDb subclasses
/** @internal*/
[_nativeDb];
/** Get the full path fileName of this iModelDb
* @note this member is only valid while the iModel is opened.
*/
get pathName() { return this[_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[_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[_nativeDb].closeIModel = () => {
if (!this.isReadonly)
this.saveChanges(); // preserve old behavior of closeIModel that was removed when renamed to closeFile
this[_nativeDb].closeFile();
};
this[_nativeDb].setIModelDb(this);
this[_resetIModelDb]();
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.onBeforeShutdown.addListener(() => {
IModelDb._openDbs.forEach((db) => {
try {
db.abandonChanges();
db.close();
}
catch { }
});
});
}
}
/** @internal */
[_resetIModelDb]() {
this.loadIModelSettings();
GeoCoordConfig.loadForImodel(this.workspace.settings); // load gcs data specified by iModel's settings dictionaries, must be done before calling initializeIModelDb
this.initializeIModelDb();
}
/**
* 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 identifier 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 IModelError(DbResult.BE_SQLITE_ERROR, "Reserved tablespace name cannot be used");
}
this[_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 IModelError(DbResult.BE_SQLITE_ERROR, "Reserved tablespace name cannot be used");
}
this.clearCaches();
this[_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[_close]();
this._locks = undefined;
this._codeService?.close();
this._codeService = undefined;
if (!this.isReadonly)
this.saveChanges();
this[_nativeDb].closeFile();
}
/** @internal */
async refreshContainerForRpc(_userAccessToken) { }
/** Event called when the iModel is about to be closed. */
onBeforeClose = new BeEvent();
/**
* Called by derived classes before closing the connection
* @internal
*/
beforeClose() {
this.onBeforeClose.raiseEvent();
this.clearCaches();
}
/** @internal */
initializeIModelDb(when) {
const props = this[_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.isValid)
return;
db.onNameChanged.addListener(() => IpcHost.notifyTxns(db, "notifyIModelNameChanged", db.name));
db.onRootSubjectChanged.addListener(() => IpcHost.notifyTxns(db, "notifyRootSubjectChanged", db.rootSubject));
db.onProjectExtentsChanged.addListener(() => IpcHost.notifyTxns(db, "notifyProjectExtentsChanged", db.projectExtents.toJSON()));
db.onGlobalOriginChanged.addListener(() => IpcHost.notifyTxns(db, "notifyGlobalOriginChanged", db.globalOrigin.toJSON()));
db.onEcefLocationChanged.addListener(() => IpcHost.notifyTxns(db, "notifyEcefLocationChanged", db.ecefLocation?.toJSON()));
db.onGeographicCoordinateSystemChanged.addListener(() => 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[_nativeDb].isOpen(); }
/** Get the briefcase Id of this iModel */
getBriefcaseId() { return this.isOpen ? this[_nativeDb].getBriefcaseId() : 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[_nativeDb].isOpen())
throw new IModelError(DbResult.BE_SQLITE_ERROR, "db not open");
const executor = {
execute: async (request) => {
return ConcurrentQuery.executeQueryRequest(this[_nativeDb], request);
},
};
return new 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(sql);
stmt.prepare(this[_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: 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: 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 IModelError(IModelStatus.BadRequest, "Max LIMIT exceeded in SELECT statement");
}
}
}
});
return ids;
}
clearCaches(params) {
if (!params?.instanceCachesOnly) {
this._statementCache.clear();
this._sqliteStatementCache.clear();
this._classMetaDataRegistry = undefined;
this._jsClassMap = undefined;
this._schemaMap = undefined;
this._schemaContext = undefined;
this[_nativeDb].clearECDbCache();
}
this.elements[_cache].clear();
this.models[_cache].clear();
this.elements[_instanceKeyCache].clear();
this.models[_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[_nativeDb].computeProjectExtents(wantFullExtents, wantOutliers);
return {
extents: Range3d.fromJSON(result.extents),
extentsWithOutliers: result.fullExtents ? 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[_nativeDb].updateIModelProps(this.toJSON());
}
/** Commit unsaved changes in memory as a Txn to this iModelDb.
* @internal
* @param descriptionOrArgs Optionally provide description or [[SaveChangesArgs]] args for 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.
* @note This method should not be called from {TxnManager.withIndirectTxnModeAsync}, {TxnManager.withIndirectTxnMode} or {RebaseHandler.recompute}.
* @see [[IModelDb.pushChanges]] to push changes to the iModelHub.
*/
saveChanges(descriptionOrArgs) {
if (this.openMode === OpenMode.Readonly)
throw new IModelError(IModelStatus.ReadOnly, "IModelDb was opened read-only");
if (this instanceof BriefcaseDb) {
if (this.txns.isIndirectChanges) {
throw new IModelError(IModelStatus.BadRequest, "Cannot save changes while in an indirect change scope");
}
}
const args = typeof descriptionOrArgs === "string" ? { description: descriptionOrArgs } : descriptionOrArgs;
if (!this[_nativeDb].hasUnsavedChanges()) {
Logger.logWarning(loggerCategory, "there are no unsaved changes", () => args);
}
const stat = this[_nativeDb].saveChanges(args ? JSON.stringify(args) : undefined);
if (DbResult.BE_SQLITE_ERROR_PropagateChangesFailed === stat)
throw new IModelError(stat, `Could not save changes due to propagation failure.`);
if (DbResult.BE_SQLITE_OK !== stat)
throw new IModelError(stat, `Could not save changes (${args?.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() {
// Clears instanceKey caches only, instead of all of the backend caches, since the changes are not saved yet
this.clearCaches({ instanceCachesOnly: true });
this[_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[_nativeDb].concurrentQueryShutdown();
this[_nativeDb].performCheckpoint();
}
}
/** @internal
* @deprecated in 4.8 - will not be removed until after 2026-06-13. Use `txns.reverseTxns`.
*/
reverseTxns(numOperations) {
return this[_nativeDb].reverseTxns(numOperations);
}
/** @internal */
reinstateTxn() {
return this[_nativeDb].reinstateTxn();
}
/** @internal */
restartTxnSession() {
return this[_nativeDb].restartTxnSession();
}
/** Removes unused schemas from the database.
*
* If the removal was successful, the database is automatically saved to disk.
* @param schemaNames Array of schema names to drop
* @throws [IModelError]($common) if the database if the operation failed.
* @alpha
*/
async dropSchemas(schemaNames) {
if (schemaNames.length === 0)
return;
if (this[_nativeDb].schemaSyncEnabled())
throw new IModelError(DbResult.BE_SQLITE_ERROR, "Cannot drop schemas when schema sync is enabled");
if (this[_nativeDb].hasUnsavedChanges())
throw new IModelError(ChangeSetStatus.HasUncommittedChanges, "Cannot drop schemas with unsaved changes");
if (this[_nativeDb].getITwinId() !== Guid.empty)
await this.acquireSchemaLock();
try {
this[_nativeDb].dropSchemas(schemaNames);
this.saveChanges(`dropped unused schemas`);
}
catch (error) {
Logger.logError(loggerCategory, `Failed to drop schemas: ${error}`);
this.abandonChanges();
throw new IModelError(DbResult.BE_SQLITE_ERROR, `Failed to drop schemas: ${error}`);
}
finally {
await this.locks.releaseAllLocks();
this.clearCaches();
}
}
/** 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.
* @note This method should not be called from {TxnManager.withIndirectTxnModeAsync} or {RebaseHandler.recompute}.
* @see querySchemaVersion
*/
async importSchemas(schemaFileNames, options) {
if (schemaFileNames.length === 0)
return;
if (this instanceof BriefcaseDb) {
if (this.txns.rebaser.isRebasing) {
throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas while rebasing");
}
if (this.txns.isIndirectChanges) {
throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas while in an indirect change scope");
}
}
const maybeCustomNativeContext = options?.ecSchemaXmlContext?.nativeContext;
if (this[_nativeDb].schemaSyncEnabled()) {
await SchemaSync.withLockedAccess(this, { openMode: OpenMode.Readonly, operationName: "schema sync" }, async (syncAccess) => {
const schemaSyncDbUri = syncAccess.getUri();
this.saveChanges();
try {
this[_nativeDb].importSchemas(schemaFileNames, { schemaLockHeld: false, ecSchemaXmlContext: maybeCustomNativeContext, schemaSyncDbUri });
}
catch (outerErr) {
if (DbResult.BE_SQLITE_ERROR_DataTransformRequired === outerErr.errorNumber) {
this.abandonChanges();
if (this[_nativeDb].getITwinId() !== Guid.empty)
await this.acquireSchemaLock();
try {
this[_nativeDb].importSchemas(schemaFileNames, { schemaLockHeld: true, ecSchemaXmlContext: maybeCustomNativeContext, schemaSyncDbUri });
}
catch (innerErr) {
throw new IModelError(innerErr.errorNumber, innerErr.message);
}
}
else {
throw new IModelError(outerErr.errorNumber, outerErr.message);
}
}
});
}
else {
const nativeImportOptions = {
schemaLockHeld: true,
ecSchemaXmlContext: maybeCustomNativeContext,
};
if (this[_nativeDb].getITwinId() !== Guid.empty) // if this iModel is associated with an iTwin, importing schema requires the schema lock
await this.acquireSchemaLock();
try {
this[_nativeDb].importSchemas(schemaFileNames, nativeImportOptions);
}
catch (err) {
throw new 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.
* @note This method should not be called from {TxnManager.withIndirectTxnModeAsync} or {RebaseHandler.recompute}.
* @see querySchemaVersion
* @alpha
*/
async importSchemaStrings(serializedXmlSchemas) {
if (serializedXmlSchemas.length === 0)
return;
if (this instanceof BriefcaseDb) {
if (this.txns.rebaser.isRebasing) {
throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas while rebasing");
}
if (this.txns.isIndirectChanges) {
throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas while in an indirect change scope");
}
}
if (this[_nativeDb].schemaSyncEnabled()) {
await SchemaSync.withLockedAccess(this, { openMode: OpenMode.Readonly, operationName: "schemaSync" }, async (syncAccess) => {
const schemaSyncDbUri = syncAccess.getUri();
this.saveChanges();
try {
this[_nativeDb].importXmlSchemas(serializedXmlSchemas, { schemaLockHeld: false, schemaSyncDbUri });
}
catch (outerErr) {
if (DbResult.BE_SQLITE_ERROR_DataTransformRequired === outerErr.errorNumber) {
this.abandonChanges();
if (this[_nativeDb].getITwinId() !== Guid.empty)
await this.acquireSchemaLock();
try {
this[_nativeDb].importXmlSchemas(serializedXmlSchemas, { schemaLockHeld: true, schemaSyncDbUri });
}
catch (innerErr) {
throw new IModelError(innerErr.errorNumber, innerErr.message);
}
}
else {
throw new IModelError(outerErr.errorNumber, outerErr.message);
}
}
});
}
else {
if (this.iTwinId && this.iTwinId !== Guid.empty) // if this iModel is associated with an iTwin, importing schema requires the schema lock
await this.acquireSchemaLock();
try {
this[_nativeDb].importXmlSchemas(serializedXmlSchemas, { schemaLockHeld: true });
}
catch (err) {
throw new 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 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 ?? Guid.createValue();
if (this.tryFindByKey(file.key))
throw new IModelError(IModelStatus.AlreadyOpen, `key [${file.key}] for file [${file.path}] is already in use`);
const isUpgradeRequested = upgradeOptions?.domain === DomainOptions.Upgrade || upgradeOptions?.profile === ProfileOptions.Upgrade;
if (isUpgradeRequested && openMode !== OpenMode.ReadWrite)
throw new IModelError(IModelStatus.UpgradeFailed, "Cannot upgrade a Readonly Db");
try {
const nativeDb = new 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 = join(IModelHost.profileDir, "CloudDbTemp", container.containerId);
IModelJsFs.recursiveMkDirSync(baseDir);
props = { ...props, tempFileBase: join(baseDir, file.path) };
}
nativeDb.openIModel(file.path, openMode, upgradeOptions, props, props?.container, props);
return nativeDb;
}
catch (err) {
throw new 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 ? OpenMode.ReadWrite : OpenMode.Readonly;
const file = { path: filePath };
let result = DbResult.BE_SQLITE_OK;
try {
const upgradeOptions = {
domain: DomainOptions.CheckRecommendedUpgrades,
};
const nativeDb = this.openDgnDb(file, openMode, upgradeOptions);
nativeDb.closeFile();
}
catch (err) {
result = err.errorNumber;
}
let schemaState = SchemaState.UpToDate;
switch (result) {
case DbResult.BE_SQLITE_OK:
schemaState = SchemaState.UpToDate;
break;
case DbResult.BE_SQLITE_ERROR_ProfileTooOld:
case DbResult.BE_SQLITE_ERROR_ProfileTooOldForReadWrite:
case DbResult.BE_SQLITE_ERROR_SchemaTooOld:
schemaState = SchemaState.TooOld;
break;
case DbResult.BE_SQLITE_ERROR_ProfileTooNew:
case DbResult.BE_SQLITE_ERROR_ProfileTooNewForReadWrite:
case DbResult.BE_SQLITE_ERROR_SchemaTooNew:
schemaState = SchemaState.TooNew;
break;
case DbResult.BE_SQLITE_ERROR_SchemaUpgradeRecommended:
schemaState = SchemaState.UpgradeRecommended;
break;
case DbResult.BE_SQLITE_ERROR_SchemaUpgradeRequired:
schemaState = SchemaState.UpgradeRequired;
break;
case DbResult.BE_SQLITE_ERROR_InvalidProfileVersion:
throw new IModelError(DbResult.BE_SQLITE_ERROR_InvalidProfileVersion, "The profile of the Db is invalid. Cannot upgrade or open the Db.");
default:
throw new IModelError(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 MetaDataRegistry();
return this._classMetaDataRegistry;
}
/**
* Allows registering js classes mapped to ECClasses
*/
get jsClassMap() {
if (this._jsClassMap === undefined)
this._jsClassMap = new 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 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 SchemaContext();
if (IModelHost.configuration && IModelHost.configuration.incrementalSchemaLoading === "enabled") {
context.addLocater(new IModelIncrementalSchemaLocater(this));
}
context.addLocater(new SchemaJsonLocater((name) => this.getSchemaProps(name)));
this._schemaContext = context;
}
return this._schemaContext;
}
/** Get the linkTableRelationships for this IModel */
get relationships() {
return this._relationships || (this._relationships = new Relationships(this));
}
/** Get the CodeSpecs in this IModel. */
get codeSpecs() {
return (this._codeSpecs !== undefined) ? this._codeSpecs : (this._codeSpecs = new 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-deprec