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 a possibility to re-add deleted elements with preserved ids #209

Merged
merged 33 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ecdea59
first commit
derbynn Sep 25, 2024
bf1e63d
remove unused imports
derbynn Sep 25, 2024
ea420ad
add validation to onExportElement
derbynn Sep 26, 2024
60f2f20
remove unnecessary check, update comment
derbynn Sep 26, 2024
ff7a432
update to check targetElementId instead of calling
derbynn Sep 27, 2024
13aa914
Change files
derbynn Sep 27, 2024
ad3bdbd
Merge branch 'main' into daniel/preserved-ids-for-filtering
derbynn Sep 27, 2024
9c0705e
update test to cover additional case for possible duplicate
derbynn Sep 30, 2024
b76adfa
Merge branch 'daniel/preserved-ids-for-filtering' of https://github.c…
derbynn Sep 30, 2024
7759b91
update description instead - updating code will not give duplicate error
derbynn Sep 30, 2024
08217bd
Improve error description
derbynn Sep 30, 2024
17917dd
fix id property
derbynn Sep 30, 2024
d8e60c8
optimize call for tryGetElementProps
derbynn Sep 30, 2024
4cc2611
add function to catch potential errors
derbynn Sep 30, 2024
29c1ef3
add markElementAsUpdate function and fromTransformation option in imp…
derbynn Sep 30, 2024
e82009c
update comments and make elementsToUpdate private
derbynn Oct 1, 2024
030db68
Improve elementToUpdate set comments
derbynn Oct 3, 2024
41ac9a5
Improve elementToUpdate set comments
derbynn Oct 3, 2024
d284496
Improve fromTransformation option description
derbynn Oct 3, 2024
1bb52a8
Update elementsToUpdate set description
derbynn Oct 3, 2024
77f0a5d
add 2 additional test cases for preserveElementIdsForFiltering
derbynn Oct 4, 2024
94996d7
add additional check
derbynn Oct 4, 2024
e6a4fc6
add eslint-disable-next-line for nativeDb line
derbynn Oct 4, 2024
f324365
use GetElement instead of tryGetElement
derbynn Oct 7, 2024
6aa4599
replace double negative variable
derbynn Oct 7, 2024
2fda8ac
add test for updating elem props if elem exists
derbynn Oct 7, 2024
0212295
remove fromTransformation and preserveElementIdsForFiltering from imp…
derbynn Oct 8, 2024
8249907
preserveElementIdsForFiltering now transform options
derbynn Oct 8, 2024
df48dd6
Make set private, add getter, update names
derbynn Oct 8, 2024
e5bc1c2
update comment
derbynn Oct 8, 2024
554f14f
updated tests
derbynn Oct 9, 2024
ff99a0f
re-add preserveElementIdsForFiltering option
derbynn Oct 9, 2024
59bd2e2
Merge branch 'main' into daniel/preserved-ids-for-filtering
derbynn Oct 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 56 additions & 49 deletions packages/transformer/src/IModelImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,6 @@ export interface IModelImportOptions {
* @see [IModelImporter Options]($docs/learning/transformer/index.md#IModelImporter)
*/
autoExtendProjectExtents?: boolean | { excludeOutliers: boolean };
/**
* Indicates whether the importer is used in a transformation process.
* If `true`, the importer was passed into the transformer, or the transformer constructed this instance of the importer
*/
fromTransformation?: boolean;
/** See [IModelTransformOptions]($transformer) */
preserveElementIdsForFiltering?: boolean;
/** If `true`, simplify the element geometry for visualization purposes. For example, convert b-reps into meshes.
* @default false
*/
Expand Down Expand Up @@ -110,17 +103,17 @@ export class IModelImporter {

/**
* A set of elementIds that the transformer adds to while exporting elements to indicate that the element already exists in the target.
* Defaults to an empty set.
* Defaults to undefined.
* @note
*
* This is used as an optimization when `preserveElementIdsForFiltering` is set to `true`
* This is used as an optimization when the transformer `preserveElementIdsForFiltering` option is set to `true`
* In normal cases where this option set to `false`,
* the importer determines whether to insert or update based off of whether the ID is defined on the `elementProps` passed to `importElement`.
* However, with `preserveElementIdsForFiltering` set to `true`, IDs are always set, so we can't determine insert/update like the normal case.
* The transformer already knows if an element exists or not by the time `importElement` is called and pushes to this set with `markElementAsUpdate`.
* The transformer already knows if an element exists or not by the time `importElement` is called and pushes to this set with `markElementToUpdateForPreserveId`.
* @note This set should stay small, as right after the transformer pushes to it, the importer will remove from the set.
*/
private _elementsToUpdate = new Set<Id64String>([]);
public elementIdsToUpdateForPreserveId: Set<Id64String> | undefined;

/** The set of elements that should not be updated by this IModelImporter.
* Defaults to an empty set.
Expand Down Expand Up @@ -152,9 +145,6 @@ export class IModelImporter {
this.targetDb = targetDb;
this.options = {
autoExtendProjectExtents: options?.autoExtendProjectExtents ?? true,
fromTransformation: options?.fromTransformation ?? false,
preserveElementIdsForFiltering:
options?.preserveElementIdsForFiltering ?? false,
simplifyElementGeometry: options?.simplifyElementGeometry ?? false,
skipPropagateChangesToRootElements:
options?.skipPropagateChangesToRootElements ?? true,
Expand All @@ -176,13 +166,19 @@ export class IModelImporter {
}

/**
* Marks an element so that it can be updated during import when [[IModelImportOptions.preserveElementIdsForFiltering]] is set to true
* Marks an element so that it can be updated during import when the [[IModelTransformOptions.preserveElementIdsForFiltering]] is set to true.
*/
public markElementAsUpdate(elementId: Id64String) {
public markElementToUpdateForPreserveId(elementId: Id64String) {
if (!this.elementIdsToUpdateForPreserveId) {
throw new Error(
"The elementIdsToUpdateForPreserveId set is not initialized. Ensure this set is initialized when `preserveElementIdsForFiltering` is set true"
);
}

if (this._rootElementIds.has(elementId)) {
return;
}
this._elementsToUpdate.add(elementId);
this.elementIdsToUpdateForPreserveId.add(elementId);
}

/** Import the specified ModelProps (either as an insert or an update) into the target iModel. */
Expand Down Expand Up @@ -257,27 +253,45 @@ export class IModelImporter {
return `${modelProps.classFullName} [${modelProps.id!}]`;
}

/** Import the specified ElementProps (either as an insert or an update) into the target iModel. */
public importElement(elementProps: ElementProps): Id64String {
const tryUpdateElement = (elemProps: ElementProps): void => {
try {
/**
* Determines whether element IDs should be preserved for filtering.
*
* This function checks if the `elementIdsToUpdateForPreserveId` set is defined.
* If it is undefined, the function returns `false`, indicating that element IDs should not be preserved.
* Otherwise, it returns `true`. *
*/
private shouldPreserveElementIdsForFiltering(): boolean {
if (this.elementIdsToUpdateForPreserveId === undefined) {
return false;
}
return true;
}

/**
* Tries to update an element with the specified element properties.
* If a duplicate code error occurs, it assigns a new unique code value and retries the update
*/
private tryUpdateElement(elemProps: ElementProps) {
try {
this.onUpdateElement(elemProps);
} catch (err) {
if ((err as IModelError).errorNumber === IModelStatus.DuplicateCode) {
assert(
elemProps.code.value !== undefined,
"NULL code values are always considered unique and cannot clash"
);
this._duplicateCodeValueMap.set(elemProps.id!, elemProps.code.value);
// Using NULL code values as an alternative is not valid because definition elements cannot have NULL code values.
elemProps.code.value = Guid.createValue();
this.onUpdateElement(elemProps);
} catch (err) {
if ((err as IModelError).errorNumber === IModelStatus.DuplicateCode) {
assert(
elemProps.code.value !== undefined,
"NULL code values are always considered unique and cannot clash"
);
this._duplicateCodeValueMap.set(elemProps.id!, elemProps.code.value);
// Using NULL code values as an alternative is not valid because definition elements cannot have NULL code values.
elemProps.code.value = Guid.createValue();
this.onUpdateElement(elemProps);
} else {
throw err;
}
} else {
throw err;
}
};
}
}

/** Import the specified ElementProps (either as an insert or an update) into the target iModel. */
public importElement(elementProps: ElementProps): Id64String {
if (
undefined !== elementProps.id &&
this.doNotUpdateElement(elementProps.id)
Expand All @@ -288,7 +302,8 @@ export class IModelImporter {
);
return elementProps.id;
}
if (this.options.preserveElementIdsForFiltering) {

if (this.shouldPreserveElementIdsForFiltering()) {
if (elementProps.id === undefined) {
throw new IModelError(
IModelStatus.BadElement,
Expand All @@ -300,30 +315,22 @@ export class IModelImporter {
// since default subcategories always exist and always will be inserted after their categories, we treat them as an update
// to prevent duplicate inserts.
// Always present elements (0xe, 0x1, 0x10) also will be updated to prevent duplicate inserts.
// Otherwise we always insert during a preserveElementIdsForFiltering operation
if (
(isSubCategory(elementProps) && isDefaultSubCategory(elementProps)) ||
this._rootElementIds.has(elementProps.id)
) {
this.onUpdateElement(elementProps);
} else {
// if [[IModelImportOptions.fromTransformation]] is false, try and get the element (update if it exists)
const shouldUpdateElement =
!this.options.fromTransformation &&
this.targetDb.elements.tryGetElement(elementProps.id);
if (
this._elementsToUpdate.has(elementProps.id) ||
shouldUpdateElement
) {
tryUpdateElement(elementProps);
this._elementsToUpdate.delete(elementProps.id);
if (this.elementIdsToUpdateForPreserveId?.has(elementProps.id)) {
this.tryUpdateElement(elementProps);
this.elementIdsToUpdateForPreserveId?.delete(elementProps.id);
} else {
this.onInsertElement(elementProps);
}
}
} else {
if (undefined !== elementProps.id) {
tryUpdateElement(elementProps);
this.tryUpdateElement(elementProps);
} else {
this.onInsertElement(elementProps); // targetElementProps.id assigned by insertElement
}
Expand All @@ -339,7 +346,7 @@ export class IModelImporter {
/* eslint-disable deprecation/deprecation */
try {
const elementId = this.targetDb.nativeDb.insertElement(elementProps, {
forceUseId: this.options.preserveElementIdsForFiltering,
forceUseId: this.shouldPreserveElementIdsForFiltering(),
});
// set the id like [IModelDb.insertElement]($backend), does, the raw nativeDb method does not
elementProps.id = elementId;
Expand Down
25 changes: 9 additions & 16 deletions packages/transformer/src/IModelTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,8 +658,6 @@ export class IModelTransformer extends IModelExportHandler {
// initialize importer and targetDb
if (target instanceof IModelDb) {
this.importer = new IModelImporter(target, {
preserveElementIdsForFiltering:
this._options.preserveElementIdsForFiltering,
skipPropagateChangesToRootElements:
this._options.skipPropagateChangesToRootElements,
});
Expand All @@ -668,7 +666,6 @@ export class IModelTransformer extends IModelExportHandler {
this.validateSharedOptionsMatch();
}
this.targetDb = this.importer.targetDb;
this.importer.options.fromTransformation = true;
// create the IModelCloneContext, it must be initialized later
this.context = new IModelCloneContext(this.sourceDb, this.targetDb);

Expand Down Expand Up @@ -697,14 +694,6 @@ export class IModelTransformer extends IModelExportHandler {
* @note This expects that the importer is already set on the transformer.
*/
private validateSharedOptionsMatch() {
if (
Boolean(this._options.preserveElementIdsForFiltering) !==
this.importer.options.preserveElementIdsForFiltering
) {
const errMessage =
"A custom importer was passed as a target but its 'preserveElementIdsForFiltering' option is out of sync with the transformer's option.";
throw new Error(errMessage);
}
if (
Boolean(this._options.skipPropagateChangesToRootElements) !==
this.importer.options.skipPropagateChangesToRootElements
Expand Down Expand Up @@ -1943,17 +1932,21 @@ export class IModelTransformer extends IModelExportHandler {
: undefined;

if (this._options.preserveElementIdsForFiltering) {
const isInvalid = !Id64.isValid(targetElementId);
// initialize set if undefined to indicate in importer that preserveElementIdsForFiltering option is true
if (!this.importer.elementIdsToUpdateForPreserveId) {
this.importer.elementIdsToUpdateForPreserveId = new Set<Id64String>([]);
}

if (!isInvalid && targetElementId !== sourceElement.id) {
const isValid = Id64.isValid(targetElementId);
if (isValid && targetElementId !== sourceElement.id) {
// Element found with different id
throw new Error(
`Element id(${sourceElement.id}) cannot be preserved. Found a different mapping(${targetElementId}) from source element`
);
} else if (!isInvalid && targetElementId === sourceElement.id) {
} else if (isValid && targetElementId === sourceElement.id) {
// targetElementId is valid (indicating update)
this.importer.markElementAsUpdate(sourceElement.id);
} else if (isInvalid) {
this.importer.markElementToUpdateForPreserveId(sourceElement.id);
} else if (!isValid) {
const sourceInTargetElemProps =
this.targetDb.elements.tryGetElementProps(sourceElement.id);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2664,7 +2664,6 @@ describe("IModelTransformer", () => {
rootSubject: { name: "iModelA" },
});
Subject.insert(sourceDb, IModel.rootSubjectId, "Subject1");
Subject.insert(sourceDb, IModel.rootSubjectId, "Subject2");
sourceDb.saveChanges();

const targetDbPath = IModelTransformerTestUtils.prepareOutputFile(
Expand Down Expand Up @@ -2697,18 +2696,6 @@ describe("IModelTransformer", () => {
targetDb.elements.deleteElement(targetSubjectId1!);
targetDb.saveChanges();

// update subject 2 in source
const code2 = Subject.createCode(
sourceDb,
IModel.rootSubjectId,
"Subject2"
);
const targetSubjectId2 = sourceDb.elements.queryElementIdByCode(code2);
const subject2 = sourceDb.elements.getElement<Subject>(targetSubjectId2!);
subject2.description = "Subject2 Updated Description";
sourceDb.elements.updateElement(subject2.toJSON());
sourceDb.saveChanges();

// Calling process() for second time with option to preserve elements in hopes of restoring deleted element
const secondTransformer = new IModelTransformer(sourceDb, targetDb, {
preserveElementIdsForFiltering: true,
Expand All @@ -2718,10 +2705,10 @@ describe("IModelTransformer", () => {

// verify that deleted element in target is added back - redundant check for explicitness
const sourceElementJSON = sourceDb.elements
.tryGetElement<Subject>(targetSubjectId1!)
.getElement<Subject>(targetSubjectId1!)
?.toJSON();
const deletedElementInTargetJSON = targetDb.elements
.tryGetElement<Subject>(targetSubjectId1!)
.getElement<Subject>(targetSubjectId1!)
?.toJSON();
expect(sourceElementJSON).to.be.deep.equal(deletedElementInTargetJSON);

Expand All @@ -2733,6 +2720,70 @@ describe("IModelTransformer", () => {
targetDb.close();
});

it("process() with preserveElementIdsForFiltering set to true should update the element properties if element exists with desired id in target", async () => {
const sourceDbPath = IModelTransformerTestUtils.prepareOutputFile(
"IModelTransformer",
"PreserveIdOnTestModel-Source.bim"
);
const sourceDb = SnapshotDb.createEmpty(sourceDbPath, {
rootSubject: { name: "iModelA" },
});
Subject.insert(sourceDb, IModel.rootSubjectId, "Subject1");
sourceDb.saveChanges();

const targetDbPath = IModelTransformerTestUtils.prepareOutputFile(
"IModelTransformer",
"PreserveIdOnTestModel-Target.bim"
);
const targetDb = SnapshotDb.createEmpty(targetDbPath, {
rootSubject: sourceDb.rootSubject,
});

// Execute process() so that elements from source are copied to target
const transformer = new IModelTransformer(sourceDb, targetDb, {
preserveElementIdsForFiltering: true,
});
await transformer.process();
targetDb.saveChanges();

let sourceContent = await getAllElementsInvariants(sourceDb);
let targetContent = await getAllElementsInvariants(targetDb);
expect(targetContent).to.deep.equal(sourceContent);

const code = Subject.createCode(targetDb, IModel.rootSubjectId, "Subject1");
const targetSubjectId = targetDb.elements.queryElementIdByCode(code);
expect(targetSubjectId).to.not.be.undefined;

// update subject in source
const sourceSubject = sourceDb.elements.getElement<Subject>(
targetSubjectId!
);
const updatedDescription = "Subject1 Updated Description";
sourceSubject.description = updatedDescription;
sourceDb.elements.updateElement(sourceSubject.toJSON());
sourceDb.saveChanges();

// Calling process() for second time with option to preserve elements in hopes of updating element with desired id
const secondTransformer = new IModelTransformer(sourceDb, targetDb, {
preserveElementIdsForFiltering: true,
});
await secondTransformer.process(); // should update description for subject element
targetDb.saveChanges();

// target subject should have updated description
const targetSubjectDescription = sourceDb.elements.getElement<Subject>(
targetSubjectId!
).description;
expect(targetSubjectDescription).to.equal(updatedDescription);

sourceContent = await getAllElementsInvariants(sourceDb);
targetContent = await getAllElementsInvariants(targetDb);
expect(targetContent).to.deep.equal(sourceContent);

sourceDb.close();
targetDb.close();
});

it("process() with preserveElementIdsForFiltering set to true should not throw when called on 2 identical iModels", async () => {
const seedDb = SnapshotDb.openFile(
TestUtils.IModelTestUtils.resolveAssetFile("CompatibilityTestSeed.bim")
Expand Down Expand Up @@ -2830,7 +2881,7 @@ describe("IModelTransformer", () => {
expect(newSubjectId).to.not.be.undefined;
targetDb.saveChanges();

// Calling process() for second time with option to preserve elements in hopes of restoring deleted element
// Calling process() for second time with option to preserve elements in hopes of throwing expected error
const secondTransformer = new IModelTransformer(sourceDb, targetDb, {
preserveElementIdsForFiltering: true,
});
Expand Down Expand Up @@ -2907,7 +2958,7 @@ describe("IModelTransformer", () => {
expect(targetSubjectId3).to.not.be.undefined;
targetDb.saveChanges();

// Calling process() for second time with option to preserve elements in hopes of restoring deleted element
// Calling process() for second time with option to preserve elements in hopes of of throwing expected error
const secondTransformer = new IModelTransformer(sourceDb, targetDb, {
preserveElementIdsForFiltering: true,
});
Expand Down
Loading