UNPKG

@itwin/core-backend

Version:
430 lines • 20.6 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Elements */ import { assert, Logger, LogLevel } from "@itwin/core-bentley"; import { IModel, QueryBinder } from "@itwin/core-common"; import { BackendLoggerCategory } from "./BackendLoggerCategory"; import { EditTxn } from "./EditTxn"; import { DefinitionContainer, DefinitionElement, DefinitionPartition, Element, Subject } from "./Element"; import { IModelDb } from "./IModelDb"; import { DefinitionModel, Model } from "./Model"; import { _implicitTxn } from "./internal/Symbols"; const loggerCategory = `${BackendLoggerCategory.IModelDb}.ElementTreeWalker`; function sortChildrenBeforeParents(iModel, ids) { const children = []; const parents = []; for (const eid of ids) { const parentId = iModel.elements.queryParent(eid); if (parentId !== undefined && ids.includes(parentId)) children.push(eid); else parents.push(eid); } if (children.length === 0) return [parents]; return [...sortChildrenBeforeParents(iModel, children), parents]; } function isModelEmpty(iModel, modelId) { return iModel.withQueryReader(`select count(*) from ${Element.classFullName} where Model.Id = ?`, (reader) => { reader.step(); return reader.current[0] === 0; }, new QueryBinder().bindId(1, modelId)); } function isDefinitionModel(model) { return (model.id !== IModel.repositoryModelId) && (model instanceof DefinitionModel); } var ElementPruningClassification; (function (ElementPruningClassification) { ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Normal"] = 0] = "PRUNING_CLASS_Normal"; ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Subject"] = 1] = "PRUNING_CLASS_Subject"; ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Definition"] = 2] = "PRUNING_CLASS_Definition"; ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_DefinitionPartition"] = 3] = "PRUNING_CLASS_DefinitionPartition"; })(ElementPruningClassification || (ElementPruningClassification = {})); function classifyElementForPruning(iModel, elementId) { const el = iModel.elements.getElement(elementId); // DefinitionContainer is submodeled by a DefinitionModel and so it must be classified as PRUNING_CLASS_DefinitionPartition for tree-walking purposes. // Since DefinitionContainer is-a DefinitionElement the (el instanceof DefinitionElement) case below would classify it as PRUNING_CLASS_Definition. // That is why we special-case it here. if (el instanceof DefinitionContainer) return ElementPruningClassification.PRUNING_CLASS_DefinitionPartition; return (el instanceof Subject) ? ElementPruningClassification.PRUNING_CLASS_Subject : (el instanceof DefinitionElement) ? ElementPruningClassification.PRUNING_CLASS_Definition : (el instanceof DefinitionPartition) ? ElementPruningClassification.PRUNING_CLASS_DefinitionPartition : ElementPruningClassification.PRUNING_CLASS_Normal; } /** Records the path that a tree search took to reach an element or model. This object is immutable. * @beta */ export class ElementTreeWalkerScope { topElement = ""; /** path of parent elements and enclosing models */ path = []; /** cached info about the immediately enclosing model (i.e., the last model in path) */ enclosingModelInfo; constructor(arg1, arg2) { if (typeof arg1 === "string") { // normal constructor assert(arg2 instanceof Model); this.topElement = arg1; this.path.push(this.enclosingModelInfo = { model: arg2, isDefinitionModel: isDefinitionModel(arg2) }); } else if (arg1 instanceof ElementTreeWalkerScope) { // copy-like constructor this.topElement = arg1.topElement; this.path = [...arg1.path]; if (typeof arg2 === "string") { // with new parent this.path.push(arg2); this.enclosingModelInfo = arg1.enclosingModelInfo; } else { // with new enclosing model this.path.push(this.enclosingModelInfo = { model: arg2, isDefinitionModel: isDefinitionModel(arg2) }); } } else { throw new Error("invalid constructor signature"); } } get enclosingModel() { return this.enclosingModelInfo.model; } get inDefinitionModel() { return this.enclosingModelInfo.isDefinitionModel; } // NB: this will return false for the RepositoryModel! get inRepositoryModel() { return this.enclosingModelInfo.model.id === IModelDb.repositoryModelId; } static createTopScope(iModel, topElementId) { const topElement = iModel.elements.getElement(topElementId); const topElementModel = iModel.models.getModel(topElement.model); return new ElementTreeWalkerScope(topElementId, topElementModel); } fmtItem(v) { if (typeof v === "string") return `element ${v}`; return `model ${v.model.id} ${v.isDefinitionModel ? "(DEFN)" : ""}`; } toString() { return `[ ${this.path.map((v) => this.fmtItem(v)).join(" / ")} ]`; } } function fmtElement(iModel, elementId) { const el = iModel.elements.getElement(elementId); return `${el.id} ${el.classFullName} ${el.getDisplayLabel()}`; } function fmtModel(model) { return `${model.id} ${model.classFullName} ${model.name}`; } let isTraceEnabledChecked = -1; function isTraceEnabled() { if (isTraceEnabledChecked === -1) isTraceEnabledChecked = Logger.isEnabled(loggerCategory, LogLevel.Trace) ? 1 : 0; return isTraceEnabledChecked === 1; } function logElement(op, iModel, elementId, scope, logChildren) { if (!isTraceEnabled()) return; Logger.logTrace(loggerCategory, `${op} ${fmtElement(iModel, elementId)} ${scope ? scope.toString() : ""}`); if (logChildren) iModel.elements.queryChildren(elementId).forEach((c) => logElement(" - ", iModel, c, undefined, true)); } function logModel(op, iModel, modelId, scope, logElements) { if (!isTraceEnabled()) return; const model = iModel.models.getModel(modelId); Logger.logTrace(loggerCategory, `${op} ${fmtModel(model)} ${scope ? scope.toString() : ""}`); if (logElements) { iModel.withQueryReader(`select ecinstanceid from ${Element.classFullName} where Model.Id = ?`, (reader) => { for (const row of reader) { logElement(" - ", iModel, row[0]); } }, new QueryBinder().bindId(1, modelId)); } } /** Does a depth-first search on the tree defined by an element and its sub-models and children. * Sub-models are visited before their modeled elements, and children are visited before their parents. * * The following callbacks allow the subclass to exclude elements and sub-trees from the search: * * [[ElementTreeBottomUp.shouldExploreModel]], [[ElementTreeBottomUp.shouldExploreChildren]] * * [[ElementTreeBottomUp.shouldVisitElement]], [[ElementTreeBottomUp.shouldVisitModel]] * * The [[ElementTreeBottomUp.visitElement]] and [[ElementTreeBottomUp.visitModel]] callbacks allow * the subclass to process the elements and models that are encountered in the search. * @beta */ export class ElementTreeBottomUp { txn; constructor(iModelOrTxn) { this.txn = iModelOrTxn instanceof EditTxn ? iModelOrTxn : iModelOrTxn[_implicitTxn]; } /** Return true if the search should recurse into this model */ shouldExploreModel(_model, _scope) { return true; } /** Return true if the search should recurse into the children (if any) of this element */ shouldExploreChildren(_parentId, _scope) { return true; } /** Return true if the search should visit this element */ shouldVisitElement(_elementId, _scope) { return true; } /** Return true if the search should visit this model */ shouldVisitModel(_model, _scope) { return true; } /** The main tree-walking function */ processElementTree(element, scope) { const subModel = this.txn.iModel.models.tryGetModel(element); if (subModel !== undefined) { if (this.shouldExploreModel(subModel, scope)) this._processSubModel(subModel, scope); if (this.shouldVisitModel(subModel, scope)) this.visitModel(subModel, scope); } if (this.shouldExploreChildren(element, scope)) this._processChildren(element, scope); if (this.shouldVisitElement(element, scope)) this.visitElement(element, scope); } /** process the children of the specified parent element */ _processChildren(parentElement, parentScope) { const children = this.txn.iModel.elements.queryChildren(parentElement); if (children.length === 0) return; const childrenScope = new ElementTreeWalkerScope(parentScope, parentElement); for (const childElement of children) this.processElementTree(childElement, childrenScope); } /** process the elements in the specified model */ _processSubModel(model, parenScope) { const scope = new ElementTreeWalkerScope(parenScope, model); // Visit only the top-level parents. processElementTree will visit their children (bottom-up). model.iModel.withQueryReader(`select ECInstanceId from bis:Element where Model.id=? and Parent.Id is null`, (reader) => { for (const row of reader) { const elementId = row[0]; this.processElementTree(elementId, scope); } }, new QueryBinder().bindId(1, model.id)); } } /** Helper class that manages the deletion of definitions and subjects */ class SpecialElements { _txn; definitionModels = []; definitions = []; subjects = []; constructor(_txn) { this._txn = _txn; } getTxn(iModel) { if (undefined !== this._txn) return this._txn; return iModel[_implicitTxn]; } recordSpecialElement(iModel, elementId) { // Defer Definitions and Subjects const cls = classifyElementForPruning(iModel, elementId); if (cls === ElementPruningClassification.PRUNING_CLASS_Subject) { this.subjects.push(elementId); return true; } else if (cls === ElementPruningClassification.PRUNING_CLASS_Definition) { this.definitions.push(elementId); return true; } else if (cls === ElementPruningClassification.PRUNING_CLASS_DefinitionPartition) { this.definitionModels.push(elementId); return true; } return false; // not a special element } /** Delete special elements - This calls Elements.deleteDefinitionElements to process the collected definition elements as a group and then * calls Models.deleteModel on collected definition models. It then deletes all collected Subjects by calling Element.deleteElement on them. * @note Caller must ensure that the special elements were recorded in a depth-first search. */ deleteSpecialElements(iModel) { const txn = this.getTxn(iModel); // It's dangerous to pass a mixture of SubCategories and Categories to deleteDefinitionElements. // That function will delete the Categories first, which automatically deletes all their child // SubCategories (in native code). If a SubCategory in the list is one of those children, then // deleteDefinitionElements will try and fail with an exception to delete that SubCategory in a subsequent step. // To work around this, we delete the SubCategories first, then everything else. // A similar problem occurs when you pass other kinds of elements to deleteDefinitionElements, where some are // children and others are parents. deleteDefinitionElements does not preserve the order that you specify, // and it does not process children before parents. for (const definitions of sortChildrenBeforeParents(iModel, this.definitions)) { if (isTraceEnabled()) definitions.forEach((e) => logElement("try delete", iModel, e)); txn.deleteDefinitionElements(definitions); // will not delete definitions that are still in use. } for (const m of this.definitionModels) { if (!isModelEmpty(iModel, m)) { logModel("Model not empty - cannot delete - may contain Definitions that are still in use", iModel, m, undefined, true); } else { logModel("delete", iModel, m); txn.deleteModel(m); txn.deleteElement(m); } } for (const e of this.subjects) { if (iModel.elements.queryChildren(e).length !== 0) { logElement("Subject still has children - cannot delete - may have child DefinitionModels", iModel, e, undefined, true); } else { logElement("delete", iModel, e); txn.deleteElement(e); } } } } /** Deletes an entire element tree, including sub-models and child elements. * Items are deleted in bottom-up order. Definitions and Subjects are deleted after normal elements. * Call deleteNormalElements on each tree. Then call deleteSpecialElements. * @see deleteElementTree for a simple way to use this class. * @beta */ export class ElementTreeDeleter extends ElementTreeBottomUp { _special; constructor(iModelOrTxn) { super(iModelOrTxn); this._special = new SpecialElements(iModelOrTxn instanceof EditTxn ? iModelOrTxn : undefined); } shouldExploreModel(_model) { return true; } shouldVisitElement(_elementId) { return true; } visitModel(model, _scope) { if (isDefinitionModel(model)) return; // we recorded definition models in visitElement when we encountered the DefinitionPartition elements. // visitElement has already deleted the elements in the model. So, now it's safe to delete the model itself. logModel("delete", this.txn.iModel, model.id, _scope); this.txn.deleteModel(model.id); } visitElement(elementId, _scope) { if (!this._special.recordSpecialElement(this.txn.iModel, elementId)) { logElement("delete", this.txn.iModel, elementId, _scope); this.txn.deleteElement(elementId); } } /** * Delete the "normal" elements and record the special elements for deferred processing. * @param topElement The parent of the sub-tree to be deleted. Top element itself is also deleted. * @param scope How the parent was found * @see deleteSpecialElements */ deleteNormalElements(topElement, scope) { const topScope = scope ?? ElementTreeWalkerScope.createTopScope(this.txn.iModel, topElement); this.processElementTree(topElement, topScope); // } /** Delete all special elements that were found and deferred by deleteNormalElements. Call this * function once after all element trees are processed by deleteNormalElements. */ deleteSpecialElements() { this._special.deleteSpecialElements(this.txn.iModel); } } /** Does a breadth-first search on the tree defined by an element and its sub-models and children. * Parents are visited first, then children, then sub-models. * The subclass can "prune" sub-trees from the search. When a sub-tree is "pruned" the search does *not* recurse into it. * If a sub-tree is not pruned, then the search does recurse into it. * @beta */ class ElementTreeTopDown { _iModel; constructor(_iModel) { this._iModel = _iModel; } /** Should the search *not* recurse into this sub-tree? */ shouldPrune(_elementId, _scope) { return false; } processElementTree(element, scope) { if (this.shouldPrune(element, scope)) { this.prune(element, scope); return; } this._processChildren(element, scope); const subModel = this._iModel.models.tryGetModel(element); if (subModel !== undefined) { this._processSubModel(subModel, scope); } } _processChildren(element, scope) { let parentScope; for (const childElement of this._iModel.elements.queryChildren(element)) { if (parentScope === undefined) parentScope = new ElementTreeWalkerScope(scope, element); this.processElementTree(childElement, parentScope); } } _processSubModel(subModel, scope) { const subModelScope = new ElementTreeWalkerScope(scope, subModel); // Visit only the top-level parents. processElementTree will recurse into their children. this._iModel.withQueryReader(`select ECInstanceId from bis:Element where Model.id=? and Parent.Id is null`, (reader) => { for (const row of reader) { const elementId = row[0]; this.processElementTree(elementId, subModelScope); } }, new QueryBinder().bindId(1, subModel.id)); } } /** Performs a breadth-first search to visit elements in top-down order. * When the supplied filter function chooses an element, ElementTreeDeleter is used to delete it and its sub-tree. * @beta */ export class ElementSubTreeDeleter extends ElementTreeTopDown { _treeDeleter; _shouldPruneCb; constructor(iModelOrTxn, shouldPruneCb) { super(iModelOrTxn instanceof EditTxn ? iModelOrTxn.iModel : iModelOrTxn); const effectiveTxn = iModelOrTxn instanceof EditTxn ? iModelOrTxn : iModelOrTxn[_implicitTxn]; this._treeDeleter = new ElementTreeDeleter(effectiveTxn); this._shouldPruneCb = shouldPruneCb; } shouldPrune(elementId, scope) { return this._shouldPruneCb(elementId, scope); } prune(elementId, scope) { this._treeDeleter.deleteNormalElements(elementId, scope); } /** Traverses the tree of elements beginning with the top element, and deletes all selected sub-trees. * Normal elements are deleted. Any special elements that are encountered are deferred. * Call deleteSpecialElementSubTrees after all top elements have been processed. */ deleteNormalElementSubTrees(topElement, scope) { const topScope = scope ?? ElementTreeWalkerScope.createTopScope(this._iModel, topElement); this.processElementTree(topElement, topScope); // deletes normal elements and their sub-trees, defers special elements } /** Delete all special elements and their sub-trees that were found in the course of processing. * The sub-trees were already expanded by ElementTreeDeleter.deleteNormalElements. */ deleteSpecialElementSubTrees() { this._treeDeleter.deleteSpecialElements(); } } /** @internal */ export function deleteElementTree(arg0, arg1, arg2) { let maxPasses; let txn; let iModel; let topElement; if (arg0 instanceof EditTxn) { assert(typeof arg1 === "string"); txn = arg0; iModel = txn.iModel; topElement = arg1; maxPasses = arg2; } else if (arg0 instanceof IModelDb) { assert(typeof arg1 === "string"); iModel = arg0; topElement = arg1; } else { iModel = arg0.iModel; txn = "txn" in arg0 ? arg0.txn : undefined; topElement = arg0.topElement; maxPasses = arg0.maxPasses; } maxPasses = maxPasses ?? 5; let pass = 0; do { const effectiveTxn = txn ?? iModel[_implicitTxn]; const del = new ElementTreeDeleter(effectiveTxn); del.deleteNormalElements(topElement); del.deleteSpecialElements(); } while ((iModel.elements.tryGetElement(topElement) !== undefined) && (++pass < maxPasses)); } export function deleteElementSubTrees(arg0, topElement, filter) { const effectiveTxn = arg0 instanceof EditTxn ? arg0 : arg0[_implicitTxn]; const del = new ElementSubTreeDeleter(effectiveTxn, filter); del.deleteNormalElementSubTrees(topElement); del.deleteSpecialElementSubTrees(); } //# sourceMappingURL=ElementTreeWalker.js.map