@itwin/imodel-transformer 0.3.18-fedguidopt.7 → 0.4.1-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 (37) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/lib/cjs/BranchProvenanceInitializer.d.ts +3 -10
  3. package/lib/cjs/BranchProvenanceInitializer.d.ts.map +1 -1
  4. package/lib/cjs/BranchProvenanceInitializer.js +13 -84
  5. package/lib/cjs/BranchProvenanceInitializer.js.map +1 -1
  6. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts +16 -0
  7. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts.map +1 -0
  8. package/lib/cjs/DetachedExportElementAspectsStrategy.js +98 -0
  9. package/lib/cjs/DetachedExportElementAspectsStrategy.js.map +1 -0
  10. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts +19 -0
  11. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts.map +1 -0
  12. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js +43 -0
  13. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js.map +1 -0
  14. package/lib/cjs/ExportElementAspectsStrategy.d.ts +35 -0
  15. package/lib/cjs/ExportElementAspectsStrategy.d.ts.map +1 -0
  16. package/lib/cjs/ExportElementAspectsStrategy.js +48 -0
  17. package/lib/cjs/ExportElementAspectsStrategy.js.map +1 -0
  18. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts +12 -0
  19. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts.map +1 -0
  20. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js +43 -0
  21. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js.map +1 -0
  22. package/lib/cjs/IModelCloneContext.d.ts.map +1 -1
  23. package/lib/cjs/IModelCloneContext.js +2 -0
  24. package/lib/cjs/IModelCloneContext.js.map +1 -1
  25. package/lib/cjs/IModelExporter.d.ts +15 -40
  26. package/lib/cjs/IModelExporter.d.ts.map +1 -1
  27. package/lib/cjs/IModelExporter.js +52 -125
  28. package/lib/cjs/IModelExporter.js.map +1 -1
  29. package/lib/cjs/IModelTransformer.d.ts +29 -161
  30. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  31. package/lib/cjs/IModelTransformer.js +286 -934
  32. package/lib/cjs/IModelTransformer.js.map +1 -1
  33. package/package.json +14 -14
  34. package/lib/cjs/Algo.d.ts +0 -7
  35. package/lib/cjs/Algo.d.ts.map +0 -1
  36. package/lib/cjs/Algo.js +0 -50
  37. package/lib/cjs/Algo.js.map +0 -1
@@ -22,7 +22,6 @@ 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");
26
25
  const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelTransformer;
27
26
  const nullLastProvenanceEntityInfo = {
28
27
  entityId: core_bentley_1.Id64.invalid,
@@ -95,12 +94,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
95
94
  get targetScopeElementId() {
96
95
  return this._options.targetScopeElementId;
97
96
  }
98
- get _isReverseSynchronization() {
99
- return this._isSynchronization && this._options.isReverseSynchronization;
100
- }
101
- get _isForwardSynchronization() {
102
- return this._isSynchronization && !this._options.isReverseSynchronization;
103
- }
104
97
  /** The element classes that are considered to define provenance in the iModel */
105
98
  static get provenanceElementClasses() {
106
99
  return [core_backend_1.FolderLink, core_backend_1.SynchronizationConfigLink, core_backend_1.ExternalSource, core_backend_1.ExternalSourceAttachment];
@@ -119,46 +112,18 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
119
112
  /** map of (unprocessed element, referencing processed element) pairs to the partially committed element that needs the reference resolved
120
113
  * and have some helper methods below for now */
121
114
  this._pendingReferences = new PendingReferenceMap_1.PendingReferenceMap();
122
- /** a set of elements for which source provenance will be explicitly tracked by ExternalSourceAspects */
123
- this._elementsWithExplicitlyTrackedProvenance = new Set();
124
115
  /** map of partially committed entities to their partial commit progress */
125
116
  this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
126
- this._isSynchronization = false;
127
- this._changesetRanges = undefined;
128
117
  /** Set of entity keys which were not exported and don't need to be tracked for pending reference resolution.
129
118
  * @note Currently only tracks elements which were not exported.
130
119
  */
131
120
  this._skippedEntities = new Set();
132
- // FIXME: add test using this
133
- /**
134
- * Previously the transformer would insert provenance always pointing to the "target" relationship.
135
- * It should (and now by default does) instead insert provenance pointing to the provenanceSource
136
- * SEE: https://github.com/iTwin/imodel-transformer/issues/54
137
- * This exists only to facilitate testing that the transformer can handle the older, flawed method
138
- */
139
- this._forceOldRelationshipProvenanceMethod = false;
140
- /** NOTE: the json properties must be converted to string before insertion */
141
- this._targetScopeProvenanceProps = undefined;
142
- /**
143
- * Index of the changeset that the transformer was at when the transformation begins (was constructed).
144
- * Used to determine at the end which changesets were part of a synchronization.
145
- */
146
- this._startingChangesetIndices = undefined;
147
- this._cachedSynchronizationVersion = undefined;
148
- this._targetClassNameToClassIdCache = new Map();
149
- // if undefined, it can be initialized by calling [[this._cacheSourceChanges]]
150
- this._hasElementChangedCache = undefined;
151
- this._deletedSourceRelationshipData = undefined;
152
121
  this._yieldManager = new core_bentley_1.YieldManager();
153
122
  /** The directory where schemas will be exported, a random temporary directory */
154
123
  this._schemaExportDir = path.join(core_backend_1.KnownLocations.tmpdir, core_bentley_1.Guid.createValue());
155
124
  this._longNamedSchemasMap = new Map();
156
125
  /** state to prevent reinitialization, @see [[initialize]] */
157
126
  this._initialized = false;
158
- /** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
159
- this._changeSummaryIds = undefined;
160
- this._sourceChangeDataState = "uninited";
161
- /** previous provenance, either a federation guid, a `${sourceFedGuid}/${targetFedGuid}` pair, or required aspect props */
162
127
  this._lastProvenanceEntityInfo = nullLastProvenanceEntityInfo;
163
128
  // initialize IModelTransformOptions
164
129
  this._options = {
@@ -206,13 +171,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
206
171
  this.targetDb = this.importer.targetDb;
207
172
  // create the IModelCloneContext, it must be initialized later
208
173
  this.context = new IModelCloneContext_1.IModelCloneContext(this.sourceDb, this.targetDb);
209
- if (this.sourceDb.isBriefcase && this.targetDb.isBriefcase) {
210
- nodeAssert(this.sourceDb.changeset.index !== undefined && this.targetDb.changeset.index !== undefined, "database has no changeset index");
211
- this._startingChangesetIndices = {
212
- target: this.targetDb.changeset.index,
213
- source: this.sourceDb.changeset.index,
214
- };
174
+ // this internal is guaranteed stable for just transformer usage
175
+ /* eslint-disable @itwin/no-internal */
176
+ if ("codeValueBehavior" in this.sourceDb) {
177
+ this.sourceDb.codeValueBehavior = "exact";
178
+ this.targetDb.codeValueBehavior = "exact";
215
179
  }
180
+ /* eslint-enable @itwin/no-internal */
216
181
  }
217
182
  /** Dispose any native resources associated with this IModelTransformer. */
218
183
  dispose() {
@@ -250,6 +215,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
250
215
  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
251
216
  static initElementProvenanceOptions(sourceElementId, targetElementId, args) {
252
217
  const elementId = args.isReverseSynchronization ? sourceElementId : targetElementId;
218
+ const version = args.isReverseSynchronization
219
+ ? args.targetDb.elements.queryLastModifiedTime(targetElementId)
220
+ : args.sourceDb.elements.queryLastModifiedTime(sourceElementId);
253
221
  const aspectIdentifier = args.isReverseSynchronization ? targetElementId : sourceElementId;
254
222
  const aspectProps = {
255
223
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
@@ -257,39 +225,17 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
257
225
  scope: { id: args.targetScopeElementId },
258
226
  identifier: aspectIdentifier,
259
227
  kind: core_backend_1.ExternalSourceAspect.Kind.Element,
260
- version: args.sourceDb.elements.queryLastModifiedTime(sourceElementId),
261
- };
262
- return aspectProps;
263
- }
264
- static initRelationshipProvenanceOptions(sourceRelInstanceId, targetRelInstanceId, args) {
265
- const provenanceDb = args.isReverseSynchronization ? args.sourceDb : args.targetDb;
266
- const aspectIdentifier = args.isReverseSynchronization ? targetRelInstanceId : sourceRelInstanceId;
267
- const provenanceRelInstanceId = args.isReverseSynchronization ? sourceRelInstanceId : targetRelInstanceId;
268
- const elementId = provenanceDb.withPreparedStatement("SELECT SourceECInstanceId FROM bis.ElementRefersToElements WHERE ECInstanceId=?", (stmt) => {
269
- stmt.bindId(1, provenanceRelInstanceId);
270
- nodeAssert(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
271
- return stmt.getValue(0).getId();
272
- });
273
- const jsonProperties = args.forceOldRelationshipProvenanceMethod
274
- ? { targetRelInstanceId }
275
- : { provenanceRelInstanceId };
276
- const aspectProps = {
277
- classFullName: core_backend_1.ExternalSourceAspect.classFullName,
278
- element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
279
- scope: { id: args.targetScopeElementId },
280
- identifier: aspectIdentifier,
281
- kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
282
- jsonProperties: JSON.stringify(jsonProperties),
228
+ version,
283
229
  };
284
230
  return aspectProps;
285
231
  }
286
232
  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
287
233
  initElementProvenance(sourceElementId, targetElementId) {
288
234
  return IModelTransformer.initElementProvenanceOptions(sourceElementId, targetElementId, {
289
- // FIXME: deprecate isReverseSync option and instead detect from targetScopeElement provenance
290
235
  isReverseSynchronization: !!this._options.isReverseSynchronization,
291
236
  targetScopeElementId: this.targetScopeElementId,
292
237
  sourceDb: this.sourceDb,
238
+ targetDb: this.targetDb,
293
239
  });
294
240
  }
295
241
  /** Create an ExternalSourceAspectProps in a standard way for a Relationship in an iModel --> iModel transformations.
@@ -298,73 +244,32 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
298
244
  * The ECInstanceId of the relationship in the target iModel will be stored in the JsonProperties of the ExternalSourceAspect.
299
245
  */
300
246
  initRelationshipProvenance(sourceRelationship, targetRelInstanceId) {
301
- return IModelTransformer.initRelationshipProvenanceOptions(sourceRelationship.id, targetRelInstanceId, {
302
- sourceDb: this.sourceDb,
303
- targetDb: this.targetDb,
304
- isReverseSynchronization: !!this._options.isReverseSynchronization,
305
- targetScopeElementId: this.targetScopeElementId,
306
- forceOldRelationshipProvenanceMethod: this._forceOldRelationshipProvenanceMethod,
307
- });
308
- }
309
- /** the changeset in the scoping element's source version found for this transformation
310
- * @note: the version depends on whether this is a reverse synchronization or not, as
311
- * it is stored separately for both synchronization directions
312
- * @note: empty string and -1 for changeset and index if it has never been transformed
313
- */
314
- get _synchronizationVersion() {
315
- if (!this._cachedSynchronizationVersion) {
316
- nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps was not set yet");
317
- const version = this._options.isReverseSynchronization
318
- ? this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion
319
- : this._targetScopeProvenanceProps.version;
320
- nodeAssert(version !== undefined, "no version contained in target scope");
321
- const [id, index] = version === ""
322
- ? ["", -1]
323
- : version.split(";");
324
- this._cachedSynchronizationVersion = { index: Number(index), id };
325
- nodeAssert(!Number.isNaN(this._cachedSynchronizationVersion.index), "bad parse: invalid index in version");
326
- }
327
- return this._cachedSynchronizationVersion;
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 = {
251
+ 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;
328
260
  }
329
- /**
330
- * Make sure there are no conflicting other scope-type external source aspects on the *target scope element*,
331
- * If there are none at all, insert one, then this must be a first synchronization.
332
- * @returns the last synced version (changesetId) on the target scope's external source aspect,
333
- * if this was a [BriefcaseDb]($backend)
334
- */
335
- initScopeProvenance() {
261
+ validateScopeProvenance() {
336
262
  const aspectProps = {
337
- id: undefined,
338
- version: undefined,
339
263
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
340
264
  element: { id: this.targetScopeElementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
341
265
  scope: { id: core_common_1.IModel.rootSubjectId },
342
- identifier: this.provenanceSourceDb.iModelId,
266
+ identifier: this._options.isReverseSynchronization ? this.targetDb.iModelId : this.sourceDb.iModelId,
343
267
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
344
- jsonProperties: undefined,
345
268
  };
346
- // FIXME: handle older transformed iModels which do NOT have the version
347
- // or reverseSyncVersion set correctly
348
- const externalSource = this.queryScopeExternalSource(aspectProps, { getJsonProperties: true }); // this query includes "identifier"
349
- aspectProps.id = externalSource.aspectId;
350
- aspectProps.version = externalSource.version;
351
- aspectProps.jsonProperties = externalSource.jsonProperties ? JSON.parse(externalSource.jsonProperties) : {};
269
+ aspectProps.id = this.queryExternalSourceAspectId(aspectProps); // this query includes "identifier"
352
270
  if (undefined === aspectProps.id) {
353
- aspectProps.version = ""; // empty since never before transformed. Will be updated in [[finalizeTransformation]]
354
- aspectProps.jsonProperties = {
355
- pendingReverseSyncChangesetIndices: [],
356
- pendingSyncChangesetIndices: [],
357
- reverseSyncVersion: "", // empty since never before transformed. Will be updated in first reverse sync
358
- };
359
271
  // this query does not include "identifier" to find possible conflicts
360
- const sql = `
361
- SELECT ECInstanceId
362
- FROM ${core_backend_1.ExternalSourceAspect.classFullName}
363
- WHERE Element.Id=:elementId
364
- AND Scope.Id=:scopeId
365
- AND Kind=:kind
366
- LIMIT 1
367
- `;
272
+ const sql = `SELECT ECInstanceId FROM ${core_backend_1.ExternalSourceAspect.classFullName} WHERE Element.Id=:elementId AND Scope.Id=:scopeId AND Kind=:kind LIMIT 1`;
368
273
  const hasConflictingScope = this.provenanceDb.withPreparedStatement(sql, (statement) => {
369
274
  statement.bindId("elementId", aspectProps.element.id);
370
275
  statement.bindId("scopeId", aspectProps.scope.id); // this scope.id can never be invalid, we create it above
@@ -375,128 +280,46 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
375
280
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidId, "Provenance scope conflict");
376
281
  }
377
282
  if (!this._options.noProvenance) {
378
- this.provenanceDb.elements.insertAspect({
379
- ...aspectProps,
380
- jsonProperties: JSON.stringify(aspectProps.jsonProperties),
381
- });
283
+ this.provenanceDb.elements.insertAspect(aspectProps);
382
284
  }
383
285
  }
384
- this._targetScopeProvenanceProps = aspectProps;
385
286
  }
386
- /**
387
- * @returns the id and version of an aspect with the given element, scope, kind, and identifier
388
- * May also return a reverseSyncVersion from json properties if requested
389
- */
390
- queryScopeExternalSource(aspectProps, { getJsonProperties = false } = {}) {
391
- const sql = `
392
- SELECT ECInstanceId, Version
393
- ${getJsonProperties ? ", JsonProperties" : ""}
394
- FROM ${core_backend_1.ExternalSourceAspect.classFullName}
395
- WHERE Element.Id=:elementId
396
- AND Scope.Id=:scopeId
397
- AND Kind=:kind
398
- AND Identifier=:identifier
399
- LIMIT 1
400
- `;
401
- const emptyResult = { aspectId: undefined, version: undefined, jsonProperties: undefined };
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`;
402
289
  return this.provenanceDb.withPreparedStatement(sql, (statement) => {
403
290
  statement.bindId("elementId", aspectProps.element.id);
404
291
  if (aspectProps.scope === undefined)
405
- return emptyResult; // return undefined instead of binding an invalid id
292
+ return undefined; // return undefined instead of binding an invalid id
406
293
  statement.bindId("scopeId", aspectProps.scope.id);
407
294
  statement.bindString("kind", aspectProps.kind);
408
295
  statement.bindString("identifier", aspectProps.identifier);
409
- if (core_bentley_1.DbResult.BE_SQLITE_ROW !== statement.step())
410
- return emptyResult;
411
- const aspectId = statement.getValue(0).getId();
412
- const version = statement.getValue(1).getString();
413
- const jsonProperties = getJsonProperties ? statement.getValue(2).getString() : undefined;
414
- return { aspectId, version, jsonProperties };
296
+ return (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) ? statement.getValue(0).getId() : undefined;
415
297
  });
416
298
  }
417
- /**
418
- * Iterate all matching federation guids and ExternalSourceAspects in the provenance iModel (target unless reverse sync)
419
- * and call a function for each one.
420
- * @note provenance is done by federation guids where possible
421
- * @note this may execute on each element more than once! Only use in cases where that is handled
422
- */
299
+ /** Iterate all matching ExternalSourceAspects in the provenance iModel (target unless reverse sync) and call a function for each one. */
423
300
  static forEachTrackedElement(args) {
424
- if (args.provenanceDb === args.provenanceSourceDb)
425
- return;
426
301
  if (!args.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
427
302
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
428
303
  }
429
- const sourceDb = args.isReverseSynchronization ? args.provenanceDb : args.provenanceSourceDb;
430
- const targetDb = args.isReverseSynchronization ? args.provenanceSourceDb : args.provenanceDb;
431
- // query for provenanceDb
432
- const elementIdByFedGuidQuery = `
433
- SELECT e.ECInstanceId, FederationGuid
434
- FROM bis.Element e
435
- WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements
436
- ORDER BY FederationGuid
437
- `;
438
- // iterate through sorted list of fed guids from both dbs to get the intersection
439
- // NOTE: if we exposed the native attach database support,
440
- // we could get the intersection of fed guids in one query, not sure if it would be faster
441
- // OR we could do a raw sqlite query...
442
- sourceDb.withStatement(elementIdByFedGuidQuery, (sourceStmt) => targetDb.withStatement(elementIdByFedGuidQuery, (targetStmt) => {
443
- if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
444
- return;
445
- let sourceRow = sourceStmt.getRow();
446
- if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
447
- return;
448
- let targetRow = targetStmt.getRow();
449
- // NOTE: these comparisons rely upon the lowercase of the guid,
450
- // and the fact that '0' < '9' < a' < 'f' in ascii/utf8
451
- while (true) {
452
- const currSourceRow = sourceRow, currTargetRow = targetRow;
453
- if (currSourceRow.federationGuid !== undefined
454
- && currTargetRow.federationGuid !== undefined
455
- && currSourceRow.federationGuid === currTargetRow.federationGuid) {
456
- // data flow direction is always sourceDb -> targetDb and it does not depend on where the explicit element provenance is stored
457
- args.fn(sourceRow.id, targetRow.id);
458
- }
459
- if (currTargetRow.federationGuid === undefined
460
- || (currSourceRow.federationGuid !== undefined
461
- && currSourceRow.federationGuid >= currTargetRow.federationGuid)) {
462
- if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
463
- return;
464
- targetRow = targetStmt.getRow();
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
465
313
  }
466
- if (currSourceRow.federationGuid === undefined
467
- || (currTargetRow.federationGuid !== undefined
468
- && currSourceRow.federationGuid <= currTargetRow.federationGuid)) {
469
- if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
470
- return;
471
- sourceRow = sourceStmt.getRow();
314
+ else {
315
+ args.fn(aspectIdentifier, elementId); // provenance coming from the targetDb
472
316
  }
473
317
  }
474
- }));
475
- // query for provenanceDb
476
- const provenanceAspectsQuery = `
477
- SELECT esa.Identifier, Element.Id
478
- FROM bis.ExternalSourceAspect esa
479
- WHERE Scope.Id=:scopeId
480
- AND Kind=:kind
481
- `;
482
- // Technically this will a second time call the function (as documented) on
483
- // victims of the old provenance method that have both fedguids and an inserted aspect.
484
- // But this is a private function with one known caller where that doesn't matter
485
- args.provenanceDb.withPreparedStatement(provenanceAspectsQuery, (stmt) => {
486
- const runFnInDataFlowDirection = (sourceId, targetId) => args.isReverseSynchronization ? args.fn(sourceId, targetId) : args.fn(targetId, sourceId);
487
- stmt.bindId("scopeId", args.targetScopeElementId);
488
- stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
489
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
490
- // ExternalSourceAspect.Identifier is of type string
491
- const aspectIdentifier = stmt.getValue(0).getString();
492
- const elementId = stmt.getValue(1).getId();
493
- runFnInDataFlowDirection(elementId, aspectIdentifier);
494
- }
495
318
  });
496
319
  }
497
320
  forEachTrackedElement(fn) {
498
321
  return IModelTransformer.forEachTrackedElement({
499
- provenanceSourceDb: this.provenanceSourceDb,
322
+ provenanceSourceDb: this._options.isReverseSynchronization ? this.sourceDb : this.targetDb,
500
323
  provenanceDb: this.provenanceDb,
501
324
  targetScopeElementId: this.targetScopeElementId,
502
325
  isReverseSynchronization: !!this._options.isReverseSynchronization,
@@ -514,366 +337,105 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
514
337
  this.context.remapElement(sourceElementId, targetElementId);
515
338
  });
516
339
  if (args)
517
- return this.remapDeletedSourceEntities();
340
+ return this.remapDeletedSourceElements(args);
518
341
  }
519
- /**
520
- * Scan changesets for deleted entities, if in a reverse synchronization, provenance has
521
- * already been deleted, so we must scan for that as well.
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.
522
345
  */
523
- async remapDeletedSourceEntities() {
346
+ async remapDeletedSourceElements(args) {
524
347
  // we need a connected iModel with changes to remap elements with deletions
525
- const notConnectedModel = this.sourceDb.iTwinId === undefined;
526
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
527
- if (notConnectedModel || noChanges)
348
+ if (this.sourceDb.iTwinId === undefined)
528
349
  return;
529
- this._deletedSourceRelationshipData = new Map();
530
- nodeAssert(this._changeSummaryIds, "change summaries should be initialized before we get here");
531
- nodeAssert(this._changeSummaryIds.length > 0, "change summaries should have at least one");
532
- const alreadyImportedElementInserts = new Set();
533
- const alreadyImportedModelInserts = new Set();
534
- this.exporter.sourceDbChanges?.element.insertIds.forEach((insertedSourceElementId) => {
535
- const targetElementId = this.context.findTargetElementId(insertedSourceElementId);
536
- if (core_bentley_1.Id64.isValid(targetElementId))
537
- alreadyImportedElementInserts.add(targetElementId);
538
- });
539
- this.exporter.sourceDbChanges?.model.insertIds.forEach((insertedSourceModelId) => {
540
- const targetModelId = this.context.findTargetElementId(insertedSourceModelId);
541
- if (core_bentley_1.Id64.isValid(targetModelId))
542
- alreadyImportedModelInserts.add(targetModelId);
543
- });
544
- // optimization: if we have provenance, use it to avoid more querying later
545
- // eventually when itwin.js supports attaching a second iModelDb in JS,
546
- // this won't have to be a conditional part of the query, and we can always have it by attaching
547
- const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
548
- const deletedEntitySql = `
549
- SELECT
550
- 1 AS IsElemNotRel,
551
- ic.ChangedInstance.Id AS InstanceId,
552
- NULL AS InstId2, -- need these columns for relationship ends in the unioned query
553
- NULL AS InstId3,
554
- ec.FederationGuid AS FedGuid,
555
- NULL AS FedGuid2,
556
- ic.ChangedInstance.ClassId AS ClassId
557
- ${queryCanAccessProvenance ? `
558
- /*
559
- -- can't coalesce these due to a bug, so do it in JS
560
- , coalesce(
561
- IIF(esa.Scope.Id=:targetScopeElement, esa.Identifier, NULL),
562
- IIF(esac.Scope.Id=:targetScopeElement, esac.Identifier, NULL)
563
- ) AS Identifier1
564
- */
565
- , CASE WHEN esa.Scope.Id = ${this.targetScopeElementId} THEN esa.Identifier ELSE NULL END AS Identifier1A
566
- -- FIXME: using :targetScopeElement parameter in this second potential identifier breaks ecsql
567
- , CASE WHEN esac.Scope.Id = ${this.targetScopeElementId} THEN esac.Identifier ELSE NULL END AS Identifier1B
568
- , NULL AS Identifier2A
569
- , NULL AS Identifier2B
570
- ` : ""}
571
- FROM ecchange.change.InstanceChange ic
572
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') ec
573
- ON ic.ChangedInstance.Id=ec.ECInstanceId
574
- ${queryCanAccessProvenance ? `
575
- LEFT JOIN bis.ExternalSourceAspect esa
576
- ON ec.ECInstanceId=esa.Element.Id
577
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') esac
578
- ON ec.ECInstanceId=esac.Element.Id
579
- ` : ""}
580
- WHERE ic.OpCode=:opDelete
581
- AND ic.Summary.Id=:changeSummaryId
582
- AND ic.ChangedInstance.ClassId IS (BisCore.Element)
583
-
584
- UNION ALL
585
-
586
- SELECT
587
- 0 AS IsElemNotRel,
588
- ic.ChangedInstance.Id AS InstanceId,
589
- coalesce(se.ECInstanceId, sec.ECInstanceId) AS InstId2,
590
- coalesce(te.ECInstanceId, tec.ECInstanceId) AS InstId3,
591
- coalesce(se.FederationGuid, sec.FederationGuid) AS FedGuid1,
592
- coalesce(te.FederationGuid, tec.FederationGuid) AS FedGuid2,
593
- ic.ChangedInstance.ClassId AS ClassId
594
- ${queryCanAccessProvenance ? `
595
- , sesa.Identifier AS Identifier1A
596
- , sesac.Identifier AS Identifier1B
597
- , tesa.Identifier AS Identifier2A
598
- , tesac.Identifier AS Identifier2B
599
- ` : ""}
600
- FROM ecchange.change.InstanceChange ic
601
- LEFT JOIN bis.ElementRefersToElements.Changes(:changeSummaryId, 'BeforeDelete') ertec
602
- ON ic.ChangedInstance.Id=ertec.ECInstanceId
603
- -- FIXME: test a deletion of both an element and a relationship at the same time
604
- LEFT JOIN bis.Element se
605
- ON se.ECInstanceId=ertec.SourceECInstanceId
606
- LEFT JOIN bis.Element te
607
- ON te.ECInstanceId=ertec.TargetECInstanceId
608
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') sec
609
- ON sec.ECInstanceId=ertec.SourceECInstanceId
610
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') tec
611
- ON tec.ECInstanceId=ertec.TargetECInstanceId
612
- ${queryCanAccessProvenance ? `
613
- -- NOTE: need to join on both se/te and sec/tec incase the element was deleted
614
- LEFT JOIN bis.ExternalSourceAspect sesa
615
- ON se.ECInstanceId=sesa.Element.Id -- don't use *esac*.Identifier because it's a string
616
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') sesac
617
- ON sec.ECInstanceId=sesac.Element.Id
618
- LEFT JOIN bis.ExternalSourceAspect tesa
619
- ON te.ECInstanceId=tesa.Element.Id
620
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') tesac
621
- ON tec.ECInstanceId=tesac.Element.Id
622
- ` : ""}
623
- WHERE ic.OpCode=:opDelete
624
- AND ic.Summary.Id=:changeSummaryId
625
- AND ic.ChangedInstance.ClassId IS (BisCore.ElementRefersToElements)
626
- ${queryCanAccessProvenance ? `
627
- AND (sesa.Scope.Id=:targetScopeElement OR sesa.Scope.Id IS NULL)
628
- AND (sesa.Kind='Relationship' OR sesa.Kind IS NULL)
629
- AND (sesac.Scope.Id=:targetScopeElement OR sesac.Scope.Id IS NULL)
630
- AND (sesac.Kind='Relationship' OR sesac.Kind IS NULL)
631
- AND (tesa.Scope.Id=:targetScopeElement OR tesa.Scope.Id IS NULL)
632
- AND (tesa.Kind='Relationship' OR tesa.Kind IS NULL)
633
- AND (tesac.Scope.Id=:targetScopeElement OR tesac.Scope.Id IS NULL)
634
- AND (tesac.Kind='Relationship' OR tesac.Kind IS NULL)
635
- ` : ""}
636
- `;
637
- for (const changeSummaryId of this._changeSummaryIds) {
638
- // FIXME: test deletion in both forward and reverse sync
639
- this.sourceDb.withPreparedStatement(deletedEntitySql, (stmt) => {
640
- stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
641
- if (queryCanAccessProvenance)
642
- stmt.bindId("targetScopeElement", this.targetScopeElementId);
643
- stmt.bindId("changeSummaryId", changeSummaryId);
644
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
645
- const isElemNotRel = stmt.getValue(0).getBoolean();
646
- const instId = stmt.getValue(1).getId();
647
- if (isElemNotRel) {
648
- const sourceElemFedGuid = stmt.getValue(4).getGuid();
649
- // "Identifier" is a string, so null value returns '' which doesn't work with ??, and I don't like ||
650
- let identifierValue;
651
- // identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
652
- if (queryCanAccessProvenance) {
653
- identifierValue = stmt.getValue(7);
654
- if (identifierValue.isNull)
655
- identifierValue = stmt.getValue(8);
656
- }
657
- // TODO: if I could attach the second db, will probably be much faster to get target id
658
- // as part of the whole query rather than with _queryElemIdByFedGuid
659
- const targetId = (queryCanAccessProvenance && identifierValue
660
- && !identifierValue.isNull
661
- && identifierValue.getString())
662
- // maybe batching these queries would perform better but we should
663
- // try to attach the second db and query both together anyway
664
- || (sourceElemFedGuid && this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid))
665
- // FIXME: describe why it's safe to assume nothing has been deleted in provenanceDb
666
- || this._queryProvenanceForElement(instId);
667
- // since we are processing one changeset at a time, we can see local source deletes
668
- // of entities that were never synced and can be safely ignored
669
- const deletionNotInTarget = !targetId;
670
- if (deletionNotInTarget)
671
- continue;
672
- this.context.remapElement(instId, targetId);
673
- // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
674
- // In such case an entity update will be triggered and we no longer need to delete the entity.
675
- if (alreadyImportedElementInserts.has(targetId)) {
676
- this.exporter.sourceDbChanges?.element.deleteIds.delete(instId);
677
- }
678
- if (alreadyImportedModelInserts.has(targetId)) {
679
- this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
680
- }
681
- }
682
- else { // is deleted relationship
683
- const classFullName = stmt.getValue(6).getClassNameForClassId();
684
- const [sourceIdInTarget, targetIdInTarget] = [
685
- // identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
686
- { guidColumn: 4, identifierColumns: { a: 7, b: 8 }, isTarget: false },
687
- { guidColumn: 5, identifierColumns: { a: 9, b: 10 }, isTarget: true },
688
- ].map(({ guidColumn, identifierColumns }) => {
689
- const fedGuid = stmt.getValue(guidColumn).getGuid();
690
- let identifierValue;
691
- // identifier must be coalesced in JS due to an ESCQL bug, so there are multiple columns
692
- if (queryCanAccessProvenance) {
693
- identifierValue = stmt.getValue(identifierColumns.a);
694
- if (identifierValue.isNull)
695
- identifierValue = stmt.getValue(identifierColumns.b);
696
- }
697
- return ((queryCanAccessProvenance && identifierValue
698
- // FIXME: this is really far from idiomatic, try to undo that
699
- && !identifierValue.isNull
700
- && identifierValue.getString())
701
- // maybe batching these queries would perform better but we should
702
- // try to attach the second db and query both together anyway
703
- || (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
704
- });
705
- // since we are processing one changeset at a time, we can see local source deletes
706
- // of entities that were never synced and can be safely ignored
707
- if (sourceIdInTarget && targetIdInTarget) {
708
- this._deletedSourceRelationshipData.set(instId, {
709
- classFullName,
710
- sourceIdInTarget,
711
- targetIdInTarget,
712
- });
713
- }
714
- else {
715
- // FIXME: describe why it's safe to assume nothing has been deleted in provenanceDb
716
- const relProvenance = this._queryProvenanceForRelationship(instId, {
717
- classFullName,
718
- sourceId: stmt.getValue(2).getId(),
719
- targetId: stmt.getValue(3).getId(),
720
- });
721
- if (relProvenance && relProvenance.relationshipId)
722
- this._deletedSourceRelationshipData.set(instId, {
723
- classFullName,
724
- relId: relProvenance.relationshipId,
725
- provenanceAspectId: relProvenance.aspectId,
726
- });
727
- }
728
- }
729
- }
730
- // NEXT: remap sourceId and targetId to target, get provenance there
731
- // NOTE: it is possible during a forward sync for the target to already have deleted
732
- // something that the source deleted, in which case we can safely ignore the gone provenance
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 },
733
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
+ }
734
397
  }
735
- }
736
- _queryProvenanceForElement(entityInProvenanceSourceId) {
737
- return this.provenanceDb.withPreparedStatement(`
738
- SELECT esa.Element.Id
739
- FROM Bis.ExternalSourceAspect esa
740
- WHERE esa.Kind=?
741
- AND esa.Scope.Id=?
742
- AND esa.Identifier=?
743
- `, (stmt) => {
744
- stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Element);
745
- stmt.bindId(2, this.targetScopeElementId);
746
- stmt.bindString(3, entityInProvenanceSourceId);
747
- if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
748
- return stmt.getValue(0).getId();
749
- else
750
- return undefined;
751
- });
752
- }
753
- _queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
754
- return this.provenanceDb.withPreparedStatement(`
755
- SELECT
756
- ECInstanceId,
757
- JSON_EXTRACT(JsonProperties, '$.targetRelInstanceId'),
758
- JSON_EXTRACT(JsonProperties, '$.provenanceRelInstanceId')
759
- FROM Bis.ExternalSourceAspect
760
- WHERE Kind=?
761
- AND Scope.Id=?
762
- AND Identifier=?
763
- `, (stmt) => {
764
- stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Relationship);
765
- stmt.bindId(2, this.targetScopeElementId);
766
- stmt.bindString(3, entityInProvenanceSourceId);
767
- if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
768
- return undefined;
769
- const aspectId = stmt.getValue(0).getId();
770
- const provenanceRelInstIdVal = stmt.getValue(2);
771
- const provenanceRelInstanceId = !provenanceRelInstIdVal.isNull
772
- ? provenanceRelInstIdVal.getString()
773
- : this._queryTargetRelId(sourceRelInfo);
774
- return {
775
- aspectId,
776
- relationshipId: provenanceRelInstanceId,
777
- };
778
- });
779
- }
780
- _queryTargetRelId(sourceRelInfo) {
781
- const targetRelInfo = {
782
- sourceId: this.context.findTargetElementId(sourceRelInfo.sourceId),
783
- targetId: this.context.findTargetElementId(sourceRelInfo.targetId),
784
- };
785
- if (targetRelInfo.sourceId === undefined || targetRelInfo.targetId === undefined)
786
- return undefined; // couldn't find an element, rel is invalid or deleted
787
- return this.targetDb.withPreparedStatement(`
788
- SELECT ECInstanceId
789
- FROM bis.ElementRefersToElements
790
- WHERE SourceECInstanceId=?
791
- AND TargetECInstanceId=?
792
- AND ECClassId=?
793
- `, (stmt) => {
794
- stmt.bindId(1, targetRelInfo.sourceId);
795
- stmt.bindId(2, targetRelInfo.targetId);
796
- stmt.bindId(3, this._targetClassNameToClassId(sourceRelInfo.classFullName));
797
- if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
798
- return undefined;
799
- return stmt.getValue(0).getId();
800
- });
801
- }
802
- _targetClassNameToClassId(classFullName) {
803
- let classId = this._targetClassNameToClassIdCache.get(classFullName);
804
- if (classId === undefined) {
805
- classId = this._getRelClassId(this.targetDb, classFullName);
806
- this._targetClassNameToClassIdCache.set(classFullName, classId);
398
+ finally {
399
+ if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
400
+ core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
807
401
  }
808
- return classId;
809
- }
810
- // NOTE: this doesn't handle remapped element classes,
811
- // but is only used for relationships rn
812
- _getRelClassId(db, classFullName) {
813
- return db.withPreparedStatement(`
814
- SELECT c.ECInstanceId
815
- FROM ECDbMeta.ECClassDef c
816
- JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
817
- WHERE s.Name=? AND c.Name=?
818
- `, (stmt) => {
819
- const [schemaName, className] = classFullName.split(".");
820
- stmt.bindString(1, schemaName);
821
- stmt.bindString(2, className);
822
- if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
823
- return stmt.getValue(0).getId();
824
- (0, core_bentley_1.assert)(false, "relationship was not found");
825
- });
826
- }
827
- _queryElemIdByFedGuid(db, fedGuid) {
828
- return db.withPreparedStatement("SELECT ECInstanceId FROM Bis.Element WHERE FederationGuid=?", (stmt) => {
829
- stmt.bindGuid(1, fedGuid);
830
- if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
831
- return stmt.getValue(0).getId();
832
- else
833
- return undefined;
834
- });
835
402
  }
836
403
  /** Returns `true` if *brute force* delete detections should be run.
837
404
  * @note Not relevant for processChanges when change history is known.
838
405
  */
839
406
  shouldDetectDeletes() {
840
- // FIXME: all synchronizations should mark this as false
841
407
  if (this._isFirstSynchronization)
842
408
  return false; // not necessary the first time since there are no deletes to detect
843
409
  if (this._options.isReverseSynchronization)
844
410
  return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
845
411
  return true;
846
412
  }
847
- /**
848
- * Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
849
- * in the source iModel.
850
- * @deprecated in 0.1.x. This method is only called during [[processAll]] when the option
851
- * [[IModelTransformerOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
852
- * necessary when using [[processChanges]] since changeset information is sufficient.
853
- * @note you do not need to call this directly unless processing a subset of an iModel.
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.
854
416
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
855
417
  */
856
418
  async detectElementDeletes() {
857
- const sql = `
858
- SELECT Identifier, Element.Id
859
- FROM BisCore.ExternalSourceAspect
860
- WHERE Scope.Id=:scopeId
861
- AND Kind=:kind
862
- `;
863
- nodeAssert(!this._options.isReverseSynchronization, "synchronizations with processChagnes already detect element deletes, don't call detectElementDeletes");
864
- this.provenanceDb.withPreparedStatement(sql, (stmt) => {
865
- stmt.bindId("scopeId", this.targetScopeElementId);
866
- stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
867
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
868
- // ExternalSourceAspect.Identifier is of type string
869
- const aspectIdentifier = stmt.getValue(0).getString();
870
- if (!core_bentley_1.Id64.isId64(aspectIdentifier)) {
871
- continue;
872
- }
873
- const targetElemId = stmt.getValue(1).getId();
874
- const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: `e${aspectIdentifier}` });
875
- if (wasDeletedInSource)
876
- this.importer.deleteElement(targetElemId);
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;
877
439
  }
878
440
  });
879
441
  }
@@ -900,53 +462,24 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
900
462
  }
901
463
  return targetElementProps;
902
464
  }
903
- // FIXME: this is a PoC, see if we minimize memory usage
904
- _cacheSourceChanges() {
905
- nodeAssert(this._changeSummaryIds && this._changeSummaryIds.length > 0, "should have changeset data by now");
906
- this._hasElementChangedCache = new Set();
907
- const query = `
908
- SELECT
909
- ic.ChangedInstance.Id AS InstId
910
- FROM ecchange.change.InstanceChange ic
911
- JOIN iModelChange.Changeset imc ON ic.Summary.Id=imc.Summary.Id
912
- -- FIXME: do relationship entities also need this cache optimization?
913
- WHERE ic.ChangedInstance.ClassId IS (BisCore.Element)
914
- AND InVirtualSet(:changeSummaryIds, ic.Summary.Id)
915
- -- ignore deleted, we take care of those in remapDeletedSourceEntities
916
- -- include inserted since inserted code-colliding elements should be considered
917
- -- a change so that the colliding element is exported to the target
918
- AND ic.OpCode<>:opDelete
919
- `;
920
- // there is a single mega-query multi-join+coalescing hack that I used originally to get around
921
- // only being able to run table.Changes() on one changeset at once, but sqlite only supports up to 64
922
- // tables in a join. Need to talk to core about .Changes being able to take a set of changesets
923
- // You can find this version in the `federation-guid-optimization-megaquery` branch
924
- // I wouldn't use it unless we prove via profiling that it speeds things up significantly
925
- // And even then let's first try scanning the raw changesets instead of applying them as these queries
926
- // require
927
- this.sourceDb.withPreparedStatement(query, (stmt) => {
928
- stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
929
- stmt.bindIdSet("changeSummaryIds", this._changeSummaryIds);
930
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
931
- const instId = stmt.getValue(0).getId();
932
- this._hasElementChangedCache.add(instId);
933
- }
934
- });
935
- }
936
465
  /** Returns true if a change within sourceElement is detected.
937
466
  * @param sourceElement The Element from the source iModel
938
467
  * @param targetElementId The Element from the target iModel to compare against.
939
468
  * @note A subclass can override this method to provide custom change detection behavior.
940
469
  */
941
- hasElementChanged(sourceElement, _targetElementId) {
942
- if (this._sourceChangeDataState === "no-changes")
943
- return false;
944
- if (this._sourceChangeDataState === "unconnected")
945
- return true;
946
- nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
947
- if (this._hasElementChangedCache === undefined)
948
- this._cacheSourceChanges();
949
- return this._hasElementChangedCache.has(sourceElement.id);
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;
950
483
  }
951
484
  static transformCallbackFor(transformer, entity) {
952
485
  if (entity instanceof core_backend_1.Element)
@@ -1136,68 +669,51 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1136
669
  targetElementId = this.context.findTargetElementId(sourceElement.id);
1137
670
  targetElementProps = this.onTransformElement(sourceElement);
1138
671
  }
1139
- // if an existing remapping was not yet found, check by FederationGuid
1140
- if (this.context.isBetweenIModels && !core_bentley_1.Id64.isValid(targetElementId) && sourceElement.federationGuid !== undefined) {
1141
- targetElementId = this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
1142
- if (core_bentley_1.Id64.isValid(targetElementId))
1143
- this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found
1144
- }
1145
672
  // 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)
1146
- if (!core_bentley_1.Id64.isValid(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
1147
- // respond the same way to undefined code value as the @see Code class, but don't use that class because is trims
673
+ if (!core_bentley_1.Id64.isValidId64(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
674
+ // respond the same way to undefined code value as the @see Code class, but don't use that class because it trims
1148
675
  // whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
1149
676
  targetElementProps.code.value = targetElementProps.code.value ?? "";
1150
- const maybeTargetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
1151
- if (undefined !== maybeTargetElementId) {
1152
- const maybeTargetElem = this.targetDb.elements.getElement(maybeTargetElementId);
1153
- if (maybeTargetElem.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
1154
- targetElementId = maybeTargetElementId;
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
1155
681
  this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
1156
682
  }
1157
683
  else {
684
+ targetElementId = undefined;
1158
685
  targetElementProps.code = core_common_1.Code.createEmpty(); // clear out invalid code
1159
686
  }
1160
687
  }
1161
688
  }
1162
- if (core_bentley_1.Id64.isValid(targetElementId) && !this.hasElementChanged(sourceElement, targetElementId))
1163
- return;
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
+ }
1164
695
  this.collectUnmappedReferences(sourceElement);
1165
- // targetElementId will be valid (indicating update) or undefined (indicating insert)
1166
- targetElementProps.id
1167
- = core_bentley_1.Id64.isValid(targetElementId)
1168
- ? targetElementId
1169
- : undefined;
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)
1170
700
  if (!this._options.wasSourceIModelCopiedToTarget) {
1171
701
  this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
1172
702
  }
1173
703
  this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
1174
704
  // now that we've mapped this elem we can fix unmapped references to it
1175
705
  this.resolvePendingReferences(sourceElement);
1176
- // the transformer does not currently 'split' or 'join' any elements, therefore, it does not
1177
- // insert external source aspects because federation guids are sufficient for this.
1178
- // Other transformer subclasses must insert the appropriate aspect (as provided by a TBD API)
1179
- // when splitting/joining elements
1180
- // physical consolidation is an example of a 'joining' transform
1181
- // FIXME: document this externally!
1182
- // verify at finalization time that we don't lose provenance on new elements
1183
- // make public and improve `initElementProvenance` API for usage by consolidators
1184
706
  if (!this._options.noProvenance) {
1185
- let provenance = this._options.forceExternalSourceAspectProvenance || this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
1186
- ? undefined
1187
- : sourceElement.federationGuid;
1188
- if (!provenance) {
1189
- const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
1190
- const aspectId = this.queryScopeExternalSource(aspectProps).aspectId;
1191
- if (aspectId === undefined) {
1192
- aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1193
- }
1194
- else {
1195
- aspectProps.id = aspectId;
1196
- this.provenanceDb.elements.updateAspect(aspectProps);
1197
- }
1198
- provenance = aspectProps;
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);
1199
714
  }
1200
- this.markLastProvenance(provenance, { isRelationship: false });
715
+ aspectProps.id = aspectId;
716
+ this.markLastProvenance(aspectProps, { isRelationship: false });
1201
717
  }
1202
718
  }
1203
719
  resolvePendingReferences(entity) {
@@ -1235,11 +751,50 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1235
751
  onDeleteModel(sourceModelId) {
1236
752
  // It is possible and apparently occasionally sensical to delete a model without deleting its underlying element.
1237
753
  // - If only the model is deleted, [[initFromExternalSourceAspects]] will have already remapped the underlying element since it still exists.
1238
- // - If both were deleted, [[remapDeletedSourceEntities]] will find and remap the deleted element making this operation valid
754
+ // - If both were deleted, [[remapDeletedSourceElements]] will find and remap the deleted element making this operation valid
1239
755
  const targetModelId = this.context.findTargetElementId(sourceModelId);
1240
- if (core_bentley_1.Id64.isValidId64(targetModelId)) {
756
+ if (!core_bentley_1.Id64.isValidId64(targetModelId))
757
+ return;
758
+ if (this.exporter.sourceDbChanges?.element.deleteIds.has(sourceModelId)) {
759
+ const isDefinitionPartition = this.targetDb.withPreparedStatement(`
760
+ SELECT 1
761
+ FROM bis.DefinitionPartition
762
+ WHERE ECInstanceId=?
763
+ `, (stmt) => {
764
+ stmt.bindId(1, targetModelId);
765
+ const val = stmt.step();
766
+ 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}'`);
770
+ }
771
+ });
772
+ if (isDefinitionPartition) {
773
+ // Skipping model deletion because model's partition will also be deleted.
774
+ // It expects that model will be present and will fail if it's missing.
775
+ // Model will be deleted when its partition will be deleted.
776
+ return;
777
+ }
778
+ }
779
+ try {
1241
780
  this.importer.deleteModel(targetModelId);
1242
781
  }
782
+ 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);
784
+ if (!isDeletionProhibitedErr)
785
+ throw error;
786
+ // Transformer tries to delete models before it deletes elements. Definition models cannot be deleted unless all of their modeled elements are deleted first.
787
+ // In case a definition model needs to be deleted we need to skip it for now and register its modeled partition for deletion.
788
+ // The `OnDeleteElement` calls `DeleteElementTree` Which deletes the model together with its partition after deleting all of the modeled elements.
789
+ this.scheduleModeledPartitionDeletion(sourceModelId);
790
+ }
791
+ }
792
+ /** Schedule modeled partition deletion */
793
+ scheduleModeledPartitionDeletion(sourceModelId) {
794
+ const deletedElements = this.exporter.sourceDbChanges?.element.deleteIds;
795
+ if (!deletedElements.has(sourceModelId)) {
796
+ deletedElements.add(sourceModelId);
797
+ }
1243
798
  }
1244
799
  /** Cause the model container, contents, and sub-models to be exported from the source iModel and imported into the target iModel.
1245
800
  * @param sourceModeledElementId Import this [Model]($backend) from the source IModelDb.
@@ -1311,68 +866,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1311
866
  * @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
1312
867
  */
1313
868
  async processDeferredElements(_numRetries = 3) { }
1314
- /** called at the end ([[finalizeTransformation]]) of a transformation,
1315
- * updates the target scope element to say that transformation up through the
1316
- * source's changeset has been performed. Also stores all changesets that occurred
1317
- * during the transformation as "pending synchronization changeset indices"
1318
- *
1319
- * You generally should not call this function yourself and use [[processChanges]] instead.
1320
- * It is public for unsupported use cases of custom synchronization transforms.
1321
- * @note if you are not running processChanges in this transformation, this will fail
1322
- * without setting the `force` option to `true`
1323
- */
1324
- updateSynchronizationVersion({ force = false } = {}) {
1325
- if (!force && (this._sourceChangeDataState !== "has-changes" && !this._isFirstSynchronization))
1326
- return;
1327
- nodeAssert(this._targetScopeProvenanceProps);
1328
- const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
1329
- const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
1330
- if (this._isFirstSynchronization) {
1331
- this._targetScopeProvenanceProps.version = sourceVersion;
1332
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = targetVersion;
1333
- }
1334
- else if (this._options.isReverseSynchronization) {
1335
- const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
1336
- core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
1337
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = sourceVersion;
1338
- }
1339
- else if (!this._options.isReverseSynchronization) {
1340
- core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
1341
- this._targetScopeProvenanceProps.version = sourceVersion;
1342
- }
1343
- if (this._isSynchronization) {
1344
- (0, core_bentley_1.assert)(this.targetDb.changeset.index !== undefined && this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1345
- const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
1346
- core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1347
- core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1348
- const [syncChangesetsToClear, syncChangesetsToUpdate] = this._isReverseSynchronization
1349
- ? [jsonProps.pendingReverseSyncChangesetIndices, jsonProps.pendingSyncChangesetIndices]
1350
- : [jsonProps.pendingSyncChangesetIndices, jsonProps.pendingReverseSyncChangesetIndices];
1351
- // NOTE that as documented in [[processChanges]], this assumes that right after
1352
- // transformation finalization, the work will be saved immediately, otherwise we've
1353
- // just marked this changeset as a synchronization to ignore, and the user can add other
1354
- // stuff to it which would break future synchronizations
1355
- // FIXME: force save for the user to prevent that
1356
- for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
1357
- syncChangesetsToUpdate.push(i);
1358
- syncChangesetsToClear.length = 0;
1359
- // if reverse sync then we may have received provenance changes which should be marked as sync changes
1360
- if (this._isReverseSynchronization) {
1361
- nodeAssert(this.sourceDb.changeset.index, "changeset didn't exist");
1362
- for (let i = this._startingChangesetIndices.source + 1; i <= this.sourceDb.changeset.index + 1; i++)
1363
- jsonProps.pendingReverseSyncChangesetIndices.push(i);
1364
- }
1365
- core_bentley_1.Logger.logTrace(loggerCategory, `new pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1366
- core_bentley_1.Logger.logTrace(loggerCategory, `new pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1367
- }
1368
- this.provenanceDb.elements.updateAspect({
1369
- ...this._targetScopeProvenanceProps,
1370
- jsonProperties: JSON.stringify(this._targetScopeProvenanceProps.jsonProperties),
1371
- });
1372
- }
1373
- // FIXME: is this necessary when manually using lowlevel transform APIs?
1374
869
  finalizeTransformation() {
1375
- this.updateSynchronizationVersion();
1376
870
  if (this._partiallyCommittedEntities.size > 0) {
1377
871
  core_bentley_1.Logger.logWarning(loggerCategory, [
1378
872
  "The following elements were never fully resolved:",
@@ -1384,11 +878,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1384
878
  partiallyCommittedElem.forceComplete();
1385
879
  }
1386
880
  }
1387
- // FIXME: make processAll have a try {} finally {} that cleans this up
1388
- if (!this._options.noDetachChangeCache) {
1389
- if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
1390
- core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
881
+ // this internal is guaranteed stable for just transformer usage
882
+ /* eslint-disable @itwin/no-internal */
883
+ if ("codeValueBehavior" in this.sourceDb) {
884
+ this.sourceDb.codeValueBehavior = "trim-unicode-whitespace";
885
+ this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
1391
886
  }
887
+ /* eslint-enable @itwin/no-internal */
1392
888
  }
1393
889
  /** Imports all relationships that subclass from the specified base class.
1394
890
  * @param baseRelClassFullName The specified base relationship class.
@@ -1406,52 +902,40 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1406
902
  * This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
1407
903
  */
1408
904
  onExportRelationship(sourceRelationship) {
1409
- const sourceFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.sourceId);
1410
- const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
1411
905
  const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
1412
906
  const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
1413
- if (!this._options.noProvenance && core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
1414
- let provenance = !this._options.forceExternalSourceAspectProvenance
1415
- ? sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`
1416
- : undefined;
1417
- if (!provenance) {
1418
- const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
1419
- aspectProps.id = this.queryScopeExternalSource(aspectProps).aspectId;
1420
- if (undefined === aspectProps.id) {
1421
- aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1422
- }
1423
- provenance = aspectProps;
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);
1424
911
  }
1425
- this.markLastProvenance(provenance, { isRelationship: true });
912
+ (0, core_bentley_1.assert)(aspectProps.id !== undefined);
913
+ this.markLastProvenance(aspectProps, { isRelationship: true });
1426
914
  }
1427
915
  }
1428
916
  /** Override of [IModelExportHandler.onDeleteRelationship]($transformer) that is called when [IModelExporter]($transformer) detects that a [Relationship]($backend) has been deleted from the source iModel.
1429
917
  * This override propagates the delete to the target iModel via [IModelImporter.deleteRelationship]($transformer).
1430
918
  */
1431
919
  onDeleteRelationship(sourceRelInstanceId) {
1432
- nodeAssert(this._deletedSourceRelationshipData, "should be defined at initialization by now");
1433
- const deletedRelData = this._deletedSourceRelationshipData.get(sourceRelInstanceId);
1434
- if (!deletedRelData) {
1435
- // this can occur if both the source and target deleted it
1436
- core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
1437
- return;
1438
- }
1439
- const relArg = deletedRelData.relId ?? {
1440
- sourceId: deletedRelData.sourceIdInTarget,
1441
- targetId: deletedRelData.targetIdInTarget,
1442
- };
1443
- //
1444
- // FIXME: make importer.deleteRelationship not need full props
1445
- const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
1446
- if (targetRelationship) {
1447
- this.importer.deleteRelationship(targetRelationship.toJSON());
1448
- }
1449
- if (deletedRelData.provenanceAspectId) {
1450
- this.provenanceDb.elements.deleteAspect(deletedRelData.provenanceAspectId);
1451
- }
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
+ }
935
+ }
936
+ });
1452
937
  }
1453
938
  /** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
1454
- * @deprecated
1455
939
  * @see processChanges
1456
940
  * @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.
1457
941
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
@@ -1461,20 +945,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1461
945
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
1462
946
  }
1463
947
  const aspectDeleteIds = [];
1464
- const sql = `
1465
- SELECT ECInstanceId, Identifier, JsonProperties
1466
- FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect
1467
- WHERE aspect.Scope.Id=:scopeId
1468
- AND aspect.Kind=:kind
1469
- `;
948
+ const sql = `SELECT ECInstanceId,Identifier,JsonProperties FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect WHERE aspect.Scope.Id=:scopeId AND aspect.Kind=:kind`;
1470
949
  await this.targetDb.withPreparedStatement(sql, async (statement) => {
1471
950
  statement.bindId("scopeId", this.targetScopeElementId);
1472
951
  statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
1473
952
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
1474
953
  const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
1475
954
  if (undefined === this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
1476
- // FIXME: make sure matches new provenance-based method
1477
- // FIXME: use sql JSON_EXTRACT
1478
955
  const json = JSON.parse(statement.getValue(2).getString());
1479
956
  if (undefined !== json.targetRelInstanceId) {
1480
957
  const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
@@ -1496,7 +973,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1496
973
  const targetRelationshipProps = sourceRelationship.toJSON();
1497
974
  targetRelationshipProps.sourceId = this.context.findTargetElementId(sourceRelationship.sourceId);
1498
975
  targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
1499
- // TODO: move to cloneRelationship in IModelCloneContext
1500
976
  sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
1501
977
  if ((core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType) && ("Id" === propertyMetaData.extendedType)) {
1502
978
  targetRelationshipProps[propertyName] = this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
@@ -1504,6 +980,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1504
980
  });
1505
981
  return targetRelationshipProps;
1506
982
  }
983
+ shouldExportElementAspect(aspect) {
984
+ // This override is needed to ensure that aspects are not exported if their element is not exported.
985
+ // This is needed in case DetachedExportElementAspectsStrategy is used.
986
+ return this.context.findTargetElementId(aspect.element.id) !== core_bentley_1.Id64.invalid;
987
+ }
1507
988
  /** Override of [IModelExportHandler.onExportElementUniqueAspect]($transformer) that imports an ElementUniqueAspect into the target iModel when it is exported from the source iModel.
1508
989
  * This override calls [[onTransformElementAspect]] and then [IModelImporter.importElementUniqueAspect]($transformer) to update the target iModel.
1509
990
  */
@@ -1658,86 +1139,26 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1658
1139
  return this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1659
1140
  }
1660
1141
  /**
1661
- * Initialize prerequisites of processing, you must initialize with an [[InitOptions]] if you
1662
- * are intending to process changes, but prefer using [[processChanges]] explicitly since it calls this.
1663
- * @note Called by all `process*` functions implicitly.
1142
+ * Initialize prerequisites of processing, you must initialize with an [[InitFromExternalSourceAspectsArgs]] if you
1143
+ * are intending process changes, but prefer using [[processChanges]]
1144
+ * Called by all `process*` functions implicitly.
1664
1145
  * Overriders must call `super.initialize()` first
1665
1146
  */
1666
1147
  async initialize(args) {
1667
1148
  if (this._initialized)
1668
1149
  return;
1669
1150
  await this.context.initialize();
1670
- await this._tryInitChangesetData(args);
1671
- await this.exporter.initialize(this.getExportInitOpts(args ?? {}));
1672
- // Exporter must be initialized prior to `initFromExternalSourceAspects` in order to handle entity recreations.
1673
1151
  // eslint-disable-next-line deprecation/deprecation
1674
1152
  await this.initFromExternalSourceAspects(args);
1675
1153
  this._initialized = true;
1676
1154
  }
1677
- async _tryInitChangesetData(args) {
1678
- if (!args || this.sourceDb.iTwinId === undefined || this.sourceDb.changeset.index === undefined) {
1679
- this._sourceChangeDataState = "unconnected";
1680
- return;
1681
- }
1682
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1683
- if (noChanges) {
1684
- this._sourceChangeDataState = "no-changes";
1685
- this._changeSummaryIds = [];
1686
- return;
1687
- }
1688
- // NOTE: that we do NOT download the changesummary for the last transformed version, we want
1689
- // to ignore those already processed changes
1690
- const startChangesetIndexOrId = args.startChangeset?.index
1691
- ?? args.startChangeset?.id
1692
- ?? this._synchronizationVersion.index + 1;
1693
- const endChangesetId = this.sourceDb.changeset.id;
1694
- const [startChangesetIndex, endChangesetIndex] = await Promise.all(([startChangesetIndexOrId, endChangesetId])
1695
- .map(async (indexOrId) => typeof indexOrId === "number"
1696
- ? indexOrId
1697
- : core_backend_1.IModelHost.hubAccess
1698
- .queryChangeset({
1699
- iModelId: this.sourceDb.iModelId,
1700
- // eslint-disable-next-line deprecation/deprecation
1701
- changeset: { id: indexOrId },
1702
- accessToken: args.accessToken,
1703
- })
1704
- .then((changeset) => changeset.index)));
1705
- const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
1706
- if (!this._options.ignoreMissingChangesetsInSynchronizations
1707
- && startChangesetIndex !== this._synchronizationVersion.index + 1
1708
- && this._synchronizationVersion.index !== -1) {
1709
- throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},`
1710
- + " startChangesetId should be"
1711
- + " exactly the first changeset *after* the previous synchronization to not miss data."
1712
- + ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}`
1713
- + ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'`
1714
- + ` which is changeset #${this._synchronizationVersion.index}. The transformer expected`
1715
- + ` #${this._synchronizationVersion.index + 1}.`);
1716
- }
1717
- nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
1718
- const changesetsToSkip = this._isReverseSynchronization
1719
- ? this._targetScopeProvenanceProps.jsonProperties.pendingReverseSyncChangesetIndices
1720
- : this._targetScopeProvenanceProps.jsonProperties.pendingSyncChangesetIndices;
1721
- core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
1722
- this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
1723
- core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
1724
- for (const [first, end] of this._changesetRanges) {
1725
- this._changeSummaryIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
1726
- accessToken: args.accessToken,
1727
- iModelId: this.sourceDb.iModelId,
1728
- iTwinId: this.sourceDb.iTwinId,
1729
- range: { first, end },
1730
- });
1731
- }
1732
- core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
1733
- this._sourceChangeDataState = "has-changes";
1734
- }
1735
1155
  /** Export everything from the source iModel and import the transformed entities into the target iModel.
1736
1156
  * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
1737
1157
  */
1738
1158
  async processAll() {
1159
+ core_bentley_1.Logger.logTrace(loggerCategory, "processAll()");
1739
1160
  this.logSettings();
1740
- this.initScopeProvenance();
1161
+ this.validateScopeProvenance();
1741
1162
  await this.initialize();
1742
1163
  await this.exporter.exportCodeSpecs();
1743
1164
  await this.exporter.exportFonts();
@@ -1745,6 +1166,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1745
1166
  await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
1746
1167
  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
1747
1168
  await this.exporter.exportSubModels(core_common_1.IModel.repositoryModelId); // start below the RepositoryModel
1169
+ await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
1748
1170
  await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
1749
1171
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1750
1172
  if (this.shouldDetectDeletes()) {
@@ -1757,15 +1179,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1757
1179
  this.finalizeTransformation();
1758
1180
  }
1759
1181
  markLastProvenance(sourceAspect, { isRelationship = false }) {
1760
- this._lastProvenanceEntityInfo
1761
- = typeof sourceAspect === "string"
1762
- ? sourceAspect
1763
- : {
1764
- entityId: sourceAspect.element.id,
1765
- aspectId: sourceAspect.id,
1766
- aspectVersion: sourceAspect.version ?? "",
1767
- aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
1768
- };
1182
+ this._lastProvenanceEntityInfo = {
1183
+ entityId: sourceAspect.element.id,
1184
+ aspectId: sourceAspect.id,
1185
+ aspectVersion: sourceAspect.version ?? "",
1186
+ aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
1187
+ };
1769
1188
  }
1770
1189
  /**
1771
1190
  * Load the state of the active transformation from an open SQLiteDb
@@ -1777,35 +1196,17 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1777
1196
  const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
1778
1197
  if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
1779
1198
  throw Error("expected row when getting lastProvenanceEntityId from target state table");
1780
- const entityId = stmt.getValueString(0);
1781
- const isGuidOrGuidPair = entityId.includes("-");
1782
- return isGuidOrGuidPair
1783
- ? entityId
1784
- : {
1785
- entityId,
1786
- aspectId: stmt.getValueString(1),
1787
- aspectVersion: stmt.getValueString(2),
1788
- aspectKind: stmt.getValueString(3),
1789
- };
1199
+ return {
1200
+ entityId: stmt.getValueString(0),
1201
+ aspectId: stmt.getValueString(1),
1202
+ aspectVersion: stmt.getValueString(2),
1203
+ aspectKind: stmt.getValueString(3),
1204
+ };
1790
1205
  });
1791
- /*
1792
- // TODO: maybe save transformer state resumption state based on target changset and require calls
1793
- // to saveChanges
1794
- if () {
1795
- const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
1796
- const isRelProvenance = targetFedGuid !== undefined;
1797
- const instanceId = isRelProvenance
1798
- ? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
1799
- : "";
1800
- //const classId =
1801
- if (isRelProvenance) {
1802
- }
1803
- }
1804
- */
1805
- const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
1806
- // ignore provenance check if it's null since we can't bind those ids
1206
+ const targetHasCorrectLastProvenance =
1207
+ // ignore provenance check if it's null since we can't bind those ids
1208
+ !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
1807
1209
  !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
1808
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
1809
1210
  this.provenanceDb.withPreparedStatement(`
1810
1211
  SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
1811
1212
  WHERE Scope.Id=:scopeId
@@ -1846,13 +1247,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1846
1247
  this.context.loadStateFromDb(db);
1847
1248
  this.importer.loadStateFromJson(state.importerState);
1848
1249
  this.exporter.loadStateFromJson(state.exporterState);
1849
- this._elementsWithExplicitlyTrackedProvenance = core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
1850
1250
  this.loadAdditionalStateJson(state.additionalState);
1851
1251
  }
1852
1252
  /**
1853
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
1854
- * from the original changeset
1855
- *
1856
1253
  * Return a new transformer instance with the same remappings state as saved from a previous [[IModelTransformer.saveStateToFile]] call.
1857
1254
  * This allows you to "resume" an iModel transformation, you will have to call [[IModelTransformer.processChanges]]/[[IModelTransformer.processAll]]
1858
1255
  * again but the remapping state will cause already mapped elements to be skipped.
@@ -1897,7 +1294,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1897
1294
  const jsonState = {
1898
1295
  transformerClass: this.constructor.name,
1899
1296
  options: this._options,
1900
- explicitlyTrackedElements: core_bentley_1.CompressedId64Set.compressSet(this._elementsWithExplicitlyTrackedProvenance),
1901
1297
  importerState: this.importer.saveStateToJson(),
1902
1298
  exporterState: this.exporter.saveStateToJson(),
1903
1299
  additionalState: this.getAdditionalStateJson(),
@@ -1907,9 +1303,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1907
1303
  throw Error("Failed to create the js state table in the state database");
1908
1304
  if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`
1909
1305
  CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
1910
- -- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
1306
+ -- because we cannot bind the invalid id which we use for our null state, we actually store the id as a hex string
1911
1307
  entityId TEXT,
1912
- -- the following are only valid if the above entityId is a hex id representation
1913
1308
  aspectId TEXT,
1914
1309
  aspectVersion TEXT,
1915
1310
  aspectKind TEXT
@@ -1923,20 +1318,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1923
1318
  throw Error("Failed to insert options into the state database");
1924
1319
  });
1925
1320
  db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
1926
- const lastProvenanceEntityInfo = this._lastProvenanceEntityInfo;
1927
- stmt.bindString(1, lastProvenanceEntityInfo?.entityId ?? this._lastProvenanceEntityInfo);
1928
- stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
1929
- stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
1930
- stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
1321
+ stmt.bindString(1, this._lastProvenanceEntityInfo.entityId);
1322
+ stmt.bindString(2, this._lastProvenanceEntityInfo.aspectId);
1323
+ stmt.bindString(3, this._lastProvenanceEntityInfo.aspectVersion);
1324
+ stmt.bindString(4, this._lastProvenanceEntityInfo.aspectKind);
1931
1325
  if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
1932
1326
  throw Error("Failed to insert options into the state database");
1933
1327
  });
1934
1328
  db.saveChanges();
1935
1329
  }
1936
1330
  /**
1937
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
1938
- * from the original changeset
1939
- *
1940
1331
  * Save the state of the active transformation to a file path, if a file at the path already exists, it will be overwritten
1941
1332
  * This state can be used by [[IModelTransformer.resumeTransformation]] to resume a transformation from this point.
1942
1333
  * The serialization format is a custom sqlite database.
@@ -1958,54 +1349,26 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1958
1349
  db.closeDb();
1959
1350
  }
1960
1351
  }
1961
- async processChanges(optionsOrAccessToken, startChangesetId) {
1962
- this._isSynchronization = true;
1963
- // FIXME: we used to validateScopeProvenance... does initing it cover that?
1964
- this.initScopeProvenance();
1965
- const args = typeof optionsOrAccessToken === "string"
1352
+ async processChanges(accessTokenOrArgs, startChangesetId) {
1353
+ core_bentley_1.Logger.logTrace(loggerCategory, "processChanges()");
1354
+ this.logSettings();
1355
+ this.validateScopeProvenance();
1356
+ const options = typeof accessTokenOrArgs === "string"
1966
1357
  ? {
1967
- accessToken: optionsOrAccessToken,
1968
- startChangeset: startChangesetId
1969
- ? { id: startChangesetId }
1970
- : { index: this._synchronizationVersion.index + 1 },
1358
+ accessToken: accessTokenOrArgs,
1359
+ startChangeset: startChangesetId ? { id: startChangesetId } : this.sourceDb.changeset,
1360
+ changedInstanceIds: undefined,
1971
1361
  }
1972
- : optionsOrAccessToken;
1973
- this.logSettings();
1974
- await this.initialize(args);
1975
- // must wait for initialization of synchronization provenance data
1976
- await this.exporter.exportChanges(this.getExportInitOpts(args));
1362
+ : accessTokenOrArgs;
1363
+ await this.initialize(options);
1364
+ await this.exporter.exportChanges(options);
1977
1365
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1366
+ await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
1978
1367
  if (this._options.optimizeGeometry)
1979
1368
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
1980
1369
  this.importer.computeProjectExtents();
1981
1370
  this.finalizeTransformation();
1982
1371
  }
1983
- /** Changeset data must be initialized in order to build correct changeOptions.
1984
- * Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
1985
- */
1986
- getExportInitOpts(opts) {
1987
- if (!this._isSynchronization)
1988
- return {};
1989
- return {
1990
- accessToken: opts.accessToken,
1991
- ...this._changesetRanges
1992
- ? { changesetRanges: this._changesetRanges }
1993
- : opts.startChangeset
1994
- ? { startChangeset: opts.startChangeset }
1995
- : { startChangeset: { index: this._synchronizationVersion.index + 1 } },
1996
- };
1997
- }
1998
- /** Combine an array of source elements into a single target element.
1999
- * All source and target elements must be created before calling this method.
2000
- * The "combine" operation is a remap and no properties from the source elements will be exported into the target
2001
- * and provenance will be explicitly tracked by ExternalSourceAspects
2002
- */
2003
- combineElements(sourceElementIds, targetElementId) {
2004
- for (const elementId of sourceElementIds) {
2005
- this.context.remapElement(elementId, targetElementId);
2006
- this._elementsWithExplicitlyTrackedProvenance.add(elementId);
2007
- }
2008
- }
2009
1372
  }
2010
1373
  exports.IModelTransformer = IModelTransformer;
2011
1374
  /** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
@@ -2036,7 +1399,6 @@ class TemplateModelCloner extends IModelTransformer {
2036
1399
  * @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
2037
1400
  */
2038
1401
  async placeTemplate3d(sourceTemplateModelId, targetModelId, placement) {
2039
- await this.initialize();
2040
1402
  this.context.remapElement(sourceTemplateModelId, targetModelId);
2041
1403
  this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(placement.origin, placement.angles.toMatrix3d());
2042
1404
  this._sourceIdToTargetIdMap = new Map();
@@ -2057,7 +1419,6 @@ class TemplateModelCloner extends IModelTransformer {
2057
1419
  * @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
2058
1420
  */
2059
1421
  async placeTemplate2d(sourceTemplateModelId, targetModelId, placement) {
2060
- await this.initialize();
2061
1422
  this.context.remapElement(sourceTemplateModelId, targetModelId);
2062
1423
  this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(core_geometry_1.Point3d.createFrom(placement.origin), placement.rotation);
2063
1424
  this._sourceIdToTargetIdMap = new Map();
@@ -2094,12 +1455,16 @@ class TemplateModelCloner extends IModelTransformer {
2094
1455
  const targetElementProps = super.onTransformElement(sourceElement);
2095
1456
  targetElementProps.federationGuid = core_bentley_1.Guid.createValue(); // clone from template should create a new federationGuid
2096
1457
  targetElementProps.code = core_common_1.Code.createEmpty(); // clone from template should not maintain codes
2097
- if (sourceElement instanceof core_backend_1.GeometricElement) {
2098
- const is3d = sourceElement instanceof core_backend_1.GeometricElement3d;
2099
- const placementClass = is3d ? core_common_1.Placement3d : core_common_1.Placement2d;
2100
- const placement = (placementClass).fromJSON(targetElementProps.placement);
1458
+ if (sourceElement instanceof core_backend_1.GeometricElement3d) {
1459
+ const placement = core_common_1.Placement3d.fromJSON(targetElementProps.placement);
1460
+ if (placement.isValid) {
1461
+ placement.multiplyTransform(this._transform3d);
1462
+ targetElementProps.placement = placement;
1463
+ }
1464
+ }
1465
+ else if (sourceElement instanceof core_backend_1.GeometricElement2d) {
1466
+ const placement = core_common_1.Placement2d.fromJSON(targetElementProps.placement);
2101
1467
  if (placement.isValid) {
2102
- nodeAssert(this._transform3d);
2103
1468
  placement.multiplyTransform(this._transform3d);
2104
1469
  targetElementProps.placement = placement;
2105
1470
  }
@@ -2109,17 +1474,4 @@ class TemplateModelCloner extends IModelTransformer {
2109
1474
  }
2110
1475
  }
2111
1476
  exports.TemplateModelCloner = TemplateModelCloner;
2112
- function queryElemFedGuid(db, elemId) {
2113
- return db.withPreparedStatement(`
2114
- SELECT FederationGuid
2115
- FROM bis.Element
2116
- WHERE ECInstanceId=?
2117
- `, (stmt) => {
2118
- stmt.bindId(1, elemId);
2119
- (0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
2120
- const result = stmt.getValue(0).getGuid();
2121
- (0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_DONE);
2122
- return result;
2123
- });
2124
- }
2125
1477
  //# sourceMappingURL=IModelTransformer.js.map