Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ElementCascadingDeleter to fix FK errors while deleting element which is referenced in code scopes #75

Merged
merged 9 commits into from
Jun 26, 2023
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"
}
46 changes: 46 additions & 0 deletions packages/transformer/src/ElementCascadingDeleter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* 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 deleteElementCascadeTree(iModel: IModelDb, topElement: Id64String): void {
DeividasDavidav marked this conversation as resolved.
Show resolved Hide resolved
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)) {
DeividasDavidav marked this conversation as resolved.
Show resolved Hide resolved
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) => {
DeividasDavidav marked this conversation as resolved.
Show resolved Hide resolved
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 { deleteElementCascadeTree } 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);
deleteElementCascadeTree(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