Skip to content

Commit

Permalink
Add ElementCascadingDeleter to fix FK errors while deleting element w…
Browse files Browse the repository at this point in the history
…hich is referenced in code scopes (#75)

Added ElementCascadingDeleter, which works similarly to
ElementTreeDeleter, but also finds elements that reference this element
in their code scopes and deletes those elements.

#61
  • Loading branch information
DeividasDavidav authored Jun 26, 2023
1 parent c434b4d commit df80ed2
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Added ElementCascadingDeleter to fix FK errors while deleting element which is referenced in code scopes of other elements",
"packageName": "@itwin/imodel-transformer",
"email": "deividas.davidavicius@bentley.com",
"dependentChangeType": "patch"
}
51 changes: 51 additions & 0 deletions packages/transformer/src/ElementCascadingDeleter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* 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 { ElementTreeDeleter, ElementTreeWalkerScope, IModelDb } from "@itwin/core-backend";
import { DbResult, Id64String } from "@itwin/core-bentley";

/** Deletes an element tree and code scope references starting with the specified top element. The top element is also deleted. Uses ElementCascadeDeleter.
* @param iModel The iModel
* @param topElement The parent of the sub-tree
*/
export function deleteElementTreeCascade(iModel: IModelDb, topElement: Id64String): void {
const del = new ElementCascadingDeleter(iModel);
del.deleteNormalElements(topElement);
del.deleteSpecialElements();
}

/** Deletes an entire element tree, including sub-models, child elements and code scope references.
* Items are deleted in bottom-up order. Definitions and Subjects are deleted after normal elements.
* Call deleteNormalElements on each tree. Then call deleteSpecialElements.
*/
export class ElementCascadingDeleter extends ElementTreeDeleter {
protected shouldVisitCodeScopes(_elementId: Id64String, _scope: ElementTreeWalkerScope) { return true; }

/** The main tree-walking function */
protected override processElementTree(element: Id64String, scope: ElementTreeWalkerScope): void {
if (this.shouldVisitCodeScopes(element, scope)) {
this._processCodeScopes(element, scope);
}
super.processElementTree(element, scope);
}
/** Process code scope references */
private _processCodeScopes(element: Id64String, scope: ElementTreeWalkerScope) {
const newScope = new ElementTreeWalkerScope(scope, element);
this._iModel.withPreparedStatement(`
SELECT ECInstanceId
FROM bis.Element
WHERE CodeScope.id=?
AND Parent.id IS NULL
`, (stmt) => {
stmt.bindId(1, element);
while (stmt.step() === DbResult.BE_SQLITE_ROW) {
const elementId = stmt.getValue(0).getId();
this.processElementTree(elementId, newScope);
}
});
}
}
5 changes: 3 additions & 2 deletions packages/transformer/src/IModelImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import {
PropertyMetaData, RelatedElement, SubCategoryProps,
} from "@itwin/core-common";
import { TransformerLoggerCategory } from "./TransformerLoggerCategory";
import { deleteElementTree, ElementAspect, ElementMultiAspect, Entity, IModelDb, Relationship, RelationshipProps, SourceAndTarget, SubCategory } from "@itwin/core-backend";
import { ElementAspect, ElementMultiAspect, Entity, IModelDb, Relationship, RelationshipProps, SourceAndTarget, SubCategory } from "@itwin/core-backend";
import type { IModelTransformOptions } from "./IModelTransformer";
import * as assert from "assert";
import { deleteElementTreeCascade } from "./ElementCascadingDeleter";

const loggerCategory: string = TransformerLoggerCategory.IModelImporter;

Expand Down Expand Up @@ -264,7 +265,7 @@ export class IModelImporter implements Required<IModelImportOptions> {
* @note A subclass may override this method to customize delete behavior but should call `super.onDeleteElement`.
*/
protected onDeleteElement(elementId: Id64String): void {
deleteElementTree(this.targetDb, elementId);
deleteElementTreeCascade(this.targetDb, elementId);
Logger.logInfo(loggerCategory, `Deleted element ${elementId} and its descendants`);
this.trackProgress();
}
Expand Down
61 changes: 50 additions & 11 deletions packages/transformer/src/test/TestUtils/IModelTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,30 @@ export class ExtensiveTestScenario {
} as any);
const relationshipId2 = sourceDb.relationships.insertInstance(relationship2.toJSON());
assert.isTrue(Id64.isValidId64(relationshipId2));

// Insert PhysicalObject5
const physicalObjectProps5: PhysicalElementProps = {
classFullName: PhysicalObject.classFullName,
model: physicalModelId,
category: spatialCategoryId,
code: Code.createEmpty(),
userLabel: "PhysicalObject5",
};

const physicalObjectId5 = sourceDb.elements.insertElement(physicalObjectProps5);
assert.isTrue(Id64.isValidId64(physicalObjectId5));

// Insert PhysicalObject6
const physicalObjectProps6: PhysicalElementProps = {
classFullName: PhysicalObject.classFullName,
model: physicalModelId,
category: spatialCategoryId,
code: { spec: "0x1", scope: physicalObjectId5 },
userLabel: "PhysicalObject6",
};

const physicalObjectId6 = sourceDb.elements.insertElement(physicalObjectProps6);
assert.isTrue(Id64.isValidId64(physicalObjectId6));
}

public static updateDb(sourceDb: IModelDb): void {
Expand Down Expand Up @@ -1088,29 +1112,42 @@ export class ExtensiveTestScenario {
assert.isTrue(Id64.isValidId64(physicalObjectId3));
sourceDb.elements.deleteElement(physicalObjectId3);
assert.equal(Id64.invalid, IModelTestUtils.queryByUserLabel(sourceDb, "PhysicalObject3"));
// Insert PhysicalObject5

// delete PhysicalObject6
const physicalObjectId6 = IModelTestUtils.queryByUserLabel(sourceDb, "PhysicalObject6");
assert.isTrue(Id64.isValidId64(physicalObjectId6));
sourceDb.elements.deleteElement(physicalObjectId6);
assert.equal(Id64.invalid, IModelTestUtils.queryByUserLabel(sourceDb, "PhysicalObject6"));

// delete PhysicalObject5
const physicalObjectId5 = IModelTestUtils.queryByUserLabel(sourceDb, "PhysicalObject5");
assert.isTrue(Id64.isValidId64(physicalObjectId5));
sourceDb.elements.deleteElement(physicalObjectId5);
assert.equal(Id64.invalid, IModelTestUtils.queryByUserLabel(sourceDb, "PhysicalObject5"));

// Insert PhysicalObject7
const physicalObjectProps5: PhysicalElementProps = {
classFullName: PhysicalObject.classFullName,
model: physicalElement1.model,
category: spatialCategoryId,
code: Code.createEmpty(),
userLabel: "PhysicalObject5",
userLabel: "PhysicalObject7",
geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)),
placement: {
origin: Point3d.create(5, 5, 5),
angles: YawPitchRollAngles.createDegrees(0, 0, 0),
},
};
const physicalObjectId5 = sourceDb.elements.insertElement(physicalObjectProps5);
assert.isTrue(Id64.isValidId64(physicalObjectId5));
const physicalObjectId7 = sourceDb.elements.insertElement(physicalObjectProps5);
assert.isTrue(Id64.isValidId64(physicalObjectId7));
// delete relationship
const drawingGraphicId1 = IModelTestUtils.queryByUserLabel(sourceDb, "DrawingGraphic1");
const drawingGraphicId2 = IModelTestUtils.queryByUserLabel(sourceDb, "DrawingGraphic2");
const relationship: Relationship = sourceDb.relationships.getInstance(DrawingGraphicRepresentsElement.classFullName, { sourceId: drawingGraphicId2, targetId: physicalObjectId1 });
relationship.delete();
// insert relationships
DrawingGraphicRepresentsElement.insert(sourceDb, drawingGraphicId1, physicalObjectId5);
DrawingGraphicRepresentsElement.insert(sourceDb, drawingGraphicId2, physicalObjectId5);
DrawingGraphicRepresentsElement.insert(sourceDb, drawingGraphicId1, physicalObjectId7);
DrawingGraphicRepresentsElement.insert(sourceDb, drawingGraphicId2, physicalObjectId7);
// update InformationRecord2
const informationRecordCodeSpec: CodeSpec = sourceDb.codeSpecs.getByName("InformationRecords");
const informationModelId = sourceDb.elements.queryElementIdByCode(InformationPartitionElement.createCode(sourceDb, subjectId, "Information"))!;
Expand Down Expand Up @@ -1202,14 +1239,14 @@ export class ExtensiveTestScenario {
const physicalElementId = IModelTestUtils.queryByUserLabel(iModelDb, "PhysicalElement1");
const physicalElement: PhysicalElement = iModelDb.elements.getElement(physicalElementId);
assert.isUndefined(physicalElement.asAny.commonNavigation);
// assert PhysicalObject5 was inserted
const physicalObjectId5 = IModelTestUtils.queryByUserLabel(iModelDb, "PhysicalObject5");
assert.isTrue(Id64.isValidId64(physicalObjectId5));
// assert PhysicalObject7 was inserted
const physicalObjectId7 = IModelTestUtils.queryByUserLabel(iModelDb, "PhysicalObject7");
assert.isTrue(Id64.isValidId64(physicalObjectId7));
// assert relationships were inserted
const drawingGraphicId1 = IModelTestUtils.queryByUserLabel(iModelDb, "DrawingGraphic1");
const drawingGraphicId2 = IModelTestUtils.queryByUserLabel(iModelDb, "DrawingGraphic2");
iModelDb.relationships.getInstance(DrawingGraphicRepresentsElement.classFullName, { sourceId: drawingGraphicId1, targetId: physicalObjectId5 });
iModelDb.relationships.getInstance(DrawingGraphicRepresentsElement.classFullName, { sourceId: drawingGraphicId2, targetId: physicalObjectId5 });
iModelDb.relationships.getInstance(DrawingGraphicRepresentsElement.classFullName, { sourceId: drawingGraphicId1, targetId: physicalObjectId7 });
iModelDb.relationships.getInstance(DrawingGraphicRepresentsElement.classFullName, { sourceId: drawingGraphicId2, targetId: physicalObjectId7 });
// assert InformationRecord2 was updated
const informationRecordCodeSpec: CodeSpec = iModelDb.codeSpecs.getByName("InformationRecords");
const informationModelId = iModelDb.elements.queryElementIdByCode(InformationPartitionElement.createCode(iModelDb, subjectId, "Information"))!;
Expand All @@ -1224,6 +1261,8 @@ export class ExtensiveTestScenario {
// detect deletes if possible - cannot detect during processAll when isReverseSynchronization is true
if (assertDeletes) {
assert.equal(Id64.invalid, IModelTestUtils.queryByUserLabel(iModelDb, "PhysicalObject3"));
assert.equal(Id64.invalid, IModelTestUtils.queryByUserLabel(iModelDb, "PhysicalObject5"));
assert.equal(Id64.invalid, IModelTestUtils.queryByUserLabel(iModelDb, "PhysicalObject6"));
assert.throws(() => iModelDb.relationships.getInstanceProps(DrawingGraphicRepresentsElement.classFullName, { sourceId: drawingGraphicId2, targetId: physicalObjectId1 }));
assert.isUndefined(iModelDb.elements.queryElementIdByCode(new Code({ spec: informationRecordCodeSpec.id, scope: informationModelId, value: "InformationRecord3" })));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ describe("IModelTransformer", () => {
assert.equal(targetImporter.numModelsUpdated, 0);
assert.equal(targetImporter.numElementsInserted, 1);
assert.equal(targetImporter.numElementsUpdated, 5);
assert.equal(targetImporter.numElementsDeleted, 2);
assert.equal(targetImporter.numElementsDeleted, 3);
assert.equal(targetImporter.numElementAspectsInserted, 0);
assert.equal(targetImporter.numElementAspectsUpdated, 2);
assert.equal(targetImporter.numRelationshipsInserted, 2);
Expand Down Expand Up @@ -264,7 +264,7 @@ describe("IModelTransformer", () => {
branchToMasterTransformer.dispose();
masterDb.saveChanges();
TransformerExtensiveTestScenario.assertUpdatesInDb(masterDb, false);
assert.equal(numBranchElements, count(masterDb, Element.classFullName) - 2); // processAll cannot detect deletes when isReverseSynchronization=true
assert.equal(numBranchElements, count(masterDb, Element.classFullName) - 4); // processAll cannot detect deletes when isReverseSynchronization=true
assert.equal(numBranchRelationships, count(masterDb, ElementRefersToElements.classFullName) - 1); // processAll cannot detect deletes when isReverseSynchronization=true
assert.equal(0, count(masterDb, ExternalSourceAspect.classFullName));

Expand Down

0 comments on commit df80ed2

Please sign in to comment.