Skip to content

Commit

Permalink
Enable saving reverse synchronization version for process all transfo…
Browse files Browse the repository at this point in the history
…rmations (#207)

- Add support to save reverse sync version needed for partial fork
workflow

---------

Co-authored-by: Nick Tessier <[email protected]>
  • Loading branch information
derbynn and nick4598 authored Sep 25, 2024
1 parent 0f2547a commit 01ffd0a
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "save reverse sync version for process all transformations",
"packageName": "@itwin/imodel-transformer",
"email": "[email protected]",
"dependentChangeType": "patch"
}
72 changes: 45 additions & 27 deletions packages/transformer/src/IModelTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2212,33 +2212,38 @@ export class IModelTransformer extends IModelExportHandler {
return targetModelProps;
}

/** called at the end of a transformation,
/**
* Called at the end of a transformation,
* updates the target scope element to say that transformation up through the
* source's changeset has been performed. Also stores all changesets that occurred
* during the transformation as "pending synchronization changeset indices" @see TargetScopeProvenanceJsonProps
*
* You generally should not call this function yourself and use [[process]] with [[IModelTransformOptions.argsForProcessChanges]] provided instead.
* It is public for unsupported use cases of custom synchronization transforms.
* @note if [[IModelTransformOptions.argsForProcessChanges]] are not defined in this transformation, this will fail
* without setting the `force` option to `true`
* @note If [[IModelTransformOptions.argsForProcessChanges]] is not defined in this transformation, this function will return early without updating the sync version,
* unless the `initializeReverseSyncVersion` option is set to `true`
*
* The `initializeReverseSyncVersion` is added to set the reverse synchronization version during a forward synchronization.
* When set to `true`, it saves the reverse sync version as the current changeset of the targetDb. This is typically used for the first transformation between a master and branch iModel.
* Setting `initializeReverseSyncVersion` to `true` has the effect of making it so any changesets in the branch iModel at the time of the first transformation will be ignored during any future reverse synchronizations from the branch to the master iModel.
*
* Note that typically, the reverseSyncVersion is saved as the last changeset merged from the branch into master.
* Setting initializeReverseSyncVersion to true during a forward transformation could overwrite this correct reverseSyncVersion and should only be done during the first transformation between a master and branch iModel.
*/
public updateSynchronizationVersion({ force = false } = {}) {
const notForcedAndHasNoChangesAndIsntProvenanceInit =
!force &&
this._sourceChangeDataState !== "has-changes" &&
!this._isProvenanceInitTransform;
if (notForcedAndHasNoChangesAndIsntProvenanceInit) return;
public updateSynchronizationVersion({
initializeReverseSyncVersion = false,
} = {}) {
const shouldSkipSyncVersionUpdate =
!initializeReverseSyncVersion &&
this._sourceChangeDataState !== "has-changes";
if (shouldSkipSyncVersionUpdate) return;

nodeAssert(this._targetScopeProvenanceProps);

const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;

if (this._isProvenanceInitTransform) {
this._targetScopeProvenanceProps.version = sourceVersion;
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
targetVersion;
} else if (this.isReverseSynchronization) {
if (this.isReverseSynchronization) {
const oldVersion =
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;

Expand All @@ -2248,17 +2253,27 @@ export class IModelTransformer extends IModelExportHandler {
);
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
sourceVersion;
} else if (!this.isReverseSynchronization) {
} else {
Logger.logInfo(
loggerCategory,
`updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`
);
this._targetScopeProvenanceProps.version = sourceVersion;

// save reverse sync version
if (initializeReverseSyncVersion) {
Logger.logInfo(
loggerCategory,
`updating reverse sync version from ${this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion} to ${targetVersion}`
);
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
targetVersion;
}
}

if (
this._options.argsForProcessChanges ||
(this._startingChangesetIndices && this._isProvenanceInitTransform)
(this._startingChangesetIndices && initializeReverseSyncVersion)
) {
nodeAssert(
this.targetDb.changeset.index !== undefined &&
Expand All @@ -2282,16 +2297,17 @@ export class IModelTransformer extends IModelExportHandler {
const pendingReverseSyncChangesetIndicesKey =
"pendingReverseSyncChangesetIndices" as const;

const [syncChangesetsToClearKey, syncChangesetsToUpdateKey] = this
.isReverseSynchronization
? [
pendingReverseSyncChangesetIndicesKey,
pendingSyncChangesetIndicesKey,
]
: [
pendingSyncChangesetIndicesKey,
pendingReverseSyncChangesetIndicesKey,
];
// Determine which keys to clear and update based on the synchronization direction
let syncChangesetsToClearKey;
let syncChangesetsToUpdateKey;

if (this.isReverseSynchronization) {
syncChangesetsToClearKey = pendingReverseSyncChangesetIndicesKey;
syncChangesetsToUpdateKey = pendingSyncChangesetIndicesKey;
} else {
syncChangesetsToClearKey = pendingSyncChangesetIndicesKey;
syncChangesetsToUpdateKey = pendingReverseSyncChangesetIndicesKey;
}

// NOTE that as documented in [[processChanges]], this assumes that right after
// transformation finalization, the work will be saved immediately, otherwise we've
Expand Down Expand Up @@ -2346,7 +2362,9 @@ export class IModelTransformer extends IModelExportHandler {
// FIXME<MIKE>: is this necessary when manually using low level transform APIs? (document if so)
private finalizeTransformation() {
this.importer.finalize();
this.updateSynchronizationVersion();
this.updateSynchronizationVersion({
initializeReverseSyncVersion: this._isProvenanceInitTransform,
});

if (this._partiallyCommittedEntities.size > 0) {
const message = [
Expand Down
136 changes: 136 additions & 0 deletions packages/transformer/src/test/standalone/IModelTransformerHub.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,142 @@ describe("IModelTransformerHub", () => {
return iModelId;
};

it("save reverse sync version for processAll transformations", async () => {
const sourceIModelId = await HubWrappers.createIModel(
accessToken,
iTwinId,
"source"
);

const targetIModelId = await HubWrappers.createIModel(
accessToken,
iTwinId,
"target"
);
assert.isTrue(Guid.isGuid(sourceIModelId));
assert.isTrue(Guid.isGuid(targetIModelId));
try {
// download and open briefcase on source imodel
const sourceBriefcase = await HubWrappers.downloadAndOpenBriefcase({
accessToken: await IModelHost.getAccessToken(),
iTwinId,
iModelId: sourceIModelId,
asOf: IModelVersion.latest().toJSON(),
});
await sourceBriefcase.locks.acquireLocks({
shared: "0x10",
exclusive: "0x1",
});
assert.isTrue(sourceBriefcase.isBriefcaseDb());
assert.isFalse(sourceBriefcase.isSnapshot);

// set up physical models
const sourceModelId0 = PhysicalModel.insert(
sourceBriefcase,
IModel.rootSubjectId,
"M0"
);
const sourceModelId1 = PhysicalModel.insert(
sourceBriefcase,
IModel.rootSubjectId,
"M1"
);
assert.isDefined(sourceModelId0);
assert.isDefined(sourceModelId1);

sourceBriefcase.saveChanges();
await sourceBriefcase.pushChanges({
description: "source changes for inserting physical elements M0 and M1",
retainLocks: true,
});

// download and open briefcase on target imodel
const targetBriefcase = await HubWrappers.downloadAndOpenBriefcase({
accessToken: await IModelHost.getAccessToken(),
iTwinId,
iModelId: targetIModelId,
asOf: IModelVersion.latest().toJSON(),
});
assert.isTrue(targetBriefcase.isBriefcaseDb());
assert.isFalse(targetBriefcase.isSnapshot);

await targetBriefcase.locks.acquireLocks({
shared: "0x10",
exclusive: "0x1",
});

// we do not expect to save reverse sync version by default for processAll transformations
const transformer1 = new IModelTransformer(
sourceBriefcase,
targetBriefcase
);
await transformer1.process();
const scopingEsa1 = transformer1["_targetScopeProvenanceProps"];
const reverseSyncVersion1 =
scopingEsa1?.jsonProperties.reverseSyncVersion;
assert.isEmpty(reverseSyncVersion1);
targetBriefcase.saveChanges();
await targetBriefcase.pushChanges({
description: "target changes for transformation 1",
retainLocks: true,
});

const sourceModelId2 = PhysicalModel.insert(
sourceBriefcase,
IModel.rootSubjectId,
"M2"
);
assert.isDefined(sourceModelId2);
sourceBriefcase.saveChanges();
await sourceBriefcase.pushChanges({
description: "source changes for inserting physical elements M2",
retainLocks: true,
});

// when initializeReverseSyncVersion is set to true, we expect to save reverse sync version
const transformer2 = new IModelTransformer(
sourceBriefcase,
targetBriefcase
);
await transformer2.process();
transformer2.updateSynchronizationVersion({
initializeReverseSyncVersion: true,
});
const scopingEsa2 = transformer2["_targetScopeProvenanceProps"];
const reverseSyncVersion2 =
scopingEsa2?.jsonProperties.reverseSyncVersion;
assert.isNotEmpty(reverseSyncVersion2);
const expectedReverseSyncVersion1 = `${targetBriefcase.changeset.id};${targetBriefcase.changeset.index}`;
assert.equal(reverseSyncVersion2, expectedReverseSyncVersion1);
// the recently pushed PendingReverseSync index should be equal to the latest target changeset index + 1
const lastPendingReverseSyncIndex1 =
scopingEsa2?.jsonProperties.pendingReverseSyncChangesetIndices.pop();
assert.equal(
lastPendingReverseSyncIndex1,
(targetBriefcase.changeset.index ?? 0) + 1
);
targetBriefcase.saveChanges();
await targetBriefcase.pushChanges({
description: "target changes for transformation 2",
retainLocks: true,
});
} finally {
try {
await IModelHost.hubAccess.deleteIModel({
iTwinId,
iModelId: sourceIModelId,
});
await IModelHost.hubAccess.deleteIModel({
iTwinId,
iModelId: targetIModelId,
});
} catch (err) {
// eslint-disable-next-line no-console
console.log("can't destroy", err);
}
}
});

it("Transform source iModel to target iModel", async () => {
const sourceIModelId = await createPopulatedIModelHubIModel(
"TransformerSource",
Expand Down

0 comments on commit 01ffd0a

Please sign in to comment.