@itwin/imodel-transformer 1.0.0-dev.1 → 1.0.0-dev.2

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.
@@ -688,6 +688,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
688
688
  skipPropagateChangesToRootElements: this._options.skipPropagateChangesToRootElements ?? false,
689
689
  });
690
690
  }
691
+ /**
692
+ * Queries the provenanceDb for an ESA whose identifier is equal to the provided 'entityInProvenanceSourceId'.
693
+ * The identifier on the ESA is the id of the element in the [[IModelTransformer.provenanceSourceDb]]
694
+ * Therefore it only makes sense to call this function when you have an id in the provenanceSourceDb.
695
+ * @param entityInProvenanceSourceId
696
+ * @returns the elementId that the ESA is stored on, esa.Element.Id
697
+ */
691
698
  _queryProvenanceForElement(entityInProvenanceSourceId) {
692
699
  return this.provenanceDb.withPreparedStatement(`
693
700
  SELECT esa.Element.Id
@@ -705,6 +712,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
705
712
  return undefined;
706
713
  });
707
714
  }
715
+ /**
716
+ * Queries the provenanceDb for an ESA whose identifier is equal to the provided 'entityInProvenanceSourceId'.
717
+ * The identifier on the ESA is the id of the relationship in the [[IModelTransformer.provenanceSourceDb]]
718
+ * Therefore it only makes sense to call this function when you have an id in the provenanceSourceDb.
719
+ * @param entityInProvenanceSourceId
720
+ * @returns
721
+ */
708
722
  _queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
709
723
  return this.provenanceDb.withPreparedStatement(`
710
724
  SELECT
@@ -850,6 +864,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
850
864
  onTransformElement(sourceElement) {
851
865
  core_bentley_1.Logger.logTrace(loggerCategory, `onTransformElement(${sourceElement.id}) "${sourceElement.getDisplayLabel()}"`);
852
866
  const targetElementProps = this.context.cloneElement(sourceElement, { binaryGeometry: this._options.cloneUsingBinaryGeometry });
867
+ // Special case: source element is the root subject
868
+ if (sourceElement.id === core_common_1.IModel.rootSubjectId) {
869
+ const targetElementId = this.context.findTargetElementId(sourceElement.id);
870
+ // When remapping rootSubject from source to non root subject in target, the code.scope gets remapped incorrectly.
871
+ // This is because the rootSubject has no parent and its code.scope is unique in that it is the id of itself.
872
+ // For all other subjects which do have parents the code.scope and its parent should be in agreement.
873
+ if (targetElementId !== core_bentley_1.Id64.invalid &&
874
+ targetElementId !== core_common_1.IModel.rootSubjectId) {
875
+ const targetElement = this.targetDb.elements.getElement(targetElementId);
876
+ targetElementProps.parent =
877
+ targetElement.parent ?? targetElementProps.parent;
878
+ targetElementProps.code.scope = targetElement.code.scope;
879
+ }
880
+ }
853
881
  if (sourceElement instanceof core_backend_1.Subject) {
854
882
  if (targetElementProps.jsonProperties?.Subject?.Job) {
855
883
  // don't propagate source channels into target (legacy bridge case)
@@ -1344,21 +1372,24 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1344
1372
  const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
1345
1373
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
1346
1374
  core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
1347
- const [syncChangesetsToClear, syncChangesetsToUpdate] = this
1375
+ const pendingSyncChangesetIndicesKey = "pendingSyncChangesetIndices";
1376
+ const pendingReverseSyncChangesetIndicesKey = "pendingReverseSyncChangesetIndices";
1377
+ const [syncChangesetsToClearKey, syncChangesetsToUpdateKey] = this
1348
1378
  .isReverseSynchronization
1349
1379
  ? [
1350
- jsonProps.pendingReverseSyncChangesetIndices,
1351
- jsonProps.pendingSyncChangesetIndices,
1380
+ pendingReverseSyncChangesetIndicesKey,
1381
+ pendingSyncChangesetIndicesKey,
1352
1382
  ]
1353
1383
  : [
1354
- jsonProps.pendingSyncChangesetIndices,
1355
- jsonProps.pendingReverseSyncChangesetIndices,
1384
+ pendingSyncChangesetIndicesKey,
1385
+ pendingReverseSyncChangesetIndicesKey,
1356
1386
  ];
1357
1387
  for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
1358
- syncChangesetsToUpdate.push(i);
1359
- // FIXME: add test to synchronize an iModel that is not at the tip, since then clearning syncChangesets is
1360
- // probably wrong, and we should filter it instead
1361
- syncChangesetsToClear.length = 0;
1388
+ jsonProps[syncChangesetsToUpdateKey].push(i);
1389
+ // Only keep the changeset indices which are greater than the source, this means they haven't been processed yet.
1390
+ jsonProps[syncChangesetsToClearKey] = jsonProps[syncChangesetsToClearKey].filter((csIndex) => {
1391
+ return csIndex > this._startingChangesetIndices.source;
1392
+ });
1362
1393
  // if reverse sync then we may have received provenance changes which should be marked as sync changes
1363
1394
  if (this.isReverseSynchronization) {
1364
1395
  nodeAssert(this.sourceDb.changeset.index !== undefined, "changeset didn't exist");
@@ -1479,15 +1510,16 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1479
1510
  core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
1480
1511
  return;
1481
1512
  }
1482
- const relArg = deletedRelData.relId ??
1483
- {
1513
+ const id = deletedRelData.relId ??
1514
+ this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, {
1484
1515
  sourceId: deletedRelData.sourceIdInTarget,
1485
1516
  targetId: deletedRelData.targetIdInTarget,
1486
- };
1487
- // FIXME: make importer.deleteRelationship not need full props
1488
- const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
1489
- if (targetRelationship) {
1490
- this.importer.deleteRelationship(targetRelationship.toJSON());
1517
+ })?.id;
1518
+ if (id) {
1519
+ this.importer.deleteRelationship({
1520
+ id,
1521
+ classFullName: deletedRelData.classFullName,
1522
+ });
1491
1523
  }
1492
1524
  if (deletedRelData.provenanceAspectId) {
1493
1525
  try {
@@ -1531,8 +1563,10 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1531
1563
  const json = JSON.parse(statement.getValue(2).getString());
1532
1564
  const targetRelInstanceId = json.targetRelInstanceId ?? json.provenanceRelInstanceId;
1533
1565
  if (targetRelInstanceId) {
1534
- const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, targetRelInstanceId);
1535
- this.importer.deleteRelationship(targetRelationship.toJSON());
1566
+ this.importer.deleteRelationship({
1567
+ id: targetRelInstanceId,
1568
+ classFullName: core_backend_1.ElementRefersToElements.classFullName,
1569
+ });
1536
1570
  }
1537
1571
  aspectDeleteIds.push(statement.getValue(0).getId());
1538
1572
  }
@@ -1815,7 +1849,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1815
1849
  if (changeType !== "Deleted" ||
1816
1850
  relationshipECClassIdsToSkip.has(ecClassId))
1817
1851
  continue;
1818
- this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
1852
+ await this.processDeletedOp(change, elemIdToScopeEsa, relationshipECClassIds.has(ecClassId ?? ""), alreadyImportedElementInserts, alreadyImportedModelInserts);
1819
1853
  }
1820
1854
  csReader.close();
1821
1855
  }
@@ -1832,116 +1866,98 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
1832
1866
  * @param alreadyImportedModelInserts used to handle entity recreation and not delete already handled model inserts.
1833
1867
  * @returns void
1834
1868
  */
1835
- processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1869
+ async processDeletedOp(change, mapOfDeletedElemIdToScopeEsas, isRelationship, alreadyImportedElementInserts, alreadyImportedModelInserts) {
1836
1870
  // we need a connected iModel with changes to remap elements with deletions
1837
1871
  const notConnectedModel = this.sourceDb.iTwinId === undefined;
1838
1872
  const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
1839
1873
  if (notConnectedModel || noChanges)
1840
1874
  return;
1841
- // optimization: if we have provenance, use it to avoid more querying later
1842
- // eventually when itwin.js supports attaching a second iModelDb in JS,
1843
- // this won't have to be a conditional part of the query, and we can always have it by attaching
1844
- const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
1845
- const instId = change.ECInstanceId;
1846
- if (!isRelationship) {
1847
- const sourceElemFedGuid = change.FederationGuid;
1875
+ /**
1876
+ * if our ChangedECInstance is in the provenanceDb, then we can use the ids we find in the ChangedECInstance to query for ESAs.
1877
+ * This is because the ESAs are stored on an element Id thats present in the provenanceDb.
1878
+ */
1879
+ const changeDataInProvenanceDb = this.sourceDb === this.provenanceDb;
1880
+ const getTargetIdFromSourceId = async (id) => {
1848
1881
  let identifierValue;
1849
- if (queryCanAccessProvenance) {
1850
- const aspects = this.sourceDb.elements.getAspects(instId, core_backend_1.ExternalSourceAspect.classFullName);
1851
- for (const aspect of aspects) {
1852
- // look for aspect where the ecInstanceId = the aspect.element.id
1853
- if (aspect.element.id === instId &&
1854
- aspect.scope.id === this.targetScopeElementId)
1855
- identifierValue = aspect.identifier;
1856
- }
1857
- // Think I need to query the esas given the instId.. not sure what db to do it on though.. soruce or target.. or provenance?
1858
- // I need to know the id of the element dpeneding on which db its stored in.
1882
+ let element;
1883
+ if (isRelationship) {
1884
+ element = this.sourceDb.elements.tryGetElement(id);
1859
1885
  }
1860
- if (queryCanAccessProvenance && !identifierValue) {
1861
- if (mapOfDeletedElemIdToScopeEsas.get(instId) !== undefined)
1862
- identifierValue =
1863
- mapOfDeletedElemIdToScopeEsas.get(instId).Identifier;
1886
+ const fedGuid = isRelationship
1887
+ ? element?.federationGuid
1888
+ : change.FederationGuid;
1889
+ if (changeDataInProvenanceDb) {
1890
+ // TODO: clarify what happens if there are multiple (e.g. elements were merged)
1891
+ 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([
1892
+ this.targetScopeElementId,
1893
+ core_backend_1.ExternalSourceAspect.Kind.Element,
1894
+ id,
1895
+ ]))) {
1896
+ identifierValue = row.Identifier;
1897
+ }
1898
+ identifierValue =
1899
+ identifierValue ?? mapOfDeletedElemIdToScopeEsas.get(id)?.Identifier;
1864
1900
  }
1865
- const targetId = (queryCanAccessProvenance && identifierValue) ||
1866
- // maybe batching these queries would perform better but we should
1867
- // try to attach the second db and query both together anyway
1868
- (sourceElemFedGuid &&
1869
- this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid)) ||
1870
- // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
1871
- this._queryProvenanceForElement(instId);
1872
- // since we are processing one changeset at a time, we can see local source deletes
1873
- // of entities that were never synced and can be safely ignored
1874
- const deletionNotInTarget = !targetId;
1875
- if (deletionNotInTarget)
1876
- return;
1877
- this.context.remapElement(instId, targetId);
1878
- // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
1879
- // In such case an entity update will be triggered and we no longer need to delete the entity.
1880
- if (alreadyImportedElementInserts.has(targetId)) {
1881
- this.exporter.sourceDbChanges?.element.deleteIds.delete(instId);
1901
+ // Check for targetId by an esa first
1902
+ if (changeDataInProvenanceDb && identifierValue) {
1903
+ const targetId = identifierValue;
1904
+ return targetId;
1882
1905
  }
1883
- if (alreadyImportedModelInserts.has(targetId)) {
1884
- this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
1906
+ // Check for targetId using sourceId's fedguid if we didn't find an esa.
1907
+ if (fedGuid) {
1908
+ const targetId = this._queryElemIdByFedGuid(this.targetDb, fedGuid);
1909
+ return targetId;
1885
1910
  }
1886
- }
1887
- else {
1888
- // is deleted relationship
1889
- const classFullName = change.$meta?.classFullName;
1911
+ return undefined;
1912
+ };
1913
+ const changedInstanceId = change.ECInstanceId;
1914
+ if (isRelationship) {
1890
1915
  const sourceIdOfRelationshipInSource = change.SourceECInstanceId;
1891
1916
  const targetIdOfRelationshipInSource = change.TargetECInstanceId;
1892
- const [sourceIdInTarget, targetIdInTarget] = [
1893
- sourceIdOfRelationshipInSource,
1894
- targetIdOfRelationshipInSource,
1895
- ].map((id) => {
1896
- let element;
1897
- try {
1898
- element = this.sourceDb.elements.getElement(id);
1899
- }
1900
- catch (err) {
1901
- return undefined;
1902
- }
1903
- const fedGuid = element.federationGuid;
1904
- let identifierValue;
1905
- if (queryCanAccessProvenance) {
1906
- const aspects = this.sourceDb.elements.getAspects(id, core_backend_1.ExternalSourceAspect.classFullName);
1907
- for (const aspect of aspects) {
1908
- if (aspect.element.id === id &&
1909
- aspect.scope.id === this.targetScopeElementId)
1910
- identifierValue = aspect.identifier;
1911
- }
1912
- if (identifierValue === undefined) {
1913
- if (mapOfDeletedElemIdToScopeEsas.get(id) !== undefined)
1914
- identifierValue =
1915
- mapOfDeletedElemIdToScopeEsas.get(id).Identifier;
1916
- }
1917
- }
1918
- return ((queryCanAccessProvenance && identifierValue) ||
1919
- // maybe batching these queries would perform better but we should
1920
- // try to attach the second db and query both together anyway
1921
- (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
1922
- });
1923
- if (sourceIdInTarget && targetIdInTarget) {
1924
- this._deletedSourceRelationshipData.set(instId, {
1917
+ const classFullName = change.$meta?.classFullName;
1918
+ const sourceIdOfRelationshipInTarget = await getTargetIdFromSourceId(sourceIdOfRelationshipInSource);
1919
+ const targetIdOfRelationshipInTarget = await getTargetIdFromSourceId(targetIdOfRelationshipInSource);
1920
+ if (sourceIdOfRelationshipInTarget && targetIdOfRelationshipInTarget) {
1921
+ this._deletedSourceRelationshipData.set(changedInstanceId, {
1925
1922
  classFullName: classFullName ?? "",
1926
- sourceIdInTarget,
1927
- targetIdInTarget,
1923
+ sourceIdInTarget: sourceIdOfRelationshipInTarget,
1924
+ targetIdInTarget: targetIdOfRelationshipInTarget,
1928
1925
  });
1929
1926
  }
1930
- else {
1931
- // FIXME<MIKE>: describe why it's safe to assume nothing has been deleted in provenanceDb
1932
- const relProvenance = this._queryProvenanceForRelationship(instId, {
1927
+ else if (this.sourceDb === this.provenanceSourceDb) {
1928
+ const relProvenance = this._queryProvenanceForRelationship(changedInstanceId, {
1933
1929
  classFullName: classFullName ?? "",
1934
1930
  sourceId: sourceIdOfRelationshipInSource,
1935
1931
  targetId: targetIdOfRelationshipInSource,
1936
1932
  });
1937
1933
  if (relProvenance && relProvenance.relationshipId)
1938
- this._deletedSourceRelationshipData.set(instId, {
1934
+ this._deletedSourceRelationshipData.set(changedInstanceId, {
1939
1935
  classFullName: classFullName ?? "",
1940
1936
  relId: relProvenance.relationshipId,
1941
1937
  provenanceAspectId: relProvenance.aspectId,
1942
1938
  });
1943
1939
  }
1944
1940
  }
1941
+ else {
1942
+ let targetId = await getTargetIdFromSourceId(changedInstanceId);
1943
+ if (targetId === undefined && this.sourceDb === this.provenanceSourceDb) {
1944
+ targetId = this._queryProvenanceForElement(changedInstanceId);
1945
+ }
1946
+ // since we are processing one changeset at a time, we can see local source deletes
1947
+ // of entities that were never synced and can be safely ignored
1948
+ const deletionNotInTarget = !targetId;
1949
+ if (deletionNotInTarget)
1950
+ return;
1951
+ this.context.remapElement(changedInstanceId, targetId);
1952
+ // If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
1953
+ // In such case an entity update will be triggered and we no longer need to delete the entity.
1954
+ if (alreadyImportedElementInserts.has(targetId)) {
1955
+ this.exporter.sourceDbChanges?.element.deleteIds.delete(changedInstanceId);
1956
+ }
1957
+ if (alreadyImportedModelInserts.has(targetId)) {
1958
+ this.exporter.sourceDbChanges?.model.deleteIds.delete(changedInstanceId);
1959
+ }
1960
+ }
1945
1961
  }
1946
1962
  async _tryInitChangesetData(args) {
1947
1963
  if (!args ||
@@ -2016,7 +2032,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2016
2032
  await this.exporter.exportCodeSpecs();
2017
2033
  await this.exporter.exportFonts();
2018
2034
  if (this._options.skipPropagateChangesToRootElements) {
2019
- // FIXME<NICK>: This option in exportAll was a maybe.
2020
2035
  // The RepositoryModel and root Subject of the target iModel should not be transformed.
2021
2036
  await this.exporter.exportChildElements(core_common_1.IModel.rootSubjectId); // start below the root Subject
2022
2037
  await this.exporter.exportModelContents(core_common_1.IModel.repositoryModelId, core_backend_1.Element.classFullName, true); // after the Subject hierarchy, process the other elements of the RepositoryModel
@@ -2051,202 +2066,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2051
2066
  : core_backend_1.ExternalSourceAspect.Kind.Element,
2052
2067
  };
2053
2068
  }
2054
- /**
2055
- * Load the state of the active transformation from an open SQLiteDb
2056
- * You can override this if you'd like to load from custom tables in the resumable dump state, but you should call
2057
- * this super implementation
2058
- * @note the SQLiteDb must be open
2059
- */
2060
- loadStateFromDb(db) {
2061
- const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
2062
- if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
2063
- throw Error("expected row when getting lastProvenanceEntityId from target state table");
2064
- const entityId = stmt.getValueString(0);
2065
- const isGuidOrGuidPair = entityId.includes("-");
2066
- return isGuidOrGuidPair
2067
- ? entityId
2068
- : {
2069
- entityId,
2070
- aspectId: stmt.getValueString(1),
2071
- aspectVersion: stmt.getValueString(2),
2072
- aspectKind: stmt.getValueString(3),
2073
- };
2074
- });
2075
- /*
2076
- // TODO: maybe save transformer state resumption state based on target changset and require calls
2077
- // to saveChanges
2078
- if () {
2079
- const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
2080
- const isRelProvenance = targetFedGuid !== undefined;
2081
- const instanceId = isRelProvenance
2082
- ? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
2083
- : "";
2084
- //const classId =
2085
- if (isRelProvenance) {
2086
- }
2087
- }
2088
- */
2089
- const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
2090
- // ignore provenance check if it's null since we can't bind those ids
2091
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
2092
- !core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
2093
- this.provenanceDb.withPreparedStatement(`
2094
- SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
2095
- WHERE Scope.Id=:scopeId
2096
- AND ECInstanceId=:aspectId
2097
- AND Kind=:kind
2098
- AND Element.Id=:entityId
2099
- `, (statement) => {
2100
- statement.bindId("scopeId", this.targetScopeElementId);
2101
- statement.bindId("aspectId", lastProvenanceEntityInfo.aspectId);
2102
- statement.bindString("kind", lastProvenanceEntityInfo.aspectKind);
2103
- statement.bindId("entityId", lastProvenanceEntityInfo.entityId);
2104
- const stepResult = statement.step();
2105
- switch (stepResult) {
2106
- case core_bentley_1.DbResult.BE_SQLITE_ROW:
2107
- const version = statement.getValue(0).getString();
2108
- return version === lastProvenanceEntityInfo.aspectVersion;
2109
- case core_bentley_1.DbResult.BE_SQLITE_DONE:
2110
- return false;
2111
- default:
2112
- throw new core_common_1.IModelError(core_bentley_1.IModelStatus.SQLiteError, `got sql error ${stepResult}`);
2113
- }
2114
- });
2115
- if (!targetHasCorrectLastProvenance)
2116
- throw Error([
2117
- "Target for resuming from does not have the expected provenance ",
2118
- "from the target that the resume state was made with",
2119
- ].join("\n"));
2120
- this._lastProvenanceEntityInfo = lastProvenanceEntityInfo;
2121
- const state = db.withSqliteStatement(`SELECT data FROM ${IModelTransformer.jsStateTable}`, (stmt) => {
2122
- if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
2123
- throw Error("expected row when getting data from js state table");
2124
- return JSON.parse(stmt.getValueString(0));
2125
- });
2126
- if (state.transformerClass !== this.constructor.name)
2127
- throw Error("resuming from a differently named transformer class, it is not necessarily valid to resume with a different transformer class");
2128
- // force assign to readonly options since we do not know how the transformer subclass takes options to pass to the superclass
2129
- this._options = state.options;
2130
- this.context.loadStateFromDb(db);
2131
- this.importer.loadStateFromJson(state.importerState);
2132
- this.exporter.loadStateFromJson(state.exporterState);
2133
- this._elementsWithExplicitlyTrackedProvenance =
2134
- core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
2135
- this.loadAdditionalStateJson(state.additionalState);
2136
- }
2137
- /**
2138
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
2139
- * from the original changeset
2140
- *
2141
- * Return a new transformer instance with the same remappings state as saved from a previous [[IModelTransformer.saveStateToFile]] call.
2142
- * This allows you to "resume" an iModel transformation, you will have to call [[IModelTransformer.processChanges]]/[[IModelTransformer.processAll]]
2143
- * again but the remapping state will cause already mapped elements to be skipped.
2144
- * To "resume" an iModel Transformation you need:
2145
- * - the sourceDb at the same changeset
2146
- * - the same targetDb in the state in which it was before
2147
- * @param statePath the path to the serialized state of the transformer, use [[IModelTransformer.saveStateToFile]] to get this from an existing transformer instance
2148
- * @param constructorArgs remaining arguments that you would normally pass to the Transformer subclass you are using, usually (sourceDb, targetDb)
2149
- * @note custom transformers with custom state may need to override this method in order to handle loading their own custom state somewhere
2150
- */
2151
- static resumeTransformation(statePath, ...constructorArgs) {
2152
- const transformer = new this(...constructorArgs);
2153
- const db = new core_backend_1.SQLiteDb();
2154
- db.openDb(statePath, core_bentley_1.OpenMode.Readonly);
2155
- try {
2156
- transformer.loadStateFromDb(db);
2157
- }
2158
- finally {
2159
- db.closeDb();
2160
- }
2161
- return transformer;
2162
- }
2163
- /**
2164
- * You may override this to store arbitrary json state in a transformer state dump, useful for some resumptions
2165
- * @see [[IModelTransformer.saveStateToFile]]
2166
- */
2167
- getAdditionalStateJson() {
2168
- return {};
2169
- }
2170
- /**
2171
- * You may override this to load arbitrary json state in a transformer state dump, useful for some resumptions
2172
- * @see [[IModelTransformer.loadStateFromFile]]
2173
- */
2174
- loadAdditionalStateJson(_additionalState) { }
2175
- /**
2176
- * Save the state of the active transformation to an open SQLiteDb
2177
- * You can override this if you'd like to write custom tables to the resumable dump state, but you should call
2178
- * this super implementation
2179
- * @note the SQLiteDb must be open
2180
- */
2181
- saveStateToDb(db) {
2182
- const jsonState = {
2183
- transformerClass: this.constructor.name,
2184
- options: this._options,
2185
- explicitlyTrackedElements: core_bentley_1.CompressedId64Set.compressSet(this._elementsWithExplicitlyTrackedProvenance),
2186
- importerState: this.importer.saveStateToJson(),
2187
- exporterState: this.exporter.saveStateToJson(),
2188
- additionalState: this.getAdditionalStateJson(),
2189
- };
2190
- this.context.saveStateToDb(db);
2191
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
2192
- db.executeSQL(`CREATE TABLE ${IModelTransformer.jsStateTable} (data TEXT)`))
2193
- throw Error("Failed to create the js state table in the state database");
2194
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !==
2195
- db.executeSQL(`
2196
- CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
2197
- -- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
2198
- entityId TEXT,
2199
- -- the following are only valid if the above entityId is a hex id representation
2200
- aspectId TEXT,
2201
- aspectVersion TEXT,
2202
- aspectKind TEXT
2203
- )
2204
- `))
2205
- throw Error("Failed to create the target state table in the state database");
2206
- db.saveChanges();
2207
- db.withSqliteStatement(`INSERT INTO ${IModelTransformer.jsStateTable} (data) VALUES (?)`, (stmt) => {
2208
- stmt.bindString(1, JSON.stringify(jsonState));
2209
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
2210
- throw Error("Failed to insert options into the state database");
2211
- });
2212
- db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
2213
- const lastProvenanceEntityInfo = this
2214
- ._lastProvenanceEntityInfo;
2215
- stmt.bindString(1, lastProvenanceEntityInfo?.entityId ??
2216
- this._lastProvenanceEntityInfo);
2217
- stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
2218
- stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
2219
- stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
2220
- if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
2221
- throw Error("Failed to insert options into the state database");
2222
- });
2223
- db.saveChanges();
2224
- }
2225
- /**
2226
- * @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
2227
- * from the original changeset
2228
- *
2229
- * Save the state of the active transformation to a file path, if a file at the path already exists, it will be overwritten
2230
- * This state can be used by [[IModelTransformer.resumeTransformation]] to resume a transformation from this point.
2231
- * The serialization format is a custom sqlite database.
2232
- * @note custom transformers with custom state may override [[IModelTransformer.saveStateToDb]] or [[IModelTransformer.getAdditionalStateJson]]
2233
- * and [[IModelTransformer.loadStateFromDb]] (with a super call) or [[IModelTransformer.loadAdditionalStateJson]]
2234
- * if they have custom state that needs to be stored with
2235
- * potentially inside the same sqlite file in separate tables
2236
- */
2237
- saveStateToFile(nativeStatePath) {
2238
- const db = new core_backend_1.SQLiteDb();
2239
- if (core_backend_1.IModelJsFs.existsSync(nativeStatePath))
2240
- core_backend_1.IModelJsFs.unlinkSync(nativeStatePath);
2241
- db.createDb(nativeStatePath);
2242
- try {
2243
- this.saveStateToDb(db);
2244
- db.saveChanges();
2245
- }
2246
- finally {
2247
- db.closeDb();
2248
- }
2249
- }
2250
2069
  /** Export changes from the source iModel and import the transformed entities into the target iModel.
2251
2070
  * Inserts, updates, and deletes are determined by inspecting the changeset(s).
2252
2071
  * @note the transformer saves and pushes changes when its work is complete.
@@ -2303,10 +2122,6 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
2303
2122
  }
2304
2123
  exports.IModelTransformer = IModelTransformer;
2305
2124
  IModelTransformer.noEsaSyncDirectionErrorMessage = "Couldn't find an external source aspect to determine sync direction. This often means that the master->branch relationship has not been established. Consider running the transformer with wasSourceIModelCopiedToTarget set to true.";
2306
- /** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
2307
- IModelTransformer.jsStateTable = "TransformerJsState";
2308
- /** @internal the name of the table where the target state heuristics is serialized in transformer state dumps */
2309
- IModelTransformer.lastProvenanceEntityInfoTable = "LastProvenanceEntityInfo";
2310
2125
  /** IModelTransformer that clones the contents of a template model.
2311
2126
  * @beta
2312
2127
  */