@itwin/imodel-transformer 2.0.0-dev.50 → 2.0.0-dev.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/lib/cjs/ChangedElementForest.d.ts +14 -0
  2. package/lib/cjs/ChangedElementForest.d.ts.map +1 -0
  3. package/lib/cjs/ChangedElementForest.js +86 -0
  4. package/lib/cjs/ChangedElementForest.js.map +1 -0
  5. package/lib/cjs/ElementAspectCleanup.d.ts.map +1 -1
  6. package/lib/cjs/ElementAspectCleanup.js +1 -2
  7. package/lib/cjs/ElementAspectCleanup.js.map +1 -1
  8. package/lib/cjs/ElementBulkDelete.d.ts +11 -0
  9. package/lib/cjs/ElementBulkDelete.d.ts.map +1 -0
  10. package/lib/cjs/ElementBulkDelete.js +55 -0
  11. package/lib/cjs/ElementBulkDelete.js.map +1 -0
  12. package/lib/cjs/IModelExporter.d.ts +19 -3
  13. package/lib/cjs/IModelExporter.d.ts.map +1 -1
  14. package/lib/cjs/IModelExporter.js +138 -56
  15. package/lib/cjs/IModelExporter.js.map +1 -1
  16. package/lib/cjs/IModelImporter.d.ts +26 -10
  17. package/lib/cjs/IModelImporter.d.ts.map +1 -1
  18. package/lib/cjs/IModelImporter.js +52 -19
  19. package/lib/cjs/IModelImporter.js.map +1 -1
  20. package/lib/cjs/IModelTransformer.d.ts +2 -4
  21. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  22. package/lib/cjs/IModelTransformer.js +11 -13
  23. package/lib/cjs/IModelTransformer.js.map +1 -1
  24. package/lib/cjs/IModelTransformerError.d.ts +2 -0
  25. package/lib/cjs/IModelTransformerError.d.ts.map +1 -1
  26. package/lib/cjs/IModelTransformerError.js +2 -0
  27. package/lib/cjs/IModelTransformerError.js.map +1 -1
  28. package/package.json +1 -1
  29. package/lib/cjs/ElementCascadingDeleter.d.ts +0 -22
  30. package/lib/cjs/ElementCascadingDeleter.d.ts.map +0 -1
  31. package/lib/cjs/ElementCascadingDeleter.js +0 -57
  32. package/lib/cjs/ElementCascadingDeleter.js.map +0 -1
@@ -18,6 +18,7 @@ const ElementAspectExportProcessor_1 = require("./ElementAspectExportProcessor")
18
18
  const ElementAspectExportCoordinator_1 = require("./ElementAspectExportCoordinator");
19
19
  const IModelTransformerError_1 = require("./IModelTransformerError");
20
20
  const ChangesetScanner_1 = require("./ChangesetScanner");
21
+ const ChangedElementForest_1 = require("./ChangedElementForest");
21
22
  const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelExporter;
22
23
  /** Handles the events generated by IModelExporter.
23
24
  * @note Change information is available when `IModelExportHandler` methods are invoked via [IModelExporter.exportChanges]($transformer), but not available when invoked via [IModelExporter.exportAll]($transformer).
@@ -75,8 +76,8 @@ class IModelExportHandler {
75
76
  * @internal
76
77
  */
77
78
  async preExportElement(_element) { }
78
- /** Called when an element should be deleted. */
79
- async onDeleteElement(_elementId) { }
79
+ /** Called once with the source IDs of all elements deleted by the exported changes. */
80
+ async onDeleteElements(_elementIds) { }
80
81
  /** If `true` is returned, then the ElementAspect will be exported.
81
82
  * @note This method can optionally be overridden to exclude an individual ElementAspect from the export. The base implementation always returns `true`.
82
83
  */
@@ -172,6 +173,10 @@ class IModelExporter {
172
173
  _progressCounter = 0;
173
174
  /** Optionally cached entity change information */
174
175
  _sourceDbChanges;
176
+ /** State shared by nested calls in one change-processing element traversal. */
177
+ _changedElementTraversal;
178
+ /** Bounds the additional hierarchy retained by the sparse changed-element path. */
179
+ _changedElementForestElementLimit = 100_000;
175
180
  /**
176
181
  * Retrieve the cached entity change information.
177
182
  * @note This will only be initialized after [IModelExporter.exportChanges] is invoked or [IModelExporter.initialize] is called.
@@ -262,7 +267,9 @@ class IModelExporter {
262
267
  excludeCodeSpec(codeSpecName) {
263
268
  this._excludedCodeSpecNames.add(codeSpecName);
264
269
  }
265
- /** Add a rule to exclude a specific Element. */
270
+ /** Add a rule to exclude a specific Element.
271
+ * @note Configure exclusions before starting an export operation. Changing exclusions while an export is in progress is unsupported.
272
+ */
266
273
  excludeElement(elementId) {
267
274
  this._excludedElementIds.add(elementId);
268
275
  }
@@ -337,27 +344,33 @@ class IModelExporter {
337
344
  (0, node_assert_1.strict)(this._sourceDbChanges !== undefined, "sourceDbChanges must be initialized.");
338
345
  await this.exportCodeSpecs();
339
346
  await this.exportFonts();
340
- await this._elementAspectExportCoordinator.run(async () => {
341
- if (initOpts.skipPropagateChangesToRootElements) {
342
- // The root Subject is in the RepositoryModel. Traverse its children
343
- // separately, then export other top-level repository elements while
344
- // excluding the root so no element is visited twice.
345
- await this.exportChildElements(core_common_1.IModel.rootSubjectId);
346
- await this.exportModelContents(core_common_1.IModel.repositoryModelId, core_backend_1.Element.classFullName, true);
347
- await this.exportSubModels(core_common_1.IModel.repositoryModelId);
347
+ (0, node_assert_1.strict)(this.beginChangedElementTraversalScope(), "exportChanges must own its changed-element traversal scope");
348
+ try {
349
+ await this._elementAspectExportCoordinator.run(async () => {
350
+ if (initOpts.skipPropagateChangesToRootElements) {
351
+ // The root Subject is in the RepositoryModel. Traverse its children
352
+ // separately, then export other top-level repository elements while
353
+ // excluding the root so no element is visited twice.
354
+ await this.exportChildElements(core_common_1.IModel.rootSubjectId);
355
+ await this.exportModelContents(core_common_1.IModel.repositoryModelId, core_backend_1.Element.classFullName, true);
356
+ await this.exportSubModels(core_common_1.IModel.repositoryModelId);
357
+ }
358
+ else {
359
+ await this.exportModel(core_common_1.IModel.repositoryModelId);
360
+ }
361
+ });
362
+ const aspectOnlyOwnerElementIds = new Set(this._sourceDbChanges.aspectOwnerElementIds);
363
+ for (const elementId of this._sourceDbChanges.element.insertIds) {
364
+ aspectOnlyOwnerElementIds.delete(elementId);
348
365
  }
349
- else {
350
- await this.exportModel(core_common_1.IModel.repositoryModelId);
366
+ for (const elementId of this._sourceDbChanges.element.updateIds) {
367
+ aspectOnlyOwnerElementIds.delete(elementId);
351
368
  }
352
- });
353
- const aspectOnlyOwnerElementIds = new Set(this._sourceDbChanges.aspectOwnerElementIds);
354
- for (const elementId of this._sourceDbChanges.element.insertIds) {
355
- aspectOnlyOwnerElementIds.delete(elementId);
369
+ await this.exportAspectsForOwners(await this.filterOwnerElementIdsForAspectExport(aspectOnlyOwnerElementIds));
356
370
  }
357
- for (const elementId of this._sourceDbChanges.element.updateIds) {
358
- aspectOnlyOwnerElementIds.delete(elementId);
371
+ finally {
372
+ this.endChangedElementTraversalScope();
359
373
  }
360
- await this.exportAspectsForOwners(await this.filterOwnerElementIdsForAspectExport(aspectOnlyOwnerElementIds));
361
374
  await this.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
362
375
  // handle deletes
363
376
  if (this.visitElements) {
@@ -365,22 +378,8 @@ class IModelExporter {
365
378
  for (const modelId of this._sourceDbChanges.model.deleteIds) {
366
379
  await this.handler.onDeleteModel(modelId);
367
380
  }
368
- for (const elementId of this._sourceDbChanges.element.deleteIds) {
369
- // We don't know how the handler wants to handle deletions, and we don't have enough information
370
- // to know if deleted entities were related, so when processing changes, ignore errors from deletion.
371
- // Technically, to keep the ignored error scope small, we ignore only the error of looking up a missing element,
372
- // that approach works at least for the IModelTransformer.
373
- // In the future, the handler may be responsible for doing the work of finding out which elements were cascade deleted,
374
- // and returning them for the exporter to use to avoid double-deleting with error ignoring
375
- try {
376
- await this.handler.onDeleteElement(elementId);
377
- }
378
- catch (err) {
379
- const isMissingErr = err instanceof core_common_1.IModelError &&
380
- err.errorNumber === core_bentley_1.IModelStatus.NotFound;
381
- if (!isMissingErr)
382
- throw err;
383
- }
381
+ if (this._sourceDbChanges.element.deleteIds.size > 0) {
382
+ await this.handler.onDeleteElements(new Set(this._sourceDbChanges.element.deleteIds));
384
383
  }
385
384
  }
386
385
  if (this.visitRelationships) {
@@ -623,6 +622,20 @@ class IModelExporter {
623
622
  !this._sourceDbChanges.model.updateIds.has(modelId)) {
624
623
  return; // this optimization assumes that the Model changes (LastMod) any time an Element in the Model changes
625
624
  }
625
+ if (this.canUseChangedElementForest() &&
626
+ elementClassFullName === core_backend_1.Element.classFullName) {
627
+ const forest = await this.getChangedElementForest();
628
+ if (forest !== undefined) {
629
+ core_bentley_1.Logger.logTrace(loggerCategory, `exportModelContents(${modelId}) via changed-element index`);
630
+ for (const rootId of forest.getModelRoots(modelId)) {
631
+ if (skipRootSubject && rootId === core_common_1.IModel.rootSubjectId)
632
+ continue;
633
+ await this.exportElement(rootId);
634
+ await this._yieldManager.allowYield();
635
+ }
636
+ return;
637
+ }
638
+ }
626
639
  }
627
640
  core_bentley_1.Logger.logTrace(loggerCategory, `exportModelContents(${modelId})`);
628
641
  let sql;
@@ -718,20 +731,19 @@ class IModelExporter {
718
731
  core_bentley_1.Logger.logTrace(loggerCategory, `visitElements=false, skipping exportElement(${elementId})`);
719
732
  return;
720
733
  }
721
- // Return early if the elementId is already in the excludedElementIds, that way we don't need to load the element from the db.
734
+ // Return early if the elementId is already excluded so it does not need to be loaded.
722
735
  if (this._excludedElementIds.has(elementId)) {
723
736
  core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementId} by Id`);
724
737
  await this.handler.onSkipElement(elementId);
725
738
  return;
726
739
  }
727
- // are we processing changes?
728
740
  const isUpdate = this._sourceDbChanges?.element.insertIds.has(elementId)
729
741
  ? false
730
742
  : this._sourceDbChanges?.element.updateIds.has(elementId)
731
743
  ? true
732
744
  : undefined;
733
- // Short-circuit: element is not in the changeset, skip its own export but still visit children
734
- if (undefined !== this._sourceDbChanges && undefined === isUpdate) {
745
+ // An unchanged element may still connect a changed descendant to its model root.
746
+ if (this._sourceDbChanges !== undefined && isUpdate === undefined) {
735
747
  return this.exportChildElements(elementId);
736
748
  }
737
749
  const element = this.sourceDb.elements.getElement({
@@ -748,9 +760,7 @@ class IModelExporter {
748
760
  await this._elementAspectExportCoordinator.addAcceptedOwner(elementId);
749
761
  return this.exportChildElements(elementId);
750
762
  }
751
- else {
752
- await this.handler.onSkipElement(element.id);
753
- }
763
+ await this.handler.onSkipElement(element.id);
754
764
  }
755
765
  /** Export the child elements of the specified element from the source iModel.
756
766
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
@@ -763,7 +773,15 @@ class IModelExporter {
763
773
  core_bentley_1.Logger.logTrace(loggerCategory, `visitElements=false, skipping exportChildElements(${elementId})`);
764
774
  return;
765
775
  }
766
- const childElementIds = this.sourceDb.elements.queryChildren(elementId);
776
+ let childElementIds;
777
+ if (this.canUseChangedElementForest()) {
778
+ const forest = await this.getChangedElementForest();
779
+ if (forest !== undefined) {
780
+ core_bentley_1.Logger.logTrace(loggerCategory, `exportChildElements(${elementId}) via changed-element index`);
781
+ childElementIds = forest.getChildren(elementId);
782
+ }
783
+ }
784
+ childElementIds ??= this.sourceDb.elements.queryChildren(elementId);
767
785
  if (childElementIds.length > 0) {
768
786
  core_bentley_1.Logger.logTrace(loggerCategory, `exportChildElements(${elementId})`);
769
787
  for (const childElementId of childElementIds) {
@@ -771,6 +789,54 @@ class IModelExporter {
771
789
  }
772
790
  }
773
791
  }
792
+ /** Whether change processing may use the sparse hierarchy as the child-id source.
793
+ * Custom traversal overrides retain the previous full element dispatch.
794
+ */
795
+ canUseChangedElementForest() {
796
+ return (this._sourceDbChanges !== undefined &&
797
+ this._changedElementTraversal?.useForest === true &&
798
+ this.exportElement === IModelExporter.prototype.exportElement &&
799
+ this.exportChildElements === IModelExporter.prototype.exportChildElements);
800
+ }
801
+ getOverriddenElementTraversalMethods() {
802
+ const methods = [];
803
+ if (this.exportElement !== IModelExporter.prototype.exportElement)
804
+ methods.push("exportElement");
805
+ if (this.exportChildElements !== IModelExporter.prototype.exportChildElements)
806
+ methods.push("exportChildElements");
807
+ return methods;
808
+ }
809
+ disableChangedElementForest(reason) {
810
+ if (!this._changedElementTraversal?.useForest)
811
+ return;
812
+ this._changedElementTraversal.useForest = false;
813
+ core_bentley_1.Logger.logInfo(loggerCategory, `Using full element traversal for change processing because ${reason}.`);
814
+ }
815
+ async createChangedElementForest() {
816
+ (0, node_assert_1.strict)(this._sourceDbChanges !== undefined, "changed-element traversal requires sourceDbChanges");
817
+ const excludedElementIds = this._excludedElementIds;
818
+ const candidateCountUpperBound = this._sourceDbChanges.element.insertIds.size +
819
+ this._sourceDbChanges.element.updateIds.size +
820
+ excludedElementIds.size;
821
+ if (candidateCountUpperBound > this._changedElementForestElementLimit) {
822
+ this.disableChangedElementForest(`${candidateCountUpperBound} candidate element references exceed the ${this._changedElementForestElementLimit}-element in-memory index limit`);
823
+ return undefined;
824
+ }
825
+ const forest = await ChangedElementForest_1.ChangedElementForest.create(this.sourceDb, [
826
+ ...this._sourceDbChanges.element.insertIds,
827
+ ...this._sourceDbChanges.element.updateIds,
828
+ ...excludedElementIds,
829
+ ], this._changedElementForestElementLimit);
830
+ if (forest === undefined) {
831
+ this.disableChangedElementForest(`the required parent hierarchy exceeds the ${this._changedElementForestElementLimit}-element in-memory index limit`);
832
+ }
833
+ return forest;
834
+ }
835
+ async getChangedElementForest() {
836
+ const traversal = this._changedElementTraversal;
837
+ (0, node_assert_1.strict)(traversal !== undefined, "changed-element index requires an active traversal scope");
838
+ return (traversal.forest ??= this.createChangedElementForest());
839
+ }
774
840
  /** Exports all aspects owned by the supplied elements. */
775
841
  async exportAspectsForOwners(ownerElementIds) {
776
842
  return this._elementAspectExportCoordinator.exportOwners(ownerElementIds);
@@ -831,8 +897,30 @@ class IModelExporter {
831
897
  }
832
898
  return this.shouldExportModelForAspect(modelFact.parentModelId, elementFacts, modelFacts);
833
899
  }
900
+ beginChangedElementTraversalScope() {
901
+ if (this._sourceDbChanges === undefined ||
902
+ this._changedElementTraversal !== undefined) {
903
+ return false;
904
+ }
905
+ this._changedElementTraversal = { useForest: true };
906
+ const overriddenMethods = this.getOverriddenElementTraversalMethods();
907
+ if (this.visitElements && overriddenMethods.length > 0) {
908
+ this.disableChangedElementForest(`${overriddenMethods.join(" and ")} ${overriddenMethods.length === 1 ? "is" : "are"} overridden by the exporter subclass`);
909
+ }
910
+ return true;
911
+ }
912
+ endChangedElementTraversalScope() {
913
+ this._changedElementTraversal = undefined;
914
+ }
834
915
  async runScopedElementExport(exportElements) {
835
- await this._elementAspectExportCoordinator.run(exportElements);
916
+ const ownsChangedElementTraversal = this.beginChangedElementTraversalScope();
917
+ try {
918
+ await this._elementAspectExportCoordinator.run(exportElements);
919
+ }
920
+ finally {
921
+ if (ownsChangedElementTraversal)
922
+ this.endChangedElementTraversalScope();
923
+ }
836
924
  }
837
925
  /** Apply the element export filter when deciding whether to process an aspect owner. @internal */
838
926
  async shouldExportElementForAspect(elementId, elementFact) {
@@ -910,8 +998,7 @@ class IModelExporter {
910
998
  ec_className(e.ECClassId, 's') schemaName,
911
999
  ec_className(e.ECClassId, 'c') className
912
1000
  FROM bis.Element e
913
- INNER JOIN IdSet(:elementIds) ids ON ids.id = e.ECInstanceId
914
- OPTIONS ENABLE_EXPERIMENTAL_FEATURES`, queryParams, { usePrimaryConn: true })) {
1001
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = e.ECInstanceId`, queryParams, { usePrimaryConn: true })) {
915
1002
  elementFacts.set(row.id, {
916
1003
  id: row.id,
917
1004
  parentId: row.parentId ?? undefined,
@@ -923,8 +1010,7 @@ class IModelExporter {
923
1010
  const categoryQueryParams = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIdBatch);
924
1011
  for await (const row of this.sourceDb.createQueryReader(`SELECT g.ECInstanceId id, g.Category.Id categoryId
925
1012
  FROM bis.GeometricElement g
926
- INNER JOIN IdSet(:elementIds) ids ON ids.id = g.ECInstanceId
927
- OPTIONS ENABLE_EXPERIMENTAL_FEATURES`, categoryQueryParams, { usePrimaryConn: true })) {
1013
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = g.ECInstanceId`, categoryQueryParams, { usePrimaryConn: true })) {
928
1014
  const elementFact = elementFacts.get(row.id);
929
1015
  if (elementFact !== undefined) {
930
1016
  elementFact.categoryId = row.categoryId;
@@ -944,8 +1030,7 @@ class IModelExporter {
944
1030
  const queryParams = new core_common_1.QueryBinder().bindIdSet("modelIds", modelIdBatch);
945
1031
  for await (const row of this.sourceDb.createQueryReader(`SELECT model.ECInstanceId id, model.ParentModel.Id parentModelId, model.IsTemplate isTemplate
946
1032
  FROM bis.Model model
947
- INNER JOIN IdSet(:modelIds) ids ON ids.id = model.ECInstanceId
948
- OPTIONS ENABLE_EXPERIMENTAL_FEATURES`, queryParams, { usePrimaryConn: true })) {
1033
+ INNER JOIN IdSet(:modelIds) ids ON ids.id = model.ECInstanceId`, queryParams, { usePrimaryConn: true })) {
949
1034
  modelFacts.set(row.id, {
950
1035
  id: row.id,
951
1036
  parentModelId: row.parentModelId ?? undefined,
@@ -1448,7 +1533,6 @@ class ChangedInstanceIds {
1448
1533
  INNER JOIN hierarchy h ON h.parentId = e.ECInstanceId
1449
1534
  )
1450
1535
  SELECT parentId FROM hierarchy where parentId is not null
1451
- OPTIONS ENABLE_EXPERIMENTAL_FEATURES
1452
1536
  `;
1453
1537
  const parentModelIds = new Set();
1454
1538
  for await (const row of this._db.createQueryReader(ecQuery, params, {
@@ -1475,8 +1559,7 @@ class ChangedInstanceIds {
1475
1559
  SELECT relationship.ECInstanceId
1476
1560
  FROM ${relationshipClassName} relationship
1477
1561
  INNER JOIN IdSet(:elementIds) ids
1478
- ON ids.id = relationship.SourceECInstanceId
1479
- OPTIONS ENABLE_EXPERIMENTAL_FEATURES`;
1562
+ ON ids.id = relationship.SourceECInstanceId`;
1480
1563
  const queryBinder = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIds);
1481
1564
  const queryReader = this._db.createQueryReader(ecQuery, queryBinder, {
1482
1565
  usePrimaryConn: true,
@@ -1492,8 +1575,7 @@ class ChangedInstanceIds {
1492
1575
  ]) {
1493
1576
  const ecQuery = `SELECT aspect.ECInstanceId, aspect.Element.Id
1494
1577
  FROM ${aspectClassName} aspect
1495
- INNER JOIN IdSet(:elementIds) ids ON ids.id = aspect.Element.Id
1496
- OPTIONS ENABLE_EXPERIMENTAL_FEATURES`;
1578
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = aspect.Element.Id`;
1497
1579
  const queryBinder = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIds);
1498
1580
  const queryReader = this._db.createQueryReader(ecQuery, queryBinder, {
1499
1581
  usePrimaryConn: true,