@itwin/imodel-transformer 0.4.18-fedguidopt.6 → 1.0.0-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/lib/cjs/Algo.d.ts.map +1 -1
  2. package/lib/cjs/Algo.js +3 -4
  3. package/lib/cjs/Algo.js.map +1 -1
  4. package/lib/cjs/BigMap.d.ts.map +1 -1
  5. package/lib/cjs/BigMap.js +1 -1
  6. package/lib/cjs/BigMap.js.map +1 -1
  7. package/lib/cjs/BranchProvenanceInitializer.d.ts.map +1 -1
  8. package/lib/cjs/BranchProvenanceInitializer.js +15 -4
  9. package/lib/cjs/BranchProvenanceInitializer.js.map +1 -1
  10. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts.map +1 -1
  11. package/lib/cjs/DetachedExportElementAspectsStrategy.js +12 -5
  12. package/lib/cjs/DetachedExportElementAspectsStrategy.js.map +1 -1
  13. package/lib/cjs/ECReferenceTypesCache.d.ts.map +1 -1
  14. package/lib/cjs/ECReferenceTypesCache.js +32 -18
  15. package/lib/cjs/ECReferenceTypesCache.js.map +1 -1
  16. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts +1 -1
  17. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts.map +1 -1
  18. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js +7 -5
  19. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js.map +1 -1
  20. package/lib/cjs/ElementCascadingDeleter.d.ts +3 -3
  21. package/lib/cjs/ElementCascadingDeleter.d.ts.map +1 -1
  22. package/lib/cjs/ElementCascadingDeleter.js +9 -7
  23. package/lib/cjs/ElementCascadingDeleter.js.map +1 -1
  24. package/lib/cjs/EntityMap.d.ts.map +1 -1
  25. package/lib/cjs/EntityMap.js.map +1 -1
  26. package/lib/cjs/EntityUnifier.d.ts +5 -0
  27. package/lib/cjs/EntityUnifier.d.ts.map +1 -1
  28. package/lib/cjs/EntityUnifier.js +22 -35
  29. package/lib/cjs/EntityUnifier.js.map +1 -1
  30. package/lib/cjs/ExportElementAspectsStrategy.d.ts.map +1 -1
  31. package/lib/cjs/ExportElementAspectsStrategy.js +5 -4
  32. package/lib/cjs/ExportElementAspectsStrategy.js.map +1 -1
  33. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts.map +1 -1
  34. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js +9 -5
  35. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js.map +1 -1
  36. package/lib/cjs/IModelCloneContext.d.ts +1 -4
  37. package/lib/cjs/IModelCloneContext.d.ts.map +1 -1
  38. package/lib/cjs/IModelCloneContext.js +21 -40
  39. package/lib/cjs/IModelCloneContext.js.map +1 -1
  40. package/lib/cjs/IModelExporter.d.ts +69 -73
  41. package/lib/cjs/IModelExporter.d.ts.map +1 -1
  42. package/lib/cjs/IModelExporter.js +240 -176
  43. package/lib/cjs/IModelExporter.js.map +1 -1
  44. package/lib/cjs/IModelImporter.d.ts +17 -65
  45. package/lib/cjs/IModelImporter.d.ts.map +1 -1
  46. package/lib/cjs/IModelImporter.js +79 -109
  47. package/lib/cjs/IModelImporter.js.map +1 -1
  48. package/lib/cjs/IModelTransformer.d.ts +143 -99
  49. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  50. package/lib/cjs/IModelTransformer.js +717 -731
  51. package/lib/cjs/IModelTransformer.js.map +1 -1
  52. package/lib/cjs/PendingReferenceMap.d.ts.map +1 -1
  53. package/lib/cjs/PendingReferenceMap.js +12 -6
  54. package/lib/cjs/PendingReferenceMap.js.map +1 -1
  55. package/lib/cjs/TransformerLoggerCategory.d.ts +2 -2
  56. package/lib/cjs/TransformerLoggerCategory.d.ts.map +1 -1
  57. package/lib/cjs/TransformerLoggerCategory.js +5 -5
  58. package/lib/cjs/TransformerLoggerCategory.js.map +1 -1
  59. package/lib/cjs/transformer.d.ts.map +1 -1
  60. package/lib/cjs/transformer.js +14 -10
  61. package/lib/cjs/transformer.js.map +1 -1
  62. package/package.json +20 -17
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TemplateModelCloner = exports.IModelTransformer = void 0;
4
4
  /*---------------------------------------------------------------------------------------------
5
- * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
6
- * See LICENSE.md in the project root for license terms and full copyright notice.
7
- *--------------------------------------------------------------------------------------------*/
5
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
6
+ * See LICENSE.md in the project root for license terms and full copyright notice.
7
+ *--------------------------------------------------------------------------------------------*/
8
8
  /** @packageDocumentation
9
9
  * @module iModels
10
10
  */
@@ -95,15 +95,115 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
95
95
  get targetScopeElementId() {
96
96
  return this._options.targetScopeElementId;
97
97
  }
98
- get _isReverseSynchronization() {
99
- return this._isSynchronization && this._options.isReverseSynchronization;
98
+ /**
99
+ * Queries for an esa which matches the props in the provided aspectProps.
100
+ * @param dbToQuery db to run the query on for scope external source
101
+ * @param aspectProps aspectProps to search for @see ExternalSourceAspectProps
102
+ */
103
+ static queryScopeExternalSourceAspect(dbToQuery, aspectProps) {
104
+ const sql = `
105
+ SELECT ECInstanceId, Version, JsonProperties
106
+ FROM ${core_backend_1.ExternalSourceAspect.classFullName}
107
+ WHERE Element.Id=:elementId
108
+ AND Scope.Id=:scopeId
109
+ AND Kind=:kind
110
+ AND Identifier=:identifier
111
+ LIMIT 1
112
+ `;
113
+ return dbToQuery.withPreparedStatement(sql, (statement) => {
114
+ statement.bindId("elementId", aspectProps.element.id);
115
+ if (aspectProps.scope === undefined)
116
+ return undefined; // return instead of binding an invalid id
117
+ statement.bindId("scopeId", aspectProps.scope.id);
118
+ statement.bindString("kind", aspectProps.kind);
119
+ statement.bindString("identifier", aspectProps.identifier);
120
+ if (core_bentley_1.DbResult.BE_SQLITE_ROW !== statement.step())
121
+ return undefined;
122
+ const aspectId = statement.getValue(0).getId();
123
+ const versionValue = statement.getValue(1);
124
+ const version = versionValue.isNull
125
+ ? undefined
126
+ : versionValue.getString();
127
+ const jsonPropsValue = statement.getValue(2);
128
+ const jsonProperties = jsonPropsValue.isNull
129
+ ? undefined
130
+ : jsonPropsValue.getString();
131
+ return { aspectId, version, jsonProperties };
132
+ });
100
133
  }
101
- get _isForwardSynchronization() {
102
- return this._isSynchronization && !this._options.isReverseSynchronization;
134
+ /**
135
+ * Determines the sync direction "forward" or "reverse" of a given sourceDb and targetDb by looking for the scoping ESA.
136
+ * If the sourceDb's iModelId is found as the identifier of the expected scoping ESA in the targetDb, then it is a forward synchronization.
137
+ * If the targetDb's iModelId is found as the identifier of the expected scoping ESA in the sourceDb, then it is a reverse synchronization.
138
+ * @throws if no scoping ESA can be found in either the sourceDb or targetDb which describes a master branch relationship between the two databases.
139
+ * @returns "forward" or "reverse"
140
+ */
141
+ static determineSyncType(sourceDb, targetDb,
142
+ /** @see [[IModelTransformOptions.targetScopeElementId]] */
143
+ targetScopeElementId) {
144
+ const aspectProps = {
145
+ id: undefined,
146
+ version: undefined,
147
+ classFullName: core_backend_1.ExternalSourceAspect.classFullName,
148
+ element: {
149
+ id: targetScopeElementId,
150
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
151
+ },
152
+ scope: { id: core_common_1.IModel.rootSubjectId },
153
+ identifier: sourceDb.iModelId,
154
+ kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
155
+ jsonProperties: undefined,
156
+ };
157
+ /** First check if the targetDb is the branch (branch is the @see provenanceDb) */
158
+ const esaPropsFromTargetDb = this.queryScopeExternalSourceAspect(targetDb, aspectProps);
159
+ if (esaPropsFromTargetDb !== undefined) {
160
+ return "forward"; // we found an esa assuming targetDb is the provenanceDb/branch so this is a forward sync.
161
+ }
162
+ // Now check if the sourceDb is the branch
163
+ aspectProps.identifier = targetDb.iModelId;
164
+ const esaPropsFromSourceDb = this.queryScopeExternalSourceAspect(sourceDb, aspectProps);
165
+ if (esaPropsFromSourceDb !== undefined) {
166
+ return "reverse"; // we found an esa assuming sourceDb is the provenanceDb/branch so this is a reverse sync.
167
+ }
168
+ throw new Error(this.noEsaSyncDirectionErrorMessage);
169
+ }
170
+ determineSyncType() {
171
+ if (this._isProvenanceInitTransform) {
172
+ return "forward";
173
+ }
174
+ if (!this._isSynchronization) {
175
+ return "not-sync";
176
+ }
177
+ try {
178
+ return IModelTransformer.determineSyncType(this.sourceDb, this.targetDb, this.targetScopeElementId);
179
+ }
180
+ catch (err) {
181
+ if (err instanceof Error &&
182
+ err.message === IModelTransformer.noEsaSyncDirectionErrorMessage &&
183
+ this._allowNoScopingESA) {
184
+ return "forward";
185
+ }
186
+ throw err;
187
+ }
188
+ }
189
+ get isReverseSynchronization() {
190
+ if (this._syncType === undefined)
191
+ this._syncType = this.determineSyncType();
192
+ return this._syncType === "reverse";
193
+ }
194
+ get isForwardSynchronization() {
195
+ if (this._syncType === undefined)
196
+ this._syncType = this.determineSyncType();
197
+ return this._syncType === "forward";
103
198
  }
104
199
  /** The element classes that are considered to define provenance in the iModel */
105
200
  static get provenanceElementClasses() {
106
- return [core_backend_1.FolderLink, core_backend_1.SynchronizationConfigLink, core_backend_1.ExternalSource, core_backend_1.ExternalSourceAttachment];
201
+ return [
202
+ core_backend_1.FolderLink,
203
+ core_backend_1.SynchronizationConfigLink,
204
+ core_backend_1.ExternalSource,
205
+ core_backend_1.ExternalSourceAttachment,
206
+ ];
107
207
  }
108
208
  /** The element aspect classes that are considered to define provenance in the iModel */
109
209
  static get provenanceElementAspectClasses() {
@@ -124,12 +224,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
124
224
  /** map of partially committed entities to their partial commit progress */
125
225
  this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
126
226
  this._isSynchronization = false;
227
+ /**
228
+ * A private variable meant to be set by tests which have an outdated way of setting up transforms. In all synchronizations today we expect to find an ESA in the branch db which describes the master -> branch relationship.
229
+ * The exception to this is the first transform aka the provenance initializing transform which requires that the master imodel and the branch imodel are identical at the time of provenance initialization.
230
+ * A couple ofoutdated tests run their first transform providing a source and targetdb that are slightly different which is no longer supported. In order to not remove these tests which are still providing value
231
+ * this private property on the IModelTransformer exists.
232
+ */
233
+ this._allowNoScopingESA = false;
127
234
  this._changesetRanges = undefined;
128
235
  /** Set of entity keys which were not exported and don't need to be tracked for pending reference resolution.
129
236
  * @note Currently only tracks elements which were not exported.
130
237
  */
131
238
  this._skippedEntities = new Set();
132
- // FIXME: add test transforming using this, then switching to new transform method
133
239
  /**
134
240
  * Previously the transformer would insert provenance always pointing to the "target" relationship.
135
241
  * It should (and now by default does) instead insert provenance pointing to the provenanceSource
@@ -146,7 +252,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
146
252
  this._startingChangesetIndices = undefined;
147
253
  this._cachedSynchronizationVersion = undefined;
148
254
  this._targetClassNameToClassIdCache = new Map();
149
- // if undefined, it can be initialized by calling [[this._cacheSourceChanges]]
255
+ // if undefined, it can be initialized by calling [[this.processChangesets]]
150
256
  this._hasElementChangedCache = undefined;
151
257
  this._deletedSourceRelationshipData = undefined;
152
258
  this._yieldManager = new core_bentley_1.YieldManager();
@@ -155,9 +261,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
155
261
  this._longNamedSchemasMap = new Map();
156
262
  /** state to prevent reinitialization, @see [[initialize]] */
157
263
  this._initialized = false;
158
- /** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
159
- this._changeSummaryIds = undefined;
160
264
  this._sourceChangeDataState = "uninited";
265
+ /** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
266
+ this._csFileProps = undefined;
161
267
  /** previous provenance, either a federation guid, a `${sourceFedGuid}/${targetFedGuid}` pair, or required aspect props */
162
268
  this._lastProvenanceEntityInfo = nullLastProvenanceEntityInfo;
163
269
  // initialize IModelTransformOptions
@@ -167,9 +273,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
167
273
  cloneUsingBinaryGeometry: options?.cloneUsingBinaryGeometry ?? true,
168
274
  targetScopeElementId: options?.targetScopeElementId ?? core_common_1.IModel.rootSubjectId,
169
275
  // eslint-disable-next-line deprecation/deprecation
170
- danglingReferencesBehavior: options?.danglingReferencesBehavior ?? options?.danglingPredecessorsBehavior ?? "reject",
276
+ danglingReferencesBehavior: options?.danglingReferencesBehavior ??
277
+ options?.danglingPredecessorsBehavior ??
278
+ "reject",
279
+ branchRelationshipDataBehavior: options?.branchRelationshipDataBehavior ?? "reject",
171
280
  };
172
- this._isFirstSynchronization = this._options.wasSourceIModelCopiedToTarget ? true : undefined;
281
+ this._isProvenanceInitTransform = this._options
282
+ .wasSourceIModelCopiedToTarget
283
+ ? true
284
+ : undefined;
173
285
  // initialize exporter and sourceDb
174
286
  if (source instanceof core_backend_1.IModelDb) {
175
287
  this.exporter = new IModelExporter_1.IModelExporter(source);
@@ -180,7 +292,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
180
292
  this.sourceDb = this.exporter.sourceDb;
181
293
  this.exporter.registerHandler(this);
182
294
  this.exporter.wantGeometry = options?.loadSourceGeometry ?? false; // optimization to not load source GeometryStreams by default
183
- if (!this._options.includeSourceProvenance) { // clone provenance from the source iModel into the target iModel?
295
+ if (!this._options.includeSourceProvenance) {
296
+ // clone provenance from the source iModel into the target iModel?
184
297
  IModelTransformer.provenanceElementClasses.forEach((cls) => this.exporter.excludeElementClass(cls.classFullName));
185
298
  IModelTransformer.provenanceElementAspectClasses.forEach((cls) => this.exporter.excludeElementAspectClass(cls.classFullName));
186
299
  }
@@ -188,26 +301,21 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
188
301
  this.exporter.excludeElementAspectClass("BisCore:TextAnnotationData"); // This ElementAspect is auto-created by the BisCore:TextAnnotation2d/3d element handlers
189
302
  // initialize importer and targetDb
190
303
  if (target instanceof core_backend_1.IModelDb) {
191
- this.importer = new IModelImporter_1.IModelImporter(target, { preserveElementIdsForFiltering: this._options.preserveElementIdsForFiltering });
304
+ this.importer = new IModelImporter_1.IModelImporter(target, {
305
+ preserveElementIdsForFiltering: this._options.preserveElementIdsForFiltering,
306
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements,
307
+ });
192
308
  }
193
309
  else {
194
310
  this.importer = target;
195
- /* eslint-disable deprecation/deprecation */
196
- if (Boolean(this._options.preserveElementIdsForFiltering) !== this.importer.preserveElementIdsForFiltering) {
197
- core_bentley_1.Logger.logWarning(loggerCategory, [
198
- "A custom importer was passed as a target but its 'preserveElementIdsForFiltering' option is out of sync with the transformer's option.",
199
- "The custom importer target's option will be force updated to use the transformer's value.",
200
- "This behavior is deprecated and will be removed in a future version, throwing an error if they are out of sync.",
201
- ].join("\n"));
202
- this.importer.preserveElementIdsForFiltering = Boolean(this._options.preserveElementIdsForFiltering);
203
- }
204
- /* eslint-enable deprecation/deprecation */
311
+ this.validateSharedOptionsMatch();
205
312
  }
206
313
  this.targetDb = this.importer.targetDb;
207
314
  // create the IModelCloneContext, it must be initialized later
208
315
  this.context = new IModelCloneContext_1.IModelCloneContext(this.sourceDb, this.targetDb);
209
316
  if (this.sourceDb.isBriefcase && this.targetDb.isBriefcase) {
210
- nodeAssert(this.sourceDb.changeset.index !== undefined && this.targetDb.changeset.index !== undefined, "database has no changeset index");
317
+ nodeAssert(this.sourceDb.changeset.index !== undefined &&
318
+ this.targetDb.changeset.index !== undefined, "database has no changeset index");
211
319
  this._startingChangesetIndices = {
212
320
  target: this.targetDb.changeset.index,
213
321
  source: this.sourceDb.changeset.index,
@@ -215,12 +323,27 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
215
323
  }
216
324
  // this internal is guaranteed stable for just transformer usage
217
325
  /* eslint-disable @itwin/no-internal */
218
- if ("codeValueBehavior" in this.sourceDb) {
326
+ if (("codeValueBehavior" in this.sourceDb)) {
219
327
  this.sourceDb.codeValueBehavior = "exact";
220
328
  this.targetDb.codeValueBehavior = "exact";
221
329
  }
222
330
  /* eslint-enable @itwin/no-internal */
223
331
  }
332
+ /** validates that the importer set on the transformer has the same values for its shared options as the transformer.
333
+ * @note This expects that the importer is already set on the transformer.
334
+ */
335
+ validateSharedOptionsMatch() {
336
+ if (Boolean(this._options.preserveElementIdsForFiltering) !==
337
+ this.importer.options.preserveElementIdsForFiltering) {
338
+ const errMessage = "A custom importer was passed as a target but its 'preserveElementIdsForFiltering' option is out of sync with the transformer's option.";
339
+ throw new Error(errMessage);
340
+ }
341
+ if (Boolean(this._options.skipPropagateChangesToRootElements) !==
342
+ this.importer.options.skipPropagateChangesToRootElements) {
343
+ const errMessage = "A custom importer was passed as a target but its 'skipPropagateChangesToRootElements' option is out of sync with the transformer's option.";
344
+ throw new Error(errMessage);
345
+ }
346
+ }
224
347
  /** Dispose any native resources associated with this IModelTransformer. */
225
348
  dispose() {
226
349
  core_bentley_1.Logger.logTrace(loggerCategory, "dispose()");
@@ -246,24 +369,31 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
246
369
  * @note This will be [[targetDb]] except when it is a reverse synchronization. In that case it be [[sourceDb]].
247
370
  */
248
371
  get provenanceDb() {
249
- return this._options.isReverseSynchronization ? this.sourceDb : this.targetDb;
372
+ return this.isReverseSynchronization ? this.sourceDb : this.targetDb;
250
373
  }
251
374
  /** Return the IModelDb where IModelTransformer looks for entities referred to by stored provenance.
252
375
  * @note This will be [[sourceDb]] except when it is a reverse synchronization. In that case it be [[targetDb]].
253
376
  */
254
377
  get provenanceSourceDb() {
255
- return this._options.isReverseSynchronization ? this.targetDb : this.sourceDb;
378
+ return this.isReverseSynchronization ? this.targetDb : this.sourceDb;
256
379
  }
257
380
  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
258
381
  static initElementProvenanceOptions(sourceElementId, targetElementId, args) {
259
- const elementId = args.isReverseSynchronization ? sourceElementId : targetElementId;
382
+ const elementId = args.isReverseSynchronization
383
+ ? sourceElementId
384
+ : targetElementId;
260
385
  const version = args.isReverseSynchronization
261
386
  ? args.targetDb.elements.queryLastModifiedTime(targetElementId)
262
387
  : args.sourceDb.elements.queryLastModifiedTime(sourceElementId);
263
- const aspectIdentifier = args.isReverseSynchronization ? targetElementId : sourceElementId;
388
+ const aspectIdentifier = args.isReverseSynchronization
389
+ ? targetElementId
390
+ : sourceElementId;
264
391
  const aspectProps = {
265
392
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
266
- element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
393
+ element: {
394
+ id: elementId,
395
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
396
+ },
267
397
  scope: { id: args.targetScopeElementId },
268
398
  identifier: aspectIdentifier,
269
399
  kind: core_backend_1.ExternalSourceAspect.Kind.Element,
@@ -272,9 +402,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
272
402
  return aspectProps;
273
403
  }
274
404
  static initRelationshipProvenanceOptions(sourceRelInstanceId, targetRelInstanceId, args) {
275
- const provenanceDb = args.isReverseSynchronization ? args.sourceDb : args.targetDb;
276
- const aspectIdentifier = args.isReverseSynchronization ? targetRelInstanceId : sourceRelInstanceId;
277
- const provenanceRelInstanceId = args.isReverseSynchronization ? sourceRelInstanceId : targetRelInstanceId;
405
+ const provenanceDb = args.isReverseSynchronization
406
+ ? args.sourceDb
407
+ : args.targetDb;
408
+ const aspectIdentifier = args.isReverseSynchronization
409
+ ? targetRelInstanceId
410
+ : sourceRelInstanceId;
411
+ const provenanceRelInstanceId = args.isReverseSynchronization
412
+ ? sourceRelInstanceId
413
+ : targetRelInstanceId;
278
414
  const elementId = provenanceDb.withPreparedStatement("SELECT SourceECInstanceId FROM bis.ElementRefersToElements WHERE ECInstanceId=?", (stmt) => {
279
415
  stmt.bindId(1, provenanceRelInstanceId);
280
416
  nodeAssert(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
@@ -285,7 +421,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
285
421
  : { provenanceRelInstanceId };
286
422
  const aspectProps = {
287
423
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
288
- element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
424
+ element: {
425
+ id: elementId,
426
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
427
+ },
289
428
  scope: { id: args.targetScopeElementId },
290
429
  identifier: aspectIdentifier,
291
430
  kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
@@ -296,7 +435,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
296
435
  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
297
436
  initElementProvenance(sourceElementId, targetElementId) {
298
437
  return IModelTransformer.initElementProvenanceOptions(sourceElementId, targetElementId, {
299
- isReverseSynchronization: !!this._options.isReverseSynchronization,
438
+ isReverseSynchronization: this.isReverseSynchronization,
300
439
  targetScopeElementId: this.targetScopeElementId,
301
440
  sourceDb: this.sourceDb,
302
441
  targetDb: this.targetDb,
@@ -311,7 +450,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
311
450
  return IModelTransformer.initRelationshipProvenanceOptions(sourceRelationship.id, targetRelInstanceId, {
312
451
  sourceDb: this.sourceDb,
313
452
  targetDb: this.targetDb,
314
- isReverseSynchronization: !!this._options.isReverseSynchronization,
453
+ isReverseSynchronization: this.isReverseSynchronization,
315
454
  targetScopeElementId: this.targetScopeElementId,
316
455
  forceOldRelationshipProvenanceMethod: this._forceOldRelationshipProvenanceMethod,
317
456
  });
@@ -325,13 +464,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
325
464
  get _synchronizationVersion() {
326
465
  if (!this._cachedSynchronizationVersion) {
327
466
  nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps was not set yet");
328
- const version = this._options.isReverseSynchronization
329
- ? this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion
467
+ const version = this.isReverseSynchronization
468
+ ? this._targetScopeProvenanceProps.jsonProperties?.reverseSyncVersion
330
469
  : this._targetScopeProvenanceProps.version;
331
470
  nodeAssert(version !== undefined, "no version contained in target scope");
332
- const [id, index] = version === ""
333
- ? ["", -1]
334
- : version.split(";");
471
+ const [id, index] = version === "" ? ["", -1] : version.split(";");
335
472
  this._cachedSynchronizationVersion = { index: Number(index), id };
336
473
  nodeAssert(!Number.isNaN(this._cachedSynchronizationVersion.index), "bad parse: invalid index in version");
337
474
  }
@@ -348,7 +485,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
348
485
  if (!provenanceScopeAspect) {
349
486
  return { index: -1, id: "" }; // first synchronization.
350
487
  }
351
- const version = this._options.isReverseSynchronization
488
+ const version = this.isReverseSynchronization
352
489
  ? JSON.parse(provenanceScopeAspect.jsonProperties ?? "{}").reverseSyncVersion
353
490
  : provenanceScopeAspect.version;
354
491
  if (!version) {
@@ -366,15 +503,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
366
503
  * Provenance scope aspect is created and inserted into provenanceDb when [[initScopeProvenance]] is invoked.
367
504
  */
368
505
  tryGetProvenanceScopeAspect() {
369
- const scopeProvenanceAspectId = this.queryScopeExternalSource({
506
+ const scopeProvenanceAspectProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, {
507
+ id: undefined,
370
508
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
371
509
  scope: { id: core_common_1.IModel.rootSubjectId },
372
510
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
373
511
  element: { id: this.targetScopeElementId ?? core_common_1.IModel.rootSubjectId },
374
512
  identifier: this.provenanceSourceDb.iModelId,
375
513
  });
376
- return scopeProvenanceAspectId.aspectId
377
- ? this.provenanceDb.elements.getAspect(scopeProvenanceAspectId.aspectId)
514
+ return scopeProvenanceAspectProps !== undefined
515
+ ? this.provenanceDb.elements.getAspect(scopeProvenanceAspectProps.aspectId)
378
516
  : undefined;
379
517
  }
380
518
  /**
@@ -388,19 +526,17 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
388
526
  id: undefined,
389
527
  version: undefined,
390
528
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
391
- element: { id: this.targetScopeElementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
529
+ element: {
530
+ id: this.targetScopeElementId,
531
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
532
+ },
392
533
  scope: { id: core_common_1.IModel.rootSubjectId },
393
534
  identifier: this.provenanceSourceDb.iModelId,
394
535
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
395
536
  jsonProperties: undefined,
396
537
  };
397
- // FIXME: handle older transformed iModels which do NOT have the version. Add test where we don't set those and then start setting them.
398
- // or reverseSyncVersion set correctly
399
- const externalSource = this.queryScopeExternalSource(aspectProps, { getJsonProperties: true }); // this query includes "identifier"
400
- aspectProps.id = externalSource.aspectId;
401
- aspectProps.version = externalSource.version;
402
- aspectProps.jsonProperties = externalSource.jsonProperties ? JSON.parse(externalSource.jsonProperties) : {};
403
- if (undefined === aspectProps.id) {
538
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps); // this query includes "identifier"
539
+ if (foundEsaProps === undefined) {
404
540
  aspectProps.version = ""; // empty since never before transformed. Will be updated in [[finalizeTransformation]]
405
541
  aspectProps.jsonProperties = {
406
542
  pendingReverseSyncChangesetIndices: [],
@@ -426,44 +562,33 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
426
562
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidId, "Provenance scope conflict");
427
563
  }
428
564
  if (!this._options.noProvenance) {
429
- this.provenanceDb.elements.insertAspect({
565
+ const id = this.provenanceDb.elements.insertAspect({
430
566
  ...aspectProps,
431
567
  jsonProperties: JSON.stringify(aspectProps.jsonProperties),
432
568
  });
569
+ aspectProps.id = id;
433
570
  }
434
571
  }
435
- this._targetScopeProvenanceProps = aspectProps;
436
- }
437
- /**
438
- * @returns the id and version of an aspect with the given element, scope, kind, and identifier
439
- * May also return a reverseSyncVersion from json properties if requested
440
- */
441
- queryScopeExternalSource(aspectProps, { getJsonProperties = false } = {}) {
442
- const sql = `
443
- SELECT ECInstanceId, Version
444
- ${getJsonProperties ? ", JsonProperties" : ""}
445
- FROM ${core_backend_1.ExternalSourceAspect.classFullName}
446
- WHERE Element.Id=:elementId
447
- AND Scope.Id=:scopeId
448
- AND Kind=:kind
449
- AND Identifier=:identifier
450
- LIMIT 1
451
- `;
452
- const emptyResult = { aspectId: undefined, version: undefined, jsonProperties: undefined };
453
- return this.provenanceDb.withPreparedStatement(sql, (statement) => {
454
- statement.bindId("elementId", aspectProps.element.id);
455
- if (aspectProps.scope === undefined)
456
- return emptyResult; // return undefined instead of binding an invalid id
457
- statement.bindId("scopeId", aspectProps.scope.id);
458
- statement.bindString("kind", aspectProps.kind);
459
- statement.bindString("identifier", aspectProps.identifier);
460
- if (core_bentley_1.DbResult.BE_SQLITE_ROW !== statement.step())
461
- return emptyResult;
462
- const aspectId = statement.getValue(0).getId();
463
- const version = statement.getValue(1).getString();
464
- const jsonProperties = getJsonProperties ? statement.getValue(2).getString() : undefined;
465
- return { aspectId, version, jsonProperties };
466
- });
572
+ else {
573
+ // foundEsaProps is defined.
574
+ aspectProps.id = foundEsaProps.aspectId;
575
+ aspectProps.version =
576
+ foundEsaProps.version ??
577
+ (this._options.branchRelationshipDataBehavior === "unsafe-migrate"
578
+ ? ""
579
+ : undefined);
580
+ aspectProps.jsonProperties = foundEsaProps.jsonProperties
581
+ ? JSON.parse(foundEsaProps.jsonProperties)
582
+ : this._options.branchRelationshipDataBehavior === "unsafe-migrate"
583
+ ? {
584
+ pendingReverseSyncChangesetIndices: [],
585
+ pendingSyncChangesetIndices: [],
586
+ reverseSyncVersion: "",
587
+ }
588
+ : undefined;
589
+ }
590
+ this._targetScopeProvenanceProps =
591
+ aspectProps;
467
592
  }
468
593
  /**
469
594
  * Iterate all matching federation guids and ExternalSourceAspects in the provenance iModel (target unless reverse sync)
@@ -477,13 +602,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
477
602
  if (!args.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
478
603
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
479
604
  }
480
- const sourceDb = args.isReverseSynchronization ? args.provenanceDb : args.provenanceSourceDb;
481
- const targetDb = args.isReverseSynchronization ? args.provenanceSourceDb : args.provenanceDb;
605
+ const sourceDb = args.isReverseSynchronization
606
+ ? args.provenanceDb
607
+ : args.provenanceSourceDb;
608
+ const targetDb = args.isReverseSynchronization
609
+ ? args.provenanceSourceDb
610
+ : args.provenanceDb;
482
611
  // query for provenanceDb
483
612
  const elementIdByFedGuidQuery = `
484
613
  SELECT e.ECInstanceId, FederationGuid
485
614
  FROM bis.Element e
486
- WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements
615
+ ${args.skipPropagateChangesToRootElements
616
+ ? "WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements"
617
+ : ""}
487
618
  ORDER BY FederationGuid
488
619
  `;
489
620
  // iterate through sorted list of fed guids from both dbs to get the intersection
@@ -501,22 +632,22 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
501
632
  // and the fact that '0' < '9' < a' < 'f' in ascii/utf8
502
633
  while (true) {
503
634
  const currSourceRow = sourceRow, currTargetRow = targetRow;
504
- if (currSourceRow.federationGuid !== undefined
505
- && currTargetRow.federationGuid !== undefined
506
- && currSourceRow.federationGuid === currTargetRow.federationGuid) {
635
+ if (currSourceRow.federationGuid !== undefined &&
636
+ currTargetRow.federationGuid !== undefined &&
637
+ currSourceRow.federationGuid === currTargetRow.federationGuid) {
507
638
  // data flow direction is always sourceDb -> targetDb and it does not depend on where the explicit element provenance is stored
508
639
  args.fn(sourceRow.id, targetRow.id);
509
640
  }
510
- if (currTargetRow.federationGuid === undefined
511
- || (currSourceRow.federationGuid !== undefined
512
- && currSourceRow.federationGuid >= currTargetRow.federationGuid)) {
641
+ if (currTargetRow.federationGuid === undefined ||
642
+ (currSourceRow.federationGuid !== undefined &&
643
+ currSourceRow.federationGuid >= currTargetRow.federationGuid)) {
513
644
  if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
514
645
  return;
515
646
  targetRow = targetStmt.getRow();
516
647
  }
517
- if (currSourceRow.federationGuid === undefined
518
- || (currTargetRow.federationGuid !== undefined
519
- && currSourceRow.federationGuid <= currTargetRow.federationGuid)) {
648
+ if (currSourceRow.federationGuid === undefined ||
649
+ (currTargetRow.federationGuid !== undefined &&
650
+ currSourceRow.federationGuid <= currTargetRow.federationGuid)) {
520
651
  if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
521
652
  return;
522
653
  sourceRow = sourceStmt.getRow();
@@ -534,7 +665,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
534
665
  // victims of the old provenance method that have both fedguids and an inserted aspect.
535
666
  // But this is a private function with one known caller where that doesn't matter
536
667
  args.provenanceDb.withPreparedStatement(provenanceAspectsQuery, (stmt) => {
537
- const runFnInDataFlowDirection = (sourceId, targetId) => args.isReverseSynchronization ? args.fn(sourceId, targetId) : args.fn(targetId, sourceId);
668
+ const runFnInDataFlowDirection = (sourceId, targetId) => args.isReverseSynchronization
669
+ ? args.fn(sourceId, targetId)
670
+ : args.fn(targetId, sourceId);
538
671
  stmt.bindId("scopeId", args.targetScopeElementId);
539
672
  stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
540
673
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
@@ -550,240 +683,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
550
683
  provenanceSourceDb: this.provenanceSourceDb,
551
684
  provenanceDb: this.provenanceDb,
552
685
  targetScopeElementId: this.targetScopeElementId,
553
- isReverseSynchronization: !!this._options.isReverseSynchronization,
686
+ isReverseSynchronization: this.isReverseSynchronization,
554
687
  fn,
688
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
555
689
  });
556
690
  }
557
- /** Initialize the source to target Element mapping from ExternalSourceAspects in the target iModel.
558
- * @note This method is called from all `process*` functions and should never need to be called directly.
559
- * @deprecated in 3.x. call [[initialize]] instead, it does the same thing among other initialization
560
- * @note Passing an [[InitFromExternalSourceAspectsArgs]] is required when processing changes, to remap any elements that may have been deleted.
561
- * You must await the returned promise as well in this case. The synchronous behavior has not changed but is deprecated and won't process everything.
562
- */
563
- initFromExternalSourceAspects(args) {
564
- this.forEachTrackedElement((sourceElementId, targetElementId) => {
565
- this.context.remapElement(sourceElementId, targetElementId);
566
- });
567
- if (args)
568
- return this.remapDeletedSourceEntities();
569
- }
570
691
  /**
571
- * Scan changesets for deleted entities, if in a reverse synchronization, provenance has
572
- * already been deleted, so we must scan for that as well.
692
+ * Queries the provenanceDb for an ESA whose identifier is equal to the provided 'entityInProvenanceSourceId'.
693
+ * The identifier on the ESA is the id of the element in the [[IModelTransformer.provenanceSourceDb]]
694
+ * Therefore it only makes sense to call this function when you have an id in the provenanceSourceDb.
695
+ * @param entityInProvenanceSourceId
696
+ * @returns the elementId that the ESA is stored on, esa.Element.Id
573
697
  */
574
- async remapDeletedSourceEntities() {
575
- // we need a connected iModel with changes to remap elements with deletions
576
- const notConnectedModel = this.sourceDb.iTwinId === undefined;
577
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
578
- if (notConnectedModel || noChanges)
579
- return;
580
- this._deletedSourceRelationshipData = new Map();
581
- nodeAssert(this._changeSummaryIds, "change summaries should be initialized before we get here");
582
- if (this._changeSummaryIds.length === 0)
583
- return;
584
- const alreadyImportedElementInserts = new Set();
585
- const alreadyImportedModelInserts = new Set();
586
- this.exporter.sourceDbChanges?.element.insertIds.forEach((insertedSourceElementId) => {
587
- const targetElementId = this.context.findTargetElementId(insertedSourceElementId);
588
- if (core_bentley_1.Id64.isValid(targetElementId))
589
- alreadyImportedElementInserts.add(targetElementId);
590
- });
591
- this.exporter.sourceDbChanges?.model.insertIds.forEach((insertedSourceModelId) => {
592
- const targetModelId = this.context.findTargetElementId(insertedSourceModelId);
593
- if (core_bentley_1.Id64.isValid(targetModelId))
594
- alreadyImportedModelInserts.add(targetModelId);
595
- });
596
- // optimization: if we have provenance, use it to avoid more querying later
597
- // eventually when itwin.js supports attaching a second iModelDb in JS,
598
- // this won't have to be a conditional part of the query, and we can always have it by attaching
599
- const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
600
- const deletedEntitySql = `
601
- SELECT
602
- 1 AS IsElemNotRel,
603
- ic.ChangedInstance.Id AS InstanceId,
604
- NULL AS InstId2, -- need these columns for relationship ends in the unioned query
605
- NULL AS InstId3,
606
- ec.FederationGuid AS FedGuid,
607
- NULL AS FedGuid2,
608
- ic.ChangedInstance.ClassId AS ClassId
609
- ${queryCanAccessProvenance ? `
610
- /*
611
- -- can't coalesce these due to a bug, so do it in JS
612
- , coalesce(
613
- IIF(esa.Scope.Id=:targetScopeElement, esa.Identifier, NULL),
614
- IIF(esac.Scope.Id=:targetScopeElement, esac.Identifier, NULL)
615
- ) AS Identifier1
616
- */
617
- , CASE WHEN esa.Scope.Id = ${this.targetScopeElementId} THEN esa.Identifier ELSE NULL END AS Identifier1A
618
- -- FIXME: using :targetScopeElement parameter in this second potential identifier breaks ecsql
619
- , CASE WHEN esac.Scope.Id = ${this.targetScopeElementId} THEN esac.Identifier ELSE NULL END AS Identifier1B
620
- , NULL AS Identifier2A
621
- , NULL AS Identifier2B
622
- ` : ""}
623
- FROM ecchange.change.InstanceChange ic
624
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') ec
625
- ON ic.ChangedInstance.Id=ec.ECInstanceId
626
- ${queryCanAccessProvenance ? `
627
- LEFT JOIN bis.ExternalSourceAspect esa
628
- ON ec.ECInstanceId=esa.Element.Id
629
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') esac
630
- ON ec.ECInstanceId=esac.Element.Id
631
- ` : ""}
632
- WHERE ic.OpCode=:opDelete
633
- AND ic.Summary.Id=:changeSummaryId
634
- AND ic.ChangedInstance.ClassId IS (BisCore.Element)
635
-
636
- UNION ALL
637
-
638
- SELECT
639
- 0 AS IsElemNotRel,
640
- ic.ChangedInstance.Id AS InstanceId,
641
- coalesce(se.ECInstanceId, sec.ECInstanceId) AS InstId2,
642
- coalesce(te.ECInstanceId, tec.ECInstanceId) AS InstId3,
643
- coalesce(se.FederationGuid, sec.FederationGuid) AS FedGuid1,
644
- coalesce(te.FederationGuid, tec.FederationGuid) AS FedGuid2,
645
- ic.ChangedInstance.ClassId AS ClassId
646
- ${queryCanAccessProvenance ? `
647
- , sesa.Identifier AS Identifier1A
648
- , sesac.Identifier AS Identifier1B
649
- , tesa.Identifier AS Identifier2A
650
- , tesac.Identifier AS Identifier2B
651
- ` : ""}
652
- FROM ecchange.change.InstanceChange ic
653
- LEFT JOIN bis.ElementRefersToElements.Changes(:changeSummaryId, 'BeforeDelete') ertec
654
- ON ic.ChangedInstance.Id=ertec.ECInstanceId
655
- -- FIXME: test a deletion of both an element and a relationship at the same time
656
- LEFT JOIN bis.Element se
657
- ON se.ECInstanceId=ertec.SourceECInstanceId
658
- LEFT JOIN bis.Element te
659
- ON te.ECInstanceId=ertec.TargetECInstanceId
660
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') sec
661
- ON sec.ECInstanceId=ertec.SourceECInstanceId
662
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') tec
663
- ON tec.ECInstanceId=ertec.TargetECInstanceId
664
- ${queryCanAccessProvenance ? `
665
- -- NOTE: need to join on both se/te and sec/tec incase the element was deleted
666
- LEFT JOIN bis.ExternalSourceAspect sesa
667
- ON se.ECInstanceId=sesa.Element.Id -- don't use *esac*.Identifier because it's a string
668
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') sesac
669
- ON sec.ECInstanceId=sesac.Element.Id
670
- LEFT JOIN bis.ExternalSourceAspect tesa
671
- ON te.ECInstanceId=tesa.Element.Id
672
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') tesac
673
- ON tec.ECInstanceId=tesac.Element.Id
674
- ` : ""}
675
- WHERE ic.OpCode=:opDelete
676
- AND ic.Summary.Id=:changeSummaryId
677
- AND ic.ChangedInstance.ClassId IS (BisCore.ElementRefersToElements)
678
- ${queryCanAccessProvenance ? `
679
- AND (sesa.Scope.Id=:targetScopeElement OR sesa.Scope.Id IS NULL)
680
- AND (sesa.Kind='Relationship' OR sesa.Kind IS NULL)
681
- AND (sesac.Scope.Id=:targetScopeElement OR sesac.Scope.Id IS NULL)
682
- AND (sesac.Kind='Relationship' OR sesac.Kind IS NULL)
683
- AND (tesa.Scope.Id=:targetScopeElement OR tesa.Scope.Id IS NULL)
684
- AND (tesa.Kind='Relationship' OR tesa.Kind IS NULL)
685
- AND (tesac.Scope.Id=:targetScopeElement OR tesac.Scope.Id IS NULL)
686
- AND (tesac.Kind='Relationship' OR tesac.Kind IS NULL)
687
- ` : ""}
688
- `;
689
- for (const changeSummaryId of this._changeSummaryIds) {
690
- this.sourceDb.withPreparedStatement(deletedEntitySql, (stmt) => {
691
- stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
692
- if (queryCanAccessProvenance)
693
- stmt.bindId("targetScopeElement", this.targetScopeElementId);
694
- stmt.bindId("changeSummaryId", changeSummaryId);
695
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
696
- const isElemNotRel = stmt.getValue(0).getBoolean();
697
- const instId = stmt.getValue(1).getId();
698
- if (isElemNotRel) {
699
- const sourceElemFedGuid = stmt.getValue(4).getGuid();
700
- // "Identifier" is a string, so null value returns '' which doesn't work with ??, and I don't like ||
701
- let identifierValue;
702
- // identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
703
- if (queryCanAccessProvenance) {
704
- identifierValue = stmt.getValue(7);
705
- if (identifierValue.isNull)
706
- identifierValue = stmt.getValue(8);
707
- }
708
- // TODO: if I could attach the second db, will probably be much faster to get target id
709
- // as part of the whole query rather than with _queryElemIdByFedGuid
710
- const targetId = (queryCanAccessProvenance && identifierValue
711
- && !identifierValue.isNull
712
- && identifierValue.getString())
713
- // maybe batching these queries would perform better but we should
714
- // try to attach the second db and query both together anyway
715
- || (sourceElemFedGuid && this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid))
716
- // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
717
- || this._queryProvenanceForElement(instId);
718
- // since we are processing one changeset at a time, we can see local source deletes
719
- // of entities that were never synced and can be safely ignored
720
- const deletionNotInTarget = !targetId;
721
- if (deletionNotInTarget)
722
- continue;
723
- this.context.remapElement(instId, targetId);
724
- // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
725
- // In such case an entity update will be triggered and we no longer need to delete the entity.
726
- if (alreadyImportedElementInserts.has(targetId)) {
727
- this.exporter.sourceDbChanges?.element.deleteIds.delete(instId);
728
- }
729
- if (alreadyImportedModelInserts.has(targetId)) {
730
- this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
731
- }
732
- }
733
- else { // is deleted relationship
734
- const classFullName = stmt.getValue(6).getClassNameForClassId();
735
- const [sourceIdInTarget, targetIdInTarget] = [
736
- // identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
737
- { guidColumn: 4, identifierColumns: { a: 7, b: 8 }, isTarget: false },
738
- { guidColumn: 5, identifierColumns: { a: 9, b: 10 }, isTarget: true },
739
- ].map(({ guidColumn, identifierColumns }) => {
740
- const fedGuid = stmt.getValue(guidColumn).getGuid();
741
- let identifierValue;
742
- // identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
743
- if (queryCanAccessProvenance) {
744
- identifierValue = stmt.getValue(identifierColumns.a);
745
- if (identifierValue.isNull)
746
- identifierValue = stmt.getValue(identifierColumns.b);
747
- }
748
- return ((queryCanAccessProvenance && identifierValue
749
- // FIXME: this is really far from idiomatic, try to undo that
750
- && !identifierValue.isNull
751
- && identifierValue.getString())
752
- // maybe batching these queries would perform better but we should
753
- // try to attach the second db and query both together anyway
754
- || (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
755
- });
756
- // since we are processing one changeset at a time, we can see local source deletes
757
- // of entities that were never synced and can be safely ignored
758
- if (sourceIdInTarget && targetIdInTarget) {
759
- this._deletedSourceRelationshipData.set(instId, {
760
- classFullName,
761
- sourceIdInTarget,
762
- targetIdInTarget,
763
- });
764
- }
765
- else {
766
- // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
767
- const relProvenance = this._queryProvenanceForRelationship(instId, {
768
- classFullName,
769
- sourceId: stmt.getValue(2).getId(),
770
- targetId: stmt.getValue(3).getId(),
771
- });
772
- if (relProvenance && relProvenance.relationshipId)
773
- this._deletedSourceRelationshipData.set(instId, {
774
- classFullName,
775
- relId: relProvenance.relationshipId,
776
- provenanceAspectId: relProvenance.aspectId,
777
- });
778
- }
779
- }
780
- }
781
- // NEXT: remap sourceId and targetId to target, get provenance there
782
- // NOTE: it is possible during a forward sync for the target to already have deleted
783
- // something that the source deleted, in which case we can safely ignore the gone provenance
784
- });
785
- }
786
- }
787
698
  _queryProvenanceForElement(entityInProvenanceSourceId) {
788
699
  return this.provenanceDb.withPreparedStatement(`
789
700
  SELECT esa.Element.Id
@@ -801,6 +712,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
801
712
  return undefined;
802
713
  });
803
714
  }
715
+ /**
716
+ * Queries the provenanceDb for an ESA whose identifier is equal to the provided 'entityInProvenanceSourceId'.
717
+ * The identifier on the ESA is the id of the relationship in the [[IModelTransformer.provenanceSourceDb]]
718
+ * Therefore it only makes sense to call this function when you have an id in the provenanceSourceDb.
719
+ * @param entityInProvenanceSourceId
720
+ * @returns
721
+ */
804
722
  _queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
805
723
  return this.provenanceDb.withPreparedStatement(`
806
724
  SELECT
@@ -833,7 +751,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
833
751
  sourceId: this.context.findTargetElementId(sourceRelInfo.sourceId),
834
752
  targetId: this.context.findTargetElementId(sourceRelInfo.targetId),
835
753
  };
836
- if (targetRelInfo.sourceId === undefined || targetRelInfo.targetId === undefined)
754
+ if (targetRelInfo.sourceId === undefined ||
755
+ targetRelInfo.targetId === undefined)
837
756
  return undefined; // couldn't find an element, rel is invalid or deleted
838
757
  return this.targetDb.withPreparedStatement(`
839
758
  SELECT ECInstanceId
@@ -867,7 +786,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
867
786
  JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
868
787
  WHERE s.Name=? AND c.Name=?
869
788
  `, (stmt) => {
870
- const [schemaName, className] = classFullName.split(".");
789
+ const [schemaName, className] = classFullName.indexOf(".") !== -1
790
+ ? classFullName.split(".")
791
+ : classFullName.split(":");
871
792
  stmt.bindString(1, schemaName);
872
793
  stmt.bindString(2, className);
873
794
  if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
@@ -885,26 +806,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
885
806
  });
886
807
  }
887
808
  /** Returns `true` if *brute force* delete detections should be run.
809
+ * @note This is only called if [[IModelTransformOptions.forceExternalSourceAspectProvenance]] option is true
888
810
  * @note Not relevant for processChanges when change history is known.
889
811
  */
890
812
  shouldDetectDeletes() {
891
- // FIXME: all synchronizations should mark this as false, but we can probably change this
892
- // to just follow the new deprecated option
893
- if (this._isFirstSynchronization)
894
- return false; // not necessary the first time since there are no deletes to detect
895
- if (this._options.isReverseSynchronization)
896
- return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
897
- // FIXME: do any tests fail? if not, consider using @see _isSynchronization
898
- if (this._isForwardSynchronization)
899
- return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
900
- return true;
813
+ nodeAssert(this._syncType !== undefined);
814
+ return this._syncType === "not-sync";
901
815
  }
902
816
  /**
903
817
  * Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
904
818
  * in the source iModel.
905
819
  * @deprecated in 1.x. Do not use this. // FIXME<MIKE>: how to better explain this?
906
820
  * This method is only called during [[processAll]] when the option
907
- * [[IModelTransformerOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
821
+ * [[IModelTransformOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
908
822
  * necessary when using [[processChanges]] since changeset information is sufficient.
909
823
  * @note you do not need to call this directly unless processing a subset of an iModel.
910
824
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
@@ -916,18 +830,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
916
830
  WHERE Scope.Id=:scopeId
917
831
  AND Kind=:kind
918
832
  `;
919
- nodeAssert(!this._options.isReverseSynchronization, "synchronizations with processChanges already detect element deletes, don't call detectElementDeletes");
833
+ nodeAssert(!this.isReverseSynchronization, "synchronizations with processChanges already detect element deletes, don't call detectElementDeletes");
920
834
  this.provenanceDb.withPreparedStatement(sql, (stmt) => {
921
835
  stmt.bindId("scopeId", this.targetScopeElementId);
922
836
  stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
923
837
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
924
838
  // ExternalSourceAspect.Identifier is of type string
925
839
  const aspectIdentifier = stmt.getValue(0).getString();
926
- if (!core_bentley_1.Id64.isId64(aspectIdentifier)) {
840
+ if (!core_bentley_1.Id64.isValidId64(aspectIdentifier)) {
927
841
  continue;
928
842
  }
929
843
  const targetElemId = stmt.getValue(1).getId();
930
- const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: `e${aspectIdentifier}` });
844
+ const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
845
+ entityReference: `e${aspectIdentifier}`,
846
+ });
931
847
  if (wasDeletedInSource)
932
848
  this.importer.deleteElement(targetElemId);
933
849
  }
@@ -937,7 +853,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
937
853
  * @deprecated in 3.x, this no longer has any effect except emitting a warning
938
854
  */
939
855
  skipElement(_sourceElement) {
940
- core_bentley_1.Logger.logWarning(loggerCategory, `Tried to defer/skip an element, which is no longer necessary`);
856
+ core_bentley_1.Logger.logWarning(loggerCategory, "Tried to defer/skip an element, which is no longer necessary");
941
857
  }
942
858
  /** Transform the specified sourceElement into ElementProps for the target iModel.
943
859
  * @param sourceElement The Element from the source iModel to transform.
@@ -948,6 +864,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
948
864
  onTransformElement(sourceElement) {
949
865
  core_bentley_1.Logger.logTrace(loggerCategory, `onTransformElement(${sourceElement.id}) "${sourceElement.getDisplayLabel()}"`);
950
866
  const targetElementProps = this.context.cloneElement(sourceElement, { binaryGeometry: this._options.cloneUsingBinaryGeometry });
867
+ // Special case: source element is the root subject
868
+ if (sourceElement.id === core_common_1.IModel.rootSubjectId) {
869
+ const targetElementId = this.context.findTargetElementId(sourceElement.id);
870
+ // When remapping rootSubject from source to non root subject in target, the code.scope gets remapped incorrectly.
871
+ // This is because the rootSubject has no parent and its code.scope is unique in that it is the id of itself.
872
+ // For all other subjects which do have parents the code.scope and its parent should be in agreement.
873
+ if (targetElementId !== core_bentley_1.Id64.invalid &&
874
+ targetElementId !== core_common_1.IModel.rootSubjectId) {
875
+ const targetElement = this.targetDb.elements.getElement(targetElementId);
876
+ targetElementProps.parent =
877
+ targetElement.parent ?? targetElementProps.parent;
878
+ targetElementProps.code.scope = targetElement.code.scope;
879
+ }
880
+ }
951
881
  if (sourceElement instanceof core_backend_1.Subject) {
952
882
  if (targetElementProps.jsonProperties?.Subject?.Job) {
953
883
  // don't propagate source channels into target (legacy bridge case)
@@ -956,39 +886,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
956
886
  }
957
887
  return targetElementProps;
958
888
  }
959
- // TODO: this is a PoC, see if we minimize memory usage
960
- _cacheSourceChanges() {
961
- nodeAssert(this._changeSummaryIds && this._changeSummaryIds.length > 0, "should have changeset data by now");
962
- this._hasElementChangedCache = new Set();
963
- const query = `
964
- SELECT
965
- ic.ChangedInstance.Id AS InstId
966
- FROM ecchange.change.InstanceChange ic
967
- JOIN iModelChange.Changeset imc ON ic.Summary.Id=imc.Summary.Id
968
- -- TODO: do relationship entities also need this cache optimization?
969
- WHERE ic.ChangedInstance.ClassId IS (BisCore.Element)
970
- AND InVirtualSet(:changeSummaryIds, ic.Summary.Id)
971
- -- ignore deleted, we take care of those in remapDeletedSourceEntities
972
- -- include inserted since inserted code-colliding elements should be considered
973
- -- a change so that the colliding element is exported to the target
974
- AND ic.OpCode<>:opDelete
975
- `;
976
- // there is a single mega-query multi-join+coalescing hack that I used originally to get around
977
- // only being able to run table.Changes() on one changeset at once, but sqlite only supports up to 64
978
- // tables in a join. Need to talk to core about .Changes being able to take a set of changesets
979
- // You can find this version in the `federation-guid-optimization-megaquery` branch
980
- // I wouldn't use it unless we prove via profiling that it speeds things up significantly
981
- // And even then let's first try scanning the raw changesets instead of applying them as these queries
982
- // require
983
- this.sourceDb.withPreparedStatement(query, (stmt) => {
984
- stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
985
- stmt.bindIdSet("changeSummaryIds", this._changeSummaryIds);
986
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
987
- const instId = stmt.getValue(0).getId();
988
- this._hasElementChangedCache.add(instId);
989
- }
990
- });
991
- }
992
889
  /** Returns true if a change within sourceElement is detected.
993
890
  * @param sourceElement The Element from the source iModel
994
891
  * @param targetElementId The Element from the target iModel to compare against.
@@ -1000,8 +897,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1000
897
  if (this._sourceChangeDataState === "unconnected")
1001
898
  return true;
1002
899
  nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
1003
- if (this._hasElementChangedCache === undefined)
1004
- this._cacheSourceChanges();
900
+ nodeAssert(this._hasElementChangedCache !== undefined, "has element changed cache should be initialized by now");
1005
901
  return this._hasElementChangedCache.has(sourceElement.id);
1006
902
  }
1007
903
  static transformCallbackFor(transformer, entity) {
@@ -1028,8 +924,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1028
924
  const updateEntity = EntityUnifier_1.EntityUnifier.updaterFor(this.targetDb, sourceEntity);
1029
925
  const targetProps = onEntityTransform.call(this, sourceEntity);
1030
926
  if (sourceEntity instanceof core_backend_1.Relationship) {
1031
- targetProps.sourceId = this.context.findTargetElementId(sourceEntity.sourceId);
1032
- targetProps.targetId = this.context.findTargetElementId(sourceEntity.targetId);
927
+ targetProps.sourceId =
928
+ this.context.findTargetElementId(sourceEntity.sourceId);
929
+ targetProps.targetId =
930
+ this.context.findTargetElementId(sourceEntity.targetId);
1033
931
  }
1034
932
  updateEntity({ ...targetProps, id: core_backend_1.EntityReferences.toId64(targetId) });
1035
933
  this._partiallyCommittedEntities.delete(sourceEntity);
@@ -1045,11 +943,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1045
943
  for (const referenceId of entity.getReferenceConcreteIds()) {
1046
944
  // TODO: probably need to rename from 'id' to 'ref' so these names aren't so ambiguous
1047
945
  const referenceIdInTarget = this.context.findTargetEntityId(referenceId);
1048
- const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) || this._skippedEntities.has(referenceId);
946
+ const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) ||
947
+ this._skippedEntities.has(referenceId);
1049
948
  if (alreadyProcessed)
1050
949
  continue;
1051
950
  core_bentley_1.Logger.logTrace(loggerCategory, `Deferring resolution of reference '${referenceId}' of element '${entity.id}'`);
1052
- const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: referenceId });
951
+ const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
952
+ entityReference: referenceId,
953
+ });
1053
954
  if (!referencedExistsInSource) {
1054
955
  core_bentley_1.Logger.logWarning(loggerCategory, `Source ${EntityUnifier_1.EntityUnifier.getReadableType(entity)} (${entity.id}) has a dangling reference to (${referenceId})`);
1055
956
  switch (this._options.danglingReferencesBehavior) {
@@ -1059,7 +960,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1059
960
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
1060
961
  `Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
1061
962
  "This must have been caused by an upstream application that changed the iModel.",
1062
- "You can set the IModelTransformerOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
963
+ "You can set the IModelTransformOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
1063
964
  "in a state where downstream consuming applications will need to handle the invalidity themselves. In some cases, writing a custom",
1064
965
  "transformer to remove the reference and fix affected elements may be suitable.",
1065
966
  ].join("\n"));
@@ -1097,7 +998,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1097
998
  /** Override of [IModelExportHandler.shouldExportElement]($transformer) that is called to determine if an element should be exported from the source iModel.
1098
999
  * @note Reaching this point means that the element has passed the standard exclusion checks in IModelExporter.
1099
1000
  */
1100
- shouldExportElement(_sourceElement) { return true; }
1001
+ shouldExportElement(_sourceElement) {
1002
+ return true;
1003
+ }
1101
1004
  onSkipElement(sourceElementId) {
1102
1005
  if (this.context.findTargetElementId(sourceElementId) !== core_bentley_1.Id64.invalid) {
1103
1006
  // element already has provenance
@@ -1128,7 +1031,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1128
1031
  const referenceType = elemClass.requiredReferenceKeyTypeMap[referenceKey];
1129
1032
  // For now we just consider all required references to be elements (as they are in biscore), and do not support
1130
1033
  // entities that refuse to be inserted without a different kind of entity (e.g. aspect or relationship) first being inserted
1131
- (0, core_bentley_1.assert)(referenceType === core_common_1.ConcreteEntityTypes.Element || referenceType === core_common_1.ConcreteEntityTypes.Model);
1034
+ (0, core_bentley_1.assert)(referenceType === core_common_1.ConcreteEntityTypes.Element ||
1035
+ referenceType === core_common_1.ConcreteEntityTypes.Model);
1132
1036
  return mapId64(idContainer, (id) => {
1133
1037
  if (id === core_bentley_1.Id64.invalid || id === core_common_1.IModel.rootSubjectId)
1134
1038
  return undefined; // not allowed to directly export the root subject
@@ -1137,13 +1041,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1137
1041
  // This is relied upon by the TemplateModelCloner
1138
1042
  // TODO: extract this out to only be in the TemplateModelCloner
1139
1043
  const asDefinitionElem = this.sourceDb.elements.tryGetElement(id, core_backend_1.DefinitionElement);
1140
- if (asDefinitionElem && !(asDefinitionElem instanceof core_backend_1.RecipeDefinitionElement)) {
1044
+ if (asDefinitionElem &&
1045
+ !(asDefinitionElem instanceof core_backend_1.RecipeDefinitionElement)) {
1141
1046
  this.context.remapElement(id, id);
1142
1047
  }
1143
1048
  }
1144
1049
  return id;
1145
- })
1146
- .filter((sourceReferenceId) => {
1050
+ }).filter((sourceReferenceId) => {
1147
1051
  if (sourceReferenceId === undefined)
1148
1052
  return false;
1149
1053
  const referenceInTargetId = this.context.findTargetElementId(sourceReferenceId);
@@ -1171,7 +1075,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1171
1075
  const isSubModeled = dbHasModel(this.sourceDb, elementId);
1172
1076
  const idOfElemInTarget = this.context.findTargetElementId(elementId);
1173
1077
  const isElemInTarget = core_bentley_1.Id64.invalid !== idOfElemInTarget;
1174
- const needsModelImport = isSubModeled && (!isElemInTarget || !dbHasModel(this.targetDb, idOfElemInTarget));
1078
+ const needsModelImport = isSubModeled &&
1079
+ (!isElemInTarget || !dbHasModel(this.targetDb, idOfElemInTarget));
1175
1080
  return { needsElemImport: !isElemInTarget, needsModelImport };
1176
1081
  }
1177
1082
  /** Override of [IModelExportHandler.onExportElement]($transformer) that imports an element into the target iModel when it is exported from the source iModel.
@@ -1186,27 +1091,33 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1186
1091
  }
1187
1092
  else if (this._options.wasSourceIModelCopiedToTarget) {
1188
1093
  targetElementId = sourceElement.id;
1189
- targetElementProps = this.targetDb.elements.getElementProps(targetElementId);
1094
+ targetElementProps =
1095
+ this.targetDb.elements.getElementProps(targetElementId);
1190
1096
  }
1191
1097
  else {
1192
1098
  targetElementId = this.context.findTargetElementId(sourceElement.id);
1193
1099
  targetElementProps = this.onTransformElement(sourceElement);
1194
1100
  }
1195
1101
  // if an existing remapping was not yet found, check by FederationGuid
1196
- if (this.context.isBetweenIModels && !core_bentley_1.Id64.isValid(targetElementId) && sourceElement.federationGuid !== undefined) {
1197
- targetElementId = this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
1102
+ if (this.context.isBetweenIModels &&
1103
+ !core_bentley_1.Id64.isValid(targetElementId) &&
1104
+ sourceElement.federationGuid !== undefined) {
1105
+ targetElementId =
1106
+ this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
1198
1107
  if (core_bentley_1.Id64.isValid(targetElementId))
1199
1108
  this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found
1200
1109
  }
1201
1110
  // if an existing remapping was not yet found, check by Code as long as the CodeScope is valid (invalid means a missing reference so not worth checking)
1202
- if (!core_bentley_1.Id64.isValidId64(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
1111
+ if (!core_bentley_1.Id64.isValidId64(targetElementId) &&
1112
+ core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
1203
1113
  // respond the same way to undefined code value as the @see Code class, but don't use that class because it trims
1204
1114
  // whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
1205
1115
  targetElementProps.code.value = targetElementProps.code.value ?? "";
1206
1116
  const maybeTargetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
1207
1117
  if (undefined !== maybeTargetElementId) {
1208
1118
  const maybeTargetElem = this.targetDb.elements.getElement(maybeTargetElementId);
1209
- if (maybeTargetElem.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
1119
+ if (maybeTargetElem.classFullName === targetElementProps.classFullName) {
1120
+ // ensure code remapping doesn't change the target class
1210
1121
  targetElementId = maybeTargetElementId;
1211
1122
  this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
1212
1123
  }
@@ -1215,14 +1126,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1215
1126
  }
1216
1127
  }
1217
1128
  }
1218
- if (core_bentley_1.Id64.isValid(targetElementId) && !this.hasElementChanged(sourceElement, targetElementId))
1129
+ if (core_bentley_1.Id64.isValid(targetElementId) &&
1130
+ !this.hasElementChanged(sourceElement, targetElementId))
1219
1131
  return;
1220
1132
  this.collectUnmappedReferences(sourceElement);
1221
1133
  // targetElementId will be valid (indicating update) or undefined (indicating insert)
1222
- targetElementProps.id
1223
- = core_bentley_1.Id64.isValid(targetElementId)
1224
- ? targetElementId
1225
- : undefined;
1134
+ targetElementProps.id = core_bentley_1.Id64.isValid(targetElementId)
1135
+ ? targetElementId
1136
+ : undefined;
1226
1137
  if (!this._options.wasSourceIModelCopiedToTarget) {
1227
1138
  this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
1228
1139
  }
@@ -1237,17 +1148,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1237
1148
  // FIXME: verify at finalization time that we don't lose provenance on new elements
1238
1149
  // FIXME: make public and improve `initElementProvenance` API for usage by consolidators
1239
1150
  if (!this._options.noProvenance) {
1240
- let provenance = this._options.forceExternalSourceAspectProvenance || this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
1151
+ let provenance = this._options.forceExternalSourceAspectProvenance ||
1152
+ this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
1241
1153
  ? undefined
1242
1154
  : sourceElement.federationGuid;
1243
1155
  if (!provenance) {
1244
1156
  const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
1245
- const aspectId = this.queryScopeExternalSource(aspectProps).aspectId;
1246
- if (aspectId === undefined) {
1157
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps);
1158
+ if (foundEsaProps === undefined)
1247
1159
  aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1248
- }
1249
1160
  else {
1250
- aspectProps.id = aspectId;
1161
+ // Since initElementProvenance sets a property 'version' on the aspectProps that we wish to persist in the provenanceDb, only grab the id from the foundEsaProps.
1162
+ aspectProps.id = foundEsaProps.aspectId;
1251
1163
  this.provenanceDb.elements.updateAspect(aspectProps);
1252
1164
  }
1253
1165
  provenance = aspectProps;
@@ -1278,10 +1190,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1278
1190
  * This override calls [[onTransformModel]] and then [IModelImporter.importModel]($transformer) to update the target iModel.
1279
1191
  */
1280
1192
  onExportModel(sourceModel) {
1281
- if (core_common_1.IModel.repositoryModelId === sourceModel.id) {
1193
+ if (this._options.skipPropagateChangesToRootElements &&
1194
+ core_common_1.IModel.repositoryModelId === sourceModel.id)
1282
1195
  return; // The RepositoryModel should not be directly imported
1283
- }
1284
1196
  const targetModeledElementId = this.context.findTargetElementId(sourceModel.id);
1197
+ // there can only be one repositoryModel per database, so ignore the repo model on remapped subjects
1198
+ const isRemappedRootSubject = sourceModel.id === core_common_1.IModel.repositoryModelId &&
1199
+ targetModeledElementId != sourceModel.id;
1200
+ if (isRemappedRootSubject)
1201
+ return;
1285
1202
  const targetModelProps = this.onTransformModel(sourceModel, targetModeledElementId);
1286
1203
  this.importer.importModel(targetModelProps);
1287
1204
  this.resolvePendingReferences(sourceModel);
@@ -1303,9 +1220,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1303
1220
  stmt.bindId(1, targetModelId);
1304
1221
  const val = stmt.step();
1305
1222
  switch (val) {
1306
- case core_bentley_1.DbResult.BE_SQLITE_ROW: return true;
1307
- case core_bentley_1.DbResult.BE_SQLITE_DONE: return false;
1308
- default: (0, core_bentley_1.assert)(false, `unexpected db result: '${stmt}'`);
1223
+ case core_bentley_1.DbResult.BE_SQLITE_ROW:
1224
+ return true;
1225
+ case core_bentley_1.DbResult.BE_SQLITE_DONE:
1226
+ return false;
1227
+ default:
1228
+ (0, core_bentley_1.assert)(false, `unexpected db result: '${stmt}'`);
1309
1229
  }
1310
1230
  });
1311
1231
  if (isDefinitionPartition) {
@@ -1319,7 +1239,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1319
1239
  this.importer.deleteModel(targetModelId);
1320
1240
  }
1321
1241
  catch (error) {
1322
- const isDeletionProhibitedErr = error instanceof core_common_1.IModelError && (error.errorNumber === core_bentley_1.IModelStatus.DeletionProhibited || error.errorNumber === core_bentley_1.IModelStatus.ForeignKeyConstraint);
1242
+ const isDeletionProhibitedErr = error instanceof core_common_1.IModelError &&
1243
+ (error.errorNumber === core_bentley_1.IModelStatus.DeletionProhibited ||
1244
+ error.errorNumber === core_bentley_1.IModelStatus.ForeignKeyConstraint);
1323
1245
  if (!isDeletionProhibitedErr)
1324
1246
  throw error;
1325
1247
  // Transformer tries to delete models before it deletes elements. Definition models cannot be deleted unless all of their modeled elements are deleted first.
@@ -1330,7 +1252,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1330
1252
  }
1331
1253
  /** Schedule modeled partition deletion */
1332
1254
  scheduleModeledPartitionDeletion(sourceModelId) {
1333
- const deletedElements = this.exporter.sourceDbChanges?.element.deleteIds;
1255
+ const deletedElements = this.exporter.sourceDbChanges?.element
1256
+ .deleteIds;
1334
1257
  if (!deletedElements.has(sourceModelId)) {
1335
1258
  deletedElements.add(sourceModelId);
1336
1259
  }
@@ -1396,7 +1319,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1396
1319
  onTransformModel(sourceModel, targetModeledElementId) {
1397
1320
  const targetModelProps = sourceModel.toJSON();
1398
1321
  // don't directly edit deep object since toJSON performs a shallow clone
1399
- targetModelProps.modeledElement = { ...targetModelProps.modeledElement, id: targetModeledElementId };
1322
+ targetModelProps.modeledElement = {
1323
+ ...targetModelProps.modeledElement,
1324
+ id: targetModeledElementId,
1325
+ };
1400
1326
  targetModelProps.id = targetModeledElementId;
1401
1327
  targetModelProps.parentModel = this.context.findTargetElementId(targetModelProps.parentModel);
1402
1328
  return targetModelProps;
@@ -1408,7 +1334,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1408
1334
  /** called at the end ([[finalizeTransformation]]) of a transformation,
1409
1335
  * updates the target scope element to say that transformation up through the
1410
1336
  * source's changeset has been performed. Also stores all changesets that occurred
1411
- * during the transformation as "pending synchronization changeset indices"
1337
+ * during the transformation as "pending synchronization changeset indices" @see TargetScopeProvenanceJsonProps
1412
1338
  *
1413
1339
  * You generally should not call this function yourself and use [[processChanges]] instead.
1414
1340
  * It is public for unsupported use cases of custom synchronization transforms.
@@ -1416,45 +1342,57 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1416
1342
  * without setting the `force` option to `true`
1417
1343
  */
1418
1344
  updateSynchronizationVersion({ force = false } = {}) {
1419
- if (!force && (this._sourceChangeDataState !== "has-changes" && !this._isFirstSynchronization))
1345
+ const notForcedAndHasNoChangesAndIsntProvenanceInit = !force &&
1346
+ this._sourceChangeDataState !== "has-changes" &&
1347
+ !this._isProvenanceInitTransform;
1348
+ if (notForcedAndHasNoChangesAndIsntProvenanceInit)
1420
1349
  return;
1421
1350
  nodeAssert(this._targetScopeProvenanceProps);
1422
1351
  const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
1423
1352
  const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
1424
- if (this._isFirstSynchronization) {
1353
+ if (this._isProvenanceInitTransform) {
1425
1354
  this._targetScopeProvenanceProps.version = sourceVersion;
1426
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = targetVersion;
1355
+ this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1356
+ targetVersion;
1427
1357
  }
1428
- else if (this._options.isReverseSynchronization) {
1358
+ else if (this.isReverseSynchronization) {
1429
1359
  const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
1430
1360
  core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
1431
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = sourceVersion;
1361
+ this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1362
+ sourceVersion;
1432
1363
  }
1433
- else if (!this._options.isReverseSynchronization) {
1364
+ else if (!this.isReverseSynchronization) {
1434
1365
  core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
1435
1366
  this._targetScopeProvenanceProps.version = sourceVersion;
1436
1367
  }
1437
- if (this._isSynchronization) {
1438
- (0, core_bentley_1.assert)(this.targetDb.changeset.index !== undefined && this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1368
+ if (this._isSynchronization ||
1369
+ (this._startingChangesetIndices && this._isProvenanceInitTransform)) {
1370
+ nodeAssert(this.targetDb.changeset.index !== undefined &&
1371
+ this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1439
1372
  const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
1440
1373
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1441
1374
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1442
- const [syncChangesetsToClear, syncChangesetsToUpdate] = this._isReverseSynchronization
1443
- ? [jsonProps.pendingReverseSyncChangesetIndices, jsonProps.pendingSyncChangesetIndices]
1444
- : [jsonProps.pendingSyncChangesetIndices, jsonProps.pendingReverseSyncChangesetIndices];
1445
- // NOTE that as documented in [[processChanges]], this assumes that right after
1446
- // transformation finalization, the work will be saved immediately, otherwise we've
1447
- // just marked this changeset as a synchronization to ignore, and the user can add other
1448
- // stuff to it which would break future synchronizations
1449
- // FIXME: force save for the user to prevent that
1375
+ const pendingSyncChangesetIndicesKey = "pendingSyncChangesetIndices";
1376
+ const pendingReverseSyncChangesetIndicesKey = "pendingReverseSyncChangesetIndices";
1377
+ const [syncChangesetsToClearKey, syncChangesetsToUpdateKey] = this
1378
+ .isReverseSynchronization
1379
+ ? [
1380
+ pendingReverseSyncChangesetIndicesKey,
1381
+ pendingSyncChangesetIndicesKey,
1382
+ ]
1383
+ : [
1384
+ pendingSyncChangesetIndicesKey,
1385
+ pendingReverseSyncChangesetIndicesKey,
1386
+ ];
1450
1387
  for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
1451
- syncChangesetsToUpdate.push(i);
1452
- // FIXME: add test to synchronize an iModel that is not at the tip, since then clearning syncChangesets is
1453
- // probably wrong, and we should filter it instead
1454
- syncChangesetsToClear.length = 0;
1388
+ jsonProps[syncChangesetsToUpdateKey].push(i);
1389
+ // Only keep the changeset indices which are greater than the source, this means they haven't been processed yet.
1390
+ jsonProps[syncChangesetsToClearKey] = jsonProps[syncChangesetsToClearKey].filter((csIndex) => {
1391
+ return csIndex > this._startingChangesetIndices.source;
1392
+ });
1455
1393
  // if reverse sync then we may have received provenance changes which should be marked as sync changes
1456
- if (this._isReverseSynchronization) {
1457
- nodeAssert(this.sourceDb.changeset.index, "changeset didn't exist");
1394
+ if (this.isReverseSynchronization) {
1395
+ nodeAssert(this.sourceDb.changeset.index !== undefined, "changeset didn't exist");
1458
1396
  for (let i = this._startingChangesetIndices.source + 1; i <= this.sourceDb.changeset.index + 1; i++)
1459
1397
  jsonProps.pendingReverseSyncChangesetIndices.push(i);
1460
1398
  }
@@ -1467,17 +1405,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1467
1405
  });
1468
1406
  }
1469
1407
  // FIXME<MIKE>: is this necessary when manually using low level transform APIs? (document if so)
1470
- finalizeTransformation() {
1408
+ async finalizeTransformation(options) {
1471
1409
  this.importer.finalize();
1472
1410
  this.updateSynchronizationVersion();
1473
1411
  if (this._partiallyCommittedEntities.size > 0) {
1474
- // FIXME: throw in this case if danglingReferenceBehavior === reject
1475
- core_bentley_1.Logger.logWarning(loggerCategory, [
1412
+ const message = [
1476
1413
  "The following elements were never fully resolved:",
1477
1414
  [...this._partiallyCommittedEntities.keys()].join(","),
1478
1415
  "This indicates that either some references were excluded from the transformation",
1479
1416
  "or the source has dangling references.",
1480
- ].join("\n"));
1417
+ ].join("\n");
1418
+ if (this._options.danglingReferencesBehavior === "reject")
1419
+ throw new Error(message);
1420
+ core_bentley_1.Logger.logWarning(loggerCategory, message);
1481
1421
  for (const partiallyCommittedElem of this._partiallyCommittedEntities.values()) {
1482
1422
  partiallyCommittedElem.forceComplete();
1483
1423
  }
@@ -1489,11 +1429,36 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1489
1429
  }
1490
1430
  // this internal is guaranteed stable for just transformer usage
1491
1431
  /* eslint-disable @itwin/no-internal */
1492
- if ("codeValueBehavior" in this.sourceDb) {
1432
+ if (("codeValueBehavior" in this.sourceDb)) {
1493
1433
  this.sourceDb.codeValueBehavior = "trim-unicode-whitespace";
1494
1434
  this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
1495
1435
  }
1496
1436
  /* eslint-enable @itwin/no-internal */
1437
+ const defaultSaveTargetChanges = () => this.targetDb.saveChanges();
1438
+ await (options?.saveTargetChanges ?? defaultSaveTargetChanges)(this);
1439
+ if (this.isReverseSynchronization)
1440
+ this.sourceDb.saveChanges();
1441
+ const description = `${this._isProvenanceInitTransform
1442
+ ? options?.provenanceInitTransformChangesetDescription ??
1443
+ `initialized branch provenance with master iModel: ${this.sourceDb.iModelId}`
1444
+ : this.isForwardSynchronization
1445
+ ? options?.forwardSyncBranchChangesetDescription ??
1446
+ `Forward sync of iModel: ${this.sourceDb.iModelId}`
1447
+ : options?.reverseSyncMasterChangesetDescription ??
1448
+ `Reverse sync of iModel: ${this.sourceDb.iModelId}`}`;
1449
+ if (this.targetDb.isBriefcaseDb()) {
1450
+ // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1451
+ await this.targetDb.pushChanges({
1452
+ description,
1453
+ });
1454
+ }
1455
+ if (this.isReverseSynchronization && this.sourceDb.isBriefcaseDb()) {
1456
+ // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1457
+ await this.sourceDb.pushChanges({
1458
+ description: options?.reverseSyncBranchChangesetDescription ??
1459
+ `Update provenance in response to a reverse sync to iModel: ${this.targetDb.iModelId}`,
1460
+ });
1461
+ }
1497
1462
  }
1498
1463
  /** Imports all relationships that subclass from the specified base class.
1499
1464
  * @param baseRelClassFullName The specified base relationship class.
@@ -1506,7 +1471,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1506
1471
  /** Override of [IModelExportHandler.shouldExportRelationship]($transformer) that is called to determine if a [Relationship]($backend) should be exported.
1507
1472
  * @note Reaching this point means that the relationship has passed the standard exclusion checks in [IModelExporter]($transformer).
1508
1473
  */
1509
- shouldExportRelationship(_sourceRelationship) { return true; }
1474
+ shouldExportRelationship(_sourceRelationship) {
1475
+ return true;
1476
+ }
1510
1477
  /** Override of [IModelExportHandler.onExportRelationship]($transformer) that imports a relationship into the target iModel when it is exported from the source iModel.
1511
1478
  * This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
1512
1479
  */
@@ -1515,14 +1482,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1515
1482
  const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
1516
1483
  const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
1517
1484
  const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
1518
- if (!this._options.noProvenance && core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
1485
+ if (!this._options.noProvenance &&
1486
+ core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
1519
1487
  let provenance = !this._options.forceExternalSourceAspectProvenance
1520
1488
  ? sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`
1521
1489
  : undefined;
1522
1490
  if (!provenance) {
1523
1491
  const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
1524
- aspectProps.id = this.queryScopeExternalSource(aspectProps).aspectId;
1525
- if (undefined === aspectProps.id) {
1492
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps);
1493
+ // onExportRelationship doesn't need to call updateAspect if esaProps were found, because relationship provenance doesn't have the same concept of a version as element provenance (which uses last mod time on the elements).
1494
+ if (undefined === foundEsaProps) {
1526
1495
  aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1527
1496
  }
1528
1497
  provenance = aspectProps;
@@ -1541,14 +1510,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1541
1510
  core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
1542
1511
  return;
1543
1512
  }
1544
- const relArg = deletedRelData.relId ?? {
1545
- sourceId: deletedRelData.sourceIdInTarget,
1546
- targetId: deletedRelData.targetIdInTarget,
1547
- };
1548
- // FIXME: make importer.deleteRelationship not need full props
1549
- const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
1550
- if (targetRelationship) {
1551
- this.importer.deleteRelationship(targetRelationship.toJSON());
1513
+ const id = deletedRelData.relId ??
1514
+ this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, {
1515
+ sourceId: deletedRelData.sourceIdInTarget,
1516
+ targetId: deletedRelData.targetIdInTarget,
1517
+ })?.id;
1518
+ if (id) {
1519
+ this.importer.deleteRelationship({
1520
+ id,
1521
+ classFullName: deletedRelData.classFullName,
1522
+ });
1552
1523
  }
1553
1524
  if (deletedRelData.provenanceAspectId) {
1554
1525
  try {
@@ -1569,7 +1540,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1569
1540
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
1570
1541
  */
1571
1542
  async detectRelationshipDeletes() {
1572
- if (this._options.isReverseSynchronization) {
1543
+ if (this.isReverseSynchronization) {
1573
1544
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
1574
1545
  }
1575
1546
  const aspectDeleteIds = [];
@@ -1584,14 +1555,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1584
1555
  statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
1585
1556
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
1586
1557
  const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
1587
- if (undefined === this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
1558
+ if (undefined ===
1559
+ this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
1588
1560
  // this function exists only to support some in-imodel transformations, which must
1589
1561
  // use the old (external source aspect) provenance method anyway so we don't need to support
1590
1562
  // new provenance
1591
1563
  const json = JSON.parse(statement.getValue(2).getString());
1592
- if (undefined !== json.targetRelInstanceId) {
1593
- const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
1594
- this.importer.deleteRelationship(targetRelationship.toJSON());
1564
+ const targetRelInstanceId = json.targetRelInstanceId ?? json.provenanceRelInstanceId;
1565
+ if (targetRelInstanceId) {
1566
+ this.importer.deleteRelationship({
1567
+ id: targetRelInstanceId,
1568
+ classFullName: core_backend_1.ElementRefersToElements.classFullName,
1569
+ });
1595
1570
  }
1596
1571
  aspectDeleteIds.push(statement.getValue(0).getId());
1597
1572
  }
@@ -1611,8 +1586,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1611
1586
  targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
1612
1587
  // TODO: move to cloneRelationship in IModelCloneContext
1613
1588
  sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
1614
- if ((core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType) && ("Id" === propertyMetaData.extendedType)) {
1615
- targetRelationshipProps[propertyName] = this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
1589
+ if (core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType &&
1590
+ "Id" === propertyMetaData.extendedType) {
1591
+ targetRelationshipProps[propertyName] =
1592
+ this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
1616
1593
  }
1617
1594
  });
1618
1595
  return targetRelationshipProps;
@@ -1644,8 +1621,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1644
1621
  sourceAspects.forEach((a) => this.collectUnmappedReferences(a));
1645
1622
  // const targetAspectsToImport = targetAspectPropsArray.filter((targetAspect, i) => hasEntityChanged(sourceAspects[i], targetAspect));
1646
1623
  const targetIds = this.importer.importElementMultiAspects(targetAspectPropsArray, (a) => {
1647
- const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect && a.scope?.id === this.targetScopeElementId;
1648
- return !this._options.includeSourceProvenance || !isExternalSourceAspectFromTransformer;
1624
+ const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect &&
1625
+ a.scope?.id === this.targetScopeElementId;
1626
+ return (!this._options.includeSourceProvenance ||
1627
+ !isExternalSourceAspectFromTransformer);
1649
1628
  });
1650
1629
  for (let i = 0; i < targetIds.length; ++i) {
1651
1630
  this.context.remapElementAspect(sourceAspects[i].id, targetIds[i]);
@@ -1684,9 +1663,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1684
1663
  let schemaFileName = schema.name + ext;
1685
1664
  // many file systems have a max file-name/path-segment size of 255, so we workaround that on all systems
1686
1665
  const systemMaxPathSegmentSize = 255;
1687
- // windows usually has a limit for the total path length of 260
1688
- const windowsMaxPathLimit = 260;
1689
- if (schemaFileName.length > systemMaxPathSegmentSize || path.join(this._schemaExportDir, schemaFileName).length >= windowsMaxPathLimit) {
1666
+ if (schemaFileName.length > systemMaxPathSegmentSize) {
1690
1667
  // this name should be well under 255 bytes
1691
1668
  // ( 100 + (Number.MAX_SAFE_INTEGER.toString().length = 16) + (ext.length = 13) ) = 129 which is less than 255
1692
1669
  // You'd have to be past 2**53-1 (Number.MAX_SAFE_INTEGER) long named schemas in order to hit decimal formatting,
@@ -1726,7 +1703,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1726
1703
  const maybeLongNameResolvingSchemaCtx = this._longNamedSchemasMap.size > 0
1727
1704
  ? this._makeLongNameResolvingSchemaCtx()
1728
1705
  : undefined;
1729
- return await this.targetDb.importSchemas(schemaFullPaths, { ecSchemaXmlContext: maybeLongNameResolvingSchemaCtx });
1706
+ return await this.targetDb.importSchemas(schemaFullPaths, {
1707
+ ecSchemaXmlContext: maybeLongNameResolvingSchemaCtx,
1708
+ });
1730
1709
  }
1731
1710
  finally {
1732
1711
  core_backend_1.IModelJsFs.removeSync(this._schemaExportDir);
@@ -1734,8 +1713,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1734
1713
  }
1735
1714
  }
1736
1715
  /** Cause all fonts to be exported from the source iModel and imported into the target iModel.
1737
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1738
- */
1716
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1717
+ */
1739
1718
  async processFonts() {
1740
1719
  // we do not need to initialize for this since no entities are exported
1741
1720
  await this.initialize();
@@ -1762,7 +1741,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1762
1741
  /** Override of [IModelExportHandler.shouldExportCodeSpec]($transformer) that is called to determine if a CodeSpec should be exported from the source iModel.
1763
1742
  * @note Reaching this point means that the CodeSpec has passed the standard exclusion checks in [IModelExporter]($transformer).
1764
1743
  */
1765
- shouldExportCodeSpec(_sourceCodeSpec) { return true; }
1744
+ shouldExportCodeSpec(_sourceCodeSpec) {
1745
+ return true;
1746
+ }
1766
1747
  /** Override of [IModelExportHandler.onExportCodeSpec]($transformer) that imports a CodeSpec into the target iModel when it is exported from the source iModel. */
1767
1748
  onExportCodeSpec(sourceCodeSpec) {
1768
1749
  this.context.importCodeSpec(sourceCodeSpec.id);
@@ -1786,33 +1767,218 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1786
1767
  async initialize(args) {
1787
1768
  if (this._initialized)
1788
1769
  return;
1789
- await this.context.initialize();
1790
1770
  await this._tryInitChangesetData(args);
1771
+ await this.context.initialize();
1772
+ // need exporter initialized to do remapdeletedsourceentities.
1791
1773
  await this.exporter.initialize(this.getExportInitOpts(args ?? {}));
1792
- // Exporter must be initialized prior to `initFromExternalSourceAspects` in order to handle entity recreations.
1793
- // eslint-disable-next-line deprecation/deprecation
1794
- await this.initFromExternalSourceAspects(args);
1774
+ // Exporter must be initialized prior to processing changesets in order to properly handle entity recreations (an entity delete followed by an insert of that same entity).
1775
+ await this.processChangesets();
1795
1776
  this._initialized = true;
1796
1777
  }
1778
+ /**
1779
+ * Reads all the changeset files in the private member of the transformer: _csFileProps and does two things with these changesets.
1780
+ * Finds the corresponding target entity for any deleted source entities and remaps the sourceId to the targetId.
1781
+ * Populates this._hasElementChangedCache with a set of elementIds that have been updated or inserted into the database.
1782
+ * This function returns early if csFileProps is undefined or is of length 0.
1783
+ * @returns void
1784
+ */
1785
+ async processChangesets() {
1786
+ this.forEachTrackedElement((sourceElementId, targetElementId) => {
1787
+ this.context.remapElement(sourceElementId, targetElementId);
1788
+ });
1789
+ if (this._csFileProps === undefined || this._csFileProps.length === 0)
1790
+ return;
1791
+ const hasElementChangedCache = new Set();
1792
+ const relationshipECClassIdsToSkip = new Set();
1793
+ for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementDrivesElement)")) {
1794
+ relationshipECClassIdsToSkip.add(row.ECInstanceId);
1795
+ }
1796
+ const relationshipECClassIds = new Set();
1797
+ for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementRefersToElements)")) {
1798
+ relationshipECClassIds.add(row.ECInstanceId);
1799
+ }
1800
+ // For later use when processing deletes.
1801
+ const alreadyImportedElementInserts = new Set();
1802
+ const alreadyImportedModelInserts = new Set();
1803
+ this.exporter.sourceDbChanges?.element.insertIds.forEach((insertedSourceElementId) => {
1804
+ const targetElementId = this.context.findTargetElementId(insertedSourceElementId);
1805
+ if (core_bentley_1.Id64.isValid(targetElementId))
1806
+ alreadyImportedElementInserts.add(targetElementId);
1807
+ });
1808
+ this.exporter.sourceDbChanges?.model.insertIds.forEach((insertedSourceModelId) => {
1809
+ const targetModelId = this.context.findTargetElementId(insertedSourceModelId);
1810
+ if (core_bentley_1.Id64.isValid(targetModelId))
1811
+ alreadyImportedModelInserts.add(targetModelId);
1812
+ });
1813
+ this._deletedSourceRelationshipData = new Map();
1814
+ for (const csFile of this._csFileProps) {
1815
+ const csReader = core_backend_1.SqliteChangesetReader.openFile({
1816
+ fileName: csFile.pathname,
1817
+ db: this.sourceDb,
1818
+ disableSchemaCheck: true,
1819
+ });
1820
+ const csAdaptor = new core_backend_1.ChangesetECAdaptor(csReader);
1821
+ const ecChangeUnifier = new core_backend_1.PartialECChangeUnifier();
1822
+ while (csAdaptor.step()) {
1823
+ ecChangeUnifier.appendFrom(csAdaptor);
1824
+ }
1825
+ const changes = [...ecChangeUnifier.instances];
1826
+ /** a map of element ids to this transformation scope's ESA data for that element, in case the ESA is deleted in the target */
1827
+ const elemIdToScopeEsa = new Map();
1828
+ for (const change of changes) {
1829
+ if (change.ECClassId !== undefined &&
1830
+ relationshipECClassIdsToSkip.has(change.ECClassId))
1831
+ continue;
1832
+ const changeType = change.$meta?.op;
1833
+ if (changeType === "Deleted" &&
1834
+ change?.$meta?.classFullName === core_backend_1.ExternalSourceAspect.classFullName &&
1835
+ change.Scope.Id === this.targetScopeElementId) {
1836
+ elemIdToScopeEsa.set(change.Element.Id, change);
1837
+ }
1838
+ else if (changeType === "Inserted" || changeType === "Updated")
1839
+ hasElementChangedCache.add(change.ECInstanceId);
1840
+ }
1841
+ // Loop to process deletes.
1842
+ for (const change of changes) {
1843
+ const changeType = change.$meta?.op;
1844
+ const ecClassId = change.ECClassId ?? change.$meta?.fallbackClassId;
1845
+ if (ecClassId === undefined)
1846
+ throw new Error(`ECClassId was not found for id: ${change.ECInstanceId}! Table is : ${change?.$meta?.tables}`);
1847
+ if (changeType === undefined)
1848
+ throw new Error(`ChangeType was undefined for id: ${change.ECInstanceId}.`);
1849
+ if (changeType !== "Deleted" ||
1850
+ relationshipECClassIdsToSkip.has(ecClassId))
1851
+ continue;
1852
+ await this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
1853
+ }
1854
+ csReader.close();
1855
+ }
1856
+ this._hasElementChangedCache = hasElementChangedCache;
1857
+ return;
1858
+ }
1859
+ /**
1860
+ * Helper function for processChangesets. Remaps the id of element deleted found in the 'change' to an element in the targetDb.
1861
+ * @param change the change to process, must be of changeType "Deleted"
1862
+ * @param mapOfDeletedElemIdToScopeEsas a map of elementIds to changedECInstances (which are ESAs). the elementId is not the id of the esa itself, but the elementid that the esa was stored on before the esa's deletion.
1863
+ * All ESAs in this map are part of the transformer's scope / ESA data and are tracked in case the ESA is deleted in the target.
1864
+ * @param isRelationship is relationship or not
1865
+ * @param alreadyImportedElementInserts used to handle entity recreation and not delete already handled element inserts.
1866
+ * @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
1867
+ * @returns void
1868
+ */
1869
+ async processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1870
+ // we need a connected iModel with changes to remap elements with deletions
1871
+ const notConnectedModel = this.sourceDb.iTwinId === undefined;
1872
+ const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1873
+ if (notConnectedModel || noChanges)
1874
+ return;
1875
+ /**
1876
+ * if our ChangedECInstance is in the provenanceDb, then we can use the ids we find in the ChangedECInstance to query for ESAs.
1877
+ * This is because the ESAs are stored on an element Id thats present in the provenanceDb.
1878
+ */
1879
+ const changeDataInProvenanceDb = this.sourceDb === this.provenanceDb;
1880
+ const getTargetIdFromSourceId = async (id) => {
1881
+ let identifierValue;
1882
+ let element;
1883
+ if (isRelationship) {
1884
+ element = this.sourceDb.elements.tryGetElement(id);
1885
+ }
1886
+ const fedGuid = isRelationship
1887
+ ? element?.federationGuid
1888
+ : change.FederationGuid;
1889
+ if (changeDataInProvenanceDb) {
1890
+ // TODO: clarify what happens if there are multiple (e.g. elements were merged)
1891
+ for await (const row of this.sourceDb.createQueryReader("SELECT esa.Identifier FROM bis.ExternalSourceAspect esa WHERE Scope.Id=:scopeId AND Kind=:kind AND Element.Id=:relatedElementId LIMIT 1", core_common_1.QueryBinder.from([
1892
+ this.targetScopeElementId,
1893
+ core_backend_1.ExternalSourceAspect.Kind.Element,
1894
+ id,
1895
+ ]))) {
1896
+ identifierValue = row.Identifier;
1897
+ }
1898
+ identifierValue =
1899
+ identifierValue ?? mapOfDeletedElemIdToScopeEsas.get(id)?.Identifier;
1900
+ }
1901
+ // Check for targetId by an esa first
1902
+ if (changeDataInProvenanceDb && identifierValue) {
1903
+ const targetId = identifierValue;
1904
+ return targetId;
1905
+ }
1906
+ // Check for targetId using sourceId's fedguid if we didn't find an esa.
1907
+ if (fedGuid) {
1908
+ const targetId = this._queryElemIdByFedGuid(this.targetDb, fedGuid);
1909
+ return targetId;
1910
+ }
1911
+ return undefined;
1912
+ };
1913
+ const changedInstanceId = change.ECInstanceId;
1914
+ if (isRelationship) {
1915
+ const sourceIdOfRelationshipInSource = change.SourceECInstanceId;
1916
+ const targetIdOfRelationshipInSource = change.TargetECInstanceId;
1917
+ const classFullName = change.$meta?.classFullName;
1918
+ const sourceIdOfRelationshipInTarget = await getTargetIdFromSourceId(sourceIdOfRelationshipInSource);
1919
+ const targetIdOfRelationshipInTarget = await getTargetIdFromSourceId(targetIdOfRelationshipInSource);
1920
+ if (sourceIdOfRelationshipInTarget && targetIdOfRelationshipInTarget) {
1921
+ this._deletedSourceRelationshipData.set(changedInstanceId, {
1922
+ classFullName: classFullName ?? "",
1923
+ sourceIdInTarget: sourceIdOfRelationshipInTarget,
1924
+ targetIdInTarget: targetIdOfRelationshipInTarget,
1925
+ });
1926
+ }
1927
+ else if (this.sourceDb === this.provenanceSourceDb) {
1928
+ const relProvenance = this._queryProvenanceForRelationship(changedInstanceId, {
1929
+ classFullName: classFullName ?? "",
1930
+ sourceId: sourceIdOfRelationshipInSource,
1931
+ targetId: targetIdOfRelationshipInSource,
1932
+ });
1933
+ if (relProvenance && relProvenance.relationshipId)
1934
+ this._deletedSourceRelationshipData.set(changedInstanceId, {
1935
+ classFullName: classFullName ?? "",
1936
+ relId: relProvenance.relationshipId,
1937
+ provenanceAspectId: relProvenance.aspectId,
1938
+ });
1939
+ }
1940
+ }
1941
+ else {
1942
+ let targetId = await getTargetIdFromSourceId(changedInstanceId);
1943
+ if (targetId === undefined && this.sourceDb === this.provenanceSourceDb) {
1944
+ targetId = this._queryProvenanceForElement(changedInstanceId);
1945
+ }
1946
+ // since we are processing one changeset at a time, we can see local source deletes
1947
+ // of entities that were never synced and can be safely ignored
1948
+ const deletionNotInTarget = !targetId;
1949
+ if (deletionNotInTarget)
1950
+ return;
1951
+ this.context.remapElement(changedInstanceId, targetId);
1952
+ // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
1953
+ // In such case an entity update will be triggered and we no longer need to delete the entity.
1954
+ if (alreadyImportedElementInserts.has(targetId)) {
1955
+ this.exporter.sourceDbChanges?.element.deleteIds.delete(changedInstanceId);
1956
+ }
1957
+ if (alreadyImportedModelInserts.has(targetId)) {
1958
+ this.exporter.sourceDbChanges?.model.deleteIds.delete(changedInstanceId);
1959
+ }
1960
+ }
1961
+ }
1797
1962
  async _tryInitChangesetData(args) {
1798
- if (!args || this.sourceDb.iTwinId === undefined || this.sourceDb.changeset.index === undefined) {
1963
+ if (!args ||
1964
+ this.sourceDb.iTwinId === undefined ||
1965
+ this.sourceDb.changeset.index === undefined) {
1799
1966
  this._sourceChangeDataState = "unconnected";
1800
1967
  return;
1801
1968
  }
1802
1969
  const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1803
1970
  if (noChanges) {
1804
1971
  this._sourceChangeDataState = "no-changes";
1805
- this._changeSummaryIds = [];
1972
+ this._csFileProps = [];
1806
1973
  return;
1807
1974
  }
1808
1975
  // NOTE: that we do NOT download the changesummary for the last transformed version, we want
1809
1976
  // to ignore those already processed changes
1810
- const startChangesetIndexOrId = args.startChangeset?.index
1811
- ?? args.startChangeset?.id
1812
- ?? this._synchronizationVersion.index + 1;
1977
+ const startChangesetIndexOrId = args.startChangeset?.index ??
1978
+ args.startChangeset?.id ??
1979
+ this._synchronizationVersion.index + 1;
1813
1980
  const endChangesetId = this.sourceDb.changeset.id;
1814
- const [startChangesetIndex, endChangesetIndex] = await Promise.all(([startChangesetIndexOrId, endChangesetId])
1815
- .map(async (indexOrId) => typeof indexOrId === "number"
1981
+ const [startChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetIndexOrId, endChangesetId].map(async (indexOrId) => typeof indexOrId === "number"
1816
1982
  ? indexOrId
1817
1983
  : core_backend_1.IModelHost.hubAccess
1818
1984
  .queryChangeset({
@@ -1823,287 +1989,102 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1823
1989
  })
1824
1990
  .then((changeset) => changeset.index)));
1825
1991
  const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
1826
- if (!this._options.ignoreMissingChangesetsInSynchronizations
1827
- && startChangesetIndex !== this._synchronizationVersion.index + 1
1828
- && this._synchronizationVersion.index !== -1) {
1829
- throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},`
1830
- + " startChangesetId should be"
1831
- + " exactly the first changeset *after* the previous synchronization to not miss data."
1832
- + ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}`
1833
- + ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'`
1834
- + ` which is changeset #${this._synchronizationVersion.index}. The transformer expected`
1835
- + ` #${this._synchronizationVersion.index + 1}.`);
1992
+ if (!this._options.ignoreMissingChangesetsInSynchronizations &&
1993
+ startChangesetIndex !== this._synchronizationVersion.index + 1 &&
1994
+ this._synchronizationVersion.index !== -1) {
1995
+ throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},` +
1996
+ " startChangesetId should be" +
1997
+ " exactly the first changeset *after* the previous synchronization to not miss data." +
1998
+ ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}` +
1999
+ ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'` +
2000
+ ` which is changeset #${this._synchronizationVersion.index}. The transformer expected` +
2001
+ ` #${this._synchronizationVersion.index + 1}.`);
1836
2002
  }
1837
2003
  nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
1838
- const changesetsToSkip = this._isReverseSynchronization
1839
- ? this._targetScopeProvenanceProps.jsonProperties.pendingReverseSyncChangesetIndices
1840
- : this._targetScopeProvenanceProps.jsonProperties.pendingSyncChangesetIndices;
2004
+ const changesetsToSkip = this.isReverseSynchronization
2005
+ ? this._targetScopeProvenanceProps.jsonProperties
2006
+ .pendingReverseSyncChangesetIndices
2007
+ : this._targetScopeProvenanceProps.jsonProperties
2008
+ .pendingSyncChangesetIndices;
1841
2009
  core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
1842
2010
  this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
1843
2011
  core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
2012
+ const csFileProps = [];
1844
2013
  for (const [first, end] of this._changesetRanges) {
1845
- this._changeSummaryIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
1846
- accessToken: args.accessToken,
2014
+ // TODO: should the first changeset in a reverse sync really be included even though its 'initialized branch provenance'? The answer is no, its a bug that needs to be fixed.
2015
+ const fileProps = await core_backend_1.IModelHost.hubAccess.downloadChangesets({
1847
2016
  iModelId: this.sourceDb.iModelId,
1848
- iTwinId: this.sourceDb.iTwinId,
2017
+ targetDir: core_backend_1.BriefcaseManager.getChangeSetsPath(this.sourceDb.iModelId),
1849
2018
  range: { first, end },
1850
2019
  });
2020
+ csFileProps.push(...fileProps);
1851
2021
  }
1852
- core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
2022
+ this._csFileProps = csFileProps;
1853
2023
  this._sourceChangeDataState = "has-changes";
1854
2024
  }
1855
2025
  /** Export everything from the source iModel and import the transformed entities into the target iModel.
1856
- * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
1857
- */
1858
- async processAll() {
2026
+ * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
2027
+ */
2028
+ async processAll(options) {
1859
2029
  this.logSettings();
1860
2030
  this.initScopeProvenance();
1861
2031
  await this.initialize();
1862
2032
  await this.exporter.exportCodeSpecs();
1863
2033
  await this.exporter.exportFonts();
1864
- // The RepositoryModel and root Subject of the target iModel should not be transformed.
1865
- await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
1866
- await this.exporter.exportModelContents(core_common_1.IModel.repositoryModelId, core_backend_1.Element.classFullName, true); // after the Subject hierarchy, process the other elements of the RepositoryModel
1867
- await this.exporter.exportSubModels(core_common_1.IModel.repositoryModelId); // start below the RepositoryModel
2034
+ if (this._options.skipPropagateChangesToRootElements) {
2035
+ // The RepositoryModel and root Subject of the target iModel should not be transformed.
2036
+ await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
2037
+ await this.exporter.exportModelContents(core_common_1.IModel.repositoryModelId, core_backend_1.Element.classFullName, true); // after the Subject hierarchy, process the other elements of the RepositoryModel
2038
+ await this.exporter.exportSubModels(core_common_1.IModel.repositoryModelId); // start below the RepositoryModel
2039
+ }
2040
+ else {
2041
+ await this.exporter.exportModel(core_common_1.IModel.repositoryModelId);
2042
+ }
1868
2043
  await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
1869
2044
  await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
1870
2045
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1871
- // FIXME: add a deprecated option to force run these, don't otherwise
1872
- if (this.shouldDetectDeletes()) {
2046
+ if (this._options.forceExternalSourceAspectProvenance &&
2047
+ this.shouldDetectDeletes()) {
1873
2048
  await this.detectElementDeletes();
1874
2049
  await this.detectRelationshipDeletes();
1875
2050
  }
1876
2051
  if (this._options.optimizeGeometry)
1877
2052
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
1878
2053
  this.importer.computeProjectExtents();
1879
- this.finalizeTransformation();
2054
+ await this.finalizeTransformation(options);
1880
2055
  }
1881
2056
  markLastProvenance(sourceAspect, { isRelationship = false }) {
1882
- this._lastProvenanceEntityInfo
1883
- = typeof sourceAspect === "string"
2057
+ this._lastProvenanceEntityInfo =
2058
+ typeof sourceAspect === "string"
1884
2059
  ? sourceAspect
1885
2060
  : {
1886
2061
  entityId: sourceAspect.element.id,
1887
2062
  aspectId: sourceAspect.id,
1888
2063
  aspectVersion: sourceAspect.version ?? "",
1889
- aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
1890
- };
1891
- }
1892
- /**
1893
- * Load the state of the active transformation from an open SQLiteDb
1894
- * You can override this if you'd like to load from custom tables in the resumable dump state, but you should call
1895
- * this super implementation
1896
- * @note the SQLiteDb must be open
1897
- */
1898
- loadStateFromDb(db) {
1899
- const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
1900
- if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
1901
- throw Error("expected row when getting lastProvenanceEntityId from target state table");
1902
- const entityId = stmt.getValueString(0);
1903
- const isGuidOrGuidPair = entityId.includes("-");
1904
- return isGuidOrGuidPair
1905
- ? entityId
1906
- : {
1907
- entityId,
1908
- aspectId: stmt.getValueString(1),
1909
- aspectVersion: stmt.getValueString(2),
1910
- aspectKind: stmt.getValueString(3),
2064
+ aspectKind: isRelationship
2065
+ ? core_backend_1.ExternalSourceAspect.Kind.Relationship
2066
+ : core_backend_1.ExternalSourceAspect.Kind.Element,
1911
2067
  };
1912
- });
1913
- /*
1914
- // TODO: maybe save transformer state resumption state based on target changset and require calls
1915
- // to saveChanges
1916
- if () {
1917
- const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
1918
- const isRelProvenance = targetFedGuid !== undefined;
1919
- const instanceId = isRelProvenance
1920
- ? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
1921
- : "";
1922
- //const classId =
1923
- if (isRelProvenance) {
1924
- }
1925
- }
1926
- */
1927
- const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
1928
- // ignore provenance check if it's null since we can't bind those ids
1929
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
1930
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
1931
- this.provenanceDb.withPreparedStatement(`
1932
- SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
1933
- WHERE Scope.Id=:scopeId
1934
- AND ECInstanceId=:aspectId
1935
- AND Kind=:kind
1936
- AND Element.Id=:entityId
1937
- `, (statement) => {
1938
- statement.bindId("scopeId", this.targetScopeElementId);
1939
- statement.bindId("aspectId", lastProvenanceEntityInfo.aspectId);
1940
- statement.bindString("kind", lastProvenanceEntityInfo.aspectKind);
1941
- statement.bindId("entityId", lastProvenanceEntityInfo.entityId);
1942
- const stepResult = statement.step();
1943
- switch (stepResult) {
1944
- case core_bentley_1.DbResult.BE_SQLITE_ROW:
1945
- const version = statement.getValue(0).getString();
1946
- return version === lastProvenanceEntityInfo.aspectVersion;
1947
- case core_bentley_1.DbResult.BE_SQLITE_DONE:
1948
- return false;
1949
- default:
1950
- throw new core_common_1.IModelError(core_bentley_1.IModelStatus.SQLiteError, `got sql error ${stepResult}`);
1951
- }
1952
- });
1953
- if (!targetHasCorrectLastProvenance)
1954
- throw Error([
1955
- "Target for resuming from does not have the expected provenance ",
1956
- "from the target that the resume state was made with",
1957
- ].join("\n"));
1958
- this._lastProvenanceEntityInfo = lastProvenanceEntityInfo;
1959
- const state = db.withSqliteStatement(`SELECT data FROM ${IModelTransformer.jsStateTable}`, (stmt) => {
1960
- if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
1961
- throw Error("expected row when getting data from js state table");
1962
- return JSON.parse(stmt.getValueString(0));
1963
- });
1964
- if (state.transformerClass !== this.constructor.name)
1965
- throw Error("resuming from a differently named transformer class, it is not necessarily valid to resume with a different transformer class");
1966
- // force assign to readonly options since we do not know how the transformer subclass takes options to pass to the superclass
1967
- this._options = state.options;
1968
- this.context.loadStateFromDb(db);
1969
- this.importer.loadStateFromJson(state.importerState);
1970
- this.exporter.loadStateFromJson(state.exporterState);
1971
- this._elementsWithExplicitlyTrackedProvenance = core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
1972
- this.loadAdditionalStateJson(state.additionalState);
1973
- }
1974
- /**
1975
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
1976
- * from the original changeset
1977
- *
1978
- * Return a new transformer instance with the same remappings state as saved from a previous [[IModelTransformer.saveStateToFile]] call.
1979
- * This allows you to "resume" an iModel transformation, you will have to call [[IModelTransformer.processChanges]]/[[IModelTransformer.processAll]]
1980
- * again but the remapping state will cause already mapped elements to be skipped.
1981
- * To "resume" an iModel Transformation you need:
1982
- * - the sourceDb at the same changeset
1983
- * - the same targetDb in the state in which it was before
1984
- * @param statePath the path to the serialized state of the transformer, use [[IModelTransformer.saveStateToFile]] to get this from an existing transformer instance
1985
- * @param constructorArgs remaining arguments that you would normally pass to the Transformer subclass you are using, usually (sourceDb, targetDb)
1986
- * @note custom transformers with custom state may need to override this method in order to handle loading their own custom state somewhere
1987
- */
1988
- static resumeTransformation(statePath, ...constructorArgs) {
1989
- const transformer = new this(...constructorArgs);
1990
- const db = new core_backend_1.SQLiteDb();
1991
- db.openDb(statePath, core_bentley_1.OpenMode.Readonly);
1992
- try {
1993
- transformer.loadStateFromDb(db);
1994
- }
1995
- finally {
1996
- db.closeDb();
1997
- }
1998
- return transformer;
1999
- }
2000
- /**
2001
- * You may override this to store arbitrary json state in a transformer state dump, useful for some resumptions
2002
- * @see [[IModelTransformer.saveStateToFile]]
2003
- */
2004
- getAdditionalStateJson() {
2005
- return {};
2006
- }
2007
- /**
2008
- * You may override this to load arbitrary json state in a transformer state dump, useful for some resumptions
2009
- * @see [[IModelTransformer.loadStateFromFile]]
2010
- */
2011
- loadAdditionalStateJson(_additionalState) { }
2012
- /**
2013
- * Save the state of the active transformation to an open SQLiteDb
2014
- * You can override this if you'd like to write custom tables to the resumable dump state, but you should call
2015
- * this super implementation
2016
- * @note the SQLiteDb must be open
2017
- */
2018
- saveStateToDb(db) {
2019
- const jsonState = {
2020
- transformerClass: this.constructor.name,
2021
- options: this._options,
2022
- explicitlyTrackedElements: core_bentley_1.CompressedId64Set.compressSet(this._elementsWithExplicitlyTrackedProvenance),
2023
- importerState: this.importer.saveStateToJson(),
2024
- exporterState: this.exporter.saveStateToJson(),
2025
- additionalState: this.getAdditionalStateJson(),
2026
- };
2027
- this.context.saveStateToDb(db);
2028
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`CREATE TABLE ${IModelTransformer.jsStateTable} (data TEXT)`))
2029
- throw Error("Failed to create the js state table in the state database");
2030
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`
2031
- CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
2032
- -- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
2033
- entityId TEXT,
2034
- -- the following are only valid if the above entityId is a hex id representation
2035
- aspectId TEXT,
2036
- aspectVersion TEXT,
2037
- aspectKind TEXT
2038
- )
2039
- `))
2040
- throw Error("Failed to create the target state table in the state database");
2041
- db.saveChanges();
2042
- db.withSqliteStatement(`INSERT INTO ${IModelTransformer.jsStateTable} (data) VALUES (?)`, (stmt) => {
2043
- stmt.bindString(1, JSON.stringify(jsonState));
2044
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
2045
- throw Error("Failed to insert options into the state database");
2046
- });
2047
- db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
2048
- const lastProvenanceEntityInfo = this._lastProvenanceEntityInfo;
2049
- stmt.bindString(1, lastProvenanceEntityInfo?.entityId ?? this._lastProvenanceEntityInfo);
2050
- stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
2051
- stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
2052
- stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
2053
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
2054
- throw Error("Failed to insert options into the state database");
2055
- });
2056
- db.saveChanges();
2057
2068
  }
2058
- /**
2059
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
2060
- * from the original changeset
2061
- *
2062
- * Save the state of the active transformation to a file path, if a file at the path already exists, it will be overwritten
2063
- * This state can be used by [[IModelTransformer.resumeTransformation]] to resume a transformation from this point.
2064
- * The serialization format is a custom sqlite database.
2065
- * @note custom transformers with custom state may override [[IModelTransformer.saveStateToDb]] or [[IModelTransformer.getAdditionalStateJson]]
2066
- * and [[IModelTransformer.loadStateFromDb]] (with a super call) or [[IModelTransformer.loadAdditionalStateJson]]
2067
- * if they have custom state that needs to be stored with
2068
- * potentially inside the same sqlite file in separate tables
2069
+ /** Export changes from the source iModel and import the transformed entities into the target iModel.
2070
+ * Inserts, updates, and deletes are determined by inspecting the changeset(s).
2071
+ * @note the transformer saves and pushes changes when its work is complete.
2072
+ * @note if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
2073
+ * will automatically be determined and used
2074
+ * @note To form a range of versions to process, set `startChangesetId` for the start (inclusive) of the desired range and open the source iModel as of the end (inclusive) of the desired range.
2069
2075
  */
2070
- saveStateToFile(nativeStatePath) {
2071
- const db = new core_backend_1.SQLiteDb();
2072
- if (core_backend_1.IModelJsFs.existsSync(nativeStatePath))
2073
- core_backend_1.IModelJsFs.unlinkSync(nativeStatePath);
2074
- db.createDb(nativeStatePath);
2075
- try {
2076
- this.saveStateToDb(db);
2077
- db.saveChanges();
2078
- }
2079
- finally {
2080
- db.closeDb();
2081
- }
2082
- }
2083
- async processChanges(optionsOrAccessToken, startChangesetId) {
2076
+ async processChanges(options) {
2084
2077
  this._isSynchronization = true;
2085
2078
  this.initScopeProvenance();
2086
- const args = typeof optionsOrAccessToken === "string"
2087
- ? {
2088
- accessToken: optionsOrAccessToken,
2089
- startChangeset: startChangesetId
2090
- ? { id: startChangesetId }
2091
- : { index: this._synchronizationVersion.index + 1 },
2092
- }
2093
- : optionsOrAccessToken;
2094
2079
  this.logSettings();
2095
- await this.initialize(args);
2080
+ await this.initialize(options);
2096
2081
  // must wait for initialization of synchronization provenance data
2097
- await this.exporter.exportChanges(this.getExportInitOpts(args));
2082
+ await this.exporter.exportChanges(this.getExportInitOpts(options));
2098
2083
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
2099
2084
  if (this._options.optimizeGeometry)
2100
2085
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
2101
2086
  this.importer.computeProjectExtents();
2102
- this.finalizeTransformation();
2103
- const defaultSaveTargetChanges = () => {
2104
- this.targetDb.saveChanges();
2105
- };
2106
- await (args.saveTargetChanges ?? defaultSaveTargetChanges)(this);
2087
+ await this.finalizeTransformation(options);
2107
2088
  }
2108
2089
  /** Changeset data must be initialized in order to build correct changeOptions.
2109
2090
  * Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
@@ -2112,12 +2093,19 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2112
2093
  if (!this._isSynchronization)
2113
2094
  return {};
2114
2095
  return {
2096
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
2115
2097
  accessToken: opts.accessToken,
2116
- ...this._changesetRanges
2117
- ? { changesetRanges: this._changesetRanges }
2118
- : opts.startChangeset
2119
- ? { startChangeset: opts.startChangeset }
2120
- : { startChangeset: { index: this._synchronizationVersion.index + 1 } },
2098
+ ...(this._csFileProps
2099
+ ? { csFileProps: this._csFileProps }
2100
+ : this._changesetRanges
2101
+ ? { changesetRanges: this._changesetRanges }
2102
+ : opts.startChangeset
2103
+ ? { startChangeset: opts.startChangeset }
2104
+ : {
2105
+ startChangeset: {
2106
+ index: this._synchronizationVersion.index + 1,
2107
+ },
2108
+ }),
2121
2109
  };
2122
2110
  }
2123
2111
  /** Combine an array of source elements into a single target element.
@@ -2133,10 +2121,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2133
2121
  }
2134
2122
  }
2135
2123
  exports.IModelTransformer = IModelTransformer;
2136
- /** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
2137
- IModelTransformer.jsStateTable = "TransformerJsState";
2138
- /** @internal the name of the table where the target state heuristics is serialized in transformer state dumps */
2139
- IModelTransformer.lastProvenanceEntityInfoTable = "LastProvenanceEntityInfo";
2124
+ IModelTransformer.noEsaSyncDirectionErrorMessage = "Couldn't find an external source aspect to determine sync direction. This often means that the master->branch relationship has not been established. Consider running the transformer with wasSourceIModelCopiedToTarget set to true.";
2140
2125
  /** IModelTransformer that clones the contents of a template model.
2141
2126
  * @beta
2142
2127
  */
@@ -2207,7 +2192,8 @@ class TemplateModelCloner extends IModelTransformer {
2207
2192
  }
2208
2193
  else {
2209
2194
  const definitionElement = this.sourceDb.elements.tryGetElement(referenceId, core_backend_1.DefinitionElement);
2210
- if (definitionElement && !(definitionElement instanceof core_backend_1.RecipeDefinitionElement)) {
2195
+ if (definitionElement &&
2196
+ !(definitionElement instanceof core_backend_1.RecipeDefinitionElement)) {
2211
2197
  this.context.remapElement(referenceId, referenceId); // when in the same iModel, can use existing DefinitionElements without remapping
2212
2198
  }
2213
2199
  else {
@@ -2222,7 +2208,7 @@ class TemplateModelCloner extends IModelTransformer {
2222
2208
  if (sourceElement instanceof core_backend_1.GeometricElement) {
2223
2209
  const is3d = sourceElement instanceof core_backend_1.GeometricElement3d;
2224
2210
  const placementClass = is3d ? core_common_1.Placement3d : core_common_1.Placement2d;
2225
- const placement = (placementClass).fromJSON(targetElementProps.placement);
2211
+ const placement = placementClass.fromJSON(targetElementProps.placement);
2226
2212
  if (placement.isValid) {
2227
2213
  nodeAssert(this._transform3d);
2228
2214
  placement.multiplyTransform(this._transform3d);