@itwin/core-backend
Version:
iTwin.js backend components
1,018 lines • 212 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, EditTxnError, EntityMetaData, FontMap, IModel, IModelError, IModelNotFoundResponse, ProfileOptions, QueryBinder, QueryRowFormat, resolveNavPropId, 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 { getRuntimeClass, reshapeInstanceRow } from "./internal/ECSqlInstanceReshaper";
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 { EditTxn } from "./EditTxn";
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 { _activeTxn, _cache, _close, _hubAccess, _implicitTxn, _instanceKeyCache, _nativeDb, _releaseAllLocks, _resetIModelDb } from "./internal/Symbols";
import { ECVersion, SchemaContext, SchemaJsonLocater, SchemaManifest, SchemaViewManager } from "@itwin/ecschema-metadata";
import { SchemaMap } from "./Schema";
import { ElementLRUCache, InstanceKeyLRUCache } from "./internal/ElementLRUCache";
import { IModelIncrementalSchemaLocater } from "./IModelIncrementalSchemaLocater";
import { ECSqlRowExecutor, releaseECSqlStatement } from "./ECSqlRowExecutor";
import { integrityCheckTypeMap, performQuickIntegrityCheck, performSpecificIntegrityCheck } from "./internal/IntegrityCheck";
import { ECSqlSyncReader } from "./ECSqlSyncReader";
// spell:ignore fontid fontmap
const loggerCategory = 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 {
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.implicitWriteEnforcement;
if (enforcement === "allow")
return;
try {
EditTxnError.throwError("implicit-txn-write-disallowed", "Implicit transaction write is disallowed. Use an explicit EditTxn instead", this.iModel.key);
}
catch (err) {
if (enforcement === "log") {
Logger.logError(loggerCategory, err);
return;
}
throw err;
}
}
}
/** @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);
}
}
/**
* Strategy for transforming data during schema import.
* @beta
*/
export 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 || (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
*/
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;
// 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 = createIModelDbFonts(this);
_workspace;
_snaps = new Map();
static _shutdownListener; // so we only register listener once
/** @internal */
_locks = 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
*/
[_implicitTxn];
/** @internal */
[_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[_activeTxn] ?? this[_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 = 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 - might be removed in next major version. 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[_nativeDb].saveChanges(); // preserve old behavior of closeIModel that was removed when renamed to closeFile
this[_activeTxn] = undefined;
this[_nativeDb].closeFile();
};
this[_nativeDb].setIModelDb(this);
this[_resetIModelDb]();
IModelDb._openDbs.set(this._fileKey, this);
this[_implicitTxn] = new ImplicitWriteTxn(this);
this[_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.onBeforeShutdown.addListener(() => {
IModelDb._openDbs.forEach((db) => {
try {
db[_nativeDb].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
* [[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 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 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.
* @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[_activeTxn] ?? this[_implicitTxn]).onClose();
this.beforeClose();
this[_activeTxn] = undefined;
if (options?.optimize)
this.optimize();
IModelDb._openDbs.delete(this._fileKey);
this._workspace?.close();
this.locks[_close]();
this._locks = undefined;
this._codeService?.close();
this._codeService = undefined;
this[_nativeDb].closeFile();
}
saveSchemaChanges(args) {
if (!this[_nativeDb].hasUnsavedChanges())
return;
const saveArgs = typeof args === "string" ? { description: args } : args;
saveArgs === undefined ? this[_nativeDb].saveChanges() : this[_nativeDb].saveChanges(JSON.stringify(saveArgs));
}
abandonSchemaChanges() {
if (!this[_nativeDb].hasUnsavedChanges())
return;
this.clearCaches({ instanceCachesOnly: true });
this[_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 IModelError(IModelStatus.BadRequest, "IModel is not open or is read-only");
this[_nativeDb].clearECDbCache();
this[_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 IModelError(IModelStatus.BadRequest, "IModel is not open or is read-only");
this[_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 IModelError(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 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(integrityCheckTypeMap)) {
if (options.specificChecks[checkKey]) {
const results = await 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 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 - 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[_nativeDb].isOpen())
throw new IModelError(DbResult.BE_SQLITE_ERROR_NOTOPEN, "db not open");
const executor = {
execute: async (request) => {
return ConcurrentQuery.executeQueryRequest(this[_nativeDb], request);
},
};
return new 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[_nativeDb].isOpen())
throw new IModelError(DbResult.BE_SQLITE_ERROR_NOTOPEN, "db not open");
// eslint-disable-next-line @typescript-eslint/no-deprecated
const stmt = this._statementCache.findAndRemove(ecsql) ?? new ECSqlStatement();
const executor = new ECSqlRowExecutor(this, stmt, loggerCategory);
const release = () => {
executor[Symbol.dispose]();
releaseECSqlStatement(stmt, this._statementCache, loggerCategory, executor.canCacheStatement);
};
try {
const reader = new 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(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.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: 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 IModelError(IModelStatus.BadRequest, "Max LIMIT exceeded in SELECT statement");
}
}
}
}, 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[_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]]
* ```
* @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[_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[_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.
* @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[_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[_implicitTxn].updateIModelProps();
}
saveChanges(descriptionOrArgs) {
this[_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[_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[_nativeDb].saveChanges();
this.clearCaches();
this[_nativeDb].concurrentQueryShutdown();
this[_nativeDb].performCheckpoint();
}
}
/** @internal
* @deprecated in 4.8 - might be removed in next major version. Use `txns.reverseTxns`.
*/
reverseTxns(numOperations) {
return this[_nativeDb].reverseTxns(numOperations);
}
/** @internal */
reinstateTxn() {
return this[_nativeDb].reinstateTxn();
}
/** @internal */
restartTxnSession() {
return this[_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 (!Id64.isValid(classId))
throw new IModelError(IModelStatus.BadRequest, `Class Id ${classId} is invalid`);
const name = this[_nativeDb].classIdToName(classId);
if (name === undefined)
throw new IModelError(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[_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.saveSchemaChanges("dropped unused schemas");
}
catch (error) {
Logger.logError(loggerCategory, `Failed to drop schemas: ${error}`);
this.abandonSchemaChanges();
throw new IModelError(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.existsSync(pathName)) {
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 IModelError(IModelStatus.BadRequest, "InMemory transform strategy requires cachedData to be provided.");
}
callbackResources.cachedData = callbackResult.cachedData;
}
if (this.isBriefcaseDb() && IModelHost.useSemanticRebase)
this.saveSchemaChanges("Save changes from schema import pre callback");
}
}
catch (callbackError) {
this.abandonSchemaChanges();
this.cleanupSnapshot(callbackResources);
throw new IModelError(callbackError.errorNumber ?? 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.existsSync(context.resources.snapshot.pathName))) {
throw new IModelError(IModelStatus.BadRequest, "Snapshot transform strategy requires a snapshot to be created");
}
if (context.resources.transformStrategy === DataTransformationStrategy.InMemory && context.resources.cachedData === undefined) {
throw new IModelError(IModelStatus.BadRequest, "InMemory transform strategy requires cachedData to be provided.");
}
try {
const postSchemaImportCallback = callback?.postSchemaImportCallback;
if (postSchemaImportCallback)
await postSchemaImportCallback(context);
if (this.isBriefcaseDb() && IModelHost.useSemanticRebase)
this.saveSchemaChanges("Save changes from schema import post callback");
}
catch (callbackError) {
this.abandonSchemaChanges();
throw new IModelError(callbackError.errorNumber ?? IModelStatus.BadRequest, `Failed to execute postSchemaImportCallback: ${callbackError.message}`);
}
finally {
// Always clean up snapshot, whether success or error
this.cleanupSnapshot(context.resources);
}
}
/** Shared implementation for importing schemas from file or string. */
async importSchemasInternal(schemas, options, nativeImportOp) {
// BriefcaseDb-specific validation checks
if (this.isBriefcaseDb()) {
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");
}
// Additional checks when semantic rebase is enabled
if (IModelHost.useSemanticRebase) {
if (this[_nativeDb].hasUnsavedChanges()) {
throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas with unsaved changes when useSemanticRebase flag is on");
}
if (this[_nativeDb].schemaSyncEnabled()) {