@itwin/imodel-transformer 0.3.18-fedguidopt.6 → 0.4.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 -85
  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 -162
  30. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  31. package/lib/cjs/IModelTransformer.js +281 -916
  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() {
@@ -261,32 +226,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
261
226
  };
262
227
  return aspectProps;
263
228
  }
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),
283
- };
284
- return aspectProps;
285
- }
286
229
  /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
287
230
  initElementProvenance(sourceElementId, targetElementId) {
288
231
  return IModelTransformer.initElementProvenanceOptions(sourceElementId, targetElementId, {
289
- // FIXME: deprecate isReverseSync option and instead detect from targetScopeElement provenance
290
232
  isReverseSynchronization: !!this._options.isReverseSynchronization,
291
233
  targetScopeElementId: this.targetScopeElementId,
292
234
  sourceDb: this.sourceDb,
@@ -298,73 +240,32 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
298
240
  * The ECInstanceId of the relationship in the target iModel will be stored in the JsonProperties of the ExternalSourceAspect.
299
241
  */
300
242
  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;
243
+ const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, targetRelInstanceId);
244
+ const elementId = this._options.isReverseSynchronization ? sourceRelationship.sourceId : targetRelationship.sourceId;
245
+ const aspectIdentifier = this._options.isReverseSynchronization ? targetRelInstanceId : sourceRelationship.id;
246
+ const aspectProps = {
247
+ classFullName: core_backend_1.ExternalSourceAspect.classFullName,
248
+ element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
249
+ scope: { id: this.targetScopeElementId },
250
+ identifier: aspectIdentifier,
251
+ kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
252
+ jsonProperties: JSON.stringify({ targetRelInstanceId }),
253
+ };
254
+ aspectProps.id = this.queryExternalSourceAspectId(aspectProps);
255
+ return aspectProps;
328
256
  }
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() {
257
+ validateScopeProvenance() {
336
258
  const aspectProps = {
337
- id: undefined,
338
- version: undefined,
339
259
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
340
260
  element: { id: this.targetScopeElementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
341
261
  scope: { id: core_common_1.IModel.rootSubjectId },
342
- identifier: this.provenanceSourceDb.iModelId,
262
+ identifier: this._options.isReverseSynchronization ? this.targetDb.iModelId : this.sourceDb.iModelId,
343
263
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
344
- jsonProperties: undefined,
345
264
  };
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) : {};
265
+ aspectProps.id = this.queryExternalSourceAspectId(aspectProps); // this query includes "identifier"
352
266
  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
267
  // 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
- `;
268
+ const sql = `SELECT ECInstanceId FROM ${core_backend_1.ExternalSourceAspect.classFullName} WHERE Element.Id=:elementId AND Scope.Id=:scopeId AND Kind=:kind LIMIT 1`;
368
269
  const hasConflictingScope = this.provenanceDb.withPreparedStatement(sql, (statement) => {
369
270
  statement.bindId("elementId", aspectProps.element.id);
370
271
  statement.bindId("scopeId", aspectProps.scope.id); // this scope.id can never be invalid, we create it above
@@ -375,128 +276,46 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
375
276
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidId, "Provenance scope conflict");
376
277
  }
377
278
  if (!this._options.noProvenance) {
378
- this.provenanceDb.elements.insertAspect({
379
- ...aspectProps,
380
- jsonProperties: JSON.stringify(aspectProps.jsonProperties),
381
- });
279
+ this.provenanceDb.elements.insertAspect(aspectProps);
382
280
  }
383
281
  }
384
- this._targetScopeProvenanceProps = aspectProps;
385
282
  }
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 };
283
+ queryExternalSourceAspectId(aspectProps) {
284
+ 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
285
  return this.provenanceDb.withPreparedStatement(sql, (statement) => {
403
286
  statement.bindId("elementId", aspectProps.element.id);
404
287
  if (aspectProps.scope === undefined)
405
- return emptyResult; // return undefined instead of binding an invalid id
288
+ return undefined; // return undefined instead of binding an invalid id
406
289
  statement.bindId("scopeId", aspectProps.scope.id);
407
290
  statement.bindString("kind", aspectProps.kind);
408
291
  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 };
292
+ return (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) ? statement.getValue(0).getId() : undefined;
415
293
  });
416
294
  }
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
- */
295
+ /** Iterate all matching ExternalSourceAspects in the provenance iModel (target unless reverse sync) and call a function for each one. */
423
296
  static forEachTrackedElement(args) {
424
- if (args.provenanceDb === args.provenanceSourceDb)
425
- return;
426
297
  if (!args.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
427
298
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
428
299
  }
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();
300
+ const sql = `SELECT Identifier,Element.Id FROM ${core_backend_1.ExternalSourceAspect.classFullName} WHERE Scope.Id=:scopeId AND Kind=:kind`;
301
+ args.provenanceDb.withPreparedStatement(sql, (statement) => {
302
+ statement.bindId("scopeId", args.targetScopeElementId);
303
+ statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
304
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
305
+ const aspectIdentifier = statement.getValue(0).getString(); // ExternalSourceAspect.Identifier is of type string
306
+ const elementId = statement.getValue(1).getId();
307
+ if (args.isReverseSynchronization) {
308
+ args.fn(elementId, aspectIdentifier); // provenance coming from the sourceDb
465
309
  }
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();
310
+ else {
311
+ args.fn(aspectIdentifier, elementId); // provenance coming from the targetDb
472
312
  }
473
313
  }
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
314
  });
496
315
  }
497
316
  forEachTrackedElement(fn) {
498
317
  return IModelTransformer.forEachTrackedElement({
499
- provenanceSourceDb: this.provenanceSourceDb,
318
+ provenanceSourceDb: this._options.isReverseSynchronization ? this.sourceDb : this.targetDb,
500
319
  provenanceDb: this.provenanceDb,
501
320
  targetScopeElementId: this.targetScopeElementId,
502
321
  isReverseSynchronization: !!this._options.isReverseSynchronization,
@@ -514,349 +333,105 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
514
333
  this.context.remapElement(sourceElementId, targetElementId);
515
334
  });
516
335
  if (args)
517
- return this.remapDeletedSourceEntities();
336
+ return this.remapDeletedSourceElements(args);
518
337
  }
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.
338
+ /** When processing deleted elements in a reverse synchronization, the [[provenanceDb]] (usually a branch iModel) has already
339
+ * deleted the [ExternalSourceAspect]($backend)s that tell us which elements in the reverse synchronization target (usually
340
+ * a master iModel) should be deleted. We must use the changesets to get the values of those before they were deleted.
522
341
  */
523
- async remapDeletedSourceEntities() {
342
+ async remapDeletedSourceElements(args) {
524
343
  // 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)
344
+ if (this.sourceDb.iTwinId === undefined)
528
345
  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
- , coalesce(esa.Identifier, esac.Identifier) AS Identifier1
559
- , NULL AS Identifier2
560
- ` : ""}
561
- FROM ecchange.change.InstanceChange ic
562
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') ec
563
- ON ic.ChangedInstance.Id=ec.ECInstanceId
564
- ${queryCanAccessProvenance ? `
565
- LEFT JOIN bis.ExternalSourceAspect esa
566
- ON ec.ECInstanceId=esa.Element.Id
567
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') esac
568
- ON ec.ECInstanceId=esac.Element.Id
569
- ` : ""}
570
- WHERE ic.OpCode=:opDelete
571
- AND ic.Summary.Id=:changeSummaryId
572
- AND ic.ChangedInstance.ClassId IS (BisCore.Element)
573
- ${queryCanAccessProvenance ? `
574
- AND (esa.Scope.Id=:targetScopeElement OR esa.Scope.Id IS NULL)
575
- AND (esa.Kind='Element' OR esa.Kind IS NULL)
576
- AND (esac.Scope.Id=:targetScopeElement OR esac.Scope.Id IS NULL)
577
- AND (esac.Kind='Element' OR esac.Kind IS NULL)
578
- ` : ""}
579
-
580
- UNION ALL
581
-
582
- SELECT
583
- 0 AS IsElemNotRel,
584
- ic.ChangedInstance.Id AS InstanceId,
585
- coalesce(se.ECInstanceId, sec.ECInstanceId) AS InstId2,
586
- coalesce(te.ECInstanceId, tec.ECInstanceId) AS InstId3,
587
- coalesce(se.FederationGuid, sec.FederationGuid) AS FedGuid1,
588
- coalesce(te.FederationGuid, tec.FederationGuid) AS FedGuid2,
589
- ic.ChangedInstance.ClassId AS ClassId
590
- ${queryCanAccessProvenance ? `
591
- , coalesce(sesa.Identifier, sesac.Identifier) AS Identifier1
592
- , coalesce(tesa.Identifier, tesac.Identifier) AS Identifier2
593
- ` : ""}
594
- FROM ecchange.change.InstanceChange ic
595
- LEFT JOIN bis.ElementRefersToElements.Changes(:changeSummaryId, 'BeforeDelete') ertec
596
- ON ic.ChangedInstance.Id=ertec.ECInstanceId
597
- -- FIXME: test a deletion of both an element and a relationship at the same time
598
- LEFT JOIN bis.Element se
599
- ON se.ECInstanceId=ertec.SourceECInstanceId
600
- LEFT JOIN bis.Element te
601
- ON te.ECInstanceId=ertec.TargetECInstanceId
602
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') sec
603
- ON sec.ECInstanceId=ertec.SourceECInstanceId
604
- LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') tec
605
- ON tec.ECInstanceId=ertec.TargetECInstanceId
606
- ${queryCanAccessProvenance ? `
607
- -- NOTE: need to join on both se/te and sec/tec incase the element was deleted
608
- LEFT JOIN bis.ExternalSourceAspect sesa
609
- ON se.ECInstanceId=sesa.Element.Id -- don't use *esac*.Identifier because it's a string
610
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') sesac
611
- ON sec.ECInstanceId=sesac.Element.Id
612
- LEFT JOIN bis.ExternalSourceAspect tesa
613
- ON te.ECInstanceId=tesa.Element.Id
614
- LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') tesac
615
- ON tec.ECInstanceId=tesac.Element.Id
616
- ` : ""}
617
- WHERE ic.OpCode=:opDelete
618
- AND ic.Summary.Id=:changeSummaryId
619
- AND ic.ChangedInstance.ClassId IS (BisCore.ElementRefersToElements)
620
- ${queryCanAccessProvenance ? `
621
- AND (sesa.Scope.Id=:targetScopeElement OR sesa.Scope.Id IS NULL)
622
- AND (sesa.Kind='Relationship' OR sesa.Kind IS NULL)
623
- AND (sesac.Scope.Id=:targetScopeElement OR sesac.Scope.Id IS NULL)
624
- AND (sesac.Kind='Relationship' OR sesac.Kind IS NULL)
625
- AND (tesa.Scope.Id=:targetScopeElement OR tesa.Scope.Id IS NULL)
626
- AND (tesa.Kind='Relationship' OR tesa.Kind IS NULL)
627
- AND (tesac.Scope.Id=:targetScopeElement OR tesac.Scope.Id IS NULL)
628
- AND (tesac.Kind='Relationship' OR tesac.Kind IS NULL)
629
- ` : ""}
630
- `;
631
- for (const changeSummaryId of this._changeSummaryIds) {
632
- // FIXME: test deletion in both forward and reverse sync
633
- this.sourceDb.withPreparedStatement(deletedEntitySql, (stmt) => {
634
- stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
635
- if (queryCanAccessProvenance)
636
- stmt.bindId("targetScopeElement", this.targetScopeElementId);
637
- stmt.bindId("changeSummaryId", changeSummaryId);
638
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
639
- const isElemNotRel = stmt.getValue(0).getBoolean();
640
- const instId = stmt.getValue(1).getId();
641
- if (isElemNotRel) {
642
- const sourceElemFedGuid = stmt.getValue(4).getGuid();
643
- // "Identifier" is a string, so null value returns '' which doesn't work with ??, and I don't like ||
644
- let identifierValue;
645
- // TODO: if I could attach the second db, will probably be much faster to get target id
646
- // as part of the whole query rather than with _queryElemIdByFedGuid
647
- const targetId = (queryCanAccessProvenance
648
- && (identifierValue = stmt.getValue(7))
649
- && !identifierValue.isNull
650
- && identifierValue.getString())
651
- // maybe batching these queries would perform better but we should
652
- // try to attach the second db and query both together anyway
653
- || (sourceElemFedGuid && this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid))
654
- // FIXME: describe why it's safe to assume nothing has been deleted in provenanceDb
655
- || this._queryProvenanceForElement(instId);
656
- // since we are processing one changeset at a time, we can see local source deletes
657
- // of entities that were never synced and can be safely ignored
658
- const deletionNotInTarget = !targetId;
659
- if (deletionNotInTarget)
660
- continue;
661
- this.context.remapElement(instId, targetId);
662
- // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
663
- // In such case an entity update will be triggered and we no longer need to delete the entity.
664
- if (alreadyImportedElementInserts.has(targetId)) {
665
- this.exporter.sourceDbChanges?.element.deleteIds.delete(instId);
666
- }
667
- if (alreadyImportedModelInserts.has(targetId)) {
668
- this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
669
- }
670
- }
671
- else { // is deleted relationship
672
- const classFullName = stmt.getValue(6).getClassNameForClassId();
673
- const [sourceIdInTarget, targetIdInTarget] = [
674
- { guidColumn: 4, identifierColumn: 7, isTarget: false },
675
- { guidColumn: 5, identifierColumn: 8, isTarget: true },
676
- ].map(({ guidColumn, identifierColumn }) => {
677
- const fedGuid = stmt.getValue(guidColumn).getGuid();
678
- let identifierValue;
679
- return ((queryCanAccessProvenance
680
- // FIXME: this is really far from idiomatic, try to undo that
681
- && (identifierValue = stmt.getValue(identifierColumn))
682
- && !identifierValue.isNull
683
- && identifierValue.getString())
684
- // maybe batching these queries would perform better but we should
685
- // try to attach the second db and query both together anyway
686
- || (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
687
- });
688
- // since we are processing one changeset at a time, we can see local source deletes
689
- // of entities that were never synced and can be safely ignored
690
- if (sourceIdInTarget && targetIdInTarget) {
691
- this._deletedSourceRelationshipData.set(instId, {
692
- classFullName,
693
- sourceIdInTarget,
694
- targetIdInTarget,
695
- });
696
- }
697
- else {
698
- // FIXME: describe why it's safe to assume nothing has been deleted in provenanceDb
699
- const relProvenance = this._queryProvenanceForRelationship(instId, {
700
- classFullName,
701
- sourceId: stmt.getValue(2).getId(),
702
- targetId: stmt.getValue(3).getId(),
703
- });
704
- if (relProvenance && relProvenance.relationshipId)
705
- this._deletedSourceRelationshipData.set(instId, {
706
- classFullName,
707
- relId: relProvenance.relationshipId,
708
- provenanceAspectId: relProvenance.aspectId,
709
- });
710
- }
711
- }
712
- }
713
- // NEXT: remap sourceId and targetId to target, get provenance there
714
- // NOTE: it is possible during a forward sync for the target to already have deleted
715
- // something that the source deleted, in which case we can safely ignore the gone provenance
346
+ try {
347
+ const startChangesetIndexOrId = args.startChangeset?.index
348
+ ?? args.startChangeset?.id
349
+ ?? this.sourceDb.changeset.index
350
+ ?? this.sourceDb.changeset.id;
351
+ const endChangesetId = this.sourceDb.changeset.id;
352
+ const [firstChangesetIndex, endChangesetIndex] = await Promise.all(([startChangesetIndexOrId, endChangesetId])
353
+ .map(async (indexOrId) => typeof indexOrId === "number"
354
+ ? indexOrId
355
+ : core_backend_1.IModelHost.hubAccess
356
+ .queryChangeset({
357
+ iModelId: this.sourceDb.iModelId,
358
+ // eslint-disable-next-line deprecation/deprecation
359
+ changeset: { id: indexOrId },
360
+ accessToken: args.accessToken,
361
+ })
362
+ .then((changeset) => changeset.index)));
363
+ const changesetIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
364
+ accessToken: args.accessToken,
365
+ iModelId: this.sourceDb.iModelId,
366
+ iTwinId: this.sourceDb.iTwinId,
367
+ range: { first: firstChangesetIndex, end: endChangesetIndex },
716
368
  });
369
+ core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
370
+ for (const changesetId of changesetIds) {
371
+ this.sourceDb.withPreparedStatement(`
372
+ SELECT esac.Element.Id, esac.Identifier
373
+ FROM ecchange.change.InstanceChange ic
374
+ JOIN BisCore.ExternalSourceAspect.Changes(:changesetId, 'BeforeDelete') esac
375
+ ON ic.ChangedInstance.Id=esac.ECInstanceId
376
+ WHERE ic.OpCode=:opcode
377
+ AND ic.Summary.Id=:changesetId
378
+ AND esac.Scope.Id=:targetScopeElementId
379
+ -- not yet documented ecsql feature to check class id
380
+ AND ic.ChangedInstance.ClassId IS (ONLY BisCore.ExternalSourceAspect)
381
+ `, (stmt) => {
382
+ stmt.bindInteger("opcode", core_common_1.ChangeOpCode.Delete);
383
+ stmt.bindInteger("changesetId", changesetId);
384
+ stmt.bindInteger("targetScopeElementId", this.targetScopeElementId);
385
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
386
+ const targetId = stmt.getValue(0).getId();
387
+ const sourceId = stmt.getValue(1).getString(); // BisCore.ExternalSourceAspect.Identifier stores a hex Id64String
388
+ // TODO: maybe delete and don't just remap
389
+ this.context.remapElement(targetId, sourceId);
390
+ }
391
+ });
392
+ }
717
393
  }
718
- }
719
- _queryProvenanceForElement(entityInProvenanceSourceId) {
720
- return this.provenanceDb.withPreparedStatement(`
721
- SELECT esa.Element.Id
722
- FROM Bis.ExternalSourceAspect esa
723
- WHERE esa.Kind=?
724
- AND esa.Scope.Id=?
725
- AND esa.Identifier=?
726
- `, (stmt) => {
727
- stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Element);
728
- stmt.bindId(2, this.targetScopeElementId);
729
- stmt.bindString(3, entityInProvenanceSourceId);
730
- if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
731
- return stmt.getValue(0).getId();
732
- else
733
- return undefined;
734
- });
735
- }
736
- _queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
737
- return this.provenanceDb.withPreparedStatement(`
738
- SELECT
739
- ECInstanceId,
740
- JSON_EXTRACT(JsonProperties, '$.targetRelInstanceId'),
741
- JSON_EXTRACT(JsonProperties, '$.provenanceRelInstanceId')
742
- FROM Bis.ExternalSourceAspect
743
- WHERE Kind=?
744
- AND Scope.Id=?
745
- AND Identifier=?
746
- `, (stmt) => {
747
- stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Relationship);
748
- stmt.bindId(2, this.targetScopeElementId);
749
- stmt.bindString(3, entityInProvenanceSourceId);
750
- if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
751
- return undefined;
752
- const aspectId = stmt.getValue(0).getId();
753
- const provenanceRelInstIdVal = stmt.getValue(2);
754
- const provenanceRelInstanceId = !provenanceRelInstIdVal.isNull
755
- ? provenanceRelInstIdVal.getString()
756
- : this._queryTargetRelId(sourceRelInfo);
757
- return {
758
- aspectId,
759
- relationshipId: provenanceRelInstanceId,
760
- };
761
- });
762
- }
763
- _queryTargetRelId(sourceRelInfo) {
764
- const targetRelInfo = {
765
- sourceId: this.context.findTargetElementId(sourceRelInfo.sourceId),
766
- targetId: this.context.findTargetElementId(sourceRelInfo.targetId),
767
- };
768
- if (targetRelInfo.sourceId === undefined || targetRelInfo.targetId === undefined)
769
- return undefined; // couldn't find an element, rel is invalid or deleted
770
- return this.targetDb.withPreparedStatement(`
771
- SELECT ECInstanceId
772
- FROM bis.ElementRefersToElements
773
- WHERE SourceECInstanceId=?
774
- AND TargetECInstanceId=?
775
- AND ECClassId=?
776
- `, (stmt) => {
777
- stmt.bindId(1, targetRelInfo.sourceId);
778
- stmt.bindId(2, targetRelInfo.targetId);
779
- stmt.bindId(3, this._targetClassNameToClassId(sourceRelInfo.classFullName));
780
- if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
781
- return undefined;
782
- return stmt.getValue(0).getId();
783
- });
784
- }
785
- _targetClassNameToClassId(classFullName) {
786
- let classId = this._targetClassNameToClassIdCache.get(classFullName);
787
- if (classId === undefined) {
788
- classId = this._getRelClassId(this.targetDb, classFullName);
789
- this._targetClassNameToClassIdCache.set(classFullName, classId);
394
+ finally {
395
+ if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
396
+ core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
790
397
  }
791
- return classId;
792
- }
793
- // NOTE: this doesn't handle remapped element classes,
794
- // but is only used for relationships rn
795
- _getRelClassId(db, classFullName) {
796
- return db.withPreparedStatement(`
797
- SELECT c.ECInstanceId
798
- FROM ECDbMeta.ECClassDef c
799
- JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
800
- WHERE s.Name=? AND c.Name=?
801
- `, (stmt) => {
802
- const [schemaName, className] = classFullName.split(".");
803
- stmt.bindString(1, schemaName);
804
- stmt.bindString(2, className);
805
- if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
806
- return stmt.getValue(0).getId();
807
- (0, core_bentley_1.assert)(false, "relationship was not found");
808
- });
809
- }
810
- _queryElemIdByFedGuid(db, fedGuid) {
811
- return db.withPreparedStatement("SELECT ECInstanceId FROM Bis.Element WHERE FederationGuid=?", (stmt) => {
812
- stmt.bindGuid(1, fedGuid);
813
- if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
814
- return stmt.getValue(0).getId();
815
- else
816
- return undefined;
817
- });
818
398
  }
819
399
  /** Returns `true` if *brute force* delete detections should be run.
820
400
  * @note Not relevant for processChanges when change history is known.
821
401
  */
822
402
  shouldDetectDeletes() {
823
- // FIXME: all synchronizations should mark this as false
824
403
  if (this._isFirstSynchronization)
825
404
  return false; // not necessary the first time since there are no deletes to detect
826
405
  if (this._options.isReverseSynchronization)
827
406
  return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
828
407
  return true;
829
408
  }
830
- /**
831
- * Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
832
- * in the source iModel.
833
- * @deprecated in 0.1.x. This method is only called during [[processAll]] when the option
834
- * [[IModelTransformerOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
835
- * necessary when using [[processChanges]] since changeset information is sufficient.
836
- * @note you do not need to call this directly unless processing a subset of an iModel.
409
+ /** Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements in the source iModel.
410
+ * @see processChanges
411
+ * @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.
837
412
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
838
413
  */
839
414
  async detectElementDeletes() {
840
- const sql = `
841
- SELECT Identifier, Element.Id
842
- FROM BisCore.ExternalSourceAspect
843
- WHERE Scope.Id=:scopeId
844
- AND Kind=:kind
845
- `;
846
- nodeAssert(!this._options.isReverseSynchronization, "synchronizations with processChagnes already detect element deletes, don't call detectElementDeletes");
847
- this.provenanceDb.withPreparedStatement(sql, (stmt) => {
848
- stmt.bindId("scopeId", this.targetScopeElementId);
849
- stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
850
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
851
- // ExternalSourceAspect.Identifier is of type string
852
- const aspectIdentifier = stmt.getValue(0).getString();
853
- if (!core_bentley_1.Id64.isId64(aspectIdentifier)) {
854
- continue;
855
- }
856
- const targetElemId = stmt.getValue(1).getId();
857
- const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: `e${aspectIdentifier}` });
858
- if (wasDeletedInSource)
859
- this.importer.deleteElement(targetElemId);
415
+ if (this._options.isReverseSynchronization) {
416
+ throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
417
+ }
418
+ const targetElementsToDelete = [];
419
+ this.forEachTrackedElement((sourceElementId, targetElementId) => {
420
+ if (undefined === this.sourceDb.elements.tryGetElementProps(sourceElementId)) {
421
+ // if the sourceElement is not found, then it must have been deleted, so propagate the delete to the target iModel
422
+ targetElementsToDelete.push(targetElementId);
423
+ }
424
+ });
425
+ targetElementsToDelete.forEach((targetElementId) => {
426
+ try {
427
+ // TODO: make it possible to delete more elements at once to prevent redundant expensive
428
+ // element reference scanning
429
+ this.importer.deleteElement(targetElementId);
430
+ }
431
+ catch (err) {
432
+ // ignore not found elements, iterative element tree deletion might have already deleted them
433
+ if (err.name !== "Not Found")
434
+ throw err;
860
435
  }
861
436
  });
862
437
  }
@@ -883,53 +458,24 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
883
458
  }
884
459
  return targetElementProps;
885
460
  }
886
- // FIXME: this is a PoC, see if we minimize memory usage
887
- _cacheSourceChanges() {
888
- nodeAssert(this._changeSummaryIds && this._changeSummaryIds.length > 0, "should have changeset data by now");
889
- this._hasElementChangedCache = new Set();
890
- const query = `
891
- SELECT
892
- ic.ChangedInstance.Id AS InstId
893
- FROM ecchange.change.InstanceChange ic
894
- JOIN iModelChange.Changeset imc ON ic.Summary.Id=imc.Summary.Id
895
- -- FIXME: do relationship entities also need this cache optimization?
896
- WHERE ic.ChangedInstance.ClassId IS (BisCore.Element)
897
- AND InVirtualSet(:changeSummaryIds, ic.Summary.Id)
898
- -- ignore deleted, we take care of those in remapDeletedSourceEntities
899
- -- include inserted since inserted code-colliding elements should be considered
900
- -- a change so that the colliding element is exported to the target
901
- AND ic.OpCode<>:opDelete
902
- `;
903
- // there is a single mega-query multi-join+coalescing hack that I used originally to get around
904
- // only being able to run table.Changes() on one changeset at once, but sqlite only supports up to 64
905
- // tables in a join. Need to talk to core about .Changes being able to take a set of changesets
906
- // You can find this version in the `federation-guid-optimization-megaquery` branch
907
- // I wouldn't use it unless we prove via profiling that it speeds things up significantly
908
- // And even then let's first try scanning the raw changesets instead of applying them as these queries
909
- // require
910
- this.sourceDb.withPreparedStatement(query, (stmt) => {
911
- stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
912
- stmt.bindIdSet("changeSummaryIds", this._changeSummaryIds);
913
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
914
- const instId = stmt.getValue(0).getId();
915
- this._hasElementChangedCache.add(instId);
916
- }
917
- });
918
- }
919
461
  /** Returns true if a change within sourceElement is detected.
920
462
  * @param sourceElement The Element from the source iModel
921
463
  * @param targetElementId The Element from the target iModel to compare against.
922
464
  * @note A subclass can override this method to provide custom change detection behavior.
923
465
  */
924
- hasElementChanged(sourceElement, _targetElementId) {
925
- if (this._sourceChangeDataState === "no-changes")
926
- return false;
927
- if (this._sourceChangeDataState === "unconnected")
928
- return true;
929
- nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
930
- if (this._hasElementChangedCache === undefined)
931
- this._cacheSourceChanges();
932
- return this._hasElementChangedCache.has(sourceElement.id);
466
+ hasElementChanged(sourceElement, targetElementId) {
467
+ const sourceAspects = this.targetDb.elements.getAspects(targetElementId, core_backend_1.ExternalSourceAspect.classFullName);
468
+ for (const sourceAspect of sourceAspects) {
469
+ if (sourceAspect.scope === undefined) // if the scope was lost, we can't correlate so assume it changed
470
+ return true;
471
+ if (sourceAspect.identifier === sourceElement.id &&
472
+ sourceAspect.scope.id === this.targetScopeElementId &&
473
+ sourceAspect.kind === core_backend_1.ExternalSourceAspect.Kind.Element) {
474
+ const lastModifiedTime = sourceElement.iModel.elements.queryLastModifiedTime(sourceElement.id);
475
+ return lastModifiedTime !== sourceAspect.version;
476
+ }
477
+ }
478
+ return true;
933
479
  }
934
480
  static transformCallbackFor(transformer, entity) {
935
481
  if (entity instanceof core_backend_1.Element)
@@ -1119,68 +665,51 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1119
665
  targetElementId = this.context.findTargetElementId(sourceElement.id);
1120
666
  targetElementProps = this.onTransformElement(sourceElement);
1121
667
  }
1122
- // if an existing remapping was not yet found, check by FederationGuid
1123
- if (this.context.isBetweenIModels && !core_bentley_1.Id64.isValid(targetElementId) && sourceElement.federationGuid !== undefined) {
1124
- targetElementId = this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
1125
- if (core_bentley_1.Id64.isValid(targetElementId))
1126
- this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found
1127
- }
1128
668
  // 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)
1129
- if (!core_bentley_1.Id64.isValid(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
1130
- // respond the same way to undefined code value as the @see Code class, but don't use that class because is trims
669
+ if (!core_bentley_1.Id64.isValidId64(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
670
+ // respond the same way to undefined code value as the @see Code class, but don't use that class because it trims
1131
671
  // whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
1132
672
  targetElementProps.code.value = targetElementProps.code.value ?? "";
1133
- const maybeTargetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
1134
- if (undefined !== maybeTargetElementId) {
1135
- const maybeTargetElem = this.targetDb.elements.getElement(maybeTargetElementId);
1136
- if (maybeTargetElem.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
1137
- targetElementId = maybeTargetElementId;
673
+ targetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
674
+ if (undefined !== targetElementId) {
675
+ const targetElement = this.targetDb.elements.getElement(targetElementId);
676
+ if (targetElement.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
1138
677
  this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
1139
678
  }
1140
679
  else {
680
+ targetElementId = undefined;
1141
681
  targetElementProps.code = core_common_1.Code.createEmpty(); // clear out invalid code
1142
682
  }
1143
683
  }
1144
684
  }
1145
- if (core_bentley_1.Id64.isValid(targetElementId) && !this.hasElementChanged(sourceElement, targetElementId))
1146
- return;
685
+ if (undefined !== targetElementId && core_bentley_1.Id64.isValidId64(targetElementId)) {
686
+ // compare LastMod of sourceElement to ExternalSourceAspect of targetElement to see there are changes to import
687
+ if (!this.hasElementChanged(sourceElement, targetElementId)) {
688
+ return;
689
+ }
690
+ }
1147
691
  this.collectUnmappedReferences(sourceElement);
1148
- // targetElementId will be valid (indicating update) or undefined (indicating insert)
1149
- targetElementProps.id
1150
- = core_bentley_1.Id64.isValid(targetElementId)
1151
- ? targetElementId
1152
- : undefined;
692
+ // TODO: untangle targetElementId state...
693
+ if (targetElementId === core_bentley_1.Id64.invalid)
694
+ targetElementId = undefined;
695
+ targetElementProps.id = targetElementId; // targetElementId will be valid (indicating update) or undefined (indicating insert)
1153
696
  if (!this._options.wasSourceIModelCopiedToTarget) {
1154
697
  this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
1155
698
  }
1156
699
  this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
1157
700
  // now that we've mapped this elem we can fix unmapped references to it
1158
701
  this.resolvePendingReferences(sourceElement);
1159
- // the transformer does not currently 'split' or 'join' any elements, therefore, it does not
1160
- // insert external source aspects because federation guids are sufficient for this.
1161
- // Other transformer subclasses must insert the appropriate aspect (as provided by a TBD API)
1162
- // when splitting/joining elements
1163
- // physical consolidation is an example of a 'joining' transform
1164
- // FIXME: document this externally!
1165
- // verify at finalization time that we don't lose provenance on new elements
1166
- // make public and improve `initElementProvenance` API for usage by consolidators
1167
702
  if (!this._options.noProvenance) {
1168
- let provenance = this._options.forceExternalSourceAspectProvenance || this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
1169
- ? undefined
1170
- : sourceElement.federationGuid;
1171
- if (!provenance) {
1172
- const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
1173
- const aspectId = this.queryScopeExternalSource(aspectProps).aspectId;
1174
- if (aspectId === undefined) {
1175
- aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1176
- }
1177
- else {
1178
- aspectProps.id = aspectId;
1179
- this.provenanceDb.elements.updateAspect(aspectProps);
1180
- }
1181
- provenance = aspectProps;
703
+ const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
704
+ let aspectId = this.queryExternalSourceAspectId(aspectProps);
705
+ if (aspectId === undefined) {
706
+ aspectId = this.provenanceDb.elements.insertAspect(aspectProps);
707
+ }
708
+ else {
709
+ this.provenanceDb.elements.updateAspect(aspectProps);
1182
710
  }
1183
- this.markLastProvenance(provenance, { isRelationship: false });
711
+ aspectProps.id = aspectId;
712
+ this.markLastProvenance(aspectProps, { isRelationship: false });
1184
713
  }
1185
714
  }
1186
715
  resolvePendingReferences(entity) {
@@ -1218,11 +747,50 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1218
747
  onDeleteModel(sourceModelId) {
1219
748
  // It is possible and apparently occasionally sensical to delete a model without deleting its underlying element.
1220
749
  // - If only the model is deleted, [[initFromExternalSourceAspects]] will have already remapped the underlying element since it still exists.
1221
- // - If both were deleted, [[remapDeletedSourceEntities]] will find and remap the deleted element making this operation valid
750
+ // - If both were deleted, [[remapDeletedSourceElements]] will find and remap the deleted element making this operation valid
1222
751
  const targetModelId = this.context.findTargetElementId(sourceModelId);
1223
- if (core_bentley_1.Id64.isValidId64(targetModelId)) {
752
+ if (!core_bentley_1.Id64.isValidId64(targetModelId))
753
+ return;
754
+ if (this.exporter.sourceDbChanges?.element.deleteIds.has(sourceModelId)) {
755
+ const isDefinitionPartition = this.targetDb.withPreparedStatement(`
756
+ SELECT 1
757
+ FROM bis.DefinitionPartition
758
+ WHERE ECInstanceId=?
759
+ `, (stmt) => {
760
+ stmt.bindId(1, targetModelId);
761
+ const val = stmt.step();
762
+ switch (val) {
763
+ case core_bentley_1.DbResult.BE_SQLITE_ROW: return true;
764
+ case core_bentley_1.DbResult.BE_SQLITE_DONE: return false;
765
+ default: (0, core_bentley_1.assert)(false, `unexpected db result: '${stmt}'`);
766
+ }
767
+ });
768
+ if (isDefinitionPartition) {
769
+ // Skipping model deletion because model's partition will also be deleted.
770
+ // It expects that model will be present and will fail if it's missing.
771
+ // Model will be deleted when its partition will be deleted.
772
+ return;
773
+ }
774
+ }
775
+ try {
1224
776
  this.importer.deleteModel(targetModelId);
1225
777
  }
778
+ catch (error) {
779
+ const isDeletionProhibitedErr = error instanceof core_common_1.IModelError && (error.errorNumber === core_bentley_1.IModelStatus.DeletionProhibited || error.errorNumber === core_bentley_1.IModelStatus.ForeignKeyConstraint);
780
+ if (!isDeletionProhibitedErr)
781
+ throw error;
782
+ // Transformer tries to delete models before it deletes elements. Definition models cannot be deleted unless all of their modeled elements are deleted first.
783
+ // In case a definition model needs to be deleted we need to skip it for now and register its modeled partition for deletion.
784
+ // The `OnDeleteElement` calls `DeleteElementTree` Which deletes the model together with its partition after deleting all of the modeled elements.
785
+ this.scheduleModeledPartitionDeletion(sourceModelId);
786
+ }
787
+ }
788
+ /** Schedule modeled partition deletion */
789
+ scheduleModeledPartitionDeletion(sourceModelId) {
790
+ const deletedElements = this.exporter.sourceDbChanges?.element.deleteIds;
791
+ if (!deletedElements.has(sourceModelId)) {
792
+ deletedElements.add(sourceModelId);
793
+ }
1226
794
  }
1227
795
  /** Cause the model container, contents, and sub-models to be exported from the source iModel and imported into the target iModel.
1228
796
  * @param sourceModeledElementId Import this [Model]($backend) from the source IModelDb.
@@ -1294,68 +862,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1294
862
  * @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
1295
863
  */
1296
864
  async processDeferredElements(_numRetries = 3) { }
1297
- /** called at the end ([[finalizeTransformation]]) of a transformation,
1298
- * updates the target scope element to say that transformation up through the
1299
- * source's changeset has been performed. Also stores all changesets that occurred
1300
- * during the transformation as "pending synchronization changeset indices"
1301
- *
1302
- * You generally should not call this function yourself and use [[processChanges]] instead.
1303
- * It is public for unsupported use cases of custom synchronization transforms.
1304
- * @note if you are not running processChanges in this transformation, this will fail
1305
- * without setting the `force` option to `true`
1306
- */
1307
- updateSynchronizationVersion({ force = false } = {}) {
1308
- if (!force && (this._sourceChangeDataState !== "has-changes" && !this._isFirstSynchronization))
1309
- return;
1310
- nodeAssert(this._targetScopeProvenanceProps);
1311
- const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
1312
- const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
1313
- if (this._isFirstSynchronization) {
1314
- this._targetScopeProvenanceProps.version = sourceVersion;
1315
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = targetVersion;
1316
- }
1317
- else if (this._options.isReverseSynchronization) {
1318
- const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
1319
- core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
1320
- this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = sourceVersion;
1321
- }
1322
- else if (!this._options.isReverseSynchronization) {
1323
- core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
1324
- this._targetScopeProvenanceProps.version = sourceVersion;
1325
- }
1326
- if (this._isSynchronization) {
1327
- (0, core_bentley_1.assert)(this.targetDb.changeset.index !== undefined && this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
1328
- const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
1329
- core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1330
- core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1331
- const [syncChangesetsToClear, syncChangesetsToUpdate] = this._isReverseSynchronization
1332
- ? [jsonProps.pendingReverseSyncChangesetIndices, jsonProps.pendingSyncChangesetIndices]
1333
- : [jsonProps.pendingSyncChangesetIndices, jsonProps.pendingReverseSyncChangesetIndices];
1334
- // NOTE that as documented in [[processChanges]], this assumes that right after
1335
- // transformation finalization, the work will be saved immediately, otherwise we've
1336
- // just marked this changeset as a synchronization to ignore, and the user can add other
1337
- // stuff to it which would break future synchronizations
1338
- // FIXME: force save for the user to prevent that
1339
- for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
1340
- syncChangesetsToUpdate.push(i);
1341
- syncChangesetsToClear.length = 0;
1342
- // if reverse sync then we may have received provenance changes which should be marked as sync changes
1343
- if (this._isReverseSynchronization) {
1344
- nodeAssert(this.sourceDb.changeset.index, "changeset didn't exist");
1345
- for (let i = this._startingChangesetIndices.source + 1; i <= this.sourceDb.changeset.index + 1; i++)
1346
- jsonProps.pendingReverseSyncChangesetIndices.push(i);
1347
- }
1348
- core_bentley_1.Logger.logTrace(loggerCategory, `new pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1349
- core_bentley_1.Logger.logTrace(loggerCategory, `new pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1350
- }
1351
- this.provenanceDb.elements.updateAspect({
1352
- ...this._targetScopeProvenanceProps,
1353
- jsonProperties: JSON.stringify(this._targetScopeProvenanceProps.jsonProperties),
1354
- });
1355
- }
1356
- // FIXME: is this necessary when manually using lowlevel transform APIs?
1357
865
  finalizeTransformation() {
1358
- this.updateSynchronizationVersion();
1359
866
  if (this._partiallyCommittedEntities.size > 0) {
1360
867
  core_bentley_1.Logger.logWarning(loggerCategory, [
1361
868
  "The following elements were never fully resolved:",
@@ -1367,11 +874,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1367
874
  partiallyCommittedElem.forceComplete();
1368
875
  }
1369
876
  }
1370
- // FIXME: make processAll have a try {} finally {} that cleans this up
1371
- if (!this._options.noDetachChangeCache) {
1372
- if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
1373
- core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
877
+ // this internal is guaranteed stable for just transformer usage
878
+ /* eslint-disable @itwin/no-internal */
879
+ if ("codeValueBehavior" in this.sourceDb) {
880
+ this.sourceDb.codeValueBehavior = "trim-unicode-whitespace";
881
+ this.targetDb.codeValueBehavior = "trim-unicode-whitespace";
1374
882
  }
883
+ /* eslint-enable @itwin/no-internal */
1375
884
  }
1376
885
  /** Imports all relationships that subclass from the specified base class.
1377
886
  * @param baseRelClassFullName The specified base relationship class.
@@ -1389,52 +898,40 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1389
898
  * This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
1390
899
  */
1391
900
  onExportRelationship(sourceRelationship) {
1392
- const sourceFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.sourceId);
1393
- const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
1394
901
  const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
1395
902
  const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
1396
- if (!this._options.noProvenance && core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
1397
- let provenance = !this._options.forceExternalSourceAspectProvenance
1398
- ? sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`
1399
- : undefined;
1400
- if (!provenance) {
1401
- const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
1402
- aspectProps.id = this.queryScopeExternalSource(aspectProps).aspectId;
1403
- if (undefined === aspectProps.id) {
1404
- aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1405
- }
1406
- provenance = aspectProps;
903
+ if (!this._options.noProvenance && core_bentley_1.Id64.isValidId64(targetRelationshipInstanceId)) {
904
+ const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
905
+ if (undefined === aspectProps.id) {
906
+ aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1407
907
  }
1408
- this.markLastProvenance(provenance, { isRelationship: true });
908
+ (0, core_bentley_1.assert)(aspectProps.id !== undefined);
909
+ this.markLastProvenance(aspectProps, { isRelationship: true });
1409
910
  }
1410
911
  }
1411
912
  /** Override of [IModelExportHandler.onDeleteRelationship]($transformer) that is called when [IModelExporter]($transformer) detects that a [Relationship]($backend) has been deleted from the source iModel.
1412
913
  * This override propagates the delete to the target iModel via [IModelImporter.deleteRelationship]($transformer).
1413
914
  */
1414
915
  onDeleteRelationship(sourceRelInstanceId) {
1415
- nodeAssert(this._deletedSourceRelationshipData, "should be defined at initialization by now");
1416
- const deletedRelData = this._deletedSourceRelationshipData.get(sourceRelInstanceId);
1417
- if (!deletedRelData) {
1418
- // this can occur if both the source and target deleted it
1419
- core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
1420
- return;
1421
- }
1422
- const relArg = deletedRelData.relId ?? {
1423
- sourceId: deletedRelData.sourceIdInTarget,
1424
- targetId: deletedRelData.targetIdInTarget,
1425
- };
1426
- //
1427
- // FIXME: make importer.deleteRelationship not need full props
1428
- const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
1429
- if (targetRelationship) {
1430
- this.importer.deleteRelationship(targetRelationship.toJSON());
1431
- }
1432
- if (deletedRelData.provenanceAspectId) {
1433
- this.provenanceDb.elements.deleteAspect(deletedRelData.provenanceAspectId);
1434
- }
916
+ const sql = `SELECT ECInstanceId,JsonProperties FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect` +
917
+ ` WHERE aspect.Scope.Id=:scopeId AND aspect.Kind=:kind AND aspect.Identifier=:identifier LIMIT 1`;
918
+ this.targetDb.withPreparedStatement(sql, (statement) => {
919
+ statement.bindId("scopeId", this.targetScopeElementId);
920
+ statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
921
+ statement.bindString("identifier", sourceRelInstanceId);
922
+ if (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
923
+ const json = JSON.parse(statement.getValue(1).getString());
924
+ if (undefined !== json.targetRelInstanceId) {
925
+ const targetRelationship = this.targetDb.relationships.tryGetInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
926
+ if (targetRelationship) {
927
+ this.importer.deleteRelationship(targetRelationship.toJSON());
928
+ }
929
+ this.targetDb.elements.deleteAspect(statement.getValue(0).getId());
930
+ }
931
+ }
932
+ });
1435
933
  }
1436
934
  /** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
1437
- * @deprecated
1438
935
  * @see processChanges
1439
936
  * @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.
1440
937
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
@@ -1444,20 +941,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1444
941
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
1445
942
  }
1446
943
  const aspectDeleteIds = [];
1447
- const sql = `
1448
- SELECT ECInstanceId, Identifier, JsonProperties
1449
- FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect
1450
- WHERE aspect.Scope.Id=:scopeId
1451
- AND aspect.Kind=:kind
1452
- `;
944
+ const sql = `SELECT ECInstanceId,Identifier,JsonProperties FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect WHERE aspect.Scope.Id=:scopeId AND aspect.Kind=:kind`;
1453
945
  await this.targetDb.withPreparedStatement(sql, async (statement) => {
1454
946
  statement.bindId("scopeId", this.targetScopeElementId);
1455
947
  statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
1456
948
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
1457
949
  const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
1458
950
  if (undefined === this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
1459
- // FIXME: make sure matches new provenance-based method
1460
- // FIXME: use sql JSON_EXTRACT
1461
951
  const json = JSON.parse(statement.getValue(2).getString());
1462
952
  if (undefined !== json.targetRelInstanceId) {
1463
953
  const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
@@ -1479,7 +969,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1479
969
  const targetRelationshipProps = sourceRelationship.toJSON();
1480
970
  targetRelationshipProps.sourceId = this.context.findTargetElementId(sourceRelationship.sourceId);
1481
971
  targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
1482
- // TODO: move to cloneRelationship in IModelCloneContext
1483
972
  sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
1484
973
  if ((core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType) && ("Id" === propertyMetaData.extendedType)) {
1485
974
  targetRelationshipProps[propertyName] = this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
@@ -1487,6 +976,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1487
976
  });
1488
977
  return targetRelationshipProps;
1489
978
  }
979
+ shouldExportElementAspect(aspect) {
980
+ // This override is needed to ensure that aspects are not exported if their element is not exported.
981
+ // This is needed in case DetachedExportElementAspectsStrategy is used.
982
+ return this.context.findTargetElementId(aspect.element.id) !== core_bentley_1.Id64.invalid;
983
+ }
1490
984
  /** Override of [IModelExportHandler.onExportElementUniqueAspect]($transformer) that imports an ElementUniqueAspect into the target iModel when it is exported from the source iModel.
1491
985
  * This override calls [[onTransformElementAspect]] and then [IModelImporter.importElementUniqueAspect]($transformer) to update the target iModel.
1492
986
  */
@@ -1641,86 +1135,26 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1641
1135
  return this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1642
1136
  }
1643
1137
  /**
1644
- * Initialize prerequisites of processing, you must initialize with an [[InitOptions]] if you
1645
- * are intending to process changes, but prefer using [[processChanges]] explicitly since it calls this.
1646
- * @note Called by all `process*` functions implicitly.
1138
+ * Initialize prerequisites of processing, you must initialize with an [[InitFromExternalSourceAspectsArgs]] if you
1139
+ * are intending process changes, but prefer using [[processChanges]]
1140
+ * Called by all `process*` functions implicitly.
1647
1141
  * Overriders must call `super.initialize()` first
1648
1142
  */
1649
1143
  async initialize(args) {
1650
1144
  if (this._initialized)
1651
1145
  return;
1652
1146
  await this.context.initialize();
1653
- await this._tryInitChangesetData(args);
1654
- await this.exporter.initialize(this.getExportInitOpts(args ?? {}));
1655
- // Exporter must be initialized prior to `initFromExternalSourceAspects` in order to handle entity recreations.
1656
1147
  // eslint-disable-next-line deprecation/deprecation
1657
1148
  await this.initFromExternalSourceAspects(args);
1658
1149
  this._initialized = true;
1659
1150
  }
1660
- async _tryInitChangesetData(args) {
1661
- if (!args || this.sourceDb.iTwinId === undefined || this.sourceDb.changeset.index === undefined) {
1662
- this._sourceChangeDataState = "unconnected";
1663
- return;
1664
- }
1665
- const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1666
- if (noChanges) {
1667
- this._sourceChangeDataState = "no-changes";
1668
- this._changeSummaryIds = [];
1669
- return;
1670
- }
1671
- // NOTE: that we do NOT download the changesummary for the last transformed version, we want
1672
- // to ignore those already processed changes
1673
- const startChangesetIndexOrId = args.startChangeset?.index
1674
- ?? args.startChangeset?.id
1675
- ?? this._synchronizationVersion.index + 1;
1676
- const endChangesetId = this.sourceDb.changeset.id;
1677
- const [startChangesetIndex, endChangesetIndex] = await Promise.all(([startChangesetIndexOrId, endChangesetId])
1678
- .map(async (indexOrId) => typeof indexOrId === "number"
1679
- ? indexOrId
1680
- : core_backend_1.IModelHost.hubAccess
1681
- .queryChangeset({
1682
- iModelId: this.sourceDb.iModelId,
1683
- // eslint-disable-next-line deprecation/deprecation
1684
- changeset: { id: indexOrId },
1685
- accessToken: args.accessToken,
1686
- })
1687
- .then((changeset) => changeset.index)));
1688
- const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
1689
- if (!this._options.ignoreMissingChangesetsInSynchronizations
1690
- && startChangesetIndex !== this._synchronizationVersion.index + 1
1691
- && this._synchronizationVersion.index !== -1) {
1692
- throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},`
1693
- + " startChangesetId should be"
1694
- + " exactly the first changeset *after* the previous synchronization to not miss data."
1695
- + ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}`
1696
- + ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'`
1697
- + ` which is changeset #${this._synchronizationVersion.index}. The transformer expected`
1698
- + ` #${this._synchronizationVersion.index + 1}.`);
1699
- }
1700
- nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
1701
- const changesetsToSkip = this._isReverseSynchronization
1702
- ? this._targetScopeProvenanceProps.jsonProperties.pendingReverseSyncChangesetIndices
1703
- : this._targetScopeProvenanceProps.jsonProperties.pendingSyncChangesetIndices;
1704
- core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
1705
- this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
1706
- core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
1707
- for (const [first, end] of this._changesetRanges) {
1708
- this._changeSummaryIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
1709
- accessToken: args.accessToken,
1710
- iModelId: this.sourceDb.iModelId,
1711
- iTwinId: this.sourceDb.iTwinId,
1712
- range: { first, end },
1713
- });
1714
- }
1715
- core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
1716
- this._sourceChangeDataState = "has-changes";
1717
- }
1718
1151
  /** Export everything from the source iModel and import the transformed entities into the target iModel.
1719
1152
  * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
1720
1153
  */
1721
1154
  async processAll() {
1155
+ core_bentley_1.Logger.logTrace(loggerCategory, "processAll()");
1722
1156
  this.logSettings();
1723
- this.initScopeProvenance();
1157
+ this.validateScopeProvenance();
1724
1158
  await this.initialize();
1725
1159
  await this.exporter.exportCodeSpecs();
1726
1160
  await this.exporter.exportFonts();
@@ -1728,6 +1162,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1728
1162
  await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
1729
1163
  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
1730
1164
  await this.exporter.exportSubModels(core_common_1.IModel.repositoryModelId); // start below the RepositoryModel
1165
+ await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
1731
1166
  await this.exporter.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
1732
1167
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1733
1168
  if (this.shouldDetectDeletes()) {
@@ -1740,15 +1175,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1740
1175
  this.finalizeTransformation();
1741
1176
  }
1742
1177
  markLastProvenance(sourceAspect, { isRelationship = false }) {
1743
- this._lastProvenanceEntityInfo
1744
- = typeof sourceAspect === "string"
1745
- ? sourceAspect
1746
- : {
1747
- entityId: sourceAspect.element.id,
1748
- aspectId: sourceAspect.id,
1749
- aspectVersion: sourceAspect.version ?? "",
1750
- aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
1751
- };
1178
+ this._lastProvenanceEntityInfo = {
1179
+ entityId: sourceAspect.element.id,
1180
+ aspectId: sourceAspect.id,
1181
+ aspectVersion: sourceAspect.version ?? "",
1182
+ aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
1183
+ };
1752
1184
  }
1753
1185
  /**
1754
1186
  * Load the state of the active transformation from an open SQLiteDb
@@ -1760,35 +1192,17 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1760
1192
  const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
1761
1193
  if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
1762
1194
  throw Error("expected row when getting lastProvenanceEntityId from target state table");
1763
- const entityId = stmt.getValueString(0);
1764
- const isGuidOrGuidPair = entityId.includes("-");
1765
- return isGuidOrGuidPair
1766
- ? entityId
1767
- : {
1768
- entityId,
1769
- aspectId: stmt.getValueString(1),
1770
- aspectVersion: stmt.getValueString(2),
1771
- aspectKind: stmt.getValueString(3),
1772
- };
1195
+ return {
1196
+ entityId: stmt.getValueString(0),
1197
+ aspectId: stmt.getValueString(1),
1198
+ aspectVersion: stmt.getValueString(2),
1199
+ aspectKind: stmt.getValueString(3),
1200
+ };
1773
1201
  });
1774
- /*
1775
- // TODO: maybe save transformer state resumption state based on target changset and require calls
1776
- // to saveChanges
1777
- if () {
1778
- const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
1779
- const isRelProvenance = targetFedGuid !== undefined;
1780
- const instanceId = isRelProvenance
1781
- ? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
1782
- : "";
1783
- //const classId =
1784
- if (isRelProvenance) {
1785
- }
1786
- }
1787
- */
1788
- const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
1789
- // ignore provenance check if it's null since we can't bind those ids
1202
+ const targetHasCorrectLastProvenance =
1203
+ // ignore provenance check if it's null since we can't bind those ids
1204
+ !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
1790
1205
  !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
1791
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
1792
1206
  this.provenanceDb.withPreparedStatement(`
1793
1207
  SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
1794
1208
  WHERE Scope.Id=:scopeId
@@ -1829,13 +1243,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1829
1243
  this.context.loadStateFromDb(db);
1830
1244
  this.importer.loadStateFromJson(state.importerState);
1831
1245
  this.exporter.loadStateFromJson(state.exporterState);
1832
- this._elementsWithExplicitlyTrackedProvenance = core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
1833
1246
  this.loadAdditionalStateJson(state.additionalState);
1834
1247
  }
1835
1248
  /**
1836
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
1837
- * from the original changeset
1838
- *
1839
1249
  * Return a new transformer instance with the same remappings state as saved from a previous [[IModelTransformer.saveStateToFile]] call.
1840
1250
  * This allows you to "resume" an iModel transformation, you will have to call [[IModelTransformer.processChanges]]/[[IModelTransformer.processAll]]
1841
1251
  * again but the remapping state will cause already mapped elements to be skipped.
@@ -1880,7 +1290,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1880
1290
  const jsonState = {
1881
1291
  transformerClass: this.constructor.name,
1882
1292
  options: this._options,
1883
- explicitlyTrackedElements: core_bentley_1.CompressedId64Set.compressSet(this._elementsWithExplicitlyTrackedProvenance),
1884
1293
  importerState: this.importer.saveStateToJson(),
1885
1294
  exporterState: this.exporter.saveStateToJson(),
1886
1295
  additionalState: this.getAdditionalStateJson(),
@@ -1890,9 +1299,8 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1890
1299
  throw Error("Failed to create the js state table in the state database");
1891
1300
  if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`
1892
1301
  CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
1893
- -- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
1302
+ -- because we cannot bind the invalid id which we use for our null state, we actually store the id as a hex string
1894
1303
  entityId TEXT,
1895
- -- the following are only valid if the above entityId is a hex id representation
1896
1304
  aspectId TEXT,
1897
1305
  aspectVersion TEXT,
1898
1306
  aspectKind TEXT
@@ -1906,20 +1314,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1906
1314
  throw Error("Failed to insert options into the state database");
1907
1315
  });
1908
1316
  db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
1909
- const lastProvenanceEntityInfo = this._lastProvenanceEntityInfo;
1910
- stmt.bindString(1, lastProvenanceEntityInfo?.entityId ?? this._lastProvenanceEntityInfo);
1911
- stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
1912
- stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
1913
- stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
1317
+ stmt.bindString(1, this._lastProvenanceEntityInfo.entityId);
1318
+ stmt.bindString(2, this._lastProvenanceEntityInfo.aspectId);
1319
+ stmt.bindString(3, this._lastProvenanceEntityInfo.aspectVersion);
1320
+ stmt.bindString(4, this._lastProvenanceEntityInfo.aspectKind);
1914
1321
  if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
1915
1322
  throw Error("Failed to insert options into the state database");
1916
1323
  });
1917
1324
  db.saveChanges();
1918
1325
  }
1919
1326
  /**
1920
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
1921
- * from the original changeset
1922
- *
1923
1327
  * Save the state of the active transformation to a file path, if a file at the path already exists, it will be overwritten
1924
1328
  * This state can be used by [[IModelTransformer.resumeTransformation]] to resume a transformation from this point.
1925
1329
  * The serialization format is a custom sqlite database.
@@ -1941,54 +1345,26 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1941
1345
  db.closeDb();
1942
1346
  }
1943
1347
  }
1944
- async processChanges(optionsOrAccessToken, startChangesetId) {
1945
- this._isSynchronization = true;
1946
- // FIXME: we used to validateScopeProvenance... does initing it cover that?
1947
- this.initScopeProvenance();
1948
- const args = typeof optionsOrAccessToken === "string"
1348
+ async processChanges(accessTokenOrArgs, startChangesetId) {
1349
+ core_bentley_1.Logger.logTrace(loggerCategory, "processChanges()");
1350
+ this.logSettings();
1351
+ this.validateScopeProvenance();
1352
+ const options = typeof accessTokenOrArgs === "string"
1949
1353
  ? {
1950
- accessToken: optionsOrAccessToken,
1951
- startChangeset: startChangesetId
1952
- ? { id: startChangesetId }
1953
- : { index: this._synchronizationVersion.index + 1 },
1354
+ accessToken: accessTokenOrArgs,
1355
+ startChangeset: startChangesetId ? { id: startChangesetId } : this.sourceDb.changeset,
1356
+ changedInstanceIds: undefined,
1954
1357
  }
1955
- : optionsOrAccessToken;
1956
- this.logSettings();
1957
- await this.initialize(args);
1958
- // must wait for initialization of synchronization provenance data
1959
- await this.exporter.exportChanges(this.getExportInitOpts(args));
1358
+ : accessTokenOrArgs;
1359
+ await this.initialize(options);
1360
+ await this.exporter.exportChanges(options);
1960
1361
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1362
+ await this.exporter["exportAllAspects"](); // eslint-disable-line @typescript-eslint/dot-notation
1961
1363
  if (this._options.optimizeGeometry)
1962
1364
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
1963
1365
  this.importer.computeProjectExtents();
1964
1366
  this.finalizeTransformation();
1965
1367
  }
1966
- /** Changeset data must be initialized in order to build correct changeOptions.
1967
- * Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
1968
- */
1969
- getExportInitOpts(opts) {
1970
- if (!this._isSynchronization)
1971
- return {};
1972
- return {
1973
- accessToken: opts.accessToken,
1974
- ...this._changesetRanges
1975
- ? { changesetRanges: this._changesetRanges }
1976
- : opts.startChangeset
1977
- ? { startChangeset: opts.startChangeset }
1978
- : { startChangeset: { index: this._synchronizationVersion.index + 1 } },
1979
- };
1980
- }
1981
- /** Combine an array of source elements into a single target element.
1982
- * All source and target elements must be created before calling this method.
1983
- * The "combine" operation is a remap and no properties from the source elements will be exported into the target
1984
- * and provenance will be explicitly tracked by ExternalSourceAspects
1985
- */
1986
- combineElements(sourceElementIds, targetElementId) {
1987
- for (const elementId of sourceElementIds) {
1988
- this.context.remapElement(elementId, targetElementId);
1989
- this._elementsWithExplicitlyTrackedProvenance.add(elementId);
1990
- }
1991
- }
1992
1368
  }
1993
1369
  exports.IModelTransformer = IModelTransformer;
1994
1370
  /** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
@@ -2019,7 +1395,6 @@ class TemplateModelCloner extends IModelTransformer {
2019
1395
  * @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
2020
1396
  */
2021
1397
  async placeTemplate3d(sourceTemplateModelId, targetModelId, placement) {
2022
- await this.initialize();
2023
1398
  this.context.remapElement(sourceTemplateModelId, targetModelId);
2024
1399
  this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(placement.origin, placement.angles.toMatrix3d());
2025
1400
  this._sourceIdToTargetIdMap = new Map();
@@ -2040,7 +1415,6 @@ class TemplateModelCloner extends IModelTransformer {
2040
1415
  * @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
2041
1416
  */
2042
1417
  async placeTemplate2d(sourceTemplateModelId, targetModelId, placement) {
2043
- await this.initialize();
2044
1418
  this.context.remapElement(sourceTemplateModelId, targetModelId);
2045
1419
  this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(core_geometry_1.Point3d.createFrom(placement.origin), placement.rotation);
2046
1420
  this._sourceIdToTargetIdMap = new Map();
@@ -2077,12 +1451,16 @@ class TemplateModelCloner extends IModelTransformer {
2077
1451
  const targetElementProps = super.onTransformElement(sourceElement);
2078
1452
  targetElementProps.federationGuid = core_bentley_1.Guid.createValue(); // clone from template should create a new federationGuid
2079
1453
  targetElementProps.code = core_common_1.Code.createEmpty(); // clone from template should not maintain codes
2080
- if (sourceElement instanceof core_backend_1.GeometricElement) {
2081
- const is3d = sourceElement instanceof core_backend_1.GeometricElement3d;
2082
- const placementClass = is3d ? core_common_1.Placement3d : core_common_1.Placement2d;
2083
- const placement = (placementClass).fromJSON(targetElementProps.placement);
1454
+ if (sourceElement instanceof core_backend_1.GeometricElement3d) {
1455
+ const placement = core_common_1.Placement3d.fromJSON(targetElementProps.placement);
1456
+ if (placement.isValid) {
1457
+ placement.multiplyTransform(this._transform3d);
1458
+ targetElementProps.placement = placement;
1459
+ }
1460
+ }
1461
+ else if (sourceElement instanceof core_backend_1.GeometricElement2d) {
1462
+ const placement = core_common_1.Placement2d.fromJSON(targetElementProps.placement);
2084
1463
  if (placement.isValid) {
2085
- nodeAssert(this._transform3d);
2086
1464
  placement.multiplyTransform(this._transform3d);
2087
1465
  targetElementProps.placement = placement;
2088
1466
  }
@@ -2092,17 +1470,4 @@ class TemplateModelCloner extends IModelTransformer {
2092
1470
  }
2093
1471
  }
2094
1472
  exports.TemplateModelCloner = TemplateModelCloner;
2095
- function queryElemFedGuid(db, elemId) {
2096
- return db.withPreparedStatement(`
2097
- SELECT FederationGuid
2098
- FROM bis.Element
2099
- WHERE ECInstanceId=?
2100
- `, (stmt) => {
2101
- stmt.bindId(1, elemId);
2102
- (0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
2103
- const result = stmt.getValue(0).getGuid();
2104
- (0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_DONE);
2105
- return result;
2106
- });
2107
- }
2108
1473
  //# sourceMappingURL=IModelTransformer.js.map