UNPKG

@itwin/core-backend

Version:
996 lines • 183 kB
"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