@itwin/imodel-transformer 0.4.4-dev.1 → 0.4.18-dev.0

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 (65) hide show
  1. package/CHANGELOG.md +1 -9
  2. package/lib/cjs/Algo.d.ts +7 -0
  3. package/lib/cjs/Algo.d.ts.map +1 -0
  4. package/lib/cjs/Algo.js +65 -0
  5. package/lib/cjs/Algo.js.map +1 -0
  6. package/lib/cjs/BigMap.d.ts +1 -6
  7. package/lib/cjs/BigMap.d.ts.map +1 -1
  8. package/lib/cjs/BigMap.js +3 -29
  9. package/lib/cjs/BigMap.js.map +1 -1
  10. package/lib/cjs/BranchProvenanceInitializer.d.ts +10 -3
  11. package/lib/cjs/BranchProvenanceInitializer.d.ts.map +1 -1
  12. package/lib/cjs/BranchProvenanceInitializer.js +101 -10
  13. package/lib/cjs/BranchProvenanceInitializer.js.map +1 -1
  14. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts.map +1 -1
  15. package/lib/cjs/DetachedExportElementAspectsStrategy.js +12 -5
  16. package/lib/cjs/DetachedExportElementAspectsStrategy.js.map +1 -1
  17. package/lib/cjs/ECReferenceTypesCache.d.ts.map +1 -1
  18. package/lib/cjs/ECReferenceTypesCache.js +32 -18
  19. package/lib/cjs/ECReferenceTypesCache.js.map +1 -1
  20. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts +1 -1
  21. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts.map +1 -1
  22. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js +7 -5
  23. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js.map +1 -1
  24. package/lib/cjs/ElementCascadingDeleter.d.ts +3 -3
  25. package/lib/cjs/ElementCascadingDeleter.d.ts.map +1 -1
  26. package/lib/cjs/ElementCascadingDeleter.js +9 -7
  27. package/lib/cjs/ElementCascadingDeleter.js.map +1 -1
  28. package/lib/cjs/EntityMap.d.ts.map +1 -1
  29. package/lib/cjs/EntityMap.js.map +1 -1
  30. package/lib/cjs/EntityUnifier.d.ts +5 -0
  31. package/lib/cjs/EntityUnifier.d.ts.map +1 -1
  32. package/lib/cjs/EntityUnifier.js +22 -35
  33. package/lib/cjs/EntityUnifier.js.map +1 -1
  34. package/lib/cjs/ExportElementAspectsStrategy.d.ts.map +1 -1
  35. package/lib/cjs/ExportElementAspectsStrategy.js +5 -4
  36. package/lib/cjs/ExportElementAspectsStrategy.js.map +1 -1
  37. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts.map +1 -1
  38. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js +9 -5
  39. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js.map +1 -1
  40. package/lib/cjs/IModelCloneContext.d.ts.map +1 -1
  41. package/lib/cjs/IModelCloneContext.js +23 -14
  42. package/lib/cjs/IModelCloneContext.js.map +1 -1
  43. package/lib/cjs/IModelExporter.d.ts +87 -21
  44. package/lib/cjs/IModelExporter.d.ts.map +1 -1
  45. package/lib/cjs/IModelExporter.js +279 -114
  46. package/lib/cjs/IModelExporter.js.map +1 -1
  47. package/lib/cjs/IModelImporter.d.ts +29 -21
  48. package/lib/cjs/IModelImporter.d.ts.map +1 -1
  49. package/lib/cjs/IModelImporter.js +115 -62
  50. package/lib/cjs/IModelImporter.js.map +1 -1
  51. package/lib/cjs/IModelTransformer.d.ts +285 -48
  52. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  53. package/lib/cjs/IModelTransformer.js +1273 -337
  54. package/lib/cjs/IModelTransformer.js.map +1 -1
  55. package/lib/cjs/PendingReferenceMap.d.ts.map +1 -1
  56. package/lib/cjs/PendingReferenceMap.js +12 -6
  57. package/lib/cjs/PendingReferenceMap.js.map +1 -1
  58. package/lib/cjs/TransformerLoggerCategory.d.ts +2 -2
  59. package/lib/cjs/TransformerLoggerCategory.d.ts.map +1 -1
  60. package/lib/cjs/TransformerLoggerCategory.js +5 -5
  61. package/lib/cjs/TransformerLoggerCategory.js.map +1 -1
  62. package/lib/cjs/transformer.d.ts.map +1 -1
  63. package/lib/cjs/transformer.js +14 -10
  64. package/lib/cjs/transformer.js.map +1 -1
  65. 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
  */
@@ -22,6 +22,7 @@ const PendingReferenceMap_1 = require("./PendingReferenceMap");
22
22
  const EntityMap_1 = require("./EntityMap");
23
23
  const IModelCloneContext_1 = require("./IModelCloneContext");
24
24
  const EntityUnifier_1 = require("./EntityUnifier");
25
+ const Algo_1 = require("./Algo");
25
26
  const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelTransformer;
26
27
  const nullLastProvenanceEntityInfo = {
27
28
  entityId: core_bentley_1.Id64.invalid,
@@ -94,9 +95,115 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
94
95
  get targetScopeElementId() {
95
96
  return this._options.targetScopeElementId;
96
97
  }
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
+ });
133
+ }
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";
198
+ }
97
199
  /** The element classes that are considered to define provenance in the iModel */
98
200
  static get provenanceElementClasses() {
99
- 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
+ ];
100
207
  }
101
208
  /** The element aspect classes that are considered to define provenance in the iModel */
102
209
  static get provenanceElementAspectClasses() {
@@ -112,18 +219,52 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
112
219
  /** map of (unprocessed element, referencing processed element) pairs to the partially committed element that needs the reference resolved
113
220
  * and have some helper methods below for now */
114
221
  this._pendingReferences = new PendingReferenceMap_1.PendingReferenceMap();
222
+ /** a set of elements for which source provenance will be explicitly tracked by ExternalSourceAspects */
223
+ this._elementsWithExplicitlyTrackedProvenance = new Set();
115
224
  /** map of partially committed entities to their partial commit progress */
116
225
  this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
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;
234
+ this._changesetRanges = undefined;
117
235
  /** Set of entity keys which were not exported and don't need to be tracked for pending reference resolution.
118
236
  * @note Currently only tracks elements which were not exported.
119
237
  */
120
238
  this._skippedEntities = new Set();
239
+ /**
240
+ * Previously the transformer would insert provenance always pointing to the "target" relationship.
241
+ * It should (and now by default does) instead insert provenance pointing to the provenanceSource
242
+ * SEE: https://github.com/iTwin/imodel-transformer/issues/54
243
+ * This exists only to facilitate testing that the transformer can handle the older, flawed method
244
+ */
245
+ this._forceOldRelationshipProvenanceMethod = false;
246
+ /** NOTE: the json properties must be converted to string before insertion */
247
+ this._targetScopeProvenanceProps = undefined;
248
+ /**
249
+ * Index of the changeset that the transformer was at when the transformation begins (was constructed).
250
+ * Used to determine at the end which changesets were part of a synchronization.
251
+ */
252
+ this._startingChangesetIndices = undefined;
253
+ this._cachedSynchronizationVersion = undefined;
254
+ this._targetClassNameToClassIdCache = new Map();
255
+ // if undefined, it can be initialized by calling [[this.processChangesets]]
256
+ this._hasElementChangedCache = undefined;
257
+ this._deletedSourceRelationshipData = undefined;
121
258
  this._yieldManager = new core_bentley_1.YieldManager();
122
259
  /** The directory where schemas will be exported, a random temporary directory */
123
260
  this._schemaExportDir = path.join(core_backend_1.KnownLocations.tmpdir, core_bentley_1.Guid.createValue());
124
261
  this._longNamedSchemasMap = new Map();
125
262
  /** state to prevent reinitialization, @see [[initialize]] */
126
263
  this._initialized = false;
264
+ this._sourceChangeDataState = "uninited";
265
+ /** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
266
+ this._csFileProps = undefined;
267
+ /** previous provenance, either a federation guid, a `${sourceFedGuid}/${targetFedGuid}` pair, or required aspect props */
127
268
  this._lastProvenanceEntityInfo = nullLastProvenanceEntityInfo;
128
269
  // initialize IModelTransformOptions
129
270
  this._options = {
@@ -132,9 +273,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
132
273
  cloneUsingBinaryGeometry: options?.cloneUsingBinaryGeometry ?? true,
133
274
  targetScopeElementId: options?.targetScopeElementId ?? core_common_1.IModel.rootSubjectId,
134
275
  // eslint-disable-next-line deprecation/deprecation
135
- danglingReferencesBehavior: options?.danglingReferencesBehavior ?? options?.danglingPredecessorsBehavior ?? "reject",
276
+ danglingReferencesBehavior: options?.danglingReferencesBehavior ??
277
+ options?.danglingPredecessorsBehavior ??
278
+ "reject",
279
+ branchRelationshipDataBehavior: options?.branchRelationshipDataBehavior ?? "reject",
136
280
  };
137
- this._isFirstSynchronization = this._options.wasSourceIModelCopiedToTarget ? true : undefined;
281
+ this._isProvenanceInitTransform = this._options
282
+ .wasSourceIModelCopiedToTarget
283
+ ? true
284
+ : undefined;
138
285
  // initialize exporter and sourceDb
139
286
  if (source instanceof core_backend_1.IModelDb) {
140
287
  this.exporter = new IModelExporter_1.IModelExporter(source);
@@ -145,7 +292,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
145
292
  this.sourceDb = this.exporter.sourceDb;
146
293
  this.exporter.registerHandler(this);
147
294
  this.exporter.wantGeometry = options?.loadSourceGeometry ?? false; // optimization to not load source GeometryStreams by default
148
- 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?
149
297
  IModelTransformer.provenanceElementClasses.forEach((cls) => this.exporter.excludeElementClass(cls.classFullName));
150
298
  IModelTransformer.provenanceElementAspectClasses.forEach((cls) => this.exporter.excludeElementAspectClass(cls.classFullName));
151
299
  }
@@ -153,32 +301,49 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
153
301
  this.exporter.excludeElementAspectClass("BisCore:TextAnnotationData"); // This ElementAspect is auto-created by the BisCore:TextAnnotation2d/3d element handlers
154
302
  // initialize importer and targetDb
155
303
  if (target instanceof core_backend_1.IModelDb) {
156
- 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
+ });
157
308
  }
158
309
  else {
159
310
  this.importer = target;
160
- /* eslint-disable deprecation/deprecation */
161
- if (Boolean(this._options.preserveElementIdsForFiltering) !== this.importer.preserveElementIdsForFiltering) {
162
- core_bentley_1.Logger.logWarning(loggerCategory, [
163
- "A custom importer was passed as a target but its 'preserveElementIdsForFiltering' option is out of sync with the transformer's option.",
164
- "The custom importer target's option will be force updated to use the transformer's value.",
165
- "This behavior is deprecated and will be removed in a future version, throwing an error if they are out of sync.",
166
- ].join("\n"));
167
- this.importer.preserveElementIdsForFiltering = Boolean(this._options.preserveElementIdsForFiltering);
168
- }
169
- /* eslint-enable deprecation/deprecation */
311
+ this.validateSharedOptionsMatch();
170
312
  }
171
313
  this.targetDb = this.importer.targetDb;
172
314
  // create the IModelCloneContext, it must be initialized later
173
315
  this.context = new IModelCloneContext_1.IModelCloneContext(this.sourceDb, this.targetDb);
316
+ if (this.sourceDb.isBriefcase && this.targetDb.isBriefcase) {
317
+ nodeAssert(this.sourceDb.changeset.index !== undefined &&
318
+ this.targetDb.changeset.index !== undefined, "database has no changeset index");
319
+ this._startingChangesetIndices = {
320
+ target: this.targetDb.changeset.index,
321
+ source: this.sourceDb.changeset.index,
322
+ };
323
+ }
174
324
  // this internal is guaranteed stable for just transformer usage
175
325
  /* eslint-disable @itwin/no-internal */
176
- if ("codeValueBehavior" in this.sourceDb) {
326
+ if (("codeValueBehavior" in this.sourceDb)) {
177
327
  this.sourceDb.codeValueBehavior = "exact";
178
328
  this.targetDb.codeValueBehavior = "exact";
179
329
  }
180
330
  /* eslint-enable @itwin/no-internal */
181
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
+ }
182
347
  /** Dispose any native resources associated with this IModelTransformer. */
183
348
  dispose() {
184
349
  core_bentley_1.Logger.logTrace(loggerCategory, "dispose()");
@@ -204,24 +369,31 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
204
369
  * @note This will be [[targetDb]] except when it is a reverse synchronization. In that case it be [[sourceDb]].
205
370
  */
206
371
  get provenanceDb() {
207
- return this._options.isReverseSynchronization ? this.sourceDb : this.targetDb;
372
+ return this.isReverseSynchronization ? this.sourceDb : this.targetDb;
208
373
  }
209
374
  /** Return the IModelDb where IModelTransformer looks for entities referred to by stored provenance.
210
375
  * @note This will be [[sourceDb]] except when it is a reverse synchronization. In that case it be [[targetDb]].
211
376
  */
212
377
  get provenanceSourceDb() {
213
- return this._options.isReverseSynchronization ? this.targetDb : this.sourceDb;
378
+ return this.isReverseSynchronization ? this.targetDb : this.sourceDb;
214
379
  }
215
380
  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
216
381
  static initElementProvenanceOptions(sourceElementId, targetElementId, args) {
217
- const elementId = args.isReverseSynchronization ? sourceElementId : targetElementId;
382
+ const elementId = args.isReverseSynchronization
383
+ ? sourceElementId
384
+ : targetElementId;
218
385
  const version = args.isReverseSynchronization
219
386
  ? args.targetDb.elements.queryLastModifiedTime(targetElementId)
220
387
  : args.sourceDb.elements.queryLastModifiedTime(sourceElementId);
221
- const aspectIdentifier = args.isReverseSynchronization ? targetElementId : sourceElementId;
388
+ const aspectIdentifier = args.isReverseSynchronization
389
+ ? targetElementId
390
+ : sourceElementId;
222
391
  const aspectProps = {
223
392
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
224
- element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
393
+ element: {
394
+ id: elementId,
395
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
396
+ },
225
397
  scope: { id: args.targetScopeElementId },
226
398
  identifier: aspectIdentifier,
227
399
  kind: core_backend_1.ExternalSourceAspect.Kind.Element,
@@ -229,10 +401,41 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
229
401
  };
230
402
  return aspectProps;
231
403
  }
404
+ static initRelationshipProvenanceOptions(sourceRelInstanceId, targetRelInstanceId, args) {
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;
414
+ const elementId = provenanceDb.withPreparedStatement("SELECT SourceECInstanceId FROM bis.ElementRefersToElements WHERE ECInstanceId=?", (stmt) => {
415
+ stmt.bindId(1, provenanceRelInstanceId);
416
+ nodeAssert(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
417
+ return stmt.getValue(0).getId();
418
+ });
419
+ const jsonProperties = args.forceOldRelationshipProvenanceMethod
420
+ ? { targetRelInstanceId }
421
+ : { provenanceRelInstanceId };
422
+ const aspectProps = {
423
+ classFullName: core_backend_1.ExternalSourceAspect.classFullName,
424
+ element: {
425
+ id: elementId,
426
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
427
+ },
428
+ scope: { id: args.targetScopeElementId },
429
+ identifier: aspectIdentifier,
430
+ kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
431
+ jsonProperties: JSON.stringify(jsonProperties),
432
+ };
433
+ return aspectProps;
434
+ }
232
435
  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
233
436
  initElementProvenance(sourceElementId, targetElementId) {
234
437
  return IModelTransformer.initElementProvenanceOptions(sourceElementId, targetElementId, {
235
- isReverseSynchronization: !!this._options.isReverseSynchronization,
438
+ isReverseSynchronization: this.isReverseSynchronization,
236
439
  targetScopeElementId: this.targetScopeElementId,
237
440
  sourceDb: this.sourceDb,
238
441
  targetDb: this.targetDb,
@@ -240,36 +443,115 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
240
443
  }
241
444
  /** Create an ExternalSourceAspectProps in a standard way for a Relationship in an iModel --> iModel transformations.
242
445
  * The ExternalSourceAspect is meant to be owned by the Element in the target iModel that is the `sourceId` of transformed relationship.
243
- * The `identifier` property of the ExternalSourceAspect will be the ECInstanceId of the relationship in the source iModel.
244
- * The ECInstanceId of the relationship in the target iModel will be stored in the JsonProperties of the ExternalSourceAspect.
446
+ * The `identifier` property of the ExternalSourceAspect will be the ECInstanceId of the relationship in the master iModel.
447
+ * The ECInstanceId of the relationship in the branch iModel will be stored in the JsonProperties of the ExternalSourceAspect.
245
448
  */
246
449
  initRelationshipProvenance(sourceRelationship, targetRelInstanceId) {
247
- const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, targetRelInstanceId);
248
- const elementId = this._options.isReverseSynchronization ? sourceRelationship.sourceId : targetRelationship.sourceId;
249
- const aspectIdentifier = this._options.isReverseSynchronization ? targetRelInstanceId : sourceRelationship.id;
250
- const aspectProps = {
450
+ return IModelTransformer.initRelationshipProvenanceOptions(sourceRelationship.id, targetRelInstanceId, {
451
+ sourceDb: this.sourceDb,
452
+ targetDb: this.targetDb,
453
+ isReverseSynchronization: this.isReverseSynchronization,
454
+ targetScopeElementId: this.targetScopeElementId,
455
+ forceOldRelationshipProvenanceMethod: this._forceOldRelationshipProvenanceMethod,
456
+ });
457
+ }
458
+ /** the changeset in the scoping element's source version found for this transformation
459
+ * @note: the version depends on whether this is a reverse synchronization or not, as
460
+ * it is stored separately for both synchronization directions.
461
+ * @note: must call [[initScopeProvenance]] before using this property.
462
+ * @note: empty string and -1 for changeset and index if it has never been transformed or was transformed before federation guid update (pre 1.x).
463
+ */
464
+ get _synchronizationVersion() {
465
+ if (!this._cachedSynchronizationVersion) {
466
+ nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps was not set yet");
467
+ const version = this.isReverseSynchronization
468
+ ? this._targetScopeProvenanceProps.jsonProperties?.reverseSyncVersion
469
+ : this._targetScopeProvenanceProps.version;
470
+ nodeAssert(version !== undefined, "no version contained in target scope");
471
+ const [id, index] = version === "" ? ["", -1] : version.split(";");
472
+ this._cachedSynchronizationVersion = { index: Number(index), id };
473
+ nodeAssert(!Number.isNaN(this._cachedSynchronizationVersion.index), "bad parse: invalid index in version");
474
+ }
475
+ return this._cachedSynchronizationVersion;
476
+ }
477
+ /** the changeset in the scoping element's source version found for this transformation
478
+ * @note: the version depends on whether this is a reverse synchronization or not, as
479
+ * it is stored separately for both synchronization directions.
480
+ * @note: empty string and -1 for changeset and index if it has never been transformed, or was transformed before federation guid update (pre 1.x).
481
+ */
482
+ get synchronizationVersion() {
483
+ if (this._cachedSynchronizationVersion === undefined) {
484
+ const provenanceScopeAspect = this.tryGetProvenanceScopeAspect();
485
+ if (!provenanceScopeAspect) {
486
+ return { index: -1, id: "" }; // first synchronization.
487
+ }
488
+ const version = this.isReverseSynchronization
489
+ ? JSON.parse(provenanceScopeAspect.jsonProperties ?? "{}").reverseSyncVersion
490
+ : provenanceScopeAspect.version;
491
+ if (!version) {
492
+ return { index: -1, id: "" }; // previous synchronization was done before fed guid update.
493
+ }
494
+ const [id, index] = version.split(";");
495
+ if (Number.isNaN(Number(index)))
496
+ throw new Error("Could not parse version data from scope aspect");
497
+ this._cachedSynchronizationVersion = { index: Number(index), id }; // synchronization version found and cached.
498
+ }
499
+ return this._cachedSynchronizationVersion;
500
+ }
501
+ /**
502
+ * @returns provenance scope aspect if it exists in the provenanceDb.
503
+ * Provenance scope aspect is created and inserted into provenanceDb when [[initScopeProvenance]] is invoked.
504
+ */
505
+ tryGetProvenanceScopeAspect() {
506
+ const scopeProvenanceAspectProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, {
507
+ id: undefined,
251
508
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
252
- element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
253
- scope: { id: this.targetScopeElementId },
254
- identifier: aspectIdentifier,
255
- kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
256
- jsonProperties: JSON.stringify({ targetRelInstanceId }),
257
- };
258
- aspectProps.id = this.queryExternalSourceAspectId(aspectProps);
259
- return aspectProps;
509
+ scope: { id: core_common_1.IModel.rootSubjectId },
510
+ kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
511
+ element: { id: this.targetScopeElementId ?? core_common_1.IModel.rootSubjectId },
512
+ identifier: this.provenanceSourceDb.iModelId,
513
+ });
514
+ return scopeProvenanceAspectProps !== undefined
515
+ ? this.provenanceDb.elements.getAspect(scopeProvenanceAspectProps.aspectId)
516
+ : undefined;
260
517
  }
261
- validateScopeProvenance() {
518
+ /**
519
+ * Make sure there are no conflicting other scope-type external source aspects on the *target scope element*,
520
+ * If there are none at all, insert one, then this must be a first synchronization.
521
+ * @returns the last synced version (changesetId) on the target scope's external source aspect,
522
+ * if this was a [BriefcaseDb]($backend)
523
+ */
524
+ initScopeProvenance() {
262
525
  const aspectProps = {
526
+ id: undefined,
527
+ version: undefined,
263
528
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
264
- element: { id: this.targetScopeElementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
529
+ element: {
530
+ id: this.targetScopeElementId,
531
+ relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName,
532
+ },
265
533
  scope: { id: core_common_1.IModel.rootSubjectId },
266
- identifier: this._options.isReverseSynchronization ? this.targetDb.iModelId : this.sourceDb.iModelId,
534
+ identifier: this.provenanceSourceDb.iModelId,
267
535
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
536
+ jsonProperties: undefined,
268
537
  };
269
- aspectProps.id = this.queryExternalSourceAspectId(aspectProps); // this query includes "identifier"
270
- if (undefined === aspectProps.id) {
538
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps); // this query includes "identifier"
539
+ if (foundEsaProps === undefined) {
540
+ aspectProps.version = ""; // empty since never before transformed. Will be updated in [[finalizeTransformation]]
541
+ aspectProps.jsonProperties = {
542
+ pendingReverseSyncChangesetIndices: [],
543
+ pendingSyncChangesetIndices: [],
544
+ reverseSyncVersion: "", // empty since never before transformed. Will be updated in first reverse sync
545
+ };
271
546
  // this query does not include "identifier" to find possible conflicts
272
- const sql = `SELECT ECInstanceId FROM ${core_backend_1.ExternalSourceAspect.classFullName} WHERE Element.Id=:elementId AND Scope.Id=:scopeId AND Kind=:kind LIMIT 1`;
547
+ const sql = `
548
+ SELECT ECInstanceId
549
+ FROM ${core_backend_1.ExternalSourceAspect.classFullName}
550
+ WHERE Element.Id=:elementId
551
+ AND Scope.Id=:scopeId
552
+ AND Kind=:kind
553
+ LIMIT 1
554
+ `;
273
555
  const hasConflictingScope = this.provenanceDb.withPreparedStatement(sql, (statement) => {
274
556
  statement.bindId("elementId", aspectProps.element.id);
275
557
  statement.bindId("scopeId", aspectProps.scope.id); // this scope.id can never be invalid, we create it above
@@ -280,162 +562,276 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
280
562
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidId, "Provenance scope conflict");
281
563
  }
282
564
  if (!this._options.noProvenance) {
283
- this.provenanceDb.elements.insertAspect(aspectProps);
565
+ const id = this.provenanceDb.elements.insertAspect({
566
+ ...aspectProps,
567
+ jsonProperties: JSON.stringify(aspectProps.jsonProperties),
568
+ });
569
+ aspectProps.id = id;
284
570
  }
285
571
  }
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;
286
592
  }
287
- queryExternalSourceAspectId(aspectProps) {
288
- const sql = `SELECT ECInstanceId FROM ${core_backend_1.ExternalSourceAspect.classFullName} WHERE Element.Id=:elementId AND Scope.Id=:scopeId AND Kind=:kind AND Identifier=:identifier LIMIT 1`;
289
- return this.provenanceDb.withPreparedStatement(sql, (statement) => {
290
- statement.bindId("elementId", aspectProps.element.id);
291
- if (aspectProps.scope === undefined)
292
- return undefined; // return undefined instead of binding an invalid id
293
- statement.bindId("scopeId", aspectProps.scope.id);
294
- statement.bindString("kind", aspectProps.kind);
295
- statement.bindString("identifier", aspectProps.identifier);
296
- return (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) ? statement.getValue(0).getId() : undefined;
297
- });
298
- }
299
- /** Iterate all matching ExternalSourceAspects in the provenance iModel (target unless reverse sync) and call a function for each one. */
593
+ /**
594
+ * Iterate all matching federation guids and ExternalSourceAspects in the provenance iModel (target unless reverse sync)
595
+ * and call a function for each one.
596
+ * @note provenance is done by federation guids where possible
597
+ * @note this may execute on each element more than once! Only use in cases where that is handled
598
+ */
300
599
  static forEachTrackedElement(args) {
600
+ if (args.provenanceDb === args.provenanceSourceDb)
601
+ return;
301
602
  if (!args.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
302
603
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
303
604
  }
304
- const sql = `SELECT Identifier,Element.Id FROM ${core_backend_1.ExternalSourceAspect.classFullName} WHERE Scope.Id=:scopeId AND Kind=:kind`;
305
- args.provenanceDb.withPreparedStatement(sql, (statement) => {
306
- statement.bindId("scopeId", args.targetScopeElementId);
307
- statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
308
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
309
- const aspectIdentifier = statement.getValue(0).getString(); // ExternalSourceAspect.Identifier is of type string
310
- const elementId = statement.getValue(1).getId();
311
- if (args.isReverseSynchronization) {
312
- args.fn(elementId, aspectIdentifier); // provenance coming from the sourceDb
605
+ const sourceDb = args.isReverseSynchronization
606
+ ? args.provenanceDb
607
+ : args.provenanceSourceDb;
608
+ const targetDb = args.isReverseSynchronization
609
+ ? args.provenanceSourceDb
610
+ : args.provenanceDb;
611
+ // query for provenanceDb
612
+ const elementIdByFedGuidQuery = `
613
+ SELECT e.ECInstanceId, FederationGuid
614
+ FROM bis.Element e
615
+ ${args.skipPropagateChangesToRootElements
616
+ ? "WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements"
617
+ : ""}
618
+ ORDER BY FederationGuid
619
+ `;
620
+ // iterate through sorted list of fed guids from both dbs to get the intersection
621
+ // NOTE: if we exposed the native attach database support,
622
+ // we could get the intersection of fed guids in one query, not sure if it would be faster
623
+ // OR we could do a raw sqlite query...
624
+ sourceDb.withStatement(elementIdByFedGuidQuery, (sourceStmt) => targetDb.withStatement(elementIdByFedGuidQuery, (targetStmt) => {
625
+ if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
626
+ return;
627
+ let sourceRow = sourceStmt.getRow();
628
+ if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
629
+ return;
630
+ let targetRow = targetStmt.getRow();
631
+ // NOTE: these comparisons rely upon the lowercase of the guid,
632
+ // and the fact that '0' < '9' < a' < 'f' in ascii/utf8
633
+ while (true) {
634
+ const currSourceRow = sourceRow, currTargetRow = targetRow;
635
+ if (currSourceRow.federationGuid !== undefined &&
636
+ currTargetRow.federationGuid !== undefined &&
637
+ currSourceRow.federationGuid === currTargetRow.federationGuid) {
638
+ // data flow direction is always sourceDb -> targetDb and it does not depend on where the explicit element provenance is stored
639
+ args.fn(sourceRow.id, targetRow.id);
313
640
  }
314
- else {
315
- args.fn(aspectIdentifier, elementId); // provenance coming from the targetDb
641
+ if (currTargetRow.federationGuid === undefined ||
642
+ (currSourceRow.federationGuid !== undefined &&
643
+ currSourceRow.federationGuid >= currTargetRow.federationGuid)) {
644
+ if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
645
+ return;
646
+ targetRow = targetStmt.getRow();
647
+ }
648
+ if (currSourceRow.federationGuid === undefined ||
649
+ (currTargetRow.federationGuid !== undefined &&
650
+ currSourceRow.federationGuid <= currTargetRow.federationGuid)) {
651
+ if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
652
+ return;
653
+ sourceRow = sourceStmt.getRow();
316
654
  }
317
655
  }
656
+ }));
657
+ // query for provenanceDb
658
+ const provenanceAspectsQuery = `
659
+ SELECT esa.Identifier, Element.Id
660
+ FROM bis.ExternalSourceAspect esa
661
+ WHERE Scope.Id=:scopeId
662
+ AND Kind=:kind
663
+ `;
664
+ // Technically this will a second time call the function (as documented) on
665
+ // victims of the old provenance method that have both fedguids and an inserted aspect.
666
+ // But this is a private function with one known caller where that doesn't matter
667
+ args.provenanceDb.withPreparedStatement(provenanceAspectsQuery, (stmt) => {
668
+ const runFnInDataFlowDirection = (sourceId, targetId) => args.isReverseSynchronization
669
+ ? args.fn(sourceId, targetId)
670
+ : args.fn(targetId, sourceId);
671
+ stmt.bindId("scopeId", args.targetScopeElementId);
672
+ stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
673
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
674
+ // ExternalSourceAspect.Identifier is of type string
675
+ const aspectIdentifier = stmt.getValue(0).getString();
676
+ const elementId = stmt.getValue(1).getId();
677
+ runFnInDataFlowDirection(elementId, aspectIdentifier);
678
+ }
318
679
  });
319
680
  }
320
681
  forEachTrackedElement(fn) {
321
682
  return IModelTransformer.forEachTrackedElement({
322
- provenanceSourceDb: this._options.isReverseSynchronization ? this.sourceDb : this.targetDb,
683
+ provenanceSourceDb: this.provenanceSourceDb,
323
684
  provenanceDb: this.provenanceDb,
324
685
  targetScopeElementId: this.targetScopeElementId,
325
- isReverseSynchronization: !!this._options.isReverseSynchronization,
686
+ isReverseSynchronization: this.isReverseSynchronization,
326
687
  fn,
688
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
327
689
  });
328
690
  }
329
- /** Initialize the source to target Element mapping from ExternalSourceAspects in the target iModel.
330
- * @note This method is called from all `process*` functions and should never need to be called directly.
331
- * @deprecated in 3.x. call [[initialize]] instead, it does the same thing among other initialization
332
- * @note Passing an [[InitFromExternalSourceAspectsArgs]] is required when processing changes, to remap any elements that may have been deleted.
333
- * 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.
334
- */
335
- initFromExternalSourceAspects(args) {
336
- this.forEachTrackedElement((sourceElementId, targetElementId) => {
337
- this.context.remapElement(sourceElementId, targetElementId);
691
+ _queryProvenanceForElement(entityInProvenanceSourceId) {
692
+ return this.provenanceDb.withPreparedStatement(`
693
+ SELECT esa.Element.Id
694
+ FROM Bis.ExternalSourceAspect esa
695
+ WHERE esa.Kind=?
696
+ AND esa.Scope.Id=?
697
+ AND esa.Identifier=?
698
+ `, (stmt) => {
699
+ stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Element);
700
+ stmt.bindId(2, this.targetScopeElementId);
701
+ stmt.bindString(3, entityInProvenanceSourceId);
702
+ if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
703
+ return stmt.getValue(0).getId();
704
+ else
705
+ return undefined;
338
706
  });
339
- if (args)
340
- return this.remapDeletedSourceElements(args);
341
707
  }
342
- /** When processing deleted elements in a reverse synchronization, the [[provenanceDb]] (usually a branch iModel) has already
343
- * deleted the [ExternalSourceAspect]($backend)s that tell us which elements in the reverse synchronization target (usually
344
- * a master iModel) should be deleted. We must use the changesets to get the values of those before they were deleted.
345
- */
346
- async remapDeletedSourceElements(args) {
347
- // we need a connected iModel with changes to remap elements with deletions
348
- if (this.sourceDb.iTwinId === undefined)
349
- return;
350
- try {
351
- const startChangesetIndexOrId = args.startChangeset?.index
352
- ?? args.startChangeset?.id
353
- ?? this.sourceDb.changeset.index
354
- ?? this.sourceDb.changeset.id;
355
- const endChangesetId = this.sourceDb.changeset.id;
356
- const [firstChangesetIndex, endChangesetIndex] = await Promise.all(([startChangesetIndexOrId, endChangesetId])
357
- .map(async (indexOrId) => typeof indexOrId === "number"
358
- ? indexOrId
359
- : core_backend_1.IModelHost.hubAccess
360
- .queryChangeset({
361
- iModelId: this.sourceDb.iModelId,
362
- // eslint-disable-next-line deprecation/deprecation
363
- changeset: { id: indexOrId },
364
- accessToken: args.accessToken,
365
- })
366
- .then((changeset) => changeset.index)));
367
- const changesetIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
368
- accessToken: args.accessToken,
369
- iModelId: this.sourceDb.iModelId,
370
- iTwinId: this.sourceDb.iTwinId,
371
- range: { first: firstChangesetIndex, end: endChangesetIndex },
372
- });
373
- core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
374
- for (const changesetId of changesetIds) {
375
- this.sourceDb.withPreparedStatement(`
376
- SELECT esac.Element.Id, esac.Identifier
377
- FROM ecchange.change.InstanceChange ic
378
- JOIN BisCore.ExternalSourceAspect.Changes(:changesetId, 'BeforeDelete') esac
379
- ON ic.ChangedInstance.Id=esac.ECInstanceId
380
- WHERE ic.OpCode=:opcode
381
- AND ic.Summary.Id=:changesetId
382
- AND esac.Scope.Id=:targetScopeElementId
383
- -- not yet documented ecsql feature to check class id
384
- AND ic.ChangedInstance.ClassId IS (ONLY BisCore.ExternalSourceAspect)
385
- `, (stmt) => {
386
- stmt.bindInteger("opcode", core_common_1.ChangeOpCode.Delete);
387
- stmt.bindInteger("changesetId", changesetId);
388
- stmt.bindInteger("targetScopeElementId", this.targetScopeElementId);
389
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
390
- const targetId = stmt.getValue(0).getId();
391
- const sourceId = stmt.getValue(1).getString(); // BisCore.ExternalSourceAspect.Identifier stores a hex Id64String
392
- // TODO: maybe delete and don't just remap
393
- this.context.remapElement(targetId, sourceId);
394
- }
395
- });
396
- }
397
- }
398
- finally {
399
- if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
400
- core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
708
+ _queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
709
+ return this.provenanceDb.withPreparedStatement(`
710
+ SELECT
711
+ ECInstanceId,
712
+ JSON_EXTRACT(JsonProperties, '$.targetRelInstanceId'),
713
+ JSON_EXTRACT(JsonProperties, '$.provenanceRelInstanceId')
714
+ FROM Bis.ExternalSourceAspect
715
+ WHERE Kind=?
716
+ AND Scope.Id=?
717
+ AND Identifier=?
718
+ `, (stmt) => {
719
+ stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Relationship);
720
+ stmt.bindId(2, this.targetScopeElementId);
721
+ stmt.bindString(3, entityInProvenanceSourceId);
722
+ if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
723
+ return undefined;
724
+ const aspectId = stmt.getValue(0).getId();
725
+ const provenanceRelInstIdVal = stmt.getValue(2);
726
+ const provenanceRelInstanceId = !provenanceRelInstIdVal.isNull
727
+ ? provenanceRelInstIdVal.getString()
728
+ : this._queryTargetRelId(sourceRelInfo);
729
+ return {
730
+ aspectId,
731
+ relationshipId: provenanceRelInstanceId,
732
+ };
733
+ });
734
+ }
735
+ _queryTargetRelId(sourceRelInfo) {
736
+ const targetRelInfo = {
737
+ sourceId: this.context.findTargetElementId(sourceRelInfo.sourceId),
738
+ targetId: this.context.findTargetElementId(sourceRelInfo.targetId),
739
+ };
740
+ if (targetRelInfo.sourceId === undefined ||
741
+ targetRelInfo.targetId === undefined)
742
+ return undefined; // couldn't find an element, rel is invalid or deleted
743
+ return this.targetDb.withPreparedStatement(`
744
+ SELECT ECInstanceId
745
+ FROM bis.ElementRefersToElements
746
+ WHERE SourceECInstanceId=?
747
+ AND TargetECInstanceId=?
748
+ AND ECClassId=?
749
+ `, (stmt) => {
750
+ stmt.bindId(1, targetRelInfo.sourceId);
751
+ stmt.bindId(2, targetRelInfo.targetId);
752
+ stmt.bindId(3, this._targetClassNameToClassId(sourceRelInfo.classFullName));
753
+ if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
754
+ return undefined;
755
+ return stmt.getValue(0).getId();
756
+ });
757
+ }
758
+ _targetClassNameToClassId(classFullName) {
759
+ let classId = this._targetClassNameToClassIdCache.get(classFullName);
760
+ if (classId === undefined) {
761
+ classId = this._getRelClassId(this.targetDb, classFullName);
762
+ this._targetClassNameToClassIdCache.set(classFullName, classId);
401
763
  }
764
+ return classId;
765
+ }
766
+ // NOTE: this doesn't handle remapped element classes,
767
+ // but is only used for relationships rn
768
+ _getRelClassId(db, classFullName) {
769
+ return db.withPreparedStatement(`
770
+ SELECT c.ECInstanceId
771
+ FROM ECDbMeta.ECClassDef c
772
+ JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
773
+ WHERE s.Name=? AND c.Name=?
774
+ `, (stmt) => {
775
+ const [schemaName, className] = classFullName.indexOf(".") !== -1
776
+ ? classFullName.split(".")
777
+ : classFullName.split(":");
778
+ stmt.bindString(1, schemaName);
779
+ stmt.bindString(2, className);
780
+ if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
781
+ return stmt.getValue(0).getId();
782
+ (0, core_bentley_1.assert)(false, "relationship was not found");
783
+ });
784
+ }
785
+ _queryElemIdByFedGuid(db, fedGuid) {
786
+ return db.withPreparedStatement("SELECT ECInstanceId FROM Bis.Element WHERE FederationGuid=?", (stmt) => {
787
+ stmt.bindGuid(1, fedGuid);
788
+ if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
789
+ return stmt.getValue(0).getId();
790
+ else
791
+ return undefined;
792
+ });
402
793
  }
403
794
  /** Returns `true` if *brute force* delete detections should be run.
795
+ * @note This is only called if [[IModelTransformOptions.forceExternalSourceAspectProvenance]] option is true
404
796
  * @note Not relevant for processChanges when change history is known.
405
797
  */
406
798
  shouldDetectDeletes() {
407
- if (this._isFirstSynchronization)
408
- return false; // not necessary the first time since there are no deletes to detect
409
- if (this._options.isReverseSynchronization)
410
- return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
411
- return true;
799
+ nodeAssert(this._syncType !== undefined);
800
+ return this._syncType === "not-sync";
412
801
  }
413
- /** Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements in the source iModel.
414
- * @see processChanges
415
- * @note This method is called from [[processAll]] and is not needed by [[processChanges]], so it only needs to be called directly when processing a subset of an iModel.
802
+ /**
803
+ * Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
804
+ * in the source iModel.
805
+ * @deprecated in 1.x. Do not use this. // FIXME<MIKE>: how to better explain this?
806
+ * This method is only called during [[processAll]] when the option
807
+ * [[IModelTransformOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
808
+ * necessary when using [[processChanges]] since changeset information is sufficient.
809
+ * @note you do not need to call this directly unless processing a subset of an iModel.
416
810
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
417
811
  */
418
812
  async detectElementDeletes() {
419
- if (this._options.isReverseSynchronization) {
420
- throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
421
- }
422
- const targetElementsToDelete = [];
423
- this.forEachTrackedElement((sourceElementId, targetElementId) => {
424
- if (undefined === this.sourceDb.elements.tryGetElementProps(sourceElementId)) {
425
- // if the sourceElement is not found, then it must have been deleted, so propagate the delete to the target iModel
426
- targetElementsToDelete.push(targetElementId);
427
- }
428
- });
429
- targetElementsToDelete.forEach((targetElementId) => {
430
- try {
431
- // TODO: make it possible to delete more elements at once to prevent redundant expensive
432
- // element reference scanning
433
- this.importer.deleteElement(targetElementId);
434
- }
435
- catch (err) {
436
- // ignore not found elements, iterative element tree deletion might have already deleted them
437
- if (err.name !== "Not Found")
438
- throw err;
813
+ const sql = `
814
+ SELECT Identifier, Element.Id
815
+ FROM BisCore.ExternalSourceAspect
816
+ WHERE Scope.Id=:scopeId
817
+ AND Kind=:kind
818
+ `;
819
+ nodeAssert(!this.isReverseSynchronization, "synchronizations with processChanges already detect element deletes, don't call detectElementDeletes");
820
+ this.provenanceDb.withPreparedStatement(sql, (stmt) => {
821
+ stmt.bindId("scopeId", this.targetScopeElementId);
822
+ stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
823
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
824
+ // ExternalSourceAspect.Identifier is of type string
825
+ const aspectIdentifier = stmt.getValue(0).getString();
826
+ if (!core_bentley_1.Id64.isValidId64(aspectIdentifier)) {
827
+ continue;
828
+ }
829
+ const targetElemId = stmt.getValue(1).getId();
830
+ const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
831
+ entityReference: `e${aspectIdentifier}`,
832
+ });
833
+ if (wasDeletedInSource)
834
+ this.importer.deleteElement(targetElemId);
439
835
  }
440
836
  });
441
837
  }
@@ -443,7 +839,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
443
839
  * @deprecated in 3.x, this no longer has any effect except emitting a warning
444
840
  */
445
841
  skipElement(_sourceElement) {
446
- core_bentley_1.Logger.logWarning(loggerCategory, `Tried to defer/skip an element, which is no longer necessary`);
842
+ core_bentley_1.Logger.logWarning(loggerCategory, "Tried to defer/skip an element, which is no longer necessary");
447
843
  }
448
844
  /** Transform the specified sourceElement into ElementProps for the target iModel.
449
845
  * @param sourceElement The Element from the source iModel to transform.
@@ -467,19 +863,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
467
863
  * @param targetElementId The Element from the target iModel to compare against.
468
864
  * @note A subclass can override this method to provide custom change detection behavior.
469
865
  */
470
- hasElementChanged(sourceElement, targetElementId) {
471
- const sourceAspects = this.targetDb.elements.getAspects(targetElementId, core_backend_1.ExternalSourceAspect.classFullName);
472
- for (const sourceAspect of sourceAspects) {
473
- if (sourceAspect.scope === undefined) // if the scope was lost, we can't correlate so assume it changed
474
- return true;
475
- if (sourceAspect.identifier === sourceElement.id &&
476
- sourceAspect.scope.id === this.targetScopeElementId &&
477
- sourceAspect.kind === core_backend_1.ExternalSourceAspect.Kind.Element) {
478
- const lastModifiedTime = sourceElement.iModel.elements.queryLastModifiedTime(sourceElement.id);
479
- return lastModifiedTime !== sourceAspect.version;
480
- }
481
- }
482
- return true;
866
+ hasElementChanged(sourceElement, _targetElementId) {
867
+ if (this._sourceChangeDataState === "no-changes")
868
+ return false;
869
+ if (this._sourceChangeDataState === "unconnected")
870
+ return true;
871
+ nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
872
+ nodeAssert(this._hasElementChangedCache !== undefined, "has element changed cache should be initialized by now");
873
+ return this._hasElementChangedCache.has(sourceElement.id);
483
874
  }
484
875
  static transformCallbackFor(transformer, entity) {
485
876
  if (entity instanceof core_backend_1.Element)
@@ -505,8 +896,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
505
896
  const updateEntity = EntityUnifier_1.EntityUnifier.updaterFor(this.targetDb, sourceEntity);
506
897
  const targetProps = onEntityTransform.call(this, sourceEntity);
507
898
  if (sourceEntity instanceof core_backend_1.Relationship) {
508
- targetProps.sourceId = this.context.findTargetElementId(sourceEntity.sourceId);
509
- targetProps.targetId = this.context.findTargetElementId(sourceEntity.targetId);
899
+ targetProps.sourceId =
900
+ this.context.findTargetElementId(sourceEntity.sourceId);
901
+ targetProps.targetId =
902
+ this.context.findTargetElementId(sourceEntity.targetId);
510
903
  }
511
904
  updateEntity({ ...targetProps, id: core_backend_1.EntityReferences.toId64(targetId) });
512
905
  this._partiallyCommittedEntities.delete(sourceEntity);
@@ -522,11 +915,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
522
915
  for (const referenceId of entity.getReferenceConcreteIds()) {
523
916
  // TODO: probably need to rename from 'id' to 'ref' so these names aren't so ambiguous
524
917
  const referenceIdInTarget = this.context.findTargetEntityId(referenceId);
525
- const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) || this._skippedEntities.has(referenceId);
918
+ const alreadyProcessed = core_backend_1.EntityReferences.isValid(referenceIdInTarget) ||
919
+ this._skippedEntities.has(referenceId);
526
920
  if (alreadyProcessed)
527
921
  continue;
528
922
  core_bentley_1.Logger.logTrace(loggerCategory, `Deferring resolution of reference '${referenceId}' of element '${entity.id}'`);
529
- const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: referenceId });
923
+ const referencedExistsInSource = EntityUnifier_1.EntityUnifier.exists(this.sourceDb, {
924
+ entityReference: referenceId,
925
+ });
530
926
  if (!referencedExistsInSource) {
531
927
  core_bentley_1.Logger.logWarning(loggerCategory, `Source ${EntityUnifier_1.EntityUnifier.getReadableType(entity)} (${entity.id}) has a dangling reference to (${referenceId})`);
532
928
  switch (this._options.danglingReferencesBehavior) {
@@ -536,7 +932,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
536
932
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, [
537
933
  `Found a reference to an element "${referenceId}" that doesn't exist while looking for references of "${entity.id}".`,
538
934
  "This must have been caused by an upstream application that changed the iModel.",
539
- "You can set the IModelTransformerOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
935
+ "You can set the IModelTransformOptions.danglingReferencesBehavior option to 'ignore' to ignore this, but this will leave the iModel",
540
936
  "in a state where downstream consuming applications will need to handle the invalidity themselves. In some cases, writing a custom",
541
937
  "transformer to remove the reference and fix affected elements may be suitable.",
542
938
  ].join("\n"));
@@ -574,7 +970,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
574
970
  /** Override of [IModelExportHandler.shouldExportElement]($transformer) that is called to determine if an element should be exported from the source iModel.
575
971
  * @note Reaching this point means that the element has passed the standard exclusion checks in IModelExporter.
576
972
  */
577
- shouldExportElement(_sourceElement) { return true; }
973
+ shouldExportElement(_sourceElement) {
974
+ return true;
975
+ }
578
976
  onSkipElement(sourceElementId) {
579
977
  if (this.context.findTargetElementId(sourceElementId) !== core_bentley_1.Id64.invalid) {
580
978
  // element already has provenance
@@ -605,7 +1003,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
605
1003
  const referenceType = elemClass.requiredReferenceKeyTypeMap[referenceKey];
606
1004
  // For now we just consider all required references to be elements (as they are in biscore), and do not support
607
1005
  // entities that refuse to be inserted without a different kind of entity (e.g. aspect or relationship) first being inserted
608
- (0, core_bentley_1.assert)(referenceType === core_common_1.ConcreteEntityTypes.Element || referenceType === core_common_1.ConcreteEntityTypes.Model);
1006
+ (0, core_bentley_1.assert)(referenceType === core_common_1.ConcreteEntityTypes.Element ||
1007
+ referenceType === core_common_1.ConcreteEntityTypes.Model);
609
1008
  return mapId64(idContainer, (id) => {
610
1009
  if (id === core_bentley_1.Id64.invalid || id === core_common_1.IModel.rootSubjectId)
611
1010
  return undefined; // not allowed to directly export the root subject
@@ -614,13 +1013,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
614
1013
  // This is relied upon by the TemplateModelCloner
615
1014
  // TODO: extract this out to only be in the TemplateModelCloner
616
1015
  const asDefinitionElem = this.sourceDb.elements.tryGetElement(id, core_backend_1.DefinitionElement);
617
- if (asDefinitionElem && !(asDefinitionElem instanceof core_backend_1.RecipeDefinitionElement)) {
1016
+ if (asDefinitionElem &&
1017
+ !(asDefinitionElem instanceof core_backend_1.RecipeDefinitionElement)) {
618
1018
  this.context.remapElement(id, id);
619
1019
  }
620
1020
  }
621
1021
  return id;
622
- })
623
- .filter((sourceReferenceId) => {
1022
+ }).filter((sourceReferenceId) => {
624
1023
  if (sourceReferenceId === undefined)
625
1024
  return false;
626
1025
  const referenceInTargetId = this.context.findTargetElementId(sourceReferenceId);
@@ -648,7 +1047,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
648
1047
  const isSubModeled = dbHasModel(this.sourceDb, elementId);
649
1048
  const idOfElemInTarget = this.context.findTargetElementId(elementId);
650
1049
  const isElemInTarget = core_bentley_1.Id64.invalid !== idOfElemInTarget;
651
- const needsModelImport = isSubModeled && (!isElemInTarget || !dbHasModel(this.targetDb, idOfElemInTarget));
1050
+ const needsModelImport = isSubModeled &&
1051
+ (!isElemInTarget || !dbHasModel(this.targetDb, idOfElemInTarget));
652
1052
  return { needsElemImport: !isElemInTarget, needsModelImport };
653
1053
  }
654
1054
  /** Override of [IModelExportHandler.onExportElement]($transformer) that imports an element into the target iModel when it is exported from the source iModel.
@@ -663,57 +1063,80 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
663
1063
  }
664
1064
  else if (this._options.wasSourceIModelCopiedToTarget) {
665
1065
  targetElementId = sourceElement.id;
666
- targetElementProps = this.targetDb.elements.getElementProps(targetElementId);
1066
+ targetElementProps =
1067
+ this.targetDb.elements.getElementProps(targetElementId);
667
1068
  }
668
1069
  else {
669
1070
  targetElementId = this.context.findTargetElementId(sourceElement.id);
670
1071
  targetElementProps = this.onTransformElement(sourceElement);
671
1072
  }
1073
+ // if an existing remapping was not yet found, check by FederationGuid
1074
+ if (this.context.isBetweenIModels &&
1075
+ !core_bentley_1.Id64.isValid(targetElementId) &&
1076
+ sourceElement.federationGuid !== undefined) {
1077
+ targetElementId =
1078
+ this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
1079
+ if (core_bentley_1.Id64.isValid(targetElementId))
1080
+ this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found
1081
+ }
672
1082
  // 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)
673
- if (!core_bentley_1.Id64.isValidId64(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
1083
+ if (!core_bentley_1.Id64.isValidId64(targetElementId) &&
1084
+ core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
674
1085
  // respond the same way to undefined code value as the @see Code class, but don't use that class because it trims
675
1086
  // whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
676
1087
  targetElementProps.code.value = targetElementProps.code.value ?? "";
677
- targetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
678
- if (undefined !== targetElementId) {
679
- const targetElement = this.targetDb.elements.getElement(targetElementId);
680
- if (targetElement.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
1088
+ const maybeTargetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
1089
+ if (undefined !== maybeTargetElementId) {
1090
+ const maybeTargetElem = this.targetDb.elements.getElement(maybeTargetElementId);
1091
+ if (maybeTargetElem.classFullName === targetElementProps.classFullName) {
1092
+ // ensure code remapping doesn't change the target class
1093
+ targetElementId = maybeTargetElementId;
681
1094
  this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
682
1095
  }
683
1096
  else {
684
- targetElementId = undefined;
685
1097
  targetElementProps.code = core_common_1.Code.createEmpty(); // clear out invalid code
686
1098
  }
687
1099
  }
688
1100
  }
689
- if (undefined !== targetElementId && core_bentley_1.Id64.isValidId64(targetElementId)) {
690
- // compare LastMod of sourceElement to ExternalSourceAspect of targetElement to see there are changes to import
691
- if (!this.hasElementChanged(sourceElement, targetElementId)) {
692
- return;
693
- }
694
- }
1101
+ if (core_bentley_1.Id64.isValid(targetElementId) &&
1102
+ !this.hasElementChanged(sourceElement, targetElementId))
1103
+ return;
695
1104
  this.collectUnmappedReferences(sourceElement);
696
- // TODO: untangle targetElementId state...
697
- if (targetElementId === core_bentley_1.Id64.invalid)
698
- targetElementId = undefined;
699
- targetElementProps.id = targetElementId; // targetElementId will be valid (indicating update) or undefined (indicating insert)
1105
+ // targetElementId will be valid (indicating update) or undefined (indicating insert)
1106
+ targetElementProps.id = core_bentley_1.Id64.isValid(targetElementId)
1107
+ ? targetElementId
1108
+ : undefined;
700
1109
  if (!this._options.wasSourceIModelCopiedToTarget) {
701
1110
  this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
702
1111
  }
703
1112
  this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
704
1113
  // now that we've mapped this elem we can fix unmapped references to it
705
1114
  this.resolvePendingReferences(sourceElement);
1115
+ // the transformer does not currently 'split' or 'join' any elements, therefore, it does not
1116
+ // insert external source aspects because federation guids are sufficient for this.
1117
+ // Other transformer subclasses must insert the appropriate aspect (as provided by a TBD API)
1118
+ // when splitting/joining elements
1119
+ // physical consolidation is an example of a 'joining' transform
1120
+ // FIXME: verify at finalization time that we don't lose provenance on new elements
1121
+ // FIXME: make public and improve `initElementProvenance` API for usage by consolidators
706
1122
  if (!this._options.noProvenance) {
707
- const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
708
- let aspectId = this.queryExternalSourceAspectId(aspectProps);
709
- if (aspectId === undefined) {
710
- aspectId = this.provenanceDb.elements.insertAspect(aspectProps);
711
- }
712
- else {
713
- this.provenanceDb.elements.updateAspect(aspectProps);
1123
+ let provenance = this._options.forceExternalSourceAspectProvenance ||
1124
+ this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
1125
+ ? undefined
1126
+ : sourceElement.federationGuid;
1127
+ if (!provenance) {
1128
+ const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
1129
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps);
1130
+ if (foundEsaProps === undefined)
1131
+ aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1132
+ else {
1133
+ // Since initElementProvenance sets a property 'version' on the aspectProps that we wish to persist in the provenanceDb, only grab the id from the foundEsaProps.
1134
+ aspectProps.id = foundEsaProps.aspectId;
1135
+ this.provenanceDb.elements.updateAspect(aspectProps);
1136
+ }
1137
+ provenance = aspectProps;
714
1138
  }
715
- aspectProps.id = aspectId;
716
- this.markLastProvenance(aspectProps, { isRelationship: false });
1139
+ this.markLastProvenance(provenance, { isRelationship: false });
717
1140
  }
718
1141
  }
719
1142
  resolvePendingReferences(entity) {
@@ -739,10 +1162,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
739
1162
  * This override calls [[onTransformModel]] and then [IModelImporter.importModel]($transformer) to update the target iModel.
740
1163
  */
741
1164
  onExportModel(sourceModel) {
742
- if (core_common_1.IModel.repositoryModelId === sourceModel.id) {
1165
+ if (this._options.skipPropagateChangesToRootElements &&
1166
+ core_common_1.IModel.repositoryModelId === sourceModel.id)
743
1167
  return; // The RepositoryModel should not be directly imported
744
- }
745
1168
  const targetModeledElementId = this.context.findTargetElementId(sourceModel.id);
1169
+ // there can only be one repositoryModel per database, so ignore the repo model on remapped subjects
1170
+ const isRemappedRootSubject = sourceModel.id === core_common_1.IModel.repositoryModelId &&
1171
+ targetModeledElementId != sourceModel.id;
1172
+ if (isRemappedRootSubject)
1173
+ return;
746
1174
  const targetModelProps = this.onTransformModel(sourceModel, targetModeledElementId);
747
1175
  this.importer.importModel(targetModelProps);
748
1176
  this.resolvePendingReferences(sourceModel);
@@ -751,7 +1179,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
751
1179
  onDeleteModel(sourceModelId) {
752
1180
  // It is possible and apparently occasionally sensical to delete a model without deleting its underlying element.
753
1181
  // - If only the model is deleted, [[initFromExternalSourceAspects]] will have already remapped the underlying element since it still exists.
754
- // - If both were deleted, [[remapDeletedSourceElements]] will find and remap the deleted element making this operation valid
1182
+ // - If both were deleted, [[remapDeletedSourceEntities]] will find and remap the deleted element making this operation valid
755
1183
  const targetModelId = this.context.findTargetElementId(sourceModelId);
756
1184
  if (!core_bentley_1.Id64.isValidId64(targetModelId))
757
1185
  return;
@@ -764,9 +1192,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
764
1192
  stmt.bindId(1, targetModelId);
765
1193
  const val = stmt.step();
766
1194
  switch (val) {
767
- case core_bentley_1.DbResult.BE_SQLITE_ROW: return true;
768
- case core_bentley_1.DbResult.BE_SQLITE_DONE: return false;
769
- default: (0, core_bentley_1.assert)(false, `unexpected db result: '${stmt}'`);
1195
+ case core_bentley_1.DbResult.BE_SQLITE_ROW:
1196
+ return true;
1197
+ case core_bentley_1.DbResult.BE_SQLITE_DONE:
1198
+ return false;
1199
+ default:
1200
+ (0, core_bentley_1.assert)(false, `unexpected db result: '${stmt}'`);
770
1201
  }
771
1202
  });
772
1203
  if (isDefinitionPartition) {
@@ -780,7 +1211,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
780
1211
  this.importer.deleteModel(targetModelId);
781
1212
  }
782
1213
  catch (error) {
783
- const isDeletionProhibitedErr = error instanceof core_common_1.IModelError && (error.errorNumber === core_bentley_1.IModelStatus.DeletionProhibited || error.errorNumber === core_bentley_1.IModelStatus.ForeignKeyConstraint);
1214
+ const isDeletionProhibitedErr = error instanceof core_common_1.IModelError &&
1215
+ (error.errorNumber === core_bentley_1.IModelStatus.DeletionProhibited ||
1216
+ error.errorNumber === core_bentley_1.IModelStatus.ForeignKeyConstraint);
784
1217
  if (!isDeletionProhibitedErr)
785
1218
  throw error;
786
1219
  // Transformer tries to delete models before it deletes elements. Definition models cannot be deleted unless all of their modeled elements are deleted first.
@@ -791,7 +1224,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
791
1224
  }
792
1225
  /** Schedule modeled partition deletion */
793
1226
  scheduleModeledPartitionDeletion(sourceModelId) {
794
- const deletedElements = this.exporter.sourceDbChanges?.element.deleteIds;
1227
+ const deletedElements = this.exporter.sourceDbChanges?.element
1228
+ .deleteIds;
795
1229
  if (!deletedElements.has(sourceModelId)) {
796
1230
  deletedElements.add(sourceModelId);
797
1231
  }
@@ -857,7 +1291,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
857
1291
  onTransformModel(sourceModel, targetModeledElementId) {
858
1292
  const targetModelProps = sourceModel.toJSON();
859
1293
  // don't directly edit deep object since toJSON performs a shallow clone
860
- targetModelProps.modeledElement = { ...targetModelProps.modeledElement, id: targetModeledElementId };
1294
+ targetModelProps.modeledElement = {
1295
+ ...targetModelProps.modeledElement,
1296
+ id: targetModeledElementId,
1297
+ };
861
1298
  targetModelProps.id = targetModeledElementId;
862
1299
  targetModelProps.parentModel = this.context.findTargetElementId(targetModelProps.parentModel);
863
1300
  return targetModelProps;
@@ -866,25 +1303,131 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
866
1303
  * @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
867
1304
  */
868
1305
  async processDeferredElements(_numRetries = 3) { }
869
- finalizeTransformation() {
1306
+ /** called at the end ([[finalizeTransformation]]) of a transformation,
1307
+ * updates the target scope element to say that transformation up through the
1308
+ * source's changeset has been performed. Also stores all changesets that occurred
1309
+ * during the transformation as "pending synchronization changeset indices" @see TargetScopeProvenanceJsonProps
1310
+ *
1311
+ * You generally should not call this function yourself and use [[processChanges]] instead.
1312
+ * It is public for unsupported use cases of custom synchronization transforms.
1313
+ * @note if you are not running processChanges in this transformation, this will fail
1314
+ * without setting the `force` option to `true`
1315
+ */
1316
+ updateSynchronizationVersion({ force = false } = {}) {
1317
+ const notForcedAndHasNoChangesAndIsntProvenanceInit = !force &&
1318
+ this._sourceChangeDataState !== "has-changes" &&
1319
+ !this._isProvenanceInitTransform;
1320
+ if (notForcedAndHasNoChangesAndIsntProvenanceInit)
1321
+ return;
1322
+ nodeAssert(this._targetScopeProvenanceProps);
1323
+ const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
1324
+ const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
1325
+ if (this._isProvenanceInitTransform) {
1326
+ this._targetScopeProvenanceProps.version = sourceVersion;
1327
+ this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1328
+ targetVersion;
1329
+ }
1330
+ else if (this.isReverseSynchronization) {
1331
+ const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
1332
+ core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
1333
+ this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion =
1334
+ sourceVersion;
1335
+ }
1336
+ else if (!this.isReverseSynchronization) {
1337
+ core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
1338
+ this._targetScopeProvenanceProps.version = sourceVersion;
1339
+ }
1340
+ if (this._isSynchronization ||
1341
+ (this._startingChangesetIndices && this._isProvenanceInitTransform)) {
1342
+ nodeAssert(this.targetDb.changeset.index !== undefined &&
1343
+ this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1344
+ const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
1345
+ core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1346
+ core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1347
+ const [syncChangesetsToClear, syncChangesetsToUpdate] = this
1348
+ .isReverseSynchronization
1349
+ ? [
1350
+ jsonProps.pendingReverseSyncChangesetIndices,
1351
+ jsonProps.pendingSyncChangesetIndices,
1352
+ ]
1353
+ : [
1354
+ jsonProps.pendingSyncChangesetIndices,
1355
+ jsonProps.pendingReverseSyncChangesetIndices,
1356
+ ];
1357
+ for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
1358
+ syncChangesetsToUpdate.push(i);
1359
+ // FIXME: add test to synchronize an iModel that is not at the tip, since then clearning syncChangesets is
1360
+ // probably wrong, and we should filter it instead
1361
+ syncChangesetsToClear.length = 0;
1362
+ // if reverse sync then we may have received provenance changes which should be marked as sync changes
1363
+ if (this.isReverseSynchronization) {
1364
+ nodeAssert(this.sourceDb.changeset.index !== undefined, "changeset didn't exist");
1365
+ for (let i = this._startingChangesetIndices.source + 1; i <= this.sourceDb.changeset.index + 1; i++)
1366
+ jsonProps.pendingReverseSyncChangesetIndices.push(i);
1367
+ }
1368
+ core_bentley_1.Logger.logTrace(loggerCategory, `new pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1369
+ core_bentley_1.Logger.logTrace(loggerCategory, `new pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1370
+ }
1371
+ this.provenanceDb.elements.updateAspect({
1372
+ ...this._targetScopeProvenanceProps,
1373
+ jsonProperties: JSON.stringify(this._targetScopeProvenanceProps.jsonProperties),
1374
+ });
1375
+ }
1376
+ // FIXME<MIKE>: is this necessary when manually using low level transform APIs? (document if so)
1377
+ async finalizeTransformation(options) {
1378
+ this.importer.finalize();
1379
+ this.updateSynchronizationVersion();
870
1380
  if (this._partiallyCommittedEntities.size > 0) {
871
- core_bentley_1.Logger.logWarning(loggerCategory, [
1381
+ const message = [
872
1382
  "The following elements were never fully resolved:",
873
1383
  [...this._partiallyCommittedEntities.keys()].join(","),
874
1384
  "This indicates that either some references were excluded from the transformation",
875
1385
  "or the source has dangling references.",
876
- ].join("\n"));
1386
+ ].join("\n");
1387
+ if (this._options.danglingReferencesBehavior === "reject")
1388
+ throw new Error(message);
1389
+ core_bentley_1.Logger.logWarning(loggerCategory, message);
877
1390
  for (const partiallyCommittedElem of this._partiallyCommittedEntities.values()) {
878
1391
  partiallyCommittedElem.forceComplete();
879
1392
  }
880
1393
  }
1394
+ // TODO: ignore if we remove change cache usage
1395
+ if (!this._options.noDetachChangeCache) {
1396
+ if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
1397
+ core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
1398
+ }
881
1399
  // this internal is guaranteed stable for just transformer usage
882
1400
  /* eslint-disable @itwin/no-internal */
883
- if ("codeValueBehavior" in this.sourceDb) {
1401
+ if (("codeValueBehavior" in this.sourceDb)) {
884
1402
  this.sourceDb.codeValueBehavior = "trim-unicode-whitespace";
885
1403
  this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
886
1404
  }
887
1405
  /* eslint-enable @itwin/no-internal */
1406
+ const defaultSaveTargetChanges = () => this.targetDb.saveChanges();
1407
+ await (options?.saveTargetChanges ?? defaultSaveTargetChanges)(this);
1408
+ if (this.isReverseSynchronization)
1409
+ this.sourceDb.saveChanges();
1410
+ const description = `${this._isProvenanceInitTransform
1411
+ ? options?.provenanceInitTransformChangesetDescription ??
1412
+ `initialized branch provenance with master iModel: ${this.sourceDb.iModelId}`
1413
+ : this.isForwardSynchronization
1414
+ ? options?.forwardSyncBranchChangesetDescription ??
1415
+ `Forward sync of iModel: ${this.sourceDb.iModelId}`
1416
+ : options?.reverseSyncMasterChangesetDescription ??
1417
+ `Reverse sync of iModel: ${this.sourceDb.iModelId}`}`;
1418
+ if (this.targetDb.isBriefcaseDb()) {
1419
+ // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1420
+ await this.targetDb.pushChanges({
1421
+ description,
1422
+ });
1423
+ }
1424
+ if (this.isReverseSynchronization && this.sourceDb.isBriefcaseDb()) {
1425
+ // This relies on authorizationClient on iModelHost being defined, otherwise this will fail
1426
+ await this.sourceDb.pushChanges({
1427
+ description: options?.reverseSyncBranchChangesetDescription ??
1428
+ `Update provenance in response to a reverse sync to iModel: ${this.targetDb.iModelId}`,
1429
+ });
1430
+ }
888
1431
  }
889
1432
  /** Imports all relationships that subclass from the specified base class.
890
1433
  * @param baseRelClassFullName The specified base relationship class.
@@ -897,64 +1440,98 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
897
1440
  /** Override of [IModelExportHandler.shouldExportRelationship]($transformer) that is called to determine if a [Relationship]($backend) should be exported.
898
1441
  * @note Reaching this point means that the relationship has passed the standard exclusion checks in [IModelExporter]($transformer).
899
1442
  */
900
- shouldExportRelationship(_sourceRelationship) { return true; }
1443
+ shouldExportRelationship(_sourceRelationship) {
1444
+ return true;
1445
+ }
901
1446
  /** Override of [IModelExportHandler.onExportRelationship]($transformer) that imports a relationship into the target iModel when it is exported from the source iModel.
902
1447
  * This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
903
1448
  */
904
1449
  onExportRelationship(sourceRelationship) {
1450
+ const sourceFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.sourceId);
1451
+ const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
905
1452
  const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
906
1453
  const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
907
- if (!this._options.noProvenance && core_bentley_1.Id64.isValidId64(targetRelationshipInstanceId)) {
908
- const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
909
- if (undefined === aspectProps.id) {
910
- aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1454
+ if (!this._options.noProvenance &&
1455
+ core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
1456
+ let provenance = !this._options.forceExternalSourceAspectProvenance
1457
+ ? sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`
1458
+ : undefined;
1459
+ if (!provenance) {
1460
+ const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
1461
+ const foundEsaProps = IModelTransformer.queryScopeExternalSourceAspect(this.provenanceDb, aspectProps);
1462
+ // 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).
1463
+ if (undefined === foundEsaProps) {
1464
+ aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1465
+ }
1466
+ provenance = aspectProps;
911
1467
  }
912
- (0, core_bentley_1.assert)(aspectProps.id !== undefined);
913
- this.markLastProvenance(aspectProps, { isRelationship: true });
1468
+ this.markLastProvenance(provenance, { isRelationship: true });
914
1469
  }
915
1470
  }
916
1471
  /** Override of [IModelExportHandler.onDeleteRelationship]($transformer) that is called when [IModelExporter]($transformer) detects that a [Relationship]($backend) has been deleted from the source iModel.
917
1472
  * This override propagates the delete to the target iModel via [IModelImporter.deleteRelationship]($transformer).
918
1473
  */
919
1474
  onDeleteRelationship(sourceRelInstanceId) {
920
- const sql = `SELECT ECInstanceId,JsonProperties FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect` +
921
- ` WHERE aspect.Scope.Id=:scopeId AND aspect.Kind=:kind AND aspect.Identifier=:identifier LIMIT 1`;
922
- this.targetDb.withPreparedStatement(sql, (statement) => {
923
- statement.bindId("scopeId", this.targetScopeElementId);
924
- statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
925
- statement.bindString("identifier", sourceRelInstanceId);
926
- if (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
927
- const json = JSON.parse(statement.getValue(1).getString());
928
- if (undefined !== json.targetRelInstanceId) {
929
- const targetRelationship = this.targetDb.relationships.tryGetInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
930
- if (targetRelationship) {
931
- this.importer.deleteRelationship(targetRelationship.toJSON());
932
- }
933
- this.targetDb.elements.deleteAspect(statement.getValue(0).getId());
934
- }
1475
+ nodeAssert(this._deletedSourceRelationshipData, "should be defined at initialization by now");
1476
+ const deletedRelData = this._deletedSourceRelationshipData.get(sourceRelInstanceId);
1477
+ if (!deletedRelData) {
1478
+ // this can occur if both the source and target deleted it
1479
+ core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
1480
+ return;
1481
+ }
1482
+ const relArg = deletedRelData.relId ??
1483
+ {
1484
+ sourceId: deletedRelData.sourceIdInTarget,
1485
+ targetId: deletedRelData.targetIdInTarget,
1486
+ };
1487
+ // FIXME: make importer.deleteRelationship not need full props
1488
+ const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
1489
+ if (targetRelationship) {
1490
+ this.importer.deleteRelationship(targetRelationship.toJSON());
1491
+ }
1492
+ if (deletedRelData.provenanceAspectId) {
1493
+ try {
1494
+ this.provenanceDb.elements.deleteAspect(deletedRelData.provenanceAspectId);
935
1495
  }
936
- });
1496
+ catch (error) {
1497
+ // This aspect may no longer exist if it was deleted at some other point during the transformation. This is fine.
1498
+ if (error.errorNumber === core_bentley_1.IModelStatus.NotFound)
1499
+ return;
1500
+ throw error;
1501
+ }
1502
+ }
937
1503
  }
938
1504
  /** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
1505
+ * @deprecated in 1.x. Don't use this anymore
939
1506
  * @see processChanges
940
1507
  * @note This method is called from [[processAll]] and is not needed by [[processChanges]], so it only needs to be called directly when processing a subset of an iModel.
941
1508
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
942
1509
  */
943
1510
  async detectRelationshipDeletes() {
944
- if (this._options.isReverseSynchronization) {
1511
+ if (this.isReverseSynchronization) {
945
1512
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
946
1513
  }
947
1514
  const aspectDeleteIds = [];
948
- const sql = `SELECT ECInstanceId,Identifier,JsonProperties FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect WHERE aspect.Scope.Id=:scopeId AND aspect.Kind=:kind`;
1515
+ const sql = `
1516
+ SELECT ECInstanceId, Identifier, JsonProperties
1517
+ FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect
1518
+ WHERE aspect.Scope.Id=:scopeId
1519
+ AND aspect.Kind=:kind
1520
+ `;
949
1521
  await this.targetDb.withPreparedStatement(sql, async (statement) => {
950
1522
  statement.bindId("scopeId", this.targetScopeElementId);
951
1523
  statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
952
1524
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
953
1525
  const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
954
- if (undefined === this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
1526
+ if (undefined ===
1527
+ this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
1528
+ // this function exists only to support some in-imodel transformations, which must
1529
+ // use the old (external source aspect) provenance method anyway so we don't need to support
1530
+ // new provenance
955
1531
  const json = JSON.parse(statement.getValue(2).getString());
956
- if (undefined !== json.targetRelInstanceId) {
957
- const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
1532
+ const targetRelInstanceId = json.targetRelInstanceId ?? json.provenanceRelInstanceId;
1533
+ if (targetRelInstanceId) {
1534
+ const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, targetRelInstanceId);
958
1535
  this.importer.deleteRelationship(targetRelationship.toJSON());
959
1536
  }
960
1537
  aspectDeleteIds.push(statement.getValue(0).getId());
@@ -973,9 +1550,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
973
1550
  const targetRelationshipProps = sourceRelationship.toJSON();
974
1551
  targetRelationshipProps.sourceId = this.context.findTargetElementId(sourceRelationship.sourceId);
975
1552
  targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
1553
+ // TODO: move to cloneRelationship in IModelCloneContext
976
1554
  sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
977
- if ((core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType) && ("Id" === propertyMetaData.extendedType)) {
978
- targetRelationshipProps[propertyName] = this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
1555
+ if (core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType &&
1556
+ "Id" === propertyMetaData.extendedType) {
1557
+ targetRelationshipProps[propertyName] =
1558
+ this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
979
1559
  }
980
1560
  });
981
1561
  return targetRelationshipProps;
@@ -1007,8 +1587,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1007
1587
  sourceAspects.forEach((a) => this.collectUnmappedReferences(a));
1008
1588
  // const targetAspectsToImport = targetAspectPropsArray.filter((targetAspect, i) => hasEntityChanged(sourceAspects[i], targetAspect));
1009
1589
  const targetIds = this.importer.importElementMultiAspects(targetAspectPropsArray, (a) => {
1010
- const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect && a.scope?.id === this.targetScopeElementId;
1011
- return !this._options.includeSourceProvenance || !isExternalSourceAspectFromTransformer;
1590
+ const isExternalSourceAspectFromTransformer = a instanceof core_backend_1.ExternalSourceAspect &&
1591
+ a.scope?.id === this.targetScopeElementId;
1592
+ return (!this._options.includeSourceProvenance ||
1593
+ !isExternalSourceAspectFromTransformer);
1012
1594
  });
1013
1595
  for (let i = 0; i < targetIds.length; ++i) {
1014
1596
  this.context.remapElementAspect(sourceAspects[i].id, targetIds[i]);
@@ -1047,9 +1629,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1047
1629
  let schemaFileName = schema.name + ext;
1048
1630
  // many file systems have a max file-name/path-segment size of 255, so we workaround that on all systems
1049
1631
  const systemMaxPathSegmentSize = 255;
1050
- // windows usually has a limit for the total path length of 260
1051
- const windowsMaxPathLimit = 260;
1052
- if (schemaFileName.length > systemMaxPathSegmentSize || path.join(this._schemaExportDir, schemaFileName).length >= windowsMaxPathLimit) {
1632
+ if (schemaFileName.length > systemMaxPathSegmentSize) {
1053
1633
  // this name should be well under 255 bytes
1054
1634
  // ( 100 + (Number.MAX_SAFE_INTEGER.toString().length = 16) + (ext.length = 13) ) = 129 which is less than 255
1055
1635
  // You'd have to be past 2**53-1 (Number.MAX_SAFE_INTEGER) long named schemas in order to hit decimal formatting,
@@ -1089,7 +1669,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1089
1669
  const maybeLongNameResolvingSchemaCtx = this._longNamedSchemasMap.size > 0
1090
1670
  ? this._makeLongNameResolvingSchemaCtx()
1091
1671
  : undefined;
1092
- return await this.targetDb.importSchemas(schemaFullPaths, { ecSchemaXmlContext: maybeLongNameResolvingSchemaCtx });
1672
+ return await this.targetDb.importSchemas(schemaFullPaths, {
1673
+ ecSchemaXmlContext: maybeLongNameResolvingSchemaCtx,
1674
+ });
1093
1675
  }
1094
1676
  finally {
1095
1677
  core_backend_1.IModelJsFs.removeSync(this._schemaExportDir);
@@ -1097,8 +1679,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1097
1679
  }
1098
1680
  }
1099
1681
  /** Cause all fonts to be exported from the source iModel and imported into the target iModel.
1100
- * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1101
- */
1682
+ * @note This method is called from [[processChanges]] and [[processAll]], so it only needs to be called directly when processing a subset of an iModel.
1683
+ */
1102
1684
  async processFonts() {
1103
1685
  // we do not need to initialize for this since no entities are exported
1104
1686
  await this.initialize();
@@ -1125,7 +1707,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1125
1707
  /** Override of [IModelExportHandler.shouldExportCodeSpec]($transformer) that is called to determine if a CodeSpec should be exported from the source iModel.
1126
1708
  * @note Reaching this point means that the CodeSpec has passed the standard exclusion checks in [IModelExporter]($transformer).
1127
1709
  */
1128
- shouldExportCodeSpec(_sourceCodeSpec) { return true; }
1710
+ shouldExportCodeSpec(_sourceCodeSpec) {
1711
+ return true;
1712
+ }
1129
1713
  /** Override of [IModelExportHandler.onExportCodeSpec]($transformer) that imports a CodeSpec into the target iModel when it is exported from the source iModel. */
1130
1714
  onExportCodeSpec(sourceCodeSpec) {
1131
1715
  this.context.importCodeSpec(sourceCodeSpec.id);
@@ -1141,52 +1725,331 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1141
1725
  return this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1142
1726
  }
1143
1727
  /**
1144
- * Initialize prerequisites of processing, you must initialize with an [[InitFromExternalSourceAspectsArgs]] if you
1145
- * are intending process changes, but prefer using [[processChanges]]
1146
- * Called by all `process*` functions implicitly.
1728
+ * Initialize prerequisites of processing, you must initialize with an [[InitOptions]] if you
1729
+ * are intending to process changes, but prefer using [[processChanges]] explicitly since it calls this.
1730
+ * @note Called by all `process*` functions implicitly.
1147
1731
  * Overriders must call `super.initialize()` first
1148
1732
  */
1149
1733
  async initialize(args) {
1150
1734
  if (this._initialized)
1151
1735
  return;
1736
+ await this._tryInitChangesetData(args);
1152
1737
  await this.context.initialize();
1153
- // eslint-disable-next-line deprecation/deprecation
1154
- await this.initFromExternalSourceAspects(args);
1738
+ // need exporter initialized to do remapdeletedsourceentities.
1739
+ await this.exporter.initialize(this.getExportInitOpts(args ?? {}));
1740
+ // 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).
1741
+ await this.processChangesets();
1155
1742
  this._initialized = true;
1156
1743
  }
1744
+ /**
1745
+ * Reads all the changeset files in the private member of the transformer: _csFileProps and does two things with these changesets.
1746
+ * Finds the corresponding target entity for any deleted source entities and remaps the sourceId to the targetId.
1747
+ * Populates this._hasElementChangedCache with a set of elementIds that have been updated or inserted into the database.
1748
+ * This function returns early if csFileProps is undefined or is of length 0.
1749
+ * @returns void
1750
+ */
1751
+ async processChangesets() {
1752
+ this.forEachTrackedElement((sourceElementId, targetElementId) => {
1753
+ this.context.remapElement(sourceElementId, targetElementId);
1754
+ });
1755
+ if (this._csFileProps === undefined || this._csFileProps.length === 0)
1756
+ return;
1757
+ const hasElementChangedCache = new Set();
1758
+ const relationshipECClassIdsToSkip = new Set();
1759
+ for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementDrivesElement)")) {
1760
+ relationshipECClassIdsToSkip.add(row.ECInstanceId);
1761
+ }
1762
+ const relationshipECClassIds = new Set();
1763
+ for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementRefersToElements)")) {
1764
+ relationshipECClassIds.add(row.ECInstanceId);
1765
+ }
1766
+ // For later use when processing deletes.
1767
+ const alreadyImportedElementInserts = new Set();
1768
+ const alreadyImportedModelInserts = new Set();
1769
+ this.exporter.sourceDbChanges?.element.insertIds.forEach((insertedSourceElementId) => {
1770
+ const targetElementId = this.context.findTargetElementId(insertedSourceElementId);
1771
+ if (core_bentley_1.Id64.isValid(targetElementId))
1772
+ alreadyImportedElementInserts.add(targetElementId);
1773
+ });
1774
+ this.exporter.sourceDbChanges?.model.insertIds.forEach((insertedSourceModelId) => {
1775
+ const targetModelId = this.context.findTargetElementId(insertedSourceModelId);
1776
+ if (core_bentley_1.Id64.isValid(targetModelId))
1777
+ alreadyImportedModelInserts.add(targetModelId);
1778
+ });
1779
+ this._deletedSourceRelationshipData = new Map();
1780
+ for (const csFile of this._csFileProps) {
1781
+ const csReader = core_backend_1.SqliteChangesetReader.openFile({
1782
+ fileName: csFile.pathname,
1783
+ db: this.sourceDb,
1784
+ disableSchemaCheck: true,
1785
+ });
1786
+ const csAdaptor = new core_backend_1.ChangesetECAdaptor(csReader);
1787
+ const ecChangeUnifier = new core_backend_1.PartialECChangeUnifier();
1788
+ while (csAdaptor.step()) {
1789
+ ecChangeUnifier.appendFrom(csAdaptor);
1790
+ }
1791
+ const changes = [...ecChangeUnifier.instances];
1792
+ /** a map of element ids to this transformation scope's ESA data for that element, in case the ESA is deleted in the target */
1793
+ const elemIdToScopeEsa = new Map();
1794
+ for (const change of changes) {
1795
+ if (change.ECClassId !== undefined &&
1796
+ relationshipECClassIdsToSkip.has(change.ECClassId))
1797
+ continue;
1798
+ const changeType = change.$meta?.op;
1799
+ if (changeType === "Deleted" &&
1800
+ change?.$meta?.classFullName === core_backend_1.ExternalSourceAspect.classFullName &&
1801
+ change.Scope.Id === this.targetScopeElementId) {
1802
+ elemIdToScopeEsa.set(change.Element.Id, change);
1803
+ }
1804
+ else if (changeType === "Inserted" || changeType === "Updated")
1805
+ hasElementChangedCache.add(change.ECInstanceId);
1806
+ }
1807
+ // Loop to process deletes.
1808
+ for (const change of changes) {
1809
+ const changeType = change.$meta?.op;
1810
+ const ecClassId = change.ECClassId ?? change.$meta?.fallbackClassId;
1811
+ if (ecClassId === undefined)
1812
+ throw new Error(`ECClassId was not found for id: ${change.ECInstanceId}! Table is : ${change?.$meta?.tables}`);
1813
+ if (changeType === undefined)
1814
+ throw new Error(`ChangeType was undefined for id: ${change.ECInstanceId}.`);
1815
+ if (changeType !== "Deleted" ||
1816
+ relationshipECClassIdsToSkip.has(ecClassId))
1817
+ continue;
1818
+ this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
1819
+ }
1820
+ csReader.close();
1821
+ }
1822
+ this._hasElementChangedCache = hasElementChangedCache;
1823
+ return;
1824
+ }
1825
+ /**
1826
+ * Helper function for processChangesets. Remaps the id of element deleted found in the 'change' to an element in the targetDb.
1827
+ * @param change the change to process, must be of changeType "Deleted"
1828
+ * @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.
1829
+ * 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.
1830
+ * @param isRelationship is relationship or not
1831
+ * @param alreadyImportedElementInserts used to handle entity recreation and not delete already handled element inserts.
1832
+ * @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
1833
+ * @returns void
1834
+ */
1835
+ processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1836
+ // we need a connected iModel with changes to remap elements with deletions
1837
+ const notConnectedModel = this.sourceDb.iTwinId === undefined;
1838
+ const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1839
+ if (notConnectedModel || noChanges)
1840
+ return;
1841
+ // optimization: if we have provenance, use it to avoid more querying later
1842
+ // eventually when itwin.js supports attaching a second iModelDb in JS,
1843
+ // this won't have to be a conditional part of the query, and we can always have it by attaching
1844
+ const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
1845
+ const instId = change.ECInstanceId;
1846
+ if (!isRelationship) {
1847
+ const sourceElemFedGuid = change.FederationGuid;
1848
+ let identifierValue;
1849
+ if (queryCanAccessProvenance) {
1850
+ const aspects = this.sourceDb.elements.getAspects(instId, core_backend_1.ExternalSourceAspect.classFullName);
1851
+ for (const aspect of aspects) {
1852
+ // look for aspect where the ecInstanceId = the aspect.element.id
1853
+ if (aspect.element.id === instId &&
1854
+ aspect.scope.id === this.targetScopeElementId)
1855
+ identifierValue = aspect.identifier;
1856
+ }
1857
+ // Think I need to query the esas given the instId.. not sure what db to do it on though.. soruce or target.. or provenance?
1858
+ // I need to know the id of the element dpeneding on which db its stored in.
1859
+ }
1860
+ if (queryCanAccessProvenance && !identifierValue) {
1861
+ if (mapOfDeletedElemIdToScopeEsas.get(instId) !== undefined)
1862
+ identifierValue =
1863
+ mapOfDeletedElemIdToScopeEsas.get(instId).Identifier;
1864
+ }
1865
+ const targetId = (queryCanAccessProvenance && identifierValue) ||
1866
+ // maybe batching these queries would perform better but we should
1867
+ // try to attach the second db and query both together anyway
1868
+ (sourceElemFedGuid &&
1869
+ this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid)) ||
1870
+ // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
1871
+ this._queryProvenanceForElement(instId);
1872
+ // since we are processing one changeset at a time, we can see local source deletes
1873
+ // of entities that were never synced and can be safely ignored
1874
+ const deletionNotInTarget = !targetId;
1875
+ if (deletionNotInTarget)
1876
+ return;
1877
+ this.context.remapElement(instId, targetId);
1878
+ // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
1879
+ // In such case an entity update will be triggered and we no longer need to delete the entity.
1880
+ if (alreadyImportedElementInserts.has(targetId)) {
1881
+ this.exporter.sourceDbChanges?.element.deleteIds.delete(instId);
1882
+ }
1883
+ if (alreadyImportedModelInserts.has(targetId)) {
1884
+ this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
1885
+ }
1886
+ }
1887
+ else {
1888
+ // is deleted relationship
1889
+ const classFullName = change.$meta?.classFullName;
1890
+ const sourceIdOfRelationshipInSource = change.SourceECInstanceId;
1891
+ const targetIdOfRelationshipInSource = change.TargetECInstanceId;
1892
+ const [sourceIdInTarget, targetIdInTarget] = [
1893
+ sourceIdOfRelationshipInSource,
1894
+ targetIdOfRelationshipInSource,
1895
+ ].map((id) => {
1896
+ let element;
1897
+ try {
1898
+ element = this.sourceDb.elements.getElement(id);
1899
+ }
1900
+ catch (err) {
1901
+ return undefined;
1902
+ }
1903
+ const fedGuid = element.federationGuid;
1904
+ let identifierValue;
1905
+ if (queryCanAccessProvenance) {
1906
+ const aspects = this.sourceDb.elements.getAspects(id, core_backend_1.ExternalSourceAspect.classFullName);
1907
+ for (const aspect of aspects) {
1908
+ if (aspect.element.id === id &&
1909
+ aspect.scope.id === this.targetScopeElementId)
1910
+ identifierValue = aspect.identifier;
1911
+ }
1912
+ if (identifierValue === undefined) {
1913
+ if (mapOfDeletedElemIdToScopeEsas.get(id) !== undefined)
1914
+ identifierValue =
1915
+ mapOfDeletedElemIdToScopeEsas.get(id).Identifier;
1916
+ }
1917
+ }
1918
+ return ((queryCanAccessProvenance && identifierValue) ||
1919
+ // maybe batching these queries would perform better but we should
1920
+ // try to attach the second db and query both together anyway
1921
+ (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
1922
+ });
1923
+ if (sourceIdInTarget && targetIdInTarget) {
1924
+ this._deletedSourceRelationshipData.set(instId, {
1925
+ classFullName: classFullName ?? "",
1926
+ sourceIdInTarget,
1927
+ targetIdInTarget,
1928
+ });
1929
+ }
1930
+ else {
1931
+ // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
1932
+ const relProvenance = this._queryProvenanceForRelationship(instId, {
1933
+ classFullName: classFullName ?? "",
1934
+ sourceId: sourceIdOfRelationshipInSource,
1935
+ targetId: targetIdOfRelationshipInSource,
1936
+ });
1937
+ if (relProvenance && relProvenance.relationshipId)
1938
+ this._deletedSourceRelationshipData.set(instId, {
1939
+ classFullName: classFullName ?? "",
1940
+ relId: relProvenance.relationshipId,
1941
+ provenanceAspectId: relProvenance.aspectId,
1942
+ });
1943
+ }
1944
+ }
1945
+ }
1946
+ async _tryInitChangesetData(args) {
1947
+ if (!args ||
1948
+ this.sourceDb.iTwinId === undefined ||
1949
+ this.sourceDb.changeset.index === undefined) {
1950
+ this._sourceChangeDataState = "unconnected";
1951
+ return;
1952
+ }
1953
+ const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1954
+ if (noChanges) {
1955
+ this._sourceChangeDataState = "no-changes";
1956
+ this._csFileProps = [];
1957
+ return;
1958
+ }
1959
+ // NOTE: that we do NOT download the changesummary for the last transformed version, we want
1960
+ // to ignore those already processed changes
1961
+ const startChangesetIndexOrId = args.startChangeset?.index ??
1962
+ args.startChangeset?.id ??
1963
+ this._synchronizationVersion.index + 1;
1964
+ const endChangesetId = this.sourceDb.changeset.id;
1965
+ const [startChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetIndexOrId, endChangesetId].map(async (indexOrId) => typeof indexOrId === "number"
1966
+ ? indexOrId
1967
+ : core_backend_1.IModelHost.hubAccess
1968
+ .queryChangeset({
1969
+ iModelId: this.sourceDb.iModelId,
1970
+ // eslint-disable-next-line deprecation/deprecation
1971
+ changeset: { id: indexOrId },
1972
+ accessToken: args.accessToken,
1973
+ })
1974
+ .then((changeset) => changeset.index)));
1975
+ const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
1976
+ if (!this._options.ignoreMissingChangesetsInSynchronizations &&
1977
+ startChangesetIndex !== this._synchronizationVersion.index + 1 &&
1978
+ this._synchronizationVersion.index !== -1) {
1979
+ throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},` +
1980
+ " startChangesetId should be" +
1981
+ " exactly the first changeset *after* the previous synchronization to not miss data." +
1982
+ ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}` +
1983
+ ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'` +
1984
+ ` which is changeset #${this._synchronizationVersion.index}. The transformer expected` +
1985
+ ` #${this._synchronizationVersion.index + 1}.`);
1986
+ }
1987
+ nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
1988
+ const changesetsToSkip = this.isReverseSynchronization
1989
+ ? this._targetScopeProvenanceProps.jsonProperties
1990
+ .pendingReverseSyncChangesetIndices
1991
+ : this._targetScopeProvenanceProps.jsonProperties
1992
+ .pendingSyncChangesetIndices;
1993
+ core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
1994
+ this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
1995
+ core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
1996
+ const csFileProps = [];
1997
+ for (const [first, end] of this._changesetRanges) {
1998
+ // 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.
1999
+ const fileProps = await core_backend_1.IModelHost.hubAccess.downloadChangesets({
2000
+ iModelId: this.sourceDb.iModelId,
2001
+ targetDir: core_backend_1.BriefcaseManager.getChangeSetsPath(this.sourceDb.iModelId),
2002
+ range: { first, end },
2003
+ });
2004
+ csFileProps.push(...fileProps);
2005
+ }
2006
+ this._csFileProps = csFileProps;
2007
+ this._sourceChangeDataState = "has-changes";
2008
+ }
1157
2009
  /** Export everything from the source iModel and import the transformed entities into the target iModel.
1158
- * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
1159
- */
1160
- async processAll() {
1161
- core_bentley_1.Logger.logTrace(loggerCategory, "processAll()");
2010
+ * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
2011
+ */
2012
+ async processAll(options) {
1162
2013
  this.logSettings();
1163
- this.validateScopeProvenance();
2014
+ this.initScopeProvenance();
1164
2015
  await this.initialize();
1165
2016
  await this.exporter.exportCodeSpecs();
1166
2017
  await this.exporter.exportFonts();
1167
- // The RepositoryModel and root Subject of the target iModel should not be transformed.
1168
- await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
1169
- 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
1170
- await this.exporter.exportSubModels(core_common_1.IModel.repositoryModelId); // start below the RepositoryModel
2018
+ if (this._options.skipPropagateChangesToRootElements) {
2019
+ // FIXME<NICK>: This option in exportAll was a maybe.
2020
+ // The RepositoryModel and root Subject of the target iModel should not be transformed.
2021
+ await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
2022
+ 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
2023
+ await this.exporter.exportSubModels(core_common_1.IModel.repositoryModelId); // start below the RepositoryModel
2024
+ }
2025
+ else {
2026
+ await this.exporter.exportModel(core_common_1.IModel.repositoryModelId);
2027
+ }
1171
2028
  await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
1172
2029
  await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
1173
2030
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1174
- if (this.shouldDetectDeletes()) {
2031
+ if (this._options.forceExternalSourceAspectProvenance &&
2032
+ this.shouldDetectDeletes()) {
1175
2033
  await this.detectElementDeletes();
1176
2034
  await this.detectRelationshipDeletes();
1177
2035
  }
1178
2036
  if (this._options.optimizeGeometry)
1179
2037
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
1180
2038
  this.importer.computeProjectExtents();
1181
- this.finalizeTransformation();
2039
+ await this.finalizeTransformation(options);
1182
2040
  }
1183
2041
  markLastProvenance(sourceAspect, { isRelationship = false }) {
1184
- this._lastProvenanceEntityInfo = {
1185
- entityId: sourceAspect.element.id,
1186
- aspectId: sourceAspect.id,
1187
- aspectVersion: sourceAspect.version ?? "",
1188
- aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
1189
- };
2042
+ this._lastProvenanceEntityInfo =
2043
+ typeof sourceAspect === "string"
2044
+ ? sourceAspect
2045
+ : {
2046
+ entityId: sourceAspect.element.id,
2047
+ aspectId: sourceAspect.id,
2048
+ aspectVersion: sourceAspect.version ?? "",
2049
+ aspectKind: isRelationship
2050
+ ? core_backend_1.ExternalSourceAspect.Kind.Relationship
2051
+ : core_backend_1.ExternalSourceAspect.Kind.Element,
2052
+ };
1190
2053
  }
1191
2054
  /**
1192
2055
  * Load the state of the active transformation from an open SQLiteDb
@@ -1198,17 +2061,35 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1198
2061
  const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
1199
2062
  if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
1200
2063
  throw Error("expected row when getting lastProvenanceEntityId from target state table");
1201
- return {
1202
- entityId: stmt.getValueString(0),
1203
- aspectId: stmt.getValueString(1),
1204
- aspectVersion: stmt.getValueString(2),
1205
- aspectKind: stmt.getValueString(3),
1206
- };
2064
+ const entityId = stmt.getValueString(0);
2065
+ const isGuidOrGuidPair = entityId.includes("-");
2066
+ return isGuidOrGuidPair
2067
+ ? entityId
2068
+ : {
2069
+ entityId,
2070
+ aspectId: stmt.getValueString(1),
2071
+ aspectVersion: stmt.getValueString(2),
2072
+ aspectKind: stmt.getValueString(3),
2073
+ };
1207
2074
  });
1208
- const targetHasCorrectLastProvenance =
1209
- // ignore provenance check if it's null since we can't bind those ids
1210
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
2075
+ /*
2076
+ // TODO: maybe save transformer state resumption state based on target changset and require calls
2077
+ // to saveChanges
2078
+ if () {
2079
+ const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
2080
+ const isRelProvenance = targetFedGuid !== undefined;
2081
+ const instanceId = isRelProvenance
2082
+ ? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
2083
+ : "";
2084
+ //const classId =
2085
+ if (isRelProvenance) {
2086
+ }
2087
+ }
2088
+ */
2089
+ const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
2090
+ // ignore provenance check if it's null since we can't bind those ids
1211
2091
  !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
2092
+ !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
1212
2093
  this.provenanceDb.withPreparedStatement(`
1213
2094
  SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
1214
2095
  WHERE Scope.Id=:scopeId
@@ -1249,10 +2130,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1249
2130
  this.context.loadStateFromDb(db);
1250
2131
  this.importer.loadStateFromJson(state.importerState);
1251
2132
  this.exporter.loadStateFromJson(state.exporterState);
2133
+ this._elementsWithExplicitlyTrackedProvenance =
2134
+ core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
1252
2135
  this.loadAdditionalStateJson(state.additionalState);
1253
2136
  }
1254
2137
  /**
1255
- * @deprecated in 0.1.x, this is buggy, and with 1.x it will be equivalently efficient to simply restart the transformation
2138
+ * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
1256
2139
  * from the original changeset
1257
2140
  *
1258
2141
  * Return a new transformer instance with the same remappings state as saved from a previous [[IModelTransformer.saveStateToFile]] call.
@@ -1299,17 +2182,21 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1299
2182
  const jsonState = {
1300
2183
  transformerClass: this.constructor.name,
1301
2184
  options: this._options,
2185
+ explicitlyTrackedElements: core_bentley_1.CompressedId64Set.compressSet(this._elementsWithExplicitlyTrackedProvenance),
1302
2186
  importerState: this.importer.saveStateToJson(),
1303
2187
  exporterState: this.exporter.saveStateToJson(),
1304
2188
  additionalState: this.getAdditionalStateJson(),
1305
2189
  };
1306
2190
  this.context.saveStateToDb(db);
1307
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`CREATE TABLE ${IModelTransformer.jsStateTable} (data TEXT)`))
2191
+ if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
2192
+ db.executeSQL(`CREATE TABLE ${IModelTransformer.jsStateTable} (data TEXT)`))
1308
2193
  throw Error("Failed to create the js state table in the state database");
1309
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`
2194
+ if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
2195
+ db.executeSQL(`
1310
2196
  CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
1311
- -- because we cannot bind the invalid id which we use for our null state, we actually store the id as a hex string
2197
+ -- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
1312
2198
  entityId TEXT,
2199
+ -- the following are only valid if the above entityId is a hex id representation
1313
2200
  aspectId TEXT,
1314
2201
  aspectVersion TEXT,
1315
2202
  aspectKind TEXT
@@ -1323,17 +2210,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1323
2210
  throw Error("Failed to insert options into the state database");
1324
2211
  });
1325
2212
  db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
1326
- stmt.bindString(1, this._lastProvenanceEntityInfo.entityId);
1327
- stmt.bindString(2, this._lastProvenanceEntityInfo.aspectId);
1328
- stmt.bindString(3, this._lastProvenanceEntityInfo.aspectVersion);
1329
- stmt.bindString(4, this._lastProvenanceEntityInfo.aspectKind);
2213
+ const lastProvenanceEntityInfo = this
2214
+ ._lastProvenanceEntityInfo;
2215
+ stmt.bindString(1, lastProvenanceEntityInfo?.entityId ??
2216
+ this._lastProvenanceEntityInfo);
2217
+ stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
2218
+ stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
2219
+ stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
1330
2220
  if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
1331
2221
  throw Error("Failed to insert options into the state database");
1332
2222
  });
1333
2223
  db.saveChanges();
1334
2224
  }
1335
2225
  /**
1336
- * @deprecated in 0.1.x, this is buggy, and with 1.x it will be equivalently efficient to simply restart the transformation
2226
+ * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
1337
2227
  * from the original changeset
1338
2228
  *
1339
2229
  * Save the state of the active transformation to a file path, if a file at the path already exists, it will be overwritten
@@ -1357,28 +2247,62 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1357
2247
  db.closeDb();
1358
2248
  }
1359
2249
  }
1360
- async processChanges(accessTokenOrArgs, startChangesetId) {
1361
- core_bentley_1.Logger.logTrace(loggerCategory, "processChanges()");
2250
+ /** Export changes from the source iModel and import the transformed entities into the target iModel.
2251
+ * Inserts, updates, and deletes are determined by inspecting the changeset(s).
2252
+ * @note the transformer saves and pushes changes when its work is complete.
2253
+ * @note if no startChangesetId or startChangeset option is provided as part of the ProcessChangesOptions, the next unsynchronized changeset
2254
+ * will automatically be determined and used
2255
+ * @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.
2256
+ */
2257
+ async processChanges(options) {
2258
+ this._isSynchronization = true;
2259
+ this.initScopeProvenance();
1362
2260
  this.logSettings();
1363
- this.validateScopeProvenance();
1364
- const options = typeof accessTokenOrArgs === "string"
1365
- ? {
1366
- accessToken: accessTokenOrArgs,
1367
- startChangeset: startChangesetId ? { id: startChangesetId } : this.sourceDb.changeset,
1368
- changedInstanceIds: undefined,
1369
- }
1370
- : accessTokenOrArgs;
1371
2261
  await this.initialize(options);
1372
- await this.exporter.exportChanges(options);
2262
+ // must wait for initialization of synchronization provenance data
2263
+ await this.exporter.exportChanges(this.getExportInitOpts(options));
1373
2264
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1374
- await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
1375
2265
  if (this._options.optimizeGeometry)
1376
2266
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
1377
2267
  this.importer.computeProjectExtents();
1378
- this.finalizeTransformation();
2268
+ await this.finalizeTransformation(options);
2269
+ }
2270
+ /** Changeset data must be initialized in order to build correct changeOptions.
2271
+ * Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
2272
+ */
2273
+ getExportInitOpts(opts) {
2274
+ if (!this._isSynchronization)
2275
+ return {};
2276
+ return {
2277
+ skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
2278
+ accessToken: opts.accessToken,
2279
+ ...(this._csFileProps
2280
+ ? { csFileProps: this._csFileProps }
2281
+ : this._changesetRanges
2282
+ ? { changesetRanges: this._changesetRanges }
2283
+ : opts.startChangeset
2284
+ ? { startChangeset: opts.startChangeset }
2285
+ : {
2286
+ startChangeset: {
2287
+ index: this._synchronizationVersion.index + 1,
2288
+ },
2289
+ }),
2290
+ };
2291
+ }
2292
+ /** Combine an array of source elements into a single target element.
2293
+ * All source and target elements must be created before calling this method.
2294
+ * The "combine" operation is a remap and no properties from the source elements will be exported into the target
2295
+ * and provenance will be explicitly tracked by ExternalSourceAspects
2296
+ */
2297
+ combineElements(sourceElementIds, targetElementId) {
2298
+ for (const elementId of sourceElementIds) {
2299
+ this.context.remapElement(elementId, targetElementId);
2300
+ this._elementsWithExplicitlyTrackedProvenance.add(elementId);
2301
+ }
1379
2302
  }
1380
2303
  }
1381
2304
  exports.IModelTransformer = IModelTransformer;
2305
+ 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.";
1382
2306
  /** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
1383
2307
  IModelTransformer.jsStateTable = "TransformerJsState";
1384
2308
  /** @internal the name of the table where the target state heuristics is serialized in transformer state dumps */
@@ -1407,6 +2331,7 @@ class TemplateModelCloner extends IModelTransformer {
1407
2331
  * @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
1408
2332
  */
1409
2333
  async placeTemplate3d(sourceTemplateModelId, targetModelId, placement) {
2334
+ await this.initialize();
1410
2335
  this.context.remapElement(sourceTemplateModelId, targetModelId);
1411
2336
  this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(placement.origin, placement.angles.toMatrix3d());
1412
2337
  this._sourceIdToTargetIdMap = new Map();
@@ -1427,6 +2352,7 @@ class TemplateModelCloner extends IModelTransformer {
1427
2352
  * @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
1428
2353
  */
1429
2354
  async placeTemplate2d(sourceTemplateModelId, targetModelId, placement) {
2355
+ await this.initialize();
1430
2356
  this.context.remapElement(sourceTemplateModelId, targetModelId);
1431
2357
  this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(core_geometry_1.Point3d.createFrom(placement.origin), placement.rotation);
1432
2358
  this._sourceIdToTargetIdMap = new Map();
@@ -1444,14 +2370,15 @@ class TemplateModelCloner extends IModelTransformer {
1444
2370
  // eslint-disable-next-line deprecation/deprecation
1445
2371
  const referenceIds = sourceElement.getReferenceConcreteIds();
1446
2372
  referenceIds.forEach((referenceId) => {
1447
- // FIXME: consider going through all definition elements at once and remapping them to themselves
2373
+ // TODO: consider going through all definition elements at once and remapping them to themselves
1448
2374
  if (!core_backend_1.EntityReferences.isValid(this.context.findTargetEntityId(referenceId))) {
1449
2375
  if (this.context.isBetweenIModels) {
1450
2376
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, `Remapping for source dependency ${referenceId} not found for target iModel`);
1451
2377
  }
1452
2378
  else {
1453
2379
  const definitionElement = this.sourceDb.elements.tryGetElement(referenceId, core_backend_1.DefinitionElement);
1454
- if (definitionElement && !(definitionElement instanceof core_backend_1.RecipeDefinitionElement)) {
2380
+ if (definitionElement &&
2381
+ !(definitionElement instanceof core_backend_1.RecipeDefinitionElement)) {
1455
2382
  this.context.remapElement(referenceId, referenceId); // when in the same iModel, can use existing DefinitionElements without remapping
1456
2383
  }
1457
2384
  else {
@@ -1463,16 +2390,12 @@ class TemplateModelCloner extends IModelTransformer {
1463
2390
  const targetElementProps = super.onTransformElement(sourceElement);
1464
2391
  targetElementProps.federationGuid = core_bentley_1.Guid.createValue(); // clone from template should create a new federationGuid
1465
2392
  targetElementProps.code = core_common_1.Code.createEmpty(); // clone from template should not maintain codes
1466
- if (sourceElement instanceof core_backend_1.GeometricElement3d) {
1467
- const placement = core_common_1.Placement3d.fromJSON(targetElementProps.placement);
1468
- if (placement.isValid) {
1469
- placement.multiplyTransform(this._transform3d);
1470
- targetElementProps.placement = placement;
1471
- }
1472
- }
1473
- else if (sourceElement instanceof core_backend_1.GeometricElement2d) {
1474
- const placement = core_common_1.Placement2d.fromJSON(targetElementProps.placement);
2393
+ if (sourceElement instanceof core_backend_1.GeometricElement) {
2394
+ const is3d = sourceElement instanceof core_backend_1.GeometricElement3d;
2395
+ const placementClass = is3d ? core_common_1.Placement3d : core_common_1.Placement2d;
2396
+ const placement = placementClass.fromJSON(targetElementProps.placement);
1475
2397
  if (placement.isValid) {
2398
+ nodeAssert(this._transform3d);
1476
2399
  placement.multiplyTransform(this._transform3d);
1477
2400
  targetElementProps.placement = placement;
1478
2401
  }
@@ -1482,4 +2405,17 @@ class TemplateModelCloner extends IModelTransformer {
1482
2405
  }
1483
2406
  }
1484
2407
  exports.TemplateModelCloner = TemplateModelCloner;
2408
+ function queryElemFedGuid(db, elemId) {
2409
+ return db.withPreparedStatement(`
2410
+ SELECT FederationGuid
2411
+ FROM bis.Element
2412
+ WHERE ECInstanceId=?
2413
+ `, (stmt) => {
2414
+ stmt.bindId(1, elemId);
2415
+ (0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
2416
+ const result = stmt.getValue(0).getGuid();
2417
+ (0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_DONE);
2418
+ return result;
2419
+ });
2420
+ }
1485
2421
  //# sourceMappingURL=IModelTransformer.js.map