@itwin/imodel-transformer 2.0.0-dev.67 → 2.0.0-dev.69

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.
@@ -61,6 +61,10 @@ const SchemaProcessingCoordinator_1 = require("./schema-processing/SchemaProcess
61
61
  const ChangesetScanner_1 = require("./ChangesetScanner");
62
62
  const IModelTransformerError_1 = require("./IModelTransformerError");
63
63
  const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelTransformer;
64
+ /** Key of a relationship's source and target element ids. */
65
+ function relationshipEndpointsKey(sourceId, targetId) {
66
+ return `${sourceId}|${targetId}`;
67
+ }
64
68
  /**
65
69
  * Apply a function to each Id64 in a supported container type of Id64s.
66
70
  * Currently only supports raw Id64String or RelatedElement-like objects containing an `id` property that is a Id64String,
@@ -537,8 +541,14 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
537
541
  const combinedTransform = targetTransformInv.multiplyTransformTransform(srcTransform);
538
542
  return combinedTransform;
539
543
  }
540
- // if undefined, it can be initialized by calling [[this.processChangesets]]
544
+ // if undefined, it can be initialized by calling [[this.prepareSourceChanges]]
541
545
  _deletedSourceRelationshipData = undefined;
546
+ /** Target relationship ids and target endpoint keys of [[_deletedSourceRelationshipData]] entries, for fast matching in [[onExportRelationship]]. */
547
+ _pendingRelationshipDeleteTargets = new Set();
548
+ /** Target relationships that this run imported from a source relationship and that a pending relationship delete also resolves to.
549
+ * A source relationship deleted and recreated with the same endpoints maps to the same target relationship, which must not be deleted.
550
+ */
551
+ _reimportedTargetRelationshipIds = new Set();
542
552
  /** Returns true if a change within sourceElement is detected.
543
553
  * @param sourceElement The Element from the source iModel
544
554
  * @note A subclass can override this method to provide custom change detection behavior.
@@ -1152,6 +1162,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1152
1162
  async onExportRelationship(sourceRelationship, _isUpdate, sourceFedGuid, targetFedGuid) {
1153
1163
  const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
1154
1164
  const targetRelationshipInstanceId = await this.importer.importRelationship(targetRelationshipProps);
1165
+ if (this._pendingRelationshipDeleteTargets.size > 0 &&
1166
+ core_bentley_1.Id64.isValid(targetRelationshipInstanceId) &&
1167
+ (this._pendingRelationshipDeleteTargets.has(targetRelationshipInstanceId) ||
1168
+ this._pendingRelationshipDeleteTargets.has(relationshipEndpointsKey(targetRelationshipProps.sourceId, targetRelationshipProps.targetId)))) {
1169
+ this._reimportedTargetRelationshipIds.add(targetRelationshipInstanceId);
1170
+ }
1155
1171
  const provenanceDb = await this.getProvenanceDb();
1156
1172
  const provenanceEditTxn = await this._provenanceManager.getProvenanceEditTxn();
1157
1173
  if (!this._options.noProvenance &&
@@ -1189,7 +1205,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1189
1205
  sourceId: deletedRelData.sourceIdInTarget,
1190
1206
  targetId: deletedRelData.targetIdInTarget,
1191
1207
  })?.id;
1192
- if (id) {
1208
+ if (id && this._reimportedTargetRelationshipIds.has(id)) {
1209
+ // The source relationship was recreated with the same endpoints and already imported into this target relationship.
1210
+ core_bentley_1.Logger.logTrace(loggerCategory, `Skipping delete of target relationship ${id} because source relationship ${sourceRelInstanceId} was recreated`);
1211
+ }
1212
+ else if (id) {
1193
1213
  await this.importer.deleteRelationship({
1194
1214
  id,
1195
1215
  classFullName: deletedRelData.classFullName,
@@ -1370,9 +1390,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1370
1390
  /** state to prevent reinitialization, @see [[initialize]] */
1371
1391
  _initialized = false;
1372
1392
  _sourceChangeDataState = "uninited";
1373
- /** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
1374
- _csFileProps = undefined;
1375
- _deletionRecordsByChangeset;
1393
+ _deletionBatches;
1376
1394
  /**
1377
1395
  * Initialize prerequisites of processing, you must initialize with an [[InitOptions]] if you
1378
1396
  * are intending to process changes. Callers may wish to explicitly call initialize if they need to execute code after initialize but before [[process]] is called.
@@ -1388,22 +1406,28 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1388
1406
  await this._cloneContext.initialize();
1389
1407
  const exporterInitOptions = await this.getExportInitOpts(this._options.argsForProcessChanges ?? {});
1390
1408
  await this.initializeChangesetScanAndExporter(exporterInitOptions);
1391
- // Exporter must be initialized prior to processing changesets in order to properly handle entity recreations (an entity delete followed by an insert of that same entity).
1392
- await this.processChangesets();
1409
+ // Exporter must be initialized prior to remapping deletions in order to properly handle entity recreations (an entity delete followed by an insert of that same entity).
1410
+ await this.prepareSourceChanges();
1393
1411
  this._initialized = true;
1394
1412
  }
1395
1413
  async initializeChangesetScanAndExporter(exporterInitOptions) {
1396
- if (this._csFileProps !== undefined &&
1397
- this._csFileProps.length > 0 &&
1414
+ const ranges = this._changesetRanges;
1415
+ if (ranges !== undefined &&
1416
+ ranges.length > 0 &&
1398
1417
  this.sourceDb.isBriefcaseDb()) {
1399
- const changedInstanceIds = this.exporter.sourceDbChanges ??
1418
+ let changedInstanceIds = this.exporter.sourceDbChanges ??
1400
1419
  ("changedInstanceIds" in exporterInitOptions
1401
1420
  ? exporterInitOptions.changedInstanceIds
1402
- : new IModelExporter_1.ChangedInstanceIds(this.sourceDb));
1403
- this._deletionRecordsByChangeset = await ChangesetScanner_1.ChangesetScanner.scan(this.sourceDb, this._csFileProps, changedInstanceIds, {
1404
- populateChangedInstanceIds: this.exporter.sourceDbChanges === undefined &&
1405
- !("changedInstanceIds" in exporterInitOptions),
1406
- });
1421
+ : undefined);
1422
+ if (changedInstanceIds !== undefined) {
1423
+ // The caller supplied changed IDs, so only deletion metadata is read.
1424
+ this._deletionBatches = await ChangesetScanner_1.ChangesetScanner.scan(this.sourceDb, await ChangesetScanner_1.ChangesetScanner.download(this.sourceDb, ranges), {});
1425
+ }
1426
+ else {
1427
+ const scanResult = await this.collectChanges(ranges);
1428
+ changedInstanceIds = scanResult.changedInstanceIds;
1429
+ this._deletionBatches = scanResult.deletionBatches;
1430
+ }
1407
1431
  await this.exporter.initialize({
1408
1432
  changedInstanceIds,
1409
1433
  skipPropagateChangesToRootElements: exporterInitOptions.skipPropagateChangesToRootElements,
@@ -1414,19 +1438,42 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1414
1438
  }
1415
1439
  }
1416
1440
  /**
1417
- * Reads all the changeset files in the private member of the transformer: _csFileProps
1418
- * and finds the corresponding target entity for any deleted source entities and remaps the sourceId to the targetId.
1419
- * This function returns early if csFileProps is undefined or is of length 0.
1420
- * @returns void
1441
+ * Reads the source changes for the changeset ranges being processed. Override to supply changes from another
1442
+ * source, such as a precomputed cache, instead of downloading and reading changesets.
1443
+ *
1444
+ * An override can supply some ranges itself and pass the rest to `super.collectChanges()`. Pass the same
1445
+ * `changedInstanceIds` to every call and handle the ranges in order, so that changes to the same instance in
1446
+ * different ranges combine correctly. Use [[ChangedInstanceIds.addChangeRecord]] to add each supplied change.
1447
+ * See [Supplying source changes]($docs/learning/transformer/change-scanning.md) for the full contract and an example.
1448
+ * @param ranges Ordered, inclusive `[first, last]` changeset index ranges. They already exclude changesets
1449
+ * that must be skipped, such as those pushed by a previous synchronization in the other direction, so an
1450
+ * override must cover exactly these ranges.
1451
+ * @param changedInstanceIds Receives the changes read from the ranges. Defaults to a new, empty instance.
1452
+ * @returns The changed instance IDs to export and the records used to find the targets of deleted instances.
1453
+ * See [[CollectedChanges]] for what the result must include.
1454
+ * @note The default implementation downloads the changesets, reads them with `ChangesetReader`, and returns one
1455
+ * deletion batch per range.
1456
+ * @note Not called when the exporter already has changed instance IDs, for example when `changedInstanceIds`
1457
+ * is passed in [[IModelTransformOptions.argsForProcessChanges]]. The transformer then still downloads and reads
1458
+ * the changesets to find deletion records.
1459
+ * @beta
1460
+ */
1461
+ async collectChanges(ranges, changedInstanceIds = new IModelExporter_1.ChangedInstanceIds(this.sourceDb)) {
1462
+ const deletionBatches = await ChangesetScanner_1.ChangesetScanner.scan(this.sourceDb, await ChangesetScanner_1.ChangesetScanner.download(this.sourceDb, ranges), { changedInstanceIds });
1463
+ return { changedInstanceIds, deletionBatches };
1464
+ }
1465
+ /**
1466
+ * Remaps tracked elements, adds custom changes, and remaps deleted source entities to their targets using the
1467
+ * deletion batches read for the changeset ranges being processed.
1421
1468
  */
1422
- async processChangesets() {
1469
+ async prepareSourceChanges() {
1423
1470
  await this._provenanceManager.forEachTrackedElement((sourceElementId, targetElementId) => {
1424
1471
  this.context.remapElement(sourceElementId, targetElementId);
1425
1472
  });
1426
1473
  if (this.exporter.sourceDbChanges)
1427
1474
  await this.addCustomChanges(this.exporter.sourceDbChanges);
1428
- const csFileProps = this._csFileProps;
1429
- if (csFileProps === undefined || csFileProps.length === 0) {
1475
+ const ranges = this._changesetRanges;
1476
+ if (ranges === undefined || ranges.length === 0) {
1430
1477
  if (this.exporter.sourceDbChanges === undefined ||
1431
1478
  !this.exporter.sourceDbChanges.hasChanges)
1432
1479
  return;
@@ -1434,21 +1481,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1434
1481
  if (this._sourceChangeDataState === "no-changes")
1435
1482
  this._sourceChangeDataState = "has-changes";
1436
1483
  }
1437
- const deletionRecordsByChangeset = this._deletionRecordsByChangeset;
1438
- if (deletionRecordsByChangeset === undefined)
1484
+ const deletionBatches = this._deletionBatches;
1485
+ if (deletionBatches === undefined)
1439
1486
  return;
1440
- const relationshipECClassIdsToSkip = new Set();
1441
- for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementDrivesElement)", undefined, { usePrimaryConn: true })) {
1442
- relationshipECClassIdsToSkip.add(row.ECInstanceId);
1443
- }
1444
- const relationshipECClassIds = new Set();
1445
- for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementRefersToElements)", undefined, { usePrimaryConn: true })) {
1446
- relationshipECClassIds.add(row.ECInstanceId);
1447
- }
1448
- const elementAspectECClassIds = new Set();
1449
- for await (const row of this.sourceDb.createQueryReader("SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (BisCore.ElementAspect)", undefined, { usePrimaryConn: true })) {
1450
- elementAspectECClassIds.add(row.ECInstanceId);
1451
- }
1452
1487
  // For later use when processing deletes.
1453
1488
  const alreadyImportedElementInserts = new Set();
1454
1489
  const alreadyImportedModelInserts = new Set();
@@ -1463,30 +1498,28 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1463
1498
  alreadyImportedModelInserts.add(targetModelId);
1464
1499
  });
1465
1500
  this._deletedSourceRelationshipData = new Map();
1466
- const isElementAspectDeletion = (change) => elementAspectECClassIds.has(change.ecClassId);
1467
- for (const changes of deletionRecordsByChangeset) {
1468
- /** a map of element ids to this transformation scope's ESA data for that element, in case the ESA is deleted in the target */
1469
- const elemIdToScopeEsa = new Map();
1470
- for (const change of changes) {
1471
- if (relationshipECClassIdsToSkip.has(change.ecClassId))
1472
- continue;
1473
- if (change.classFullName === core_backend_1.ExternalSourceAspect.classFullName &&
1474
- change.scopeId === this.targetScopeElementId &&
1475
- change.kind === core_backend_1.ExternalSourceAspect.Kind.Element &&
1476
- change.elementId !== undefined) {
1477
- elemIdToScopeEsa.set(change.elementId, change);
1478
- }
1479
- }
1480
- // Loop to process deletes.
1481
- for (const change of changes) {
1482
- if (relationshipECClassIdsToSkip.has(change.ecClassId))
1483
- continue;
1484
- if (isElementAspectDeletion(change))
1485
- continue;
1486
- await this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(change.ecClassId), alreadyImportedElementInserts, alreadyImportedModelInserts);
1501
+ this._pendingRelationshipDeleteTargets.clear();
1502
+ this._reimportedTargetRelationshipIds.clear();
1503
+ for (const batch of deletionBatches) {
1504
+ // Deletion records can only resolve targets within their own batch, so these lookups are rebuilt for each batch.
1505
+ // They cover elements and scope ESAs deleted in the source, which can no longer be read from it.
1506
+ const deletedScopeEsaByElementId = new Map();
1507
+ for (const aspect of batch.externalSourceAspects) {
1508
+ if (aspect.scopeId === this.targetScopeElementId &&
1509
+ aspect.kind === core_backend_1.ExternalSourceAspect.Kind.Element)
1510
+ deletedScopeEsaByElementId.set(aspect.elementId, aspect);
1487
1511
  }
1512
+ const deletedFederationGuidByElementId = new Map();
1513
+ for (const element of batch.elements)
1514
+ if (element.federationGuid)
1515
+ deletedFederationGuidByElementId.set(element.ecInstanceId, element.federationGuid);
1516
+ for (const relationship of batch.relationships)
1517
+ await this.processDeletedRelationship(relationship, deletedScopeEsaByElementId, deletedFederationGuidByElementId);
1518
+ for (const element of batch.elements)
1519
+ await this.processDeletedElement(element.ecInstanceId, element.federationGuid, deletedScopeEsaByElementId, alreadyImportedElementInserts, alreadyImportedModelInserts);
1520
+ for (const model of batch.models)
1521
+ await this.processDeletedElement(model.ecInstanceId, undefined, deletedScopeEsaByElementId, alreadyImportedElementInserts, alreadyImportedModelInserts);
1488
1522
  }
1489
- return;
1490
1523
  }
1491
1524
  /**
1492
1525
  * This will be called when transformer is called with [[IModelTransformOptions.argsForProcessChanges]] to process changes.
@@ -1498,127 +1531,110 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1498
1531
  */
1499
1532
  async addCustomChanges(_sourceDbChanges) { }
1500
1533
  /**
1501
- * Helper function for processChangesets. Remaps the id of element deleted found in the 'change' to an element in the targetDb.
1502
- * @param change the change to process, must be of changeType "Deleted"
1503
- * @param mapOfDeletedElemIdToScopeEsas a map of elementIds to changedECInstances (which are ESAs). the elementId is not the id of the esa itself, but the elementid that the esa was stored on before the esa's deletion.
1504
- * All ESAs in this map are part of the transformer's scope / ESA data and are tracked in case the ESA is deleted in the target.
1505
- * @param isRelationship is relationship or not
1506
- * @param alreadyImportedElementInserts used to handle entity recreation and not delete already handled element inserts.
1507
- * @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
1508
- * @returns void
1509
- */
1510
- async processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1511
- // we need a connected iModel with changes to remap elements with deletions
1512
- const notConnectedModel = this.sourceDb.iTwinId === undefined;
1513
- const noChanges = (await this.getSynchronizationVersion()).index ===
1514
- this.sourceDb.changeset.index &&
1515
- (this.exporter.sourceDbChanges === undefined ||
1516
- !this.exporter.sourceDbChanges.hasChanges);
1517
- if (notConnectedModel || noChanges)
1518
- return;
1534
+ * Finds the target ID of a source element while processing deletions, first through the transformation's
1535
+ * ExternalSourceAspects when the source holds the provenance, then through the federation GUID. The element may be
1536
+ * deleted itself, or be a surviving endpoint of a deleted relationship.
1537
+ * @param sourceId the source element ID.
1538
+ * @param federationGuid the source element's federation GUID, if known.
1539
+ * @param deletedScopeEsaByElementId this transformation scope's deleted ESAs in the same batch, keyed by the ID of the element that owned each one.
1540
+ */
1541
+ async findTargetElementIdForDeletion(sourceId, federationGuid, deletedScopeEsaByElementId) {
1519
1542
  /**
1520
1543
  * If the deleted source entity is in the provenanceDb, then we can use its ids to query for ESAs.
1521
1544
  * This is because the ESAs are stored on an element Id thats present in the provenanceDb.
1522
1545
  */
1523
- const changeDataInProvenanceDb = this.sourceDb === (await this.getProvenanceDb());
1524
- const getTargetIdFromSourceId = async (id) => {
1546
+ if (this.sourceDb === (await this.getProvenanceDb())) {
1525
1547
  let identifierValue;
1526
- let element;
1527
- if (isRelationship) {
1528
- element = this.sourceDb.elements.tryGetElement(id);
1529
- }
1530
- const fedGuid = isRelationship
1531
- ? element?.federationGuid
1532
- : change.federationGuid;
1533
- if (changeDataInProvenanceDb) {
1534
- // TODO: clarify what happens if there are multiple (e.g. elements were merged)
1535
- for await (const row of this.sourceDb.createQueryReader("SELECT esa.Identifier FROM bis.ExternalSourceAspect esa WHERE Scope.Id=:scopeId AND Kind=:kind AND Element.Id=:relatedElementId LIMIT 1", core_common_1.QueryBinder.from([
1536
- this.targetScopeElementId,
1537
- core_backend_1.ExternalSourceAspect.Kind.Element,
1538
- id,
1539
- ]), { usePrimaryConn: true })) {
1540
- identifierValue = row.Identifier;
1541
- }
1542
- identifierValue =
1543
- identifierValue ?? mapOfDeletedElemIdToScopeEsas.get(id)?.identifier;
1548
+ // TODO: clarify what happens if there are multiple (e.g. elements were merged)
1549
+ for await (const row of this.sourceDb.createQueryReader("SELECT esa.Identifier FROM bis.ExternalSourceAspect esa WHERE Scope.Id=:scopeId AND Kind=:kind AND Element.Id=:relatedElementId LIMIT 1", core_common_1.QueryBinder.from([
1550
+ this.targetScopeElementId,
1551
+ core_backend_1.ExternalSourceAspect.Kind.Element,
1552
+ sourceId,
1553
+ ]), { usePrimaryConn: true })) {
1554
+ identifierValue = row.Identifier;
1544
1555
  }
1556
+ identifierValue =
1557
+ identifierValue ?? deletedScopeEsaByElementId.get(sourceId)?.identifier;
1545
1558
  // Check for targetId by an esa first
1546
- if (changeDataInProvenanceDb && identifierValue) {
1547
- const targetId = identifierValue;
1548
- return targetId;
1549
- }
1550
- // Check for targetId using sourceId's fedguid if we didn't find an esa.
1551
- if (fedGuid) {
1552
- const targetId = this.targetDb.elements.getIdFromFederationGuid(fedGuid);
1553
- return targetId;
1554
- }
1555
- return undefined;
1556
- };
1557
- const changedInstanceId = change.ecInstanceId;
1558
- if (isRelationship) {
1559
- const sourceIdOfRelationshipInSource = change.sourceECInstanceId;
1560
- const targetIdOfRelationshipInSource = change.targetECInstanceId;
1561
- const classFullName = change.classFullName;
1562
- if (sourceIdOfRelationshipInSource === undefined ||
1563
- targetIdOfRelationshipInSource === undefined) {
1564
- core_bentley_1.ITwinError.throwError({
1565
- iTwinErrorId: {
1566
- scope: IModelTransformerError_1.IModelTransformerErrorScope,
1567
- key: IModelTransformerError_1.IModelTransformerError.ChangedInstanceMetadataMissing,
1568
- },
1569
- message: `Relationship deletion ${changedInstanceId} is missing an endpoint.`,
1570
- });
1571
- }
1572
- const sourceIdOfRelationshipInTarget = await getTargetIdFromSourceId(sourceIdOfRelationshipInSource);
1573
- const targetIdOfRelationshipInTarget = await getTargetIdFromSourceId(targetIdOfRelationshipInSource);
1574
- if (sourceIdOfRelationshipInTarget && targetIdOfRelationshipInTarget) {
1575
- this._deletedSourceRelationshipData?.set(changedInstanceId, {
1576
- classFullName: classFullName ?? "",
1577
- sourceIdInTarget: sourceIdOfRelationshipInTarget,
1578
- targetIdInTarget: targetIdOfRelationshipInTarget,
1579
- });
1580
- }
1581
- else if (this.sourceDb ===
1582
- (await this._provenanceManager.getProvenanceSourceDb())) {
1583
- const relProvenance = await this._provenanceManager.queryProvenanceForRelationship(changedInstanceId, {
1584
- classFullName: classFullName ?? "",
1585
- sourceId: sourceIdOfRelationshipInSource,
1586
- targetId: targetIdOfRelationshipInSource,
1559
+ if (identifierValue)
1560
+ return identifierValue;
1561
+ }
1562
+ // Check for targetId using sourceId's fedguid if we didn't find an esa.
1563
+ if (federationGuid)
1564
+ return this.targetDb.elements.getIdFromFederationGuid(federationGuid);
1565
+ return undefined;
1566
+ }
1567
+ /**
1568
+ * Helper function for [[prepareSourceChanges]]. Records how to find the target relationship of a deleted source relationship.
1569
+ * @param deletion the deleted source relationship.
1570
+ * @param deletedScopeEsaByElementId this transformation scope's deleted ESAs in the same batch, keyed by the ID of the element that owned each one.
1571
+ * @param deletedFederationGuidByElementId federation GUIDs of the elements deleted in the same batch, for endpoints that no longer exist in the source.
1572
+ */
1573
+ async processDeletedRelationship(deletion, deletedScopeEsaByElementId, deletedFederationGuidByElementId) {
1574
+ const { ecInstanceId } = deletion;
1575
+ const classFullName = this.sourceDb.getClassNameFromId(deletion.ecClassId);
1576
+ const getEndpointInTarget = async (sourceId) => this.findTargetElementIdForDeletion(sourceId, this.sourceDb.elements.tryGetElement(sourceId)?.federationGuid ??
1577
+ deletedFederationGuidByElementId.get(sourceId), deletedScopeEsaByElementId);
1578
+ const sourceIdInTarget = await getEndpointInTarget(deletion.sourceECInstanceId);
1579
+ const targetIdInTarget = await getEndpointInTarget(deletion.targetECInstanceId);
1580
+ const isProvenanceSource = this.sourceDb === (await this._provenanceManager.getProvenanceSourceDb());
1581
+ const queryRelationshipProvenance = async () => this._provenanceManager.queryProvenanceForRelationship(ecInstanceId, {
1582
+ classFullName,
1583
+ sourceId: deletion.sourceECInstanceId,
1584
+ targetId: deletion.targetECInstanceId,
1585
+ });
1586
+ if (sourceIdInTarget && targetIdInTarget) {
1587
+ // relationships with ESA provenance must have their aspect deleted too, even when the target relationship is found by endpoints
1588
+ const provenanceAspectId = this._options.forceExternalSourceAspectProvenance && isProvenanceSource
1589
+ ? (await queryRelationshipProvenance())?.aspectId
1590
+ : undefined;
1591
+ this._deletedSourceRelationshipData?.set(ecInstanceId, {
1592
+ classFullName,
1593
+ sourceIdInTarget,
1594
+ targetIdInTarget,
1595
+ provenanceAspectId,
1596
+ });
1597
+ this._pendingRelationshipDeleteTargets.add(relationshipEndpointsKey(sourceIdInTarget, targetIdInTarget));
1598
+ }
1599
+ else if (isProvenanceSource) {
1600
+ const relProvenance = await queryRelationshipProvenance();
1601
+ if (relProvenance && relProvenance.relationshipId) {
1602
+ this._deletedSourceRelationshipData?.set(ecInstanceId, {
1603
+ classFullName,
1604
+ relId: relProvenance.relationshipId,
1605
+ provenanceAspectId: relProvenance.aspectId,
1587
1606
  });
1588
- if (relProvenance && relProvenance.relationshipId)
1589
- this._deletedSourceRelationshipData?.set(changedInstanceId, {
1590
- classFullName: classFullName ?? "",
1591
- relId: relProvenance.relationshipId,
1592
- provenanceAspectId: relProvenance.aspectId,
1593
- });
1607
+ this._pendingRelationshipDeleteTargets.add(relProvenance.relationshipId);
1594
1608
  }
1595
1609
  }
1596
- else {
1597
- let targetId = await getTargetIdFromSourceId(changedInstanceId);
1598
- if (targetId === undefined &&
1599
- this.sourceDb ===
1600
- (await this._provenanceManager.getProvenanceSourceDb())) {
1601
- const contextTargetId = this.context.findTargetElementId(changedInstanceId);
1602
- if (core_bentley_1.Id64.isValidId64(contextTargetId))
1603
- targetId = contextTargetId;
1604
- }
1605
- // since we are processing one changeset at a time, we can see local source deletes
1606
- // of entities that were never synced and can be safely ignored
1607
- const deletionNotInTarget = !targetId;
1608
- if (deletionNotInTarget)
1609
- return;
1610
- if (targetId === undefined) {
1611
- throw new Error("targetId should be acquired from source id or transformation context");
1612
- }
1613
- this.context.remapElement(changedInstanceId, targetId);
1614
- // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
1615
- // In such case an entity update will be triggered and we no longer need to delete the entity.
1616
- if (alreadyImportedElementInserts.has(targetId)) {
1617
- this.exporter.sourceDbChanges?.element.deleteIds.delete(changedInstanceId);
1618
- }
1619
- if (alreadyImportedModelInserts.has(targetId)) {
1620
- this.exporter.sourceDbChanges?.model.deleteIds.delete(changedInstanceId);
1621
- }
1610
+ }
1611
+ /**
1612
+ * Helper function for [[prepareSourceChanges]]. Remaps a deleted source element or model to its target.
1613
+ * @param sourceId the deleted source element or model.
1614
+ * @param federationGuid the deleted source element's federation GUID, if it had one. Undefined for models.
1615
+ * @param deletedScopeEsaByElementId this transformation scope's deleted ESAs in the same batch, keyed by the ID of the element that owned each one.
1616
+ * @param alreadyImportedElementInserts used to handle entity recreation and not delete already handled element inserts.
1617
+ * @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
1618
+ */
1619
+ async processDeletedElement(sourceId, federationGuid, deletedScopeEsaByElementId, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1620
+ let targetId = await this.findTargetElementIdForDeletion(sourceId, federationGuid, deletedScopeEsaByElementId);
1621
+ if (targetId === undefined &&
1622
+ this.sourceDb === (await this._provenanceManager.getProvenanceSourceDb())) {
1623
+ const contextTargetId = this.context.findTargetElementId(sourceId);
1624
+ if (core_bentley_1.Id64.isValidId64(contextTargetId))
1625
+ targetId = contextTargetId;
1626
+ }
1627
+ // Entities inserted and deleted since the last synchronization were never synced and can be safely ignored.
1628
+ if (!targetId)
1629
+ return;
1630
+ this.context.remapElement(sourceId, targetId);
1631
+ // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
1632
+ // In such case an entity update will be triggered and we no longer need to delete the entity.
1633
+ if (alreadyImportedElementInserts.has(targetId)) {
1634
+ this.exporter.sourceDbChanges?.element.deleteIds.delete(sourceId);
1635
+ }
1636
+ if (alreadyImportedModelInserts.has(targetId)) {
1637
+ this.exporter.sourceDbChanges?.model.deleteIds.delete(sourceId);
1622
1638
  }
1623
1639
  }
1624
1640
  async _tryInitChangesetData(args) {
@@ -1632,7 +1648,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1632
1648
  const noChanges = syncVersion.index === this.sourceDb.changeset.index;
1633
1649
  if (noChanges) {
1634
1650
  this._sourceChangeDataState = "no-changes";
1635
- this._csFileProps = [];
1651
+ this._changesetRanges = [];
1636
1652
  return;
1637
1653
  }
1638
1654
  const startChangeset = "startChangeset" in args ? args.startChangeset : undefined;
@@ -1669,20 +1685,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1669
1685
  core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
1670
1686
  this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
1671
1687
  core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
1672
- const csFileProps = [];
1673
- for (const [first, end] of this._changesetRanges) {
1674
- // TODO: should the first changeset in a reverse sync really be included even though its 'initialized branch provenance'? The answer is no, its a bug that needs to be fixed.
1675
- const fileProps = await core_backend_1.BriefcaseManager.downloadChangesets({
1676
- iModelId: this.sourceDb.iModelId,
1677
- targetDir: core_backend_1.BriefcaseManager.getChangeSetsPath(this.sourceDb.iModelId),
1678
- range: { first, end },
1679
- });
1680
- csFileProps.push(...fileProps);
1681
- }
1682
- this._csFileProps = csFileProps;
1683
- /** Theres a possibility that our csFileProps length is still 0 here, since we skip cs indices found in the pendingSync and pendingReverseSync indices arrays. */
1688
+ /** Skipping indices found in the pendingSync and pendingReverseSync arrays can leave no ranges to process. */
1684
1689
  this._sourceChangeDataState =
1685
- this._csFileProps.length === 0 ? "no-changes" : "has-changes";
1690
+ this._changesetRanges.length === 0 ? "no-changes" : "has-changes";
1686
1691
  }
1687
1692
  /** Asserts that the EditTxn is active before any write operations. */
1688
1693
  assertEditTxnActive() {
@@ -1808,17 +1813,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1808
1813
  const startChangeset = "startChangeset" in opts ? opts.startChangeset : undefined;
1809
1814
  return {
1810
1815
  skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements,
1811
- ...(this._csFileProps
1812
- ? { csFileProps: this._csFileProps }
1813
- : this._changesetRanges
1814
- ? { changesetRanges: this._changesetRanges }
1815
- : startChangeset
1816
- ? { startChangeset }
1817
- : {
1818
- startChangeset: {
1819
- index: (await this.getSynchronizationVersion()).index + 1,
1820
- },
1821
- }),
1816
+ ...(this._changesetRanges
1817
+ ? { changesetRanges: this._changesetRanges }
1818
+ : startChangeset
1819
+ ? { startChangeset }
1820
+ : {
1821
+ startChangeset: {
1822
+ index: (await this.getSynchronizationVersion()).index + 1,
1823
+ },
1824
+ }),
1822
1825
  };
1823
1826
  }
1824
1827
  /** Combine an array of source elements into a single target element.