UNPKG

@itwin/core-backend

Version:
994 lines • 221 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.DataTransformationStrategy = 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 ECSqlInstanceReshaper_1 = require("./internal/ECSqlInstanceReshaper"); 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 EditTxn_1 = require("./EditTxn"); 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"); const IModelIncrementalSchemaLocater_1 = require("./IModelIncrementalSchemaLocater"); const ECSqlRowExecutor_1 = require("./ECSqlRowExecutor"); const IntegrityCheck_1 = require("./internal/IntegrityCheck"); const ECSqlSyncReader_1 = require("./ECSqlSyncReader"); // spell:ignore fontid fontmap const loggerCategory = BackendLoggerCategory_1.BackendLoggerCategory.IModelDb; /** * Internal write surface used to preserve legacy implicit-transaction mutators while callers migrate to explicit [[EditTxn]] scopes. * * Unlike an explicit [[EditTxn]], this transaction is always available for writable iModels and cannot be manually started or ended. * When implicit-write enforcement is enabled, attempts to write through this transaction are logged or rejected. */ class ImplicitWriteTxn extends EditTxn_1.EditTxn { constructor(iModel) { super(iModel, "implicit"); } start() { throw new Error("ImplicitWriteTxn cannot be started"); } end(_mode = "save", _args) { throw new Error("ImplicitWriteTxn cannot be ended"); } verifyWriteable() { const enforcement = EditTxn_1.EditTxn.implicitWriteEnforcement; if (enforcement === "allow") return; try { core_common_1.EditTxnError.throwError("implicit-txn-write-disallowed", "Implicit transaction write is disallowed. Use an explicit EditTxn instead", this.iModel.key); } catch (err) { if (enforcement === "log") { core_bentley_1.Logger.logError(loggerCategory, err); return; } throw err; } } } /** @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); } } /** * Strategy for transforming data during schema import. * @beta */ var DataTransformationStrategy; (function (DataTransformationStrategy) { /** No data transformation will be performed after schema import. */ DataTransformationStrategy["None"] = "None"; /** Data transformation will be performed using a temporary snapshot created before schema import. * Useful for complex transformations requiring full read access to complete pre-import state for lazy conversion. * Note: Creates a complete copy of the briefcase file, which may be large. */ DataTransformationStrategy["Snapshot"] = "Snapshot"; /** Data transformation will be performed using in-memory cached data created before schema import. * Useful for lightweight transformations involving limited data. */ DataTransformationStrategy["InMemory"] = "InMemory"; })(DataTransformationStrategy || (exports.DataTransformationStrategy = DataTransformationStrategy = {})); /** 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; // Created lazily on the first getSchemaView call. Owns the SchemaView's lifetime and does all its // data access through the SchemaViewDataProvider implemented below. _schemaViewManager; /** @deprecated in 5.0.0 - might be removed in next major version. 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; /** * The always-available implicit transaction for this iModel. * * Legacy mutating APIs route through this transaction for backwards compatibility until they are fully migrated to explicit [[EditTxn]] usage. * @internal */ [Symbols_1._implicitTxn]; /** @internal */ [Symbols_1._activeTxn]; /** Returns the active [[EditTxn]] if one is current, otherwise the implicit transaction. * Use this inside element and relationship callbacks that may be invoked either during an explicit transaction or * during indirect change processing. * @note This method is a temporary workaround until [[OnElementArg]] (and related callback arg types) are updated * to carry the transaction directly in a future PR. * @internal */ getIndirectTxn() { return this[Symbols_1._activeTxn] ?? this[Symbols_1._implicitTxn]; } /** @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 - might be removed in next major version. 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[Symbols_1._nativeDb].saveChanges(); // preserve old behavior of closeIModel that was removed when renamed to closeFile this[Symbols_1._activeTxn] = undefined; this[Symbols_1._nativeDb].closeFile(); }; this[Symbols_1._nativeDb].setIModelDb(this); this[Symbols_1._resetIModelDb](); IModelDb._openDbs.set(this._fileKey, this); this[Symbols_1._implicitTxn] = new ImplicitWriteTxn(this); this[Symbols_1._activeTxn] = undefined; 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[Symbols_1._nativeDb].abandonChanges(); db.close(); } catch { } }); }); } } /** @internal */ [Symbols_1._resetIModelDb]() { 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(); } /** * 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 * [[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 reserved table names that cannot be used. They are 'main', 'schema_sync_db', 'ecchange' & 'temp' * @param alias identifier that was used in the call to [[attachDb]] * * @example [[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. * @param options Options for closing the iModel. */ close(options) { if (!this.isOpen) return; // don't continue if already closed // Give the active txn a chance to save or abandon before beforeClose() cleanup runs. // StandaloneDb.beforeClose() saves any unsaved changes, so onClose() must run first so // subclasses that override onClose() to abandon changes can do so before that save. if (!this.isReadonly) (this[Symbols_1._activeTxn] ?? this[Symbols_1._implicitTxn]).onClose(); this.beforeClose(); this[Symbols_1._activeTxn] = undefined; if (options?.optimize) this.optimize(); IModelDb._openDbs.delete(this._fileKey); this._workspace?.close(); this.locks[Symbols_1._close](); this._locks = undefined; this._codeService?.close(); this._codeService = undefined; this[Symbols_1._nativeDb].closeFile(); } saveSchemaChanges(args) { if (!this[Symbols_1._nativeDb].hasUnsavedChanges()) return; const saveArgs = typeof args === "string" ? { description: args } : args; saveArgs === undefined ? this[Symbols_1._nativeDb].saveChanges() : this[Symbols_1._nativeDb].saveChanges(JSON.stringify(saveArgs)); } abandonSchemaChanges() { if (!this[Symbols_1._nativeDb].hasUnsavedChanges()) return; this.clearCaches({ instanceCachesOnly: true }); this[Symbols_1._nativeDb].abandonChanges(); } /** Optimize this iModel by vacuuming, and analyzing. * * @note This operation requires exclusive access to the database and may take some time on large files. * @beta */ optimize() { // Vacuum to reclaim space and defragment this.vacuum(); // Analyze to update statistics for query optimizer this.analyze(); } /** * Vacuum the model to reclaim space and defragment. * @throws [[IModelError]] if the iModel is not open or is read-only. * @beta */ vacuum() { if (!this.isOpen || this.isReadonly) throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "IModel is not open or is read-only"); this[Symbols_1._nativeDb].clearECDbCache(); this[Symbols_1._nativeDb].vacuum(); } /** * Update SQLite query optimizer statistics for this iModel. * This helps SQLite choose better query plans. * * @throws [[IModelError]] if the iModel is not open or is read-only. * @beta */ analyze() { if (!this.isOpen || this.isReadonly) throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "IModel is not open or is read-only"); this[Symbols_1._nativeDb].analyze(); } /** * Performs integrity checks on this iModel. * Types of integrity checks that can be performed are: * * Default Check: * - Quick Check: Runs all integrity checks below and returns whether each check passed or failed, without detailed results. * * Specific Checks: * - Data Columns Check: Checks if all the required columns exist in data tables. Issues are returned as a list of those tables/columns. * - EC Profile Check: Checks if the profile table, indexes, and triggers are present. Does not check be_* tables. Issues are returned as a list of tables/indexes/triggers which were not found or have different DDL. * - Navigation Class Ids Check: Checks if RelClassId of a Navigation property is a valid ECClassId. It does not check the value to match the relationship class. * - Navigation Ids Check: Checks if Id of a Navigation property matches a valid row primary class. * - Linktable Foreign Key Class Ids Check: Checks if SourceECClassId or TargetECClassId of a link table matches a valid ECClassId. * - Linktable Foreign Key Ids Check: Checks if SourceECInstanceId or TargetECInstanceId of a link table matches a valid row in primary class. * - Class Ids Check: Checks persisted ECClassId in all data tables and makes sure they are valid. * - Data Schema Check: Checks if all the required data tables and indexes exist for mapped classes. Issues are returned as a list of tables/columns which were not found or have different DDL. * - Schema Load Check: Checks if all schemas can be loaded into memory. * - Missing Child Rows Check: Checks if all child rows have a corresponding parent row. * * @param options Options specifying which integrity checks to perform. If no options are provided or all options are false, a quick check will be performed by default. * @returns An array of integrity check results. * @throws [[IModelError]] if the iModel is not open. * @beta */ async integrityCheck(options) { if (!this.isOpen) throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "IModel is not open"); // Default to quick check if no options provided at all, or if not explicitly set and no specific checks are enabled if (!options || (!options.quickCheck && (!options.specificChecks || !Object.values(options.specificChecks).some(Boolean)))) { options = { ...options, quickCheck: true }; } const integrityCheckResults = []; // Perform a quick check if requested if (options.quickCheck) { const results = await (0, IntegrityCheck_1.performQuickIntegrityCheck)(this); const passed = results.every((result) => result.passed); integrityCheckResults.push({ check: "Quick Check", passed, results }); } // Perform all specific checks requested if (options.specificChecks) { for (const [checkKey, checkParams] of Object.entries(IntegrityCheck_1.integrityCheckTypeMap)) { if (options.specificChecks[checkKey]) { const results = await (0, IntegrityCheck_1.performSpecificIntegrityCheck)(this, checkKey); const passed = results.length === 0; integrityCheckResults.push({ check: checkParams.name, passed, results }); } } } return integrityCheckResults; } /** @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 - might be removed in next major version. 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 - might be removed in next major version. 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. * Should be used when we donot want true step by step behaviour and want to take advantage of caching capabilities of the reader. * @public * */ createQueryReader(ecsql, params, config) { if (!this[Symbols_1._nativeDb].isOpen()) throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR_NOTOPEN, "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); } /** Allow to execute query and read results along with meta data. The result are stepped one by one. * * See also: * - [ECSQL Overview]($docs/learning/backend/ExecutingECSQL) * - [Code Examples]($docs/learning/backend/ECSQLCodeExamples) * - [ECSQL Row Format]($docs/learning/ECSQLRowFormat) * @param ecsql The ECSQL query to execute. * @param callback the callback to invoke on the prepared ECSqlReader * @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 the value returned by `callback`. * @throws IModelError if db is not open. * Use this method for true step-by-step row consumption without intermediate result or page caching. * The prepared ECSQL statement may be reused from the statement cache between completed calls. * @beta * */ withQueryReader(ecsql, callback, params, config) { if (!this[Symbols_1._nativeDb].isOpen()) throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR_NOTOPEN, "db not open"); // eslint-disable-next-line @typescript-eslint/no-deprecated const stmt = this._statementCache.findAndRemove(ecsql) ?? new ECSqlStatement_1.ECSqlStatement(); const executor = new ECSqlRowExecutor_1.ECSqlRowExecutor(this, stmt, loggerCategory); const release = () => { executor[Symbol.dispose](); (0, ECSqlRowExecutor_1.releaseECSqlStatement)(stmt, this._statementCache, loggerCategory, executor.canCacheStatement); }; try { const reader = new ECSqlSyncReader_1.ECSqlSyncReader(executor, ecsql, params, config); const val = callback(reader); if (val instanceof Promise) { val.then(release, release); } else { release(); } return val; } catch (err) { release(); throw err; } } /** * 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.UseECSqlPropertyNames })) { 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.UseECSqlPropertyNames })) { 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(); this.withQueryReader(sql, (reader) => { for (const row of reader) { const id = row[0]; if (id !== undefined) { ids.add(id); if (ids.size > IModelDb.maxLimit) { throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Max LIMIT exceeded in SELECT statement"); } } } }, core_common_1.QueryBinder.fromSkippingNullish(params.bindings)); 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._schemaViewManager?.reset(); this[Symbols_1._nativeDb].clearECDbCache(); } 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]] * ``` * @deprecated in 5.9.0 - will not be removed until after 2027-05-04. Use EditTxn.updateProjectExtents instead, within an explicit EditTxn scope (or via withEditTxn). See EditTxn documentation for migration help. */ updateProjectExtents(newExtents) { this[Symbols_1._implicitTxn].updateProjectExtents(newExtents); } /** 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. * @deprecated in 5.9.0 - will not be removed until after 2027-05-04. Use EditTxn.updateEcefLocation instead, within an explicit EditTxn scope (or via withEditTxn). See EditTxn documentation for migration help. */ updateEcefLocation(ecef) { this[Symbols_1._implicitTxn].updateEcefLocation(ecef); } /** Update the IModelProps of this iModel in the database. * @deprecated in 5.9.0 - will not be removed until after 2027-05-04. Use EditTxn.updateIModelProps instead, within an explicit EditTxn scope (or via withEditTxn). */ updateIModelProps() { this[Symbols_1._implicitTxn].updateIModelProps(); } saveChanges(descriptionOrArgs) { this[Symbols_1._implicitTxn].saveChanges(descriptionOrArgs); } /** Abandon changes in memory that have not been saved as a Txn to this iModelDb. * @deprecated in 5.9.0 - will not be removed until after 2027-05-04. Use EditTxn.abandonChanges instead, within an explicit EditTxn scope (or via withEditTxn). See EditTxn documentation for migration help. */ abandonChanges() { this[Symbols_1._implicitTxn].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[Symbols_1._nativeDb].saveChanges(); this.clearCaches(); this[Symbols_1._nativeDb].concurrentQueryShutdown(); this[Symbols_1._nativeDb].performCheckpoint(); } } /** @internal * @deprecated in 4.8 - might be removed in next major version. 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(); } /** * Get the class full name from a class Id. * @param classId the Id of the class to look up * @returns the full name of the class (e.g. "BisCore:Element") * @throws IModelError if the classId is invalid or the class is not found. * @internal */ getClassNameFromId(classId) { if (!core_bentley_1.Id64.isValid(classId)) throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, `Class Id ${classId} is invalid`); const name = this[Symbols_1._nativeDb].classIdToName(classId); if (name === undefined) throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, `Class not found: ${classId}`); return name; } /** 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]] if the operation fails. * @alpha */ async dropSchemas(schemaNames) { if (schemaNames.length === 0) return; if (this[Symbols_1._nativeDb].schemaSyncEnabled()) throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, "Cannot drop schemas when schema sync is enabled"); if (this[Symbols_1._nativeDb].hasUnsavedChanges()) throw new core_common_1.IModelError(core_bentley_1.ChangeSetStatus.HasUncommittedChanges, "Cannot drop schemas with unsaved changes"); if (this[Symbols_1._nativeDb].getITwinId() !== core_bentley_1.Guid.empty) await this.acquireSchemaLock(); try { this[Symbols_1._nativeDb].dropSchemas(schemaNames); this.saveSchemaChanges("dropped unused schemas"); } catch (error) { core_bentley_1.Logger.logError(loggerCategory, `Failed to drop schemas: ${error}`); this.abandonSchemaChanges(); throw new core_common_1.IModelError(core_bentley_1.DbResult.BE_SQLITE_ERROR, `Failed to drop schemas: ${error}`); } finally { await this.locks.releaseAllLocks(); this.clearCaches(); } } /** Helper to clean up snapshot resources safely * @internal */ cleanupSnapshot(resources) { if (resources.snapshot) { const pathName = resources.snapshot.pathName; resources.snapshot.close(); if (pathName && IModelJsFs_1.IModelJsFs.existsSync(pathName)) { IModelJsFs_1.IModelJsFs.removeSync(pathName); } } } async preSchemaImportCallback(callback, context) { const callbackResources = { transformStrategy: DataTransformationStrategy.None, }; try { const preSchemaImportCallback = callback?.preSchemaImportCallback; if (preSchemaImportCallback) { const callbackResult = await preSchemaImportCallback(context); callbackResources.transformStrategy = callbackResult.transformStrategy; if (callbackResult.transformStrategy === DataTransformationStrategy.Snapshot) { // Create temporary snapshot file const snapshotDb = SnapshotDb.createFrom(this, `${this.pathName}.snapshot-${Date.now()}`); callbackResources.snapshot = snapshotDb; } else if (callbackResult.transformStrategy === DataTransformationStrategy.InMemory) { if (callbackResult.cachedData === undefined) { throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "InMemory transform strategy requires cachedData to be provided."); } callbackResources.cachedData = callbackResult.cachedData; } if (this.isBriefcaseDb() && IModelHost_1.IModelHost.useSemanticRebase) this.saveSchemaChanges("Save changes from schema import pre callback"); } } catch (callbackError) { this.abandonSchemaChanges(); this.cleanupSnapshot(callbackResources); throw new core_common_1.IModelError(callbackError.errorNumber ?? core_bentley_1.IModelStatus.BadRequest, `Failed to execute preSchemaImportCallback: ${callbackError.message}`); } return callbackResources; } async postSchemaImportCallback(callback, context) { if (context.resources.transformStrategy === DataTransformationStrategy.Snapshot && (context.resources.snapshot === undefined || !IModelJsFs_1.IModelJsFs.existsSync(context.resources.snapshot.pathName))) { throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Snapshot transform strategy requires a snapshot to be created"); } if (context.resources.transformStrategy === DataTransformationStrategy.InMemory && context.resources.cachedData === undefined) { throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "InMemory transform strategy requires cachedData to be provided."); } try { const postSchemaImportCallback = callback?.postSchemaImportCallback; if (postSchemaImportCallback) await postSchemaImportCallback(context); if (this.isBriefcaseDb() && IModelHost_1.IModelHost.useSema