@itwin/imodel-transformer 0.1.13 → 0.1.14-fedguidopt.1

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.
@@ -22,6 +22,7 @@ const PendingReferenceMap_1 = require("./PendingReferenceMap");
22
22
  const EntityMap_1 = require("./EntityMap");
23
23
  const IModelCloneContext_1 = require("./IModelCloneContext");
24
24
  const EntityUnifier_1 = require("./EntityUnifier");
25
+ const Algo_1 = require("./Algo");
25
26
  const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelTransformer;
26
27
  const nullLastProvenanceEntityInfo = {
27
28
  entityId: core_bentley_1.Id64.invalid,
@@ -94,6 +95,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
94
95
  get targetScopeElementId() {
95
96
  return this._options.targetScopeElementId;
96
97
  }
98
+ get _isReverseSynchronization() {
99
+ return this._isSynchronization && this._options.isReverseSynchronization;
100
+ }
101
+ get _isForwardSynchronization() {
102
+ return this._isSynchronization && !this._options.isReverseSynchronization;
103
+ }
97
104
  /** The element classes that are considered to define provenance in the iModel */
98
105
  static get provenanceElementClasses() {
99
106
  return [core_backend_1.FolderLink, core_backend_1.SynchronizationConfigLink, core_backend_1.ExternalSource, core_backend_1.ExternalSourceAttachment];
@@ -114,12 +121,38 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
114
121
  this._pendingReferences = new PendingReferenceMap_1.PendingReferenceMap();
115
122
  /** map of partially committed entities to their partial commit progress */
116
123
  this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
124
+ this._isSynchronization = false;
125
+ this._changesetRanges = undefined;
126
+ // FIXME: add test using this
127
+ /**
128
+ * Previously the transformer would insert provenance always pointing to the "target" relationship.
129
+ * It should (and now by default does) instead insert provenance pointing to the provenanceSource
130
+ * SEE: https://github.com/iTwin/imodel-transformer/issues/54
131
+ * This exists only to facilitate testing that the transformer can handle the older, flawed method
132
+ */
133
+ this._forceOldRelationshipProvenanceMethod = false;
134
+ /** NOTE: the json properties must be converted to string before insertion */
135
+ this._targetScopeProvenanceProps = undefined;
136
+ /**
137
+ * Index of the changeset that the transformer was at when the transformation begins (was constructed).
138
+ * Used to determine at the end which changesets were part of a synchronization.
139
+ */
140
+ this._startingTargetChangesetIndex = undefined;
141
+ this._cachedSynchronizationVersion = undefined;
142
+ this._targetClassNameToClassIdCache = new Map();
143
+ // if undefined, it can be initialized by calling [[this._cacheSourceChanges]]
144
+ this._hasElementChangedCache = undefined;
145
+ this._deletedSourceRelationshipData = undefined;
117
146
  this._yieldManager = new core_bentley_1.YieldManager();
118
147
  /** The directory where schemas will be exported, a random temporary directory */
119
148
  this._schemaExportDir = path.join(core_backend_1.KnownLocations.tmpdir, core_bentley_1.Guid.createValue());
120
149
  this._longNamedSchemasMap = new Map();
121
150
  /** state to prevent reinitialization, @see [[initialize]] */
122
151
  this._initialized = false;
152
+ /** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
153
+ this._changeSummaryIds = undefined;
154
+ this._sourceChangeDataState = "uninited";
155
+ /** previous provenance, either a federation guid, a `${sourceFedGuid}/${targetFedGuid}` pair, or required aspect props */
123
156
  this._lastProvenanceEntityInfo = nullLastProvenanceEntityInfo;
124
157
  // initialize IModelTransformOptions
125
158
  this._options = {
@@ -167,6 +200,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
167
200
  this.targetDb = this.importer.targetDb;
168
201
  // create the IModelCloneContext, it must be initialized later
169
202
  this.context = new IModelCloneContext_1.IModelCloneContext(this.sourceDb, this.targetDb);
203
+ this._startingTargetChangesetIndex = this.targetDb?.changeset.index;
170
204
  }
171
205
  /** Dispose any native resources associated with this IModelTransformer. */
172
206
  dispose() {
@@ -195,8 +229,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
195
229
  get provenanceDb() {
196
230
  return this._options.isReverseSynchronization ? this.sourceDb : this.targetDb;
197
231
  }
198
- /** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
232
+ /** Return the IModelDb where IModelTransformer will NOT store its provenance.
233
+ * @note This will be [[sourceDb]] except when it is a reverse synchronization. In that case it be [[targetDb]].
234
+ */
235
+ get provenanceSourceDb() {
236
+ return this._options.isReverseSynchronization ? this.targetDb : this.sourceDb;
237
+ }
199
238
  initElementProvenance(sourceElementId, targetElementId) {
239
+ // FIXME: deprecate isReverseSync option and instead detect from targetScopeElement provenance
200
240
  const elementId = this._options.isReverseSynchronization ? sourceElementId : targetElementId;
201
241
  const aspectIdentifier = this._options.isReverseSynchronization ? targetElementId : sourceElementId;
202
242
  const aspectProps = {
@@ -215,32 +255,89 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
215
255
  * The ECInstanceId of the relationship in the target iModel will be stored in the JsonProperties of the ExternalSourceAspect.
216
256
  */
217
257
  initRelationshipProvenance(sourceRelationship, targetRelInstanceId) {
218
- const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, targetRelInstanceId);
219
- const elementId = this._options.isReverseSynchronization ? sourceRelationship.sourceId : targetRelationship.sourceId;
258
+ const elementId = this._options.isReverseSynchronization
259
+ ? sourceRelationship.sourceId
260
+ : this.targetDb.withPreparedStatement("SELECT SourceECInstanceId FROM Bis.ElementRefersToElements WHERE ECInstanceId=?", (stmt) => {
261
+ stmt.bindId(1, targetRelInstanceId);
262
+ nodeAssert(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
263
+ return stmt.getValue(0).getId();
264
+ });
220
265
  const aspectIdentifier = this._options.isReverseSynchronization ? targetRelInstanceId : sourceRelationship.id;
266
+ const jsonProperties = this._forceOldRelationshipProvenanceMethod
267
+ ? { targetRelInstanceId }
268
+ : { provenanceRelInstanceId: this._isReverseSynchronization
269
+ ? sourceRelationship.id
270
+ : targetRelInstanceId,
271
+ };
221
272
  const aspectProps = {
222
273
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
223
274
  element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
224
275
  scope: { id: this.targetScopeElementId },
225
276
  identifier: aspectIdentifier,
226
277
  kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
227
- jsonProperties: JSON.stringify({ targetRelInstanceId }),
278
+ jsonProperties: JSON.stringify(jsonProperties),
228
279
  };
229
- aspectProps.id = this.queryExternalSourceAspectId(aspectProps);
230
280
  return aspectProps;
231
281
  }
232
- validateScopeProvenance() {
282
+ /** the changeset in the scoping element's source version found for this transformation
283
+ * @note: the version depends on whether this is a reverse synchronization or not, as
284
+ * it is stored separately for both synchronization directions
285
+ * @note: empty string and -1 for changeset and index if it has never been transformed
286
+ */
287
+ get _synchronizationVersion() {
288
+ if (!this._cachedSynchronizationVersion) {
289
+ nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps was not set yet");
290
+ const version = this._options.isReverseSynchronization
291
+ ? this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion
292
+ : this._targetScopeProvenanceProps.version;
293
+ nodeAssert(version !== undefined, "no version contained in target scope");
294
+ const [id, index] = version === ""
295
+ ? ["", -1]
296
+ : version.split(";");
297
+ this._cachedSynchronizationVersion = { index: Number(index), id };
298
+ nodeAssert(!Number.isNaN(this._cachedSynchronizationVersion.index), "bad parse: invalid index in version");
299
+ }
300
+ return this._cachedSynchronizationVersion;
301
+ }
302
+ /**
303
+ * Make sure there are no conflicting other scope-type external source aspects on the *target scope element*,
304
+ * If there are none at all, insert one, then this must be a first synchronization.
305
+ * @returns the last synced version (changesetId) on the target scope's external source aspect,
306
+ * if this was a [BriefcaseDb]($backend)
307
+ */
308
+ initScopeProvenance() {
233
309
  const aspectProps = {
310
+ id: undefined,
311
+ version: undefined,
234
312
  classFullName: core_backend_1.ExternalSourceAspect.classFullName,
235
313
  element: { id: this.targetScopeElementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
236
314
  scope: { id: core_common_1.IModel.rootSubjectId },
237
- identifier: this._options.isReverseSynchronization ? this.targetDb.iModelId : this.sourceDb.iModelId,
315
+ identifier: this.provenanceSourceDb.iModelId,
238
316
  kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
317
+ jsonProperties: undefined,
239
318
  };
240
- aspectProps.id = this.queryExternalSourceAspectId(aspectProps); // this query includes "identifier"
319
+ // FIXME: handle older transformed iModels which do NOT have the version
320
+ // or reverseSyncVersion set correctly
321
+ const externalSource = this.queryScopeExternalSource(aspectProps, { getJsonProperties: true }); // this query includes "identifier"
322
+ aspectProps.id = externalSource.aspectId;
323
+ aspectProps.version = externalSource.version;
324
+ aspectProps.jsonProperties = externalSource.jsonProperties ? JSON.parse(externalSource.jsonProperties) : {};
241
325
  if (undefined === aspectProps.id) {
326
+ aspectProps.version = ""; // empty since never before transformed. Will be updated in [[finalizeTransformation]]
327
+ aspectProps.jsonProperties = {
328
+ pendingReverseSyncChangesetIndices: [],
329
+ pendingSyncChangesetIndices: [],
330
+ reverseSyncVersion: "", // empty since never before transformed. Will be updated in first reverse sync
331
+ };
242
332
  // this query does not include "identifier" to find possible conflicts
243
- const sql = `SELECT ECInstanceId FROM ${core_backend_1.ExternalSourceAspect.classFullName} WHERE Element.Id=:elementId AND Scope.Id=:scopeId AND Kind=:kind LIMIT 1`;
333
+ const sql = `
334
+ SELECT ECInstanceId
335
+ FROM ${core_backend_1.ExternalSourceAspect.classFullName}
336
+ WHERE Element.Id=:elementId
337
+ AND Scope.Id=:scopeId
338
+ AND Kind=:kind
339
+ LIMIT 1
340
+ `;
244
341
  const hasConflictingScope = this.provenanceDb.withPreparedStatement(sql, (statement) => {
245
342
  statement.bindId("elementId", aspectProps.element.id);
246
343
  statement.bindId("scopeId", aspectProps.scope.id); // this scope.id can never be invalid, we create it above
@@ -251,42 +348,113 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
251
348
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidId, "Provenance scope conflict");
252
349
  }
253
350
  if (!this._options.noProvenance) {
254
- this.provenanceDb.elements.insertAspect(aspectProps);
351
+ this.provenanceDb.elements.insertAspect({
352
+ ...aspectProps,
353
+ jsonProperties: JSON.stringify(aspectProps.jsonProperties),
354
+ });
255
355
  }
256
356
  }
357
+ this._targetScopeProvenanceProps = aspectProps;
257
358
  }
258
- queryExternalSourceAspectId(aspectProps) {
259
- 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`;
359
+ /**
360
+ * @returns the id and version of an aspect with the given element, scope, kind, and identifier
361
+ * May also return a reverseSyncVersion from json properties if requested
362
+ */
363
+ queryScopeExternalSource(aspectProps, { getJsonProperties = false } = {}) {
364
+ const sql = `
365
+ SELECT ECInstanceId, Version
366
+ ${getJsonProperties ? ", JsonProperties" : ""}
367
+ FROM ${core_backend_1.ExternalSourceAspect.classFullName}
368
+ WHERE Element.Id=:elementId
369
+ AND Scope.Id=:scopeId
370
+ AND Kind=:kind
371
+ AND Identifier=:identifier
372
+ LIMIT 1
373
+ `;
374
+ const emptyResult = { aspectId: undefined, version: undefined, jsonProperties: undefined };
260
375
  return this.provenanceDb.withPreparedStatement(sql, (statement) => {
261
376
  statement.bindId("elementId", aspectProps.element.id);
262
377
  if (aspectProps.scope === undefined)
263
- return undefined; // return undefined instead of binding an invalid id
378
+ return emptyResult; // return undefined instead of binding an invalid id
264
379
  statement.bindId("scopeId", aspectProps.scope.id);
265
380
  statement.bindString("kind", aspectProps.kind);
266
381
  statement.bindString("identifier", aspectProps.identifier);
267
- return (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) ? statement.getValue(0).getId() : undefined;
382
+ if (core_bentley_1.DbResult.BE_SQLITE_ROW !== statement.step())
383
+ return emptyResult;
384
+ const aspectId = statement.getValue(0).getId();
385
+ const version = statement.getValue(1).getString();
386
+ const jsonProperties = getJsonProperties ? statement.getValue(2).getString() : undefined;
387
+ return { aspectId, version, jsonProperties };
268
388
  });
269
389
  }
270
- /** Iterate all matching ExternalSourceAspects in the provenance iModel (target unless reverse sync) and call a function for each one. */
390
+ /**
391
+ * Iterate all matching ExternalSourceAspects in the provenance iModel (target unless reverse sync) and call a function for each one.
392
+ * @note provenance is done by federation guids where possible
393
+ */
271
394
  forEachTrackedElement(fn) {
395
+ // FIXME: do we need an alternative for in-iModel transforms?
396
+ if (!this.context.isBetweenIModels)
397
+ return;
272
398
  if (!this.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
273
399
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
274
400
  }
275
- const sql = `SELECT Identifier,Element.Id FROM ${core_backend_1.ExternalSourceAspect.classFullName} WHERE Scope.Id=:scopeId AND Kind=:kind`;
276
- this.provenanceDb.withPreparedStatement(sql, (statement) => {
277
- statement.bindId("scopeId", this.targetScopeElementId);
278
- statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
279
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
280
- const aspectIdentifier = statement.getValue(0).getString(); // ExternalSourceAspect.Identifier is of type string
281
- const elementId = statement.getValue(1).getId();
282
- if (this._options.isReverseSynchronization) {
283
- fn(elementId, aspectIdentifier); // provenance coming from the sourceDb
401
+ // query for provenanceDb
402
+ const provenanceContainerQuery = `
403
+ SELECT e.ECInstanceId, FederationGuid, esa.Identifier as AspectIdentifier
404
+ FROM bis.Element e
405
+ LEFT JOIN bis.ExternalSourceAspect esa ON e.ECInstanceId=esa.Element.Id
406
+ WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements
407
+ AND ((Scope.Id IS NULL AND KIND IS NULL) OR (Scope.Id=:scopeId AND Kind=:kind))
408
+ ORDER BY FederationGuid
409
+ `;
410
+ // query for nonProvenanceDb, the source to which the provenance is referring
411
+ const provenanceSourceQuery = `
412
+ SELECT e.ECInstanceId, FederationGuid
413
+ FROM bis.Element e
414
+ WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements
415
+ ORDER BY FederationGuid
416
+ `;
417
+ // iterate through sorted list of fed guids from both dbs to get the intersection
418
+ // NOTE: if we exposed the native attach database support,
419
+ // we could get the intersection of fed guids in one query, not sure if it would be faster
420
+ // OR we could do a raw sqlite query...
421
+ this.provenanceSourceDb.withStatement(provenanceSourceQuery, (sourceStmt) => this.provenanceDb.withStatement(provenanceContainerQuery, (containerStmt) => {
422
+ containerStmt.bindId("scopeId", this.targetScopeElementId);
423
+ containerStmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
424
+ if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
425
+ return;
426
+ let sourceRow = sourceStmt.getRow();
427
+ if (containerStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
428
+ return;
429
+ let containerRow = containerStmt.getRow();
430
+ const runFnInProvDirection = (sourceId, targetId) => this._options.isReverseSynchronization ? fn(sourceId, targetId) : fn(targetId, sourceId);
431
+ // NOTE: these comparisons rely upon the lowercase of the guid,
432
+ // and the fact that '0' < '9' < a' < 'f' in ascii/utf8
433
+ while (true) {
434
+ const currSourceRow = sourceRow, currContainerRow = containerRow;
435
+ if (currSourceRow.federationGuid !== undefined
436
+ && currContainerRow.federationGuid !== undefined
437
+ && currSourceRow.federationGuid === currContainerRow.federationGuid) {
438
+ fn(sourceRow.id, containerRow.id);
284
439
  }
285
- else {
286
- fn(aspectIdentifier, elementId); // provenance coming from the targetDb
440
+ if (currContainerRow.federationGuid === undefined
441
+ || (currSourceRow.federationGuid !== undefined
442
+ && currSourceRow.federationGuid >= currContainerRow.federationGuid)) {
443
+ if (containerStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
444
+ return;
445
+ containerRow = containerStmt.getRow();
446
+ }
447
+ if (currSourceRow.federationGuid === undefined
448
+ || (currContainerRow.federationGuid !== undefined
449
+ && currSourceRow.federationGuid <= currContainerRow.federationGuid)) {
450
+ if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
451
+ return;
452
+ sourceRow = sourceStmt.getRow();
287
453
  }
454
+ if (!currContainerRow.federationGuid && currContainerRow.aspectIdentifier)
455
+ runFnInProvDirection(currContainerRow.id, currContainerRow.aspectIdentifier);
288
456
  }
289
- });
457
+ }));
290
458
  }
291
459
  /** Initialize the source to target Element mapping from ExternalSourceAspects in the target iModel.
292
460
  * @note This method is called from all `process*` functions and should never need to be called directly.
@@ -299,99 +467,326 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
299
467
  this.context.remapElement(sourceElementId, targetElementId);
300
468
  });
301
469
  if (args)
302
- return this.remapDeletedSourceElements(args);
470
+ return this.remapDeletedSourceEntities();
303
471
  }
304
- /** When processing deleted elements in a reverse synchronization, the [[provenanceDb]] (usually a branch iModel) has already
305
- * deleted the [ExternalSourceAspect]($backend)s that tell us which elements in the reverse synchronization target (usually
306
- * a master iModel) should be deleted. We must use the changesets to get the values of those before they were deleted.
472
+ /**
473
+ * Scan changesets for deleted entities, if in a reverse synchronization, provenance has
474
+ * already been deleted, so we must scan for that as well.
307
475
  */
308
- async remapDeletedSourceElements(args) {
476
+ async remapDeletedSourceEntities() {
309
477
  // we need a connected iModel with changes to remap elements with deletions
310
- if (this.sourceDb.iTwinId === undefined)
478
+ const notConnectedModel = this.sourceDb.iTwinId === undefined;
479
+ const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
480
+ if (notConnectedModel || noChanges)
311
481
  return;
312
- try {
313
- const startChangesetId = args.startChangesetId ?? this.sourceDb.changeset.id;
314
- const endChangesetId = this.sourceDb.changeset.id;
315
- const [firstChangesetIndex, endChangesetIndex] = await Promise.all([startChangesetId, endChangesetId]
316
- .map(async (id) => core_backend_1.IModelHost.hubAccess
317
- .queryChangeset({
318
- iModelId: this.sourceDb.iModelId,
319
- changeset: { id },
320
- accessToken: args.accessToken,
321
- })
322
- .then((changeset) => changeset.index)));
323
- const changesetIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
324
- accessToken: args.accessToken,
325
- iModelId: this.sourceDb.iModelId,
326
- iTwinId: this.sourceDb.iTwinId,
327
- range: { first: firstChangesetIndex, end: endChangesetIndex },
328
- });
329
- core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
330
- for (const changesetId of changesetIds) {
331
- this.sourceDb.withPreparedStatement(`
332
- SELECT esac.Element.Id, esac.Identifier
333
- FROM ecchange.change.InstanceChange ic
334
- JOIN BisCore.ExternalSourceAspect.Changes(:changesetId, 'BeforeDelete') esac
335
- ON ic.ChangedInstance.Id=esac.ECInstanceId
336
- WHERE ic.OpCode=:opcode
337
- AND ic.Summary.Id=:changesetId
338
- AND esac.Scope.Id=:targetScopeElementId
339
- -- not yet documented ecsql feature to check class id
340
- AND ic.ChangedInstance.ClassId IS (ONLY BisCore.ExternalSourceAspect)
341
- `, (stmt) => {
342
- stmt.bindInteger("opcode", core_common_1.ChangeOpCode.Delete);
343
- stmt.bindInteger("changesetId", changesetId);
344
- stmt.bindInteger("targetScopeElementId", this.targetScopeElementId);
345
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
346
- const targetId = stmt.getValue(0).getId();
347
- const sourceId = stmt.getValue(1).getString(); // BisCore.ExternalSourceAspect.Identifier stores a hex Id64String
348
- // TODO: maybe delete and don't just remap
349
- this.context.remapElement(targetId, sourceId);
482
+ this._deletedSourceRelationshipData = new Map();
483
+ nodeAssert(this._changeSummaryIds, "change summaries should be initialized before we get here");
484
+ nodeAssert(this._changeSummaryIds.length > 0, "change summaries should have at least one");
485
+ // optimization: if we have provenance, use it to avoid more querying later
486
+ // eventually when itwin.js supports attaching a second iModelDb in JS,
487
+ // this won't have to be a conditional part of the query, and we can always have it by attaching
488
+ const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
489
+ const deletedEntitySql = `
490
+ SELECT
491
+ 1 AS IsElemNotRel,
492
+ ic.ChangedInstance.Id AS InstanceId,
493
+ NULL AS InstId2, -- need these columns for relationship ends in the unioned query
494
+ NULL AS InstId3,
495
+ ec.FederationGuid AS FedGuid,
496
+ NULL AS FedGuid2,
497
+ ic.ChangedInstance.ClassId AS ClassId
498
+ ${queryCanAccessProvenance ? `
499
+ , coalesce(esa.Identifier, esac.Identifier) AS Identifier1
500
+ , NULL AS Identifier2
501
+ ` : ""}
502
+ FROM ecchange.change.InstanceChange ic
503
+ LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') ec
504
+ ON ic.ChangedInstance.Id=ec.ECInstanceId
505
+ ${queryCanAccessProvenance ? `
506
+ LEFT JOIN bis.ExternalSourceAspect esa
507
+ ON ec.ECInstanceId=esa.Element.Id
508
+ LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') esac
509
+ ON ec.ECInstanceId=esac.Element.Id
510
+ ` : ""}
511
+ WHERE ic.OpCode=:opDelete
512
+ AND ic.Summary.Id=:changeSummaryId
513
+ AND ic.ChangedInstance.ClassId IS (BisCore.Element)
514
+ ${queryCanAccessProvenance ? `
515
+ AND (esa.Scope.Id=:targetScopeElement OR esa.Scope.Id IS NULL)
516
+ AND (esa.Kind='Element' OR esa.Kind IS NULL)
517
+ AND (esac.Scope.Id=:targetScopeElement OR esac.Scope.Id IS NULL)
518
+ AND (esac.Kind='Element' OR esac.Kind IS NULL)
519
+ ` : ""}
520
+
521
+ UNION ALL
522
+
523
+ SELECT
524
+ 0 AS IsElemNotRel,
525
+ ic.ChangedInstance.Id AS InstanceId,
526
+ coalesce(se.ECInstanceId, sec.ECInstanceId) AS InstId2,
527
+ coalesce(te.ECInstanceId, tec.ECInstanceId) AS InstId3,
528
+ coalesce(se.FederationGuid, sec.FederationGuid) AS FedGuid1,
529
+ coalesce(te.FederationGuid, tec.FederationGuid) AS FedGuid2,
530
+ ic.ChangedInstance.ClassId AS ClassId
531
+ ${queryCanAccessProvenance ? `
532
+ , coalesce(sesa.Identifier, sesac.Identifier) AS Identifier1
533
+ , coalesce(tesa.Identifier, tesac.Identifier) AS Identifier2
534
+ ` : ""}
535
+ FROM ecchange.change.InstanceChange ic
536
+ LEFT JOIN bis.ElementRefersToElements.Changes(:changeSummaryId, 'BeforeDelete') ertec
537
+ ON ic.ChangedInstance.Id=ertec.ECInstanceId
538
+ -- FIXME: test a deletion of both an element and a relationship at the same time
539
+ LEFT JOIN bis.Element se
540
+ ON se.ECInstanceId=ertec.SourceECInstanceId
541
+ LEFT JOIN bis.Element te
542
+ ON te.ECInstanceId=ertec.TargetECInstanceId
543
+ LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') sec
544
+ ON sec.ECInstanceId=ertec.SourceECInstanceId
545
+ LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') tec
546
+ ON tec.ECInstanceId=ertec.TargetECInstanceId
547
+ ${queryCanAccessProvenance ? `
548
+ -- NOTE: need to join on both se/te and sec/tec incase the element was deleted
549
+ LEFT JOIN bis.ExternalSourceAspect sesa
550
+ ON se.ECInstanceId=sesa.Element.Id -- don't use *esac*.Identifier because it's a string
551
+ LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') sesac
552
+ ON sec.ECInstanceId=sesac.Element.Id
553
+ LEFT JOIN bis.ExternalSourceAspect tesa
554
+ ON te.ECInstanceId=tesa.Element.Id
555
+ LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') tesac
556
+ ON tec.ECInstanceId=tesac.Element.Id
557
+ ` : ""}
558
+ WHERE ic.OpCode=:opDelete
559
+ AND ic.Summary.Id=:changeSummaryId
560
+ AND ic.ChangedInstance.ClassId IS (BisCore.ElementRefersToElements)
561
+ ${queryCanAccessProvenance ? `
562
+ AND (sesa.Scope.Id=:targetScopeElement OR sesa.Scope.Id IS NULL)
563
+ AND (sesa.Kind='Relationship' OR sesa.Kind IS NULL)
564
+ AND (sesac.Scope.Id=:targetScopeElement OR sesac.Scope.Id IS NULL)
565
+ AND (sesac.Kind='Relationship' OR sesac.Kind IS NULL)
566
+ AND (tesa.Scope.Id=:targetScopeElement OR tesa.Scope.Id IS NULL)
567
+ AND (tesa.Kind='Relationship' OR tesa.Kind IS NULL)
568
+ AND (tesac.Scope.Id=:targetScopeElement OR tesac.Scope.Id IS NULL)
569
+ AND (tesac.Kind='Relationship' OR tesac.Kind IS NULL)
570
+ ` : ""}
571
+ `;
572
+ for (const changeSummaryId of this._changeSummaryIds) {
573
+ // FIXME: test deletion in both forward and reverse sync
574
+ this.sourceDb.withPreparedStatement(deletedEntitySql, (stmt) => {
575
+ stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
576
+ if (queryCanAccessProvenance)
577
+ stmt.bindId("targetScopeElement", this.targetScopeElementId);
578
+ stmt.bindId("changeSummaryId", changeSummaryId);
579
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
580
+ const isElemNotRel = stmt.getValue(0).getBoolean();
581
+ const instId = stmt.getValue(1).getId();
582
+ if (isElemNotRel) {
583
+ const sourceElemFedGuid = stmt.getValue(4).getGuid();
584
+ // "Identifier" is a string, so null value returns '' which doesn't work with ??, and I don't like ||
585
+ let identifierValue;
586
+ // TODO: if I could attach the second db, will probably be much faster to get target id
587
+ // as part of the whole query rather than with _queryElemIdByFedGuid
588
+ const targetId = (queryCanAccessProvenance
589
+ && (identifierValue = stmt.getValue(7))
590
+ && !identifierValue.isNull
591
+ && identifierValue.getString())
592
+ // maybe batching these queries would perform better but we should
593
+ // try to attach the second db and query both together anyway
594
+ || (sourceElemFedGuid && this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid))
595
+ // FIXME: describe why it's safe to assume nothing has been deleted in provenanceDb
596
+ || this._queryProvenanceForElement(instId);
597
+ // since we are processing one changeset at a time, we can see local source deletes
598
+ // of entities that were never synced and can be safely ignored
599
+ const deletionNotInTarget = !targetId;
600
+ if (deletionNotInTarget)
601
+ continue;
602
+ this.context.remapElement(instId, targetId);
350
603
  }
351
- });
352
- }
604
+ else { // is deleted relationship
605
+ const classFullName = stmt.getValue(6).getClassNameForClassId();
606
+ const [sourceIdInTarget, targetIdInTarget] = [
607
+ { guidColumn: 4, identifierColumn: 7, isTarget: false },
608
+ { guidColumn: 5, identifierColumn: 8, isTarget: true },
609
+ ].map(({ guidColumn, identifierColumn }) => {
610
+ const fedGuid = stmt.getValue(guidColumn).getGuid();
611
+ let identifierValue;
612
+ return ((queryCanAccessProvenance
613
+ // FIXME: this is really far from idiomatic, try to undo that
614
+ && (identifierValue = stmt.getValue(identifierColumn))
615
+ && !identifierValue.isNull
616
+ && identifierValue.getString())
617
+ // maybe batching these queries would perform better but we should
618
+ // try to attach the second db and query both together anyway
619
+ || (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
620
+ });
621
+ // since we are processing one changeset at a time, we can see local source deletes
622
+ // of entities that were never synced and can be safely ignored
623
+ if (sourceIdInTarget && targetIdInTarget) {
624
+ this._deletedSourceRelationshipData.set(instId, {
625
+ classFullName,
626
+ sourceIdInTarget,
627
+ targetIdInTarget,
628
+ });
629
+ }
630
+ else {
631
+ // FIXME: describe why it's safe to assume nothing has been deleted in provenanceDb
632
+ const relProvenance = this._queryProvenanceForRelationship(instId, {
633
+ classFullName,
634
+ sourceId: stmt.getValue(2).getId(),
635
+ targetId: stmt.getValue(3).getId(),
636
+ });
637
+ if (relProvenance && relProvenance.relationshipId)
638
+ this._deletedSourceRelationshipData.set(instId, {
639
+ classFullName,
640
+ relId: relProvenance.relationshipId,
641
+ provenanceAspectId: relProvenance.aspectId,
642
+ });
643
+ }
644
+ }
645
+ }
646
+ // NEXT: remap sourceId and targetId to target, get provenance there
647
+ // NOTE: it is possible during a forward sync for the target to already have deleted
648
+ // something that the source deleted, in which case we can safely ignore the gone provenance
649
+ });
353
650
  }
354
- finally {
355
- if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
356
- core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
651
+ }
652
+ _queryProvenanceForElement(entityInProvenanceSourceId) {
653
+ return this.provenanceDb.withPreparedStatement(`
654
+ SELECT esa.Element.Id
655
+ FROM Bis.ExternalSourceAspect esa
656
+ WHERE esa.Kind=?
657
+ AND esa.Scope.Id=?
658
+ AND esa.Identifier=?
659
+ `, (stmt) => {
660
+ stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Element);
661
+ stmt.bindId(2, this.targetScopeElementId);
662
+ stmt.bindString(3, entityInProvenanceSourceId);
663
+ if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
664
+ return stmt.getValue(0).getId();
665
+ else
666
+ return undefined;
667
+ });
668
+ }
669
+ _queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
670
+ return this.provenanceDb.withPreparedStatement(`
671
+ SELECT
672
+ ECInstanceId,
673
+ JSON_EXTRACT(JsonProperties, '$.targetRelInstanceId'),
674
+ JSON_EXTRACT(JsonProperties, '$.provenanceRelInstanceId')
675
+ FROM Bis.ExternalSourceAspect
676
+ WHERE Kind=?
677
+ AND Scope.Id=?
678
+ AND Identifier=?
679
+ `, (stmt) => {
680
+ stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Relationship);
681
+ stmt.bindId(2, this.targetScopeElementId);
682
+ stmt.bindString(3, entityInProvenanceSourceId);
683
+ if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
684
+ return undefined;
685
+ const aspectId = stmt.getValue(0).getId();
686
+ const provenanceRelInstIdVal = stmt.getValue(2);
687
+ const provenanceRelInstanceId = !provenanceRelInstIdVal.isNull
688
+ ? provenanceRelInstIdVal.getString()
689
+ : this._queryTargetRelId(sourceRelInfo);
690
+ return {
691
+ aspectId,
692
+ relationshipId: provenanceRelInstanceId,
693
+ };
694
+ });
695
+ }
696
+ _queryTargetRelId(sourceRelInfo) {
697
+ const targetRelInfo = {
698
+ sourceId: this.context.findTargetElementId(sourceRelInfo.sourceId),
699
+ targetId: this.context.findTargetElementId(sourceRelInfo.targetId),
700
+ };
701
+ if (targetRelInfo.sourceId === undefined || targetRelInfo.targetId === undefined)
702
+ return undefined; // couldn't find an element, rel is invalid or deleted
703
+ return this.targetDb.withPreparedStatement(`
704
+ SELECT ECInstanceId
705
+ FROM bis.ElementRefersToElements
706
+ WHERE SourceECInstanceId=?
707
+ AND TargetECInstanceId=?
708
+ AND ECClassId=?
709
+ `, (stmt) => {
710
+ stmt.bindId(1, targetRelInfo.sourceId);
711
+ stmt.bindId(2, targetRelInfo.targetId);
712
+ stmt.bindId(3, this._targetClassNameToClassId(sourceRelInfo.classFullName));
713
+ if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
714
+ return undefined;
715
+ return stmt.getValue(0).getId();
716
+ });
717
+ }
718
+ _targetClassNameToClassId(classFullName) {
719
+ let classId = this._targetClassNameToClassIdCache.get(classFullName);
720
+ if (classId === undefined) {
721
+ classId = this._getRelClassId(this.targetDb, classFullName);
722
+ this._targetClassNameToClassIdCache.set(classFullName, classId);
357
723
  }
724
+ return classId;
725
+ }
726
+ // NOTE: this doesn't handle remapped element classes,
727
+ // but is only used for relationships rn
728
+ _getRelClassId(db, classFullName) {
729
+ return db.withPreparedStatement(`
730
+ SELECT c.ECInstanceId
731
+ FROM ECDbMeta.ECClassDef c
732
+ JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
733
+ WHERE s.Name=? AND c.Name=?
734
+ `, (stmt) => {
735
+ const [schemaName, className] = classFullName.split(".");
736
+ stmt.bindString(1, schemaName);
737
+ stmt.bindString(2, className);
738
+ if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
739
+ return stmt.getValue(0).getId();
740
+ (0, core_bentley_1.assert)(false, "relationship was not found");
741
+ });
742
+ }
743
+ _queryElemIdByFedGuid(db, fedGuid) {
744
+ return db.withPreparedStatement("SELECT ECInstanceId FROM Bis.Element WHERE FederationGuid=?", (stmt) => {
745
+ stmt.bindGuid(1, fedGuid);
746
+ if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
747
+ return stmt.getValue(0).getId();
748
+ else
749
+ return undefined;
750
+ });
358
751
  }
359
752
  /** Returns `true` if *brute force* delete detections should be run.
360
753
  * @note Not relevant for processChanges when change history is known.
361
754
  */
362
755
  shouldDetectDeletes() {
756
+ // FIXME: all synchronizations should mark this as false
363
757
  if (this._isFirstSynchronization)
364
758
  return false; // not necessary the first time since there are no deletes to detect
365
759
  if (this._options.isReverseSynchronization)
366
760
  return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
367
761
  return true;
368
762
  }
369
- /** Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements in the source iModel.
370
- * @see processChanges
371
- * @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.
763
+ /**
764
+ * Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
765
+ * in the source iModel.
766
+ * @deprecated in 0.1.x. This method is only called during [[processAll]] when the option
767
+ * [[IModelTransformerOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
768
+ * necessary when using [[processChanges]] since changeset information is sufficient.
769
+ * @note you do not need to call this directly unless processing a subset of an iModel.
372
770
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
373
771
  */
374
772
  async detectElementDeletes() {
375
- if (this._options.isReverseSynchronization) {
376
- throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
377
- }
378
- const targetElementsToDelete = [];
379
- this.forEachTrackedElement((sourceElementId, targetElementId) => {
380
- if (undefined === this.sourceDb.elements.tryGetElementProps(sourceElementId)) {
381
- // if the sourceElement is not found, then it must have been deleted, so propagate the delete to the target iModel
382
- targetElementsToDelete.push(targetElementId);
383
- }
384
- });
385
- targetElementsToDelete.forEach((targetElementId) => {
386
- try {
387
- // TODO: make it possible to delete more elements at once to prevent redundant expensive
388
- // element reference scanning
389
- this.importer.deleteElement(targetElementId);
390
- }
391
- catch (err) {
392
- // ignore not found elements, iterative element tree deletion might have already deleted them
393
- if (err.name !== "Not Found")
394
- throw err;
773
+ const sql = `
774
+ SELECT Identifier, Element.Id
775
+ FROM BisCore.ExternalSourceAspect
776
+ WHERE Scope.Id=:scopeId
777
+ AND Kind=:kind
778
+ `;
779
+ nodeAssert(!this._options.isReverseSynchronization, "synchronizations with processChagnes already detect element deletes, don't call detectElementDeletes");
780
+ this.provenanceDb.withPreparedStatement(sql, (stmt) => {
781
+ stmt.bindId("scopeId", this.targetScopeElementId);
782
+ stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
783
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
784
+ // ExternalSourceAspect.Identifier is of type string
785
+ const aspectIdentifier = stmt.getValue(0).getString();
786
+ const targetElemId = stmt.getValue(1).getId();
787
+ const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: `e${aspectIdentifier}` });
788
+ if (wasDeletedInSource)
789
+ this.importer.deleteElement(targetElemId);
395
790
  }
396
791
  });
397
792
  }
@@ -418,24 +813,53 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
418
813
  }
419
814
  return targetElementProps;
420
815
  }
816
+ // FIXME: this is a PoC, see if we minimize memory usage
817
+ _cacheSourceChanges() {
818
+ nodeAssert(this._changeSummaryIds && this._changeSummaryIds.length > 0, "should have changeset data by now");
819
+ this._hasElementChangedCache = new Set();
820
+ const query = `
821
+ SELECT
822
+ ic.ChangedInstance.Id AS InstId
823
+ FROM ecchange.change.InstanceChange ic
824
+ JOIN iModelChange.Changeset imc ON ic.Summary.Id=imc.Summary.Id
825
+ -- FIXME: do relationship entities also need this cache optimization?
826
+ WHERE ic.ChangedInstance.ClassId IS (BisCore.Element)
827
+ AND InVirtualSet(:changeSummaryIds, ic.Summary.Id)
828
+ -- ignore deleted, we take care of those in remapDeletedSourceEntities
829
+ -- include inserted since inserted code-colliding elements should be considered
830
+ -- a change so that the colliding element is exported to the target
831
+ AND ic.OpCode<>:opDelete
832
+ `;
833
+ // there is a single mega-query multi-join+coalescing hack that I used originally to get around
834
+ // only being able to run table.Changes() on one changeset at once, but sqlite only supports up to 64
835
+ // tables in a join. Need to talk to core about .Changes being able to take a set of changesets
836
+ // You can find this version in the `federation-guid-optimization-megaquery` branch
837
+ // I wouldn't use it unless we prove via profiling that it speeds things up significantly
838
+ // And even then let's first try scanning the raw changesets instead of applying them as these queries
839
+ // require
840
+ this.sourceDb.withPreparedStatement(query, (stmt) => {
841
+ stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
842
+ stmt.bindIdSet("changeSummaryIds", this._changeSummaryIds);
843
+ while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
844
+ const instId = stmt.getValue(0).getId();
845
+ this._hasElementChangedCache.add(instId);
846
+ }
847
+ });
848
+ }
421
849
  /** Returns true if a change within sourceElement is detected.
422
850
  * @param sourceElement The Element from the source iModel
423
851
  * @param targetElementId The Element from the target iModel to compare against.
424
852
  * @note A subclass can override this method to provide custom change detection behavior.
425
853
  */
426
- hasElementChanged(sourceElement, targetElementId) {
427
- const sourceAspects = this.targetDb.elements.getAspects(targetElementId, core_backend_1.ExternalSourceAspect.classFullName);
428
- for (const sourceAspect of sourceAspects) {
429
- if (sourceAspect.scope === undefined) // if the scope was lost, we can't correlate so assume it changed
430
- return true;
431
- if (sourceAspect.identifier === sourceElement.id &&
432
- sourceAspect.scope.id === this.targetScopeElementId &&
433
- sourceAspect.kind === core_backend_1.ExternalSourceAspect.Kind.Element) {
434
- const lastModifiedTime = sourceElement.iModel.elements.queryLastModifiedTime(sourceElement.id);
435
- return lastModifiedTime !== sourceAspect.version;
436
- }
437
- }
438
- return true;
854
+ hasElementChanged(sourceElement, _targetElementId) {
855
+ if (this._sourceChangeDataState === "no-changes")
856
+ return false;
857
+ if (this._sourceChangeDataState === "unconnected")
858
+ return true;
859
+ nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
860
+ if (this._hasElementChangedCache === undefined)
861
+ this._cacheSourceChanges();
862
+ return this._hasElementChangedCache.has(sourceElement.id);
439
863
  }
440
864
  static transformCallbackFor(transformer, entity) {
441
865
  if (entity instanceof core_backend_1.Element)
@@ -607,51 +1031,68 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
607
1031
  targetElementId = this.context.findTargetElementId(sourceElement.id);
608
1032
  targetElementProps = this.onTransformElement(sourceElement);
609
1033
  }
1034
+ // if an existing remapping was not yet found, check by FederationGuid
1035
+ if (this.context.isBetweenIModels && !core_bentley_1.Id64.isValid(targetElementId) && sourceElement.federationGuid !== undefined) {
1036
+ targetElementId = this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
1037
+ if (core_bentley_1.Id64.isValid(targetElementId))
1038
+ this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found
1039
+ }
610
1040
  // 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)
611
- if (!core_bentley_1.Id64.isValidId64(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
1041
+ if (!core_bentley_1.Id64.isValid(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
612
1042
  // respond the same way to undefined code value as the @see Code class, but don't use that class because is trims
613
1043
  // whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
614
1044
  targetElementProps.code.value = targetElementProps.code.value ?? "";
615
- targetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
616
- if (undefined !== targetElementId) {
617
- const targetElement = this.targetDb.elements.getElement(targetElementId);
618
- if (targetElement.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
1045
+ const maybeTargetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
1046
+ if (undefined !== maybeTargetElementId) {
1047
+ const maybeTargetElem = this.targetDb.elements.getElement(maybeTargetElementId);
1048
+ if (maybeTargetElem.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
1049
+ targetElementId = maybeTargetElementId;
619
1050
  this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
620
1051
  }
621
1052
  else {
622
- targetElementId = undefined;
623
1053
  targetElementProps.code = core_common_1.Code.createEmpty(); // clear out invalid code
624
1054
  }
625
1055
  }
626
1056
  }
627
- if (undefined !== targetElementId && core_bentley_1.Id64.isValidId64(targetElementId)) {
628
- // compare LastMod of sourceElement to ExternalSourceAspect of targetElement to see there are changes to import
629
- if (!this.hasElementChanged(sourceElement, targetElementId)) {
630
- return;
631
- }
632
- }
1057
+ if (core_bentley_1.Id64.isValid(targetElementId) && !this.hasElementChanged(sourceElement, targetElementId))
1058
+ return;
633
1059
  this.collectUnmappedReferences(sourceElement);
634
- // TODO: untangle targetElementId state...
635
- if (targetElementId === core_bentley_1.Id64.invalid)
636
- targetElementId = undefined;
637
- targetElementProps.id = targetElementId; // targetElementId will be valid (indicating update) or undefined (indicating insert)
1060
+ // targetElementId will be valid (indicating update) or undefined (indicating insert)
1061
+ targetElementProps.id
1062
+ = core_bentley_1.Id64.isValid(targetElementId)
1063
+ ? targetElementId
1064
+ : undefined;
638
1065
  if (!this._options.wasSourceIModelCopiedToTarget) {
639
1066
  this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
640
1067
  }
641
1068
  this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
642
1069
  // now that we've mapped this elem we can fix unmapped references to it
643
1070
  this.resolvePendingReferences(sourceElement);
1071
+ // the transformer does not currently 'split' or 'join' any elements, therefore, it does not
1072
+ // insert external source aspects because federation guids are sufficient for this.
1073
+ // Other transformer subclasses must insert the appropriate aspect (as provided by a TBD API)
1074
+ // when splitting/joining elements
1075
+ // physical consolidation is an example of a 'joining' transform
1076
+ // FIXME: document this externally!
1077
+ // verify at finalization time that we don't lose provenance on new elements
1078
+ // make public and improve `initElementProvenance` API for usage by consolidators
644
1079
  if (!this._options.noProvenance) {
645
- const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
646
- let aspectId = this.queryExternalSourceAspectId(aspectProps);
647
- if (aspectId === undefined) {
648
- aspectId = this.provenanceDb.elements.insertAspect(aspectProps);
649
- }
650
- else {
651
- this.provenanceDb.elements.updateAspect(aspectProps);
1080
+ let provenance = !this._options.forceExternalSourceAspectProvenance
1081
+ ? sourceElement.federationGuid
1082
+ : undefined;
1083
+ if (!provenance) {
1084
+ const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
1085
+ let aspectId = this.queryScopeExternalSource(aspectProps).aspectId;
1086
+ if (aspectId === undefined) {
1087
+ aspectId = this.provenanceDb.elements.insertAspect(aspectProps);
1088
+ }
1089
+ else {
1090
+ this.provenanceDb.elements.updateAspect(aspectProps);
1091
+ }
1092
+ aspectProps.id = aspectId;
1093
+ provenance = aspectProps;
652
1094
  }
653
- aspectProps.id = aspectId;
654
- this.markLastProvenance(aspectProps, { isRelationship: false });
1095
+ this.markLastProvenance(provenance, { isRelationship: false });
655
1096
  }
656
1097
  }
657
1098
  resolvePendingReferences(entity) {
@@ -689,7 +1130,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
689
1130
  onDeleteModel(sourceModelId) {
690
1131
  // It is possible and apparently occasionally sensical to delete a model without deleting its underlying element.
691
1132
  // - If only the model is deleted, [[initFromExternalSourceAspects]] will have already remapped the underlying element since it still exists.
692
- // - If both were deleted, [[remapDeletedSourceElements]] will find and remap the deleted element making this operation valid
1133
+ // - If both were deleted, [[remapDeletedSourceEntities]] will find and remap the deleted element making this operation valid
693
1134
  const targetModelId = this.context.findTargetElementId(sourceModelId);
694
1135
  if (core_bentley_1.Id64.isValidId64(targetModelId)) {
695
1136
  this.importer.deleteModel(targetModelId);
@@ -765,7 +1206,52 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
765
1206
  * @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
766
1207
  */
767
1208
  async processDeferredElements(_numRetries = 3) { }
1209
+ /** called at the end ([[finalizeTransformation]]) of a transformation,
1210
+ * updates the target scope element to say that transformation up through the
1211
+ * source's changeset has been performed.
1212
+ */
1213
+ _updateSynchronizationVersion() {
1214
+ if (this._sourceChangeDataState !== "has-changes" && !this._isFirstSynchronization)
1215
+ return;
1216
+ nodeAssert(this._targetScopeProvenanceProps);
1217
+ const newVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
1218
+ if (this._options.isReverseSynchronization || this._isFirstSynchronization) {
1219
+ const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
1220
+ core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${newVersion}`);
1221
+ // FIXME: could technically just put a delimiter in the version field to avoid using json properties
1222
+ this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = newVersion;
1223
+ }
1224
+ if (!this._options.isReverseSynchronization || this._isFirstSynchronization) {
1225
+ core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${newVersion}`);
1226
+ this._targetScopeProvenanceProps.version = newVersion;
1227
+ }
1228
+ if (this._isSynchronization) {
1229
+ (0, core_bentley_1.assert)(this.targetDb.changeset.index !== undefined && this._startingTargetChangesetIndex !== undefined, "_updateSynchronizationVersion was called without change history");
1230
+ const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
1231
+ core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1232
+ core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1233
+ const [syncChangesetsToClear, syncChangesetsToUpdate] = this._isReverseSynchronization
1234
+ ? [jsonProps.pendingReverseSyncChangesetIndices, jsonProps.pendingSyncChangesetIndices]
1235
+ : [jsonProps.pendingSyncChangesetIndices, jsonProps.pendingReverseSyncChangesetIndices];
1236
+ // NOTE that as documented in [[processChanges]], this assumes that right after
1237
+ // transformation finalization, the work will be saved immediately, otherwise we've
1238
+ // just marked this changeset as a synchronization to ignore, and the user can add other
1239
+ // stuff to it which would break future synchronizations
1240
+ // FIXME: force save for the user to prevent that
1241
+ for (let i = this._startingTargetChangesetIndex + 1; i <= this.targetDb.changeset.index + 1; i++)
1242
+ syncChangesetsToUpdate.push(i);
1243
+ syncChangesetsToClear.length = 0;
1244
+ core_bentley_1.Logger.logTrace(loggerCategory, `new pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1245
+ core_bentley_1.Logger.logTrace(loggerCategory, `new pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1246
+ }
1247
+ this.provenanceDb.elements.updateAspect({
1248
+ ...this._targetScopeProvenanceProps,
1249
+ jsonProperties: JSON.stringify(this._targetScopeProvenanceProps.jsonProperties),
1250
+ });
1251
+ }
1252
+ // FIXME: is this necessary when manually using lowlevel transform APIs?
768
1253
  finalizeTransformation() {
1254
+ this._updateSynchronizationVersion();
769
1255
  if (this._partiallyCommittedEntities.size > 0) {
770
1256
  core_bentley_1.Logger.logWarning(loggerCategory, [
771
1257
  "The following elements were never fully resolved:",
@@ -777,6 +1263,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
777
1263
  partiallyCommittedElem.forceComplete();
778
1264
  }
779
1265
  }
1266
+ // FIXME: make processAll have a try {} finally {} that cleans this up
1267
+ if (!this._options.noDetachChangeCache) {
1268
+ if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
1269
+ core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
1270
+ }
780
1271
  }
781
1272
  /** Imports all relationships that subclass from the specified base class.
782
1273
  * @param baseRelClassFullName The specified base relationship class.
@@ -794,40 +1285,52 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
794
1285
  * This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
795
1286
  */
796
1287
  onExportRelationship(sourceRelationship) {
1288
+ const sourceFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.sourceId);
1289
+ const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
797
1290
  const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
798
1291
  const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
799
- if (!this._options.noProvenance && core_bentley_1.Id64.isValidId64(targetRelationshipInstanceId)) {
800
- const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
801
- if (undefined === aspectProps.id) {
802
- aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1292
+ if (!this._options.noProvenance && core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
1293
+ let provenance = !this._options.forceExternalSourceAspectProvenance
1294
+ ? sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`
1295
+ : undefined;
1296
+ if (!provenance) {
1297
+ const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
1298
+ aspectProps.id = this.queryScopeExternalSource(aspectProps).aspectId;
1299
+ if (undefined === aspectProps.id) {
1300
+ aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
1301
+ }
1302
+ provenance = aspectProps;
803
1303
  }
804
- (0, core_bentley_1.assert)(aspectProps.id !== undefined);
805
- this.markLastProvenance(aspectProps, { isRelationship: true });
1304
+ this.markLastProvenance(provenance, { isRelationship: true });
806
1305
  }
807
1306
  }
808
1307
  /** Override of [IModelExportHandler.onDeleteRelationship]($transformer) that is called when [IModelExporter]($transformer) detects that a [Relationship]($backend) has been deleted from the source iModel.
809
1308
  * This override propagates the delete to the target iModel via [IModelImporter.deleteRelationship]($transformer).
810
1309
  */
811
1310
  onDeleteRelationship(sourceRelInstanceId) {
812
- const sql = `SELECT ECInstanceId,JsonProperties FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect` +
813
- ` WHERE aspect.Scope.Id=:scopeId AND aspect.Kind=:kind AND aspect.Identifier=:identifier LIMIT 1`;
814
- this.targetDb.withPreparedStatement(sql, (statement) => {
815
- statement.bindId("scopeId", this.targetScopeElementId);
816
- statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
817
- statement.bindString("identifier", sourceRelInstanceId);
818
- if (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
819
- const json = JSON.parse(statement.getValue(1).getString());
820
- if (undefined !== json.targetRelInstanceId) {
821
- const targetRelationship = this.targetDb.relationships.tryGetInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
822
- if (targetRelationship) {
823
- this.importer.deleteRelationship(targetRelationship.toJSON());
824
- }
825
- this.targetDb.elements.deleteAspect(statement.getValue(0).getId());
826
- }
827
- }
828
- });
1311
+ nodeAssert(this._deletedSourceRelationshipData, "should be defined at initialization by now");
1312
+ const deletedRelData = this._deletedSourceRelationshipData.get(sourceRelInstanceId);
1313
+ if (!deletedRelData) {
1314
+ // this can occur if both the source and target deleted it
1315
+ core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
1316
+ return;
1317
+ }
1318
+ const relArg = deletedRelData.relId ?? {
1319
+ sourceId: deletedRelData.sourceIdInTarget,
1320
+ targetId: deletedRelData.targetIdInTarget,
1321
+ };
1322
+ //
1323
+ // FIXME: make importer.deleteRelationship not need full props
1324
+ const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
1325
+ if (targetRelationship) {
1326
+ this.importer.deleteRelationship(targetRelationship.toJSON());
1327
+ }
1328
+ if (deletedRelData.provenanceAspectId) {
1329
+ this.provenanceDb.elements.deleteAspect(deletedRelData.provenanceAspectId);
1330
+ }
829
1331
  }
830
1332
  /** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
1333
+ * @deprecated
831
1334
  * @see processChanges
832
1335
  * @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.
833
1336
  * @throws [[IModelError]] If the required provenance information is not available to detect deletes.
@@ -837,13 +1340,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
837
1340
  throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
838
1341
  }
839
1342
  const aspectDeleteIds = [];
840
- const sql = `SELECT ECInstanceId,Identifier,JsonProperties FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect WHERE aspect.Scope.Id=:scopeId AND aspect.Kind=:kind`;
1343
+ const sql = `
1344
+ SELECT ECInstanceId, Identifier, JsonProperties
1345
+ FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect
1346
+ WHERE aspect.Scope.Id=:scopeId
1347
+ AND aspect.Kind=:kind
1348
+ `;
841
1349
  await this.targetDb.withPreparedStatement(sql, async (statement) => {
842
1350
  statement.bindId("scopeId", this.targetScopeElementId);
843
1351
  statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
844
1352
  while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
845
1353
  const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
846
1354
  if (undefined === this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
1355
+ // FIXME: make sure matches new provenance-based method
1356
+ // FIXME: use sql JSON_EXTRACT
847
1357
  const json = JSON.parse(statement.getValue(2).getString());
848
1358
  if (undefined !== json.targetRelInstanceId) {
849
1359
  const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
@@ -865,6 +1375,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
865
1375
  const targetRelationshipProps = sourceRelationship.toJSON();
866
1376
  targetRelationshipProps.sourceId = this.context.findTargetElementId(sourceRelationship.sourceId);
867
1377
  targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
1378
+ // TODO: move to cloneRelationship in IModelCloneContext
868
1379
  sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
869
1380
  if ((core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType) && ("Id" === propertyMetaData.extendedType)) {
870
1381
  targetRelationshipProps[propertyName] = this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
@@ -1026,26 +1537,86 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1026
1537
  return this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1027
1538
  }
1028
1539
  /**
1029
- * Initialize prerequisites of processing, you must initialize with an [[InitFromExternalSourceAspectsArgs]] if you
1030
- * are intending process changes, but prefer using [[processChanges]]
1031
- * Called by all `process*` functions implicitly.
1540
+ * Initialize prerequisites of processing, you must initialize with an [[InitArgs]] if you
1541
+ * are intending to process changes, but prefer using [[processChanges]] explicitly since it calls this.
1542
+ * @note Called by all `process*` functions implicitly.
1032
1543
  * Overriders must call `super.initialize()` first
1033
1544
  */
1034
1545
  async initialize(args) {
1035
1546
  if (this._initialized)
1036
1547
  return;
1037
1548
  await this.context.initialize();
1549
+ await this._tryInitChangesetData(args);
1038
1550
  // eslint-disable-next-line deprecation/deprecation
1039
1551
  await this.initFromExternalSourceAspects(args);
1040
1552
  this._initialized = true;
1041
1553
  }
1554
+ async _tryInitChangesetData(args) {
1555
+ if (!args || this.sourceDb.iTwinId === undefined || this.sourceDb.changeset.index === undefined) {
1556
+ this._sourceChangeDataState = "unconnected";
1557
+ return;
1558
+ }
1559
+ const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1560
+ if (noChanges) {
1561
+ this._sourceChangeDataState = "no-changes";
1562
+ this._changeSummaryIds = [];
1563
+ return;
1564
+ }
1565
+ // NOTE: that we do NOT download the changesummary for the last transformed version, we want
1566
+ // to ignore those already processed changes
1567
+ const startChangesetIndexOrId = args.startChangeset?.index
1568
+ ?? args.startChangeset?.id
1569
+ ?? args.startChangesetId // eslint-disable-line deprecation/deprecation
1570
+ ?? this._synchronizationVersion.index + 1;
1571
+ const endChangesetId = this.sourceDb.changeset.id;
1572
+ const [startChangesetIndex, endChangesetIndex] = await Promise.all(([startChangesetIndexOrId, endChangesetId])
1573
+ .map(async (indexOrId) => typeof indexOrId === "number"
1574
+ ? indexOrId
1575
+ : core_backend_1.IModelHost.hubAccess
1576
+ .queryChangeset({
1577
+ iModelId: this.sourceDb.iModelId,
1578
+ // eslint-disable-next-line deprecation/deprecation
1579
+ changeset: { id: indexOrId },
1580
+ accessToken: args.accessToken,
1581
+ })
1582
+ .then((changeset) => changeset.index)));
1583
+ const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
1584
+ // FIXME: add an option to ignore this check
1585
+ if (!this._options.ignoreMissingChangesetsInSynchronizations
1586
+ && startChangesetIndex !== this._synchronizationVersion.index + 1
1587
+ && this._synchronizationVersion.index !== -1) {
1588
+ throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},`
1589
+ + " startChangesetId should be"
1590
+ + " exactly the first changeset *after* the previous synchronization to not miss data."
1591
+ + ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}`
1592
+ + ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'`
1593
+ + ` which is changeset #${this._synchronizationVersion.index}. The transformer expected`
1594
+ + ` #${this._synchronizationVersion.index + 1}.`);
1595
+ }
1596
+ nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
1597
+ const changesetsToSkip = this._isReverseSynchronization
1598
+ ? this._targetScopeProvenanceProps.jsonProperties.pendingReverseSyncChangesetIndices
1599
+ : this._targetScopeProvenanceProps.jsonProperties.pendingSyncChangesetIndices;
1600
+ core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
1601
+ this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
1602
+ core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
1603
+ for (const [first, end] of this._changesetRanges) {
1604
+ this._changeSummaryIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
1605
+ accessToken: args.accessToken,
1606
+ iModelId: this.sourceDb.iModelId,
1607
+ iTwinId: this.sourceDb.iTwinId,
1608
+ range: { first, end },
1609
+ });
1610
+ }
1611
+ core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
1612
+ this._sourceChangeDataState = "has-changes";
1613
+ }
1042
1614
  /** Export everything from the source iModel and import the transformed entities into the target iModel.
1043
1615
  * @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
1044
1616
  */
1045
1617
  async processAll() {
1046
- core_bentley_1.Logger.logTrace(loggerCategory, "processAll()");
1047
1618
  this.logSettings();
1048
- this.validateScopeProvenance();
1619
+ this.initScopeProvenance();
1049
1620
  await this.initialize();
1050
1621
  await this.exporter.exportCodeSpecs();
1051
1622
  await this.exporter.exportFonts();
@@ -1065,12 +1636,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1065
1636
  this.finalizeTransformation();
1066
1637
  }
1067
1638
  markLastProvenance(sourceAspect, { isRelationship = false }) {
1068
- this._lastProvenanceEntityInfo = {
1069
- entityId: sourceAspect.element.id,
1070
- aspectId: sourceAspect.id,
1071
- aspectVersion: sourceAspect.version ?? "",
1072
- aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
1073
- };
1639
+ this._lastProvenanceEntityInfo
1640
+ = typeof sourceAspect === "string"
1641
+ ? sourceAspect
1642
+ : {
1643
+ entityId: sourceAspect.element.id,
1644
+ aspectId: sourceAspect.id,
1645
+ aspectVersion: sourceAspect.version ?? "",
1646
+ aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
1647
+ };
1074
1648
  }
1075
1649
  /**
1076
1650
  * Load the state of the active transformation from an open SQLiteDb
@@ -1082,17 +1656,35 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1082
1656
  const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
1083
1657
  if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
1084
1658
  throw Error("expected row when getting lastProvenanceEntityId from target state table");
1085
- return {
1086
- entityId: stmt.getValueString(0),
1087
- aspectId: stmt.getValueString(1),
1088
- aspectVersion: stmt.getValueString(2),
1089
- aspectKind: stmt.getValueString(3),
1090
- };
1659
+ const entityId = stmt.getValueString(0);
1660
+ const isGuidOrGuidPair = entityId.includes('-');
1661
+ return isGuidOrGuidPair
1662
+ ? entityId
1663
+ : {
1664
+ entityId,
1665
+ aspectId: stmt.getValueString(1),
1666
+ aspectVersion: stmt.getValueString(2),
1667
+ aspectKind: stmt.getValueString(3),
1668
+ };
1091
1669
  });
1092
- const targetHasCorrectLastProvenance =
1093
- // ignore provenance check if it's null since we can't bind those ids
1094
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
1670
+ /*
1671
+ // TODO: maybe save transformer state resumption state based on target changset and require calls
1672
+ // to saveChanges
1673
+ if () {
1674
+ const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
1675
+ const isRelProvenance = targetFedGuid !== undefined;
1676
+ const instanceId = isRelProvenance
1677
+ ? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
1678
+ : "";
1679
+ //const classId =
1680
+ if (isRelProvenance) {
1681
+ }
1682
+ }
1683
+ */
1684
+ const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
1685
+ // ignore provenance check if it's null since we can't bind those ids
1095
1686
  !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
1687
+ !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
1096
1688
  this.provenanceDb.withPreparedStatement(`
1097
1689
  SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
1098
1690
  WHERE Scope.Id=:scopeId
@@ -1189,8 +1781,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1189
1781
  throw Error("Failed to create the js state table in the state database");
1190
1782
  if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`
1191
1783
  CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
1192
- -- because we cannot bind the invalid id which we use for our null state, we actually store the id as a hex string
1784
+ -- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
1193
1785
  entityId TEXT,
1786
+ -- the following are only valid if the above entityId is a hex id representation
1194
1787
  aspectId TEXT,
1195
1788
  aspectVersion TEXT,
1196
1789
  aspectKind TEXT
@@ -1204,10 +1797,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1204
1797
  throw Error("Failed to insert options into the state database");
1205
1798
  });
1206
1799
  db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
1207
- stmt.bindString(1, this._lastProvenanceEntityInfo.entityId);
1208
- stmt.bindString(2, this._lastProvenanceEntityInfo.aspectId);
1209
- stmt.bindString(3, this._lastProvenanceEntityInfo.aspectVersion);
1210
- stmt.bindString(4, this._lastProvenanceEntityInfo.aspectKind);
1800
+ const lastProvenanceEntityInfo = this._lastProvenanceEntityInfo;
1801
+ stmt.bindString(1, lastProvenanceEntityInfo?.entityId ?? this._lastProvenanceEntityInfo);
1802
+ stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
1803
+ stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
1804
+ stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
1211
1805
  if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
1212
1806
  throw Error("Failed to insert options into the state database");
1213
1807
  });
@@ -1235,19 +1829,34 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1235
1829
  db.closeDb();
1236
1830
  }
1237
1831
  }
1238
- /** Export changes from the source iModel and import the transformed entities into the target iModel.
1239
- * Inserts, updates, and deletes are determined by inspecting the changeset(s).
1240
- * @param accessToken A valid access token string
1241
- * @param startChangesetId Include changes from this changeset up through and including the current changeset.
1242
- * If this parameter is not provided, then just the current changeset will be exported.
1243
- * @note To form a range of versions to process, set `startChangesetId` for the start (inclusive) of the desired range and open the source iModel as of the end (inclusive) of the desired range.
1244
- */
1245
- async processChanges(accessToken, startChangesetId) {
1246
- core_bentley_1.Logger.logTrace(loggerCategory, "processChanges()");
1832
+ async processChanges(optionsOrAccessToken, startChangesetId) {
1833
+ this._isSynchronization = true;
1834
+ const args = typeof optionsOrAccessToken === "string"
1835
+ ? {
1836
+ accessToken: optionsOrAccessToken,
1837
+ startChangeset: startChangesetId
1838
+ ? { id: startChangesetId }
1839
+ : this.sourceDb.changeset,
1840
+ }
1841
+ : {
1842
+ ...optionsOrAccessToken,
1843
+ startChangeset: optionsOrAccessToken.startChangeset
1844
+ /* eslint-disable deprecation/deprecation */
1845
+ ?? (optionsOrAccessToken.startChangesetId !== undefined
1846
+ ? { id: optionsOrAccessToken.startChangesetId }
1847
+ : undefined),
1848
+ /* eslint-enable deprecation/deprecation */
1849
+ };
1247
1850
  this.logSettings();
1248
- this.validateScopeProvenance();
1249
- await this.initialize({ accessToken, startChangesetId });
1250
- await this.exporter.exportChanges(accessToken, startChangesetId);
1851
+ this.initScopeProvenance();
1852
+ await this.initialize(args);
1853
+ // must wait for initialization of synchronization provenance data
1854
+ const changeArgs = this._changesetRanges
1855
+ ? { changesetRanges: this._changesetRanges }
1856
+ : args.startChangeset
1857
+ ? { startChangeset: args.startChangeset }
1858
+ : { startChangeset: { index: this._synchronizationVersion.index + 1 } };
1859
+ await this.exporter.exportChanges({ accessToken: args.accessToken, ...changeArgs });
1251
1860
  await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
1252
1861
  if (this._options.optimizeGeometry)
1253
1862
  this.importer.optimizeGeometry(this._options.optimizeGeometry);
@@ -1284,6 +1893,7 @@ class TemplateModelCloner extends IModelTransformer {
1284
1893
  * @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
1285
1894
  */
1286
1895
  async placeTemplate3d(sourceTemplateModelId, targetModelId, placement) {
1896
+ await this.initialize();
1287
1897
  this.context.remapElement(sourceTemplateModelId, targetModelId);
1288
1898
  this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(placement.origin, placement.angles.toMatrix3d());
1289
1899
  this._sourceIdToTargetIdMap = new Map();
@@ -1304,6 +1914,7 @@ class TemplateModelCloner extends IModelTransformer {
1304
1914
  * @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
1305
1915
  */
1306
1916
  async placeTemplate2d(sourceTemplateModelId, targetModelId, placement) {
1917
+ await this.initialize();
1307
1918
  this.context.remapElement(sourceTemplateModelId, targetModelId);
1308
1919
  this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(core_geometry_1.Point3d.createFrom(placement.origin), placement.rotation);
1309
1920
  this._sourceIdToTargetIdMap = new Map();
@@ -1340,16 +1951,12 @@ class TemplateModelCloner extends IModelTransformer {
1340
1951
  const targetElementProps = super.onTransformElement(sourceElement);
1341
1952
  targetElementProps.federationGuid = core_bentley_1.Guid.createValue(); // clone from template should create a new federationGuid
1342
1953
  targetElementProps.code = core_common_1.Code.createEmpty(); // clone from template should not maintain codes
1343
- if (sourceElement instanceof core_backend_1.GeometricElement3d) {
1344
- const placement = core_common_1.Placement3d.fromJSON(targetElementProps.placement);
1345
- if (placement.isValid) {
1346
- placement.multiplyTransform(this._transform3d);
1347
- targetElementProps.placement = placement;
1348
- }
1349
- }
1350
- else if (sourceElement instanceof core_backend_1.GeometricElement2d) {
1351
- const placement = core_common_1.Placement2d.fromJSON(targetElementProps.placement);
1954
+ if (sourceElement instanceof core_backend_1.GeometricElement) {
1955
+ const is3d = sourceElement instanceof core_backend_1.GeometricElement3d;
1956
+ const placementClass = is3d ? core_common_1.Placement3d : core_common_1.Placement2d;
1957
+ const placement = (placementClass).fromJSON(targetElementProps.placement);
1352
1958
  if (placement.isValid) {
1959
+ nodeAssert(this._transform3d);
1353
1960
  placement.multiplyTransform(this._transform3d);
1354
1961
  targetElementProps.placement = placement;
1355
1962
  }
@@ -1359,4 +1966,17 @@ class TemplateModelCloner extends IModelTransformer {
1359
1966
  }
1360
1967
  }
1361
1968
  exports.TemplateModelCloner = TemplateModelCloner;
1969
+ function queryElemFedGuid(db, elemId) {
1970
+ return db.withPreparedStatement(`
1971
+ SELECT FederationGuid
1972
+ FROM bis.Element
1973
+ WHERE ECInstanceId=?
1974
+ `, (stmt) => {
1975
+ stmt.bindId(1, elemId);
1976
+ (0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
1977
+ const result = stmt.getValue(0).getGuid();
1978
+ (0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_DONE);
1979
+ return result;
1980
+ });
1981
+ }
1362
1982
  //# sourceMappingURL=IModelTransformer.js.map