@itwin/imodel-transformer 2.0.0-dev.9 → 2.0.0-experimental.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +13 -27
  2. package/lib/cjs/Algo.d.ts +0 -4
  3. package/lib/cjs/Algo.d.ts.map +1 -1
  4. package/lib/cjs/Algo.js +0 -11
  5. package/lib/cjs/Algo.js.map +1 -1
  6. package/lib/cjs/BigMap.d.ts.map +1 -1
  7. package/lib/cjs/BigMap.js.map +1 -1
  8. package/lib/cjs/BranchProvenanceInitializer.d.ts.map +1 -1
  9. package/lib/cjs/BranchProvenanceInitializer.js +26 -26
  10. package/lib/cjs/BranchProvenanceInitializer.js.map +1 -1
  11. package/lib/cjs/ChangedElementForest.d.ts +14 -0
  12. package/lib/cjs/ChangedElementForest.d.ts.map +1 -0
  13. package/lib/cjs/ChangedElementForest.js +86 -0
  14. package/lib/cjs/ChangedElementForest.js.map +1 -0
  15. package/lib/cjs/ChangesetScanner.d.ts +61 -0
  16. package/lib/cjs/ChangesetScanner.d.ts.map +1 -0
  17. package/lib/cjs/ChangesetScanner.js +104 -0
  18. package/lib/cjs/ChangesetScanner.js.map +1 -0
  19. package/lib/cjs/ECReferenceTypesCache.d.ts +0 -4
  20. package/lib/cjs/ECReferenceTypesCache.d.ts.map +1 -1
  21. package/lib/cjs/ECReferenceTypesCache.js +54 -102
  22. package/lib/cjs/ECReferenceTypesCache.js.map +1 -1
  23. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts.map +1 -1
  24. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js.map +1 -1
  25. package/lib/cjs/ElementAspectCleanup.d.ts +56 -0
  26. package/lib/cjs/ElementAspectCleanup.d.ts.map +1 -0
  27. package/lib/cjs/ElementAspectCleanup.js +175 -0
  28. package/lib/cjs/ElementAspectCleanup.js.map +1 -0
  29. package/lib/cjs/ElementAspectExportCoordinator.d.ts +47 -0
  30. package/lib/cjs/ElementAspectExportCoordinator.d.ts.map +1 -0
  31. package/lib/cjs/ElementAspectExportCoordinator.js +160 -0
  32. package/lib/cjs/ElementAspectExportCoordinator.js.map +1 -0
  33. package/lib/cjs/ElementAspectExportProcessor.d.ts +44 -0
  34. package/lib/cjs/ElementAspectExportProcessor.d.ts.map +1 -0
  35. package/lib/cjs/ElementAspectExportProcessor.js +152 -0
  36. package/lib/cjs/ElementAspectExportProcessor.js.map +1 -0
  37. package/lib/cjs/ElementBulkDelete.d.ts +30 -0
  38. package/lib/cjs/ElementBulkDelete.d.ts.map +1 -0
  39. package/lib/cjs/ElementBulkDelete.js +358 -0
  40. package/lib/cjs/ElementBulkDelete.js.map +1 -0
  41. package/lib/cjs/EntityExistenceCache.d.ts +43 -0
  42. package/lib/cjs/EntityExistenceCache.d.ts.map +1 -0
  43. package/lib/cjs/EntityExistenceCache.js +91 -0
  44. package/lib/cjs/EntityExistenceCache.js.map +1 -0
  45. package/lib/cjs/EntityUnifier.d.ts +7 -7
  46. package/lib/cjs/EntityUnifier.d.ts.map +1 -1
  47. package/lib/cjs/EntityUnifier.js +55 -32
  48. package/lib/cjs/EntityUnifier.js.map +1 -1
  49. package/lib/cjs/IModelCloneContext.d.ts +8 -2
  50. package/lib/cjs/IModelCloneContext.d.ts.map +1 -1
  51. package/lib/cjs/IModelCloneContext.js +42 -12
  52. package/lib/cjs/IModelCloneContext.js.map +1 -1
  53. package/lib/cjs/IModelExporter.d.ts +143 -30
  54. package/lib/cjs/IModelExporter.d.ts.map +1 -1
  55. package/lib/cjs/IModelExporter.js +871 -172
  56. package/lib/cjs/IModelExporter.js.map +1 -1
  57. package/lib/cjs/IModelImporter.d.ts +87 -31
  58. package/lib/cjs/IModelImporter.d.ts.map +1 -1
  59. package/lib/cjs/IModelImporter.js +361 -139
  60. package/lib/cjs/IModelImporter.js.map +1 -1
  61. package/lib/cjs/IModelTransformContext.d.ts +54 -0
  62. package/lib/cjs/IModelTransformContext.d.ts.map +1 -0
  63. package/lib/cjs/IModelTransformContext.js +3 -0
  64. package/lib/cjs/IModelTransformContext.js.map +1 -0
  65. package/lib/cjs/IModelTransformer.d.ts +132 -174
  66. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  67. package/lib/cjs/IModelTransformer.js +679 -1025
  68. package/lib/cjs/IModelTransformer.js.map +1 -1
  69. package/lib/cjs/IModelTransformerError.d.ts +84 -0
  70. package/lib/cjs/IModelTransformerError.d.ts.map +1 -0
  71. package/lib/cjs/IModelTransformerError.js +93 -0
  72. package/lib/cjs/IModelTransformerError.js.map +1 -0
  73. package/lib/cjs/ProvenanceManager.d.ts +194 -0
  74. package/lib/cjs/ProvenanceManager.d.ts.map +1 -0
  75. package/lib/cjs/ProvenanceManager.js +771 -0
  76. package/lib/cjs/ProvenanceManager.js.map +1 -0
  77. package/lib/cjs/SyncTypeResolver.d.ts +32 -0
  78. package/lib/cjs/SyncTypeResolver.d.ts.map +1 -0
  79. package/lib/cjs/SyncTypeResolver.js +92 -0
  80. package/lib/cjs/SyncTypeResolver.js.map +1 -0
  81. package/lib/cjs/TransformerLoggerCategory.d.ts +1 -1
  82. package/lib/cjs/TransformerLoggerCategory.js +1 -1
  83. package/lib/cjs/TransformerLoggerCategory.js.map +1 -1
  84. package/lib/cjs/TransformerPackageMetadata.d.ts +8 -0
  85. package/lib/cjs/TransformerPackageMetadata.d.ts.map +1 -0
  86. package/lib/cjs/TransformerPackageMetadata.js +47 -0
  87. package/lib/cjs/TransformerPackageMetadata.js.map +1 -0
  88. package/lib/cjs/imodel-transformer.d.ts +27 -0
  89. package/lib/cjs/imodel-transformer.d.ts.map +1 -1
  90. package/lib/cjs/imodel-transformer.js +61 -34
  91. package/lib/cjs/imodel-transformer.js.map +1 -1
  92. package/lib/cjs/schema-processing/DynamicSchemaUnionStrategy.d.ts +29 -0
  93. package/lib/cjs/schema-processing/DynamicSchemaUnionStrategy.d.ts.map +1 -0
  94. package/lib/cjs/schema-processing/DynamicSchemaUnionStrategy.js +212 -0
  95. package/lib/cjs/schema-processing/DynamicSchemaUnionStrategy.js.map +1 -0
  96. package/lib/cjs/schema-processing/SchemaProcessingCoordinator.d.ts +25 -0
  97. package/lib/cjs/schema-processing/SchemaProcessingCoordinator.d.ts.map +1 -0
  98. package/lib/cjs/schema-processing/SchemaProcessingCoordinator.js +196 -0
  99. package/lib/cjs/schema-processing/SchemaProcessingCoordinator.js.map +1 -0
  100. package/lib/cjs/schema-processing/SchemaProcessingStrategy.d.ts +59 -0
  101. package/lib/cjs/schema-processing/SchemaProcessingStrategy.d.ts.map +1 -0
  102. package/lib/cjs/schema-processing/SchemaProcessingStrategy.js +28 -0
  103. package/lib/cjs/schema-processing/SchemaProcessingStrategy.js.map +1 -0
  104. package/lib/cjs/schema-processing.d.ts +7 -0
  105. package/lib/cjs/schema-processing.d.ts.map +1 -0
  106. package/lib/cjs/schema-processing.js +27 -0
  107. package/lib/cjs/schema-processing.js.map +1 -0
  108. package/package.json +70 -57
  109. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts +0 -16
  110. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts.map +0 -1
  111. package/lib/cjs/DetachedExportElementAspectsStrategy.js +0 -106
  112. package/lib/cjs/DetachedExportElementAspectsStrategy.js.map +0 -1
  113. package/lib/cjs/ElementCascadingDeleter.d.ts +0 -22
  114. package/lib/cjs/ElementCascadingDeleter.d.ts.map +0 -1
  115. package/lib/cjs/ElementCascadingDeleter.js +0 -57
  116. package/lib/cjs/ElementCascadingDeleter.js.map +0 -1
  117. package/lib/cjs/EntityMap.d.ts +0 -26
  118. package/lib/cjs/EntityMap.d.ts.map +0 -1
  119. package/lib/cjs/EntityMap.js +0 -54
  120. package/lib/cjs/EntityMap.js.map +0 -1
  121. package/lib/cjs/ExportElementAspectsStrategy.d.ts +0 -35
  122. package/lib/cjs/ExportElementAspectsStrategy.d.ts.map +0 -1
  123. package/lib/cjs/ExportElementAspectsStrategy.js +0 -52
  124. package/lib/cjs/ExportElementAspectsStrategy.js.map +0 -1
  125. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts +0 -12
  126. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts.map +0 -1
  127. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js +0 -47
  128. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js.map +0 -1
@@ -13,8 +13,12 @@ const core_bentley_1 = require("@itwin/core-bentley");
13
13
  const core_common_1 = require("@itwin/core-common");
14
14
  const ecschema_metadata_1 = require("@itwin/ecschema-metadata");
15
15
  const TransformerLoggerCategory_1 = require("./TransformerLoggerCategory");
16
- const nodeAssert = require("assert");
17
- const ExportElementAspectsWithElementsStrategy_1 = require("./ExportElementAspectsWithElementsStrategy");
16
+ const node_assert_1 = require("node:assert");
17
+ const ElementAspectExportProcessor_1 = require("./ElementAspectExportProcessor");
18
+ const ElementAspectExportCoordinator_1 = require("./ElementAspectExportCoordinator");
19
+ const IModelTransformerError_1 = require("./IModelTransformerError");
20
+ const ChangesetScanner_1 = require("./ChangesetScanner");
21
+ const ChangedElementForest_1 = require("./ChangedElementForest");
18
22
  const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelExporter;
19
23
  /** Handles the events generated by IModelExporter.
20
24
  * @note Change information is available when `IModelExportHandler` methods are invoked via [IModelExporter.exportChanges]($transformer), but not available when invoked via [IModelExporter.exportAll]($transformer).
@@ -26,7 +30,7 @@ class IModelExportHandler {
26
30
  /** If `true` is returned, then the CodeSpec will be exported.
27
31
  * @note This method can optionally be overridden to exclude an individual CodeSpec from the export. The base implementation always returns `true`.
28
32
  */
29
- shouldExportCodeSpec(_codeSpec) {
33
+ async shouldExportCodeSpec(_codeSpec) {
30
34
  return true;
31
35
  }
32
36
  /** Called when a CodeSpec should be exported.
@@ -34,31 +38,33 @@ class IModelExportHandler {
34
38
  * @param isUpdate If defined, then `true` indicates an UPDATE operation while `false` indicates an INSERT operation. If not defined, then INSERT vs. UPDATE is not known.
35
39
  * @note This should be overridden to actually do the export.
36
40
  */
37
- onExportCodeSpec(_codeSpec, _isUpdate) { }
41
+ async onExportCodeSpec(_codeSpec, _isUpdate) { }
38
42
  /** Called when a font should be exported.
39
43
  * @param font The font to export
40
44
  * @param isUpdate If defined, then `true` indicates an UPDATE operation while `false` indicates an INSERT operation. If not defined, then INSERT vs. UPDATE is not known.
41
45
  * @note This should be overridden to actually do the export.
42
46
  */
43
- onExportFont(_font, _isUpdate) { }
47
+ async onExportFont(_font, _isUpdate) { }
44
48
  /** Called when a model should be exported.
45
49
  * @param model The model to export
46
50
  * @param isUpdate If defined, then `true` indicates an UPDATE operation while `false` indicates an INSERT operation. If not defined, then INSERT vs. UPDATE is not known.
47
51
  * @note This should be overridden to actually do the export.
48
52
  */
49
- onExportModel(_model, _isUpdate) { }
53
+ async onExportModel(_model, _isUpdate) { }
50
54
  /** Called when a model should be deleted. */
51
55
  async onDeleteModel(_modelId) { }
52
56
  /** If `true` is returned, then the element will be exported.
53
57
  * @note This method can optionally be overridden to exclude an individual Element (and its children and ElementAspects) from the export. The base implementation always returns `true`.
58
+ * @note During [IModelExporter.exportChanges]($transformer), this method is also called once for each unchanged ancestor of a changed element, so that a rejected ancestor excludes its changed descendants.
54
59
  */
55
- shouldExportElement(_element) {
60
+ async shouldExportElement(_element) {
56
61
  return true;
57
62
  }
58
63
  /** Called when element is skipped instead of exported.
59
64
  * @note When an element is skipped, exporter will not export any of its child elements. Because of this, [[onSkipElement]] will not be invoked for any children of a "skipped" element.
65
+ * @note During [IModelExporter.exportChanges]($transformer), an unchanged ancestor is filtered only when a changed descendant is reached. An element excluded by ID below it may therefore receive [[onSkipElement]] before the ancestor is rejected.
60
66
  */
61
- onSkipElement(_elementId) { }
67
+ async onSkipElement(_elementId) { }
62
68
  /** Called when an element should be exported.
63
69
  * @param element The element to export
64
70
  * @param isUpdate If defined, then `true` indicates an UPDATE operation while `false` indicates an INSERT operation. If not defined, then INSERT vs. UPDATE is not known.
@@ -72,12 +78,12 @@ class IModelExportHandler {
72
78
  * @internal
73
79
  */
74
80
  async preExportElement(_element) { }
75
- /** Called when an element should be deleted. */
76
- onDeleteElement(_elementId) { }
81
+ /** Called once with the source IDs of all elements deleted by the exported changes. */
82
+ async onDeleteElements(_elementIds) { }
77
83
  /** If `true` is returned, then the ElementAspect will be exported.
78
84
  * @note This method can optionally be overridden to exclude an individual ElementAspect from the export. The base implementation always returns `true`.
79
85
  */
80
- shouldExportElementAspect(_aspect) {
86
+ async shouldExportElementAspect(_aspect) {
81
87
  return true;
82
88
  }
83
89
  /** Called when an ElementUniqueAspect should be exported.
@@ -93,21 +99,23 @@ class IModelExportHandler {
93
99
  /** If `true` is returned, then the relationship will be exported.
94
100
  * @note This method can optionally be overridden to exclude an individual CodeSpec from the export. The base implementation always returns `true`.
95
101
  */
96
- shouldExportRelationship(_relationship) {
102
+ async shouldExportRelationship(_relationship) {
97
103
  return true;
98
104
  }
99
105
  /** Called when a Relationship should be exported.
100
106
  * @param relationship The Relationship to export
101
107
  * @param isUpdate If defined, then `true` indicates an UPDATE operation while `false` indicates an INSERT operation. If not defined, then INSERT vs. UPDATE is not known.
108
+ * @param sourceFedGuid The FederationGuid of the relationship's source endpoint, if available.
109
+ * @param targetFedGuid The FederationGuid of the relationship's target endpoint, if available.
102
110
  * @note This should be overridden to actually do the export.
103
111
  */
104
- async onExportRelationship(_relationship, _isUpdate) { }
112
+ async onExportRelationship(_relationship, _isUpdate, _sourceFedGuid, _targetFedGuid) { }
105
113
  /** Called when a relationship should be deleted. */
106
- onDeleteRelationship(_relInstanceId) { }
114
+ async onDeleteRelationship(_relInstanceId) { }
107
115
  /** If `true` is returned, then the schema will be exported.
108
116
  * @note This method can optionally be overridden to exclude an individual schema from the export. The base implementation always returns `true`.
109
117
  */
110
- shouldExportSchema(_schemaKey) {
118
+ async shouldExportSchema(_schemaKey) {
111
119
  return true;
112
120
  }
113
121
  /** Called when a schema should be exported.
@@ -126,10 +134,14 @@ class IModelExportHandler {
126
134
  exports.IModelExportHandler = IModelExportHandler;
127
135
  /** Base class for exporting data from an iModel.
128
136
  * @note Most uses cases will not require a custom subclass of `IModelExporter`. Instead, it is more typical to subclass/customize [IModelExportHandler]($transformer).
137
+ * ElementAspects are exported separately from element callbacks in bounded,
138
+ * owner-scoped groups. Aspect callbacks are not guaranteed to be adjacent to
139
+ * callbacks for their owners.
129
140
  * @see [iModel Transformation and Data Exchange]($docs/learning/transformer/index.md), [[registerHandler]], [IModelTransformer]($transformer), [IModelImporter]($transformer)
130
141
  * @beta
131
142
  */
132
143
  class IModelExporter {
144
+ static _elementAspectOwnerBatchSize = 1000;
133
145
  /** The read-only source iModel. */
134
146
  sourceDb;
135
147
  /** A flag that indicates whether element GeometryStreams are loaded or not.
@@ -163,6 +175,12 @@ class IModelExporter {
163
175
  _progressCounter = 0;
164
176
  /** Optionally cached entity change information */
165
177
  _sourceDbChanges;
178
+ /** State shared by nested calls in one change-processing element traversal. */
179
+ _changedElementTraversal;
180
+ /** Unchanged elements on the current change-processing path. Each is filtered only when a changed descendant is reached. */
181
+ _unchangedAncestors = [];
182
+ /** Bounds the additional hierarchy retained by the sparse changed-element path. */
183
+ _changedElementForestElementLimit = 100_000;
166
184
  /**
167
185
  * Retrieve the cached entity change information.
168
186
  * @note This will only be initialized after [IModelExporter.exportChanges] is invoked or [IModelExporter.initialize] is called.
@@ -175,7 +193,13 @@ class IModelExporter {
175
193
  /** The handler called by this IModelExporter. */
176
194
  get handler() {
177
195
  if (undefined === this._handler) {
178
- throw new Error("IModelExportHandler not registered");
196
+ core_bentley_1.ITwinError.throwError({
197
+ iTwinErrorId: {
198
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
199
+ key: IModelTransformerError_1.IModelTransformerError.ExportHandlerNotRegistered,
200
+ },
201
+ message: "IModelExportHandler not registered",
202
+ });
179
203
  }
180
204
  return this._handler;
181
205
  }
@@ -189,20 +213,37 @@ class IModelExporter {
189
213
  _excludedElementClasses = new Set();
190
214
  /** The set of classes of Relationships that will be excluded (polymorphically) from transformation to the target iModel. */
191
215
  _excludedRelationshipClasses = new Set();
192
- /** Strategy for how ElementAspects are exported */
193
- _exportElementAspectsStrategy;
216
+ /** Exports ElementAspects for accepted owner groups. */
217
+ _elementAspectExportProcessor;
218
+ /** Coordinates accepted-owner scopes and bounded group processing. */
219
+ _elementAspectExportCoordinator;
220
+ /** Coordinates accepted ElementAspect owners and bounded group processing.
221
+ * @internal
222
+ */
223
+ get elementAspectExportCoordinator() {
224
+ return this._elementAspectExportCoordinator;
225
+ }
226
+ /** Whether change traversal skips root-owned entities. */
227
+ _skipPropagateChangesToRootElements = false;
228
+ _rootElementIds = new Set([
229
+ core_common_1.IModel.rootSubjectId,
230
+ core_common_1.IModel.repositoryModelId,
231
+ core_common_1.IModel.dictionaryId,
232
+ "0xe", // The reality-data-sources modelId, skipped with root propagation.
233
+ ]);
194
234
  /** Construct a new IModelExporter
195
235
  * @param sourceDb The source IModelDb
196
236
  * @see registerHandler
197
237
  */
198
- constructor(sourceDb, elementAspectsStrategy = ExportElementAspectsWithElementsStrategy_1.ExportElementAspectsWithElementsStrategy) {
238
+ constructor(sourceDb) {
199
239
  this.sourceDb = sourceDb;
200
- this._exportElementAspectsStrategy = new elementAspectsStrategy(this.sourceDb, {
240
+ this._elementAspectExportProcessor = new ElementAspectExportProcessor_1.ElementAspectExportProcessor(this.sourceDb, {
201
241
  onExportElementMultiAspects: async (aspects) => this.handler.onExportElementMultiAspects(aspects),
202
242
  onExportElementUniqueAspect: async (aspect, isUpdate) => this.handler.onExportElementUniqueAspect(aspect, isUpdate),
203
- shouldExportElementAspect: (aspect) => this.handler.shouldExportElementAspect(aspect),
243
+ shouldExportElementAspect: async (aspect) => this.handler.shouldExportElementAspect(aspect),
204
244
  trackProgress: async () => this.trackProgress(),
205
245
  });
246
+ this._elementAspectExportCoordinator = new ElementAspectExportCoordinator_1.ElementAspectExportCoordinator(IModelExporter._elementAspectOwnerBatchSize, () => this._elementAspectExportProcessor.excludedElementAspectClassFullNames, async (elementIds) => this._elementAspectExportProcessor.exportAllElementAspects(elementIds), () => this._elementAspectExportProcessor.resetCaches());
206
247
  }
207
248
  /**
208
249
  * Initialize prerequisites of exporting. This is implicitly done by any `export*` calls that need initialization
@@ -220,7 +261,7 @@ class IModelExporter {
220
261
  });
221
262
  if (this._sourceDbChanges === undefined)
222
263
  return;
223
- this._exportElementAspectsStrategy.setAspectChanges(this._sourceDbChanges.aspect);
264
+ this._elementAspectExportProcessor.setAspectChanges(this._sourceDbChanges.aspect);
224
265
  }
225
266
  /** Register the handler that will be called by IModelExporter. */
226
267
  registerHandler(handler) {
@@ -230,7 +271,9 @@ class IModelExporter {
230
271
  excludeCodeSpec(codeSpecName) {
231
272
  this._excludedCodeSpecNames.add(codeSpecName);
232
273
  }
233
- /** Add a rule to exclude a specific Element. */
274
+ /** Add a rule to exclude a specific Element.
275
+ * @note Configure exclusions before starting an export operation. Changing exclusions while an export is in progress is unsupported.
276
+ */
234
277
  excludeElement(elementId) {
235
278
  this._excludedElementIds.add(elementId);
236
279
  }
@@ -244,7 +287,7 @@ class IModelExporter {
244
287
  }
245
288
  /** Add a rule to exclude all ElementAspects of a specified class. */
246
289
  excludeElementAspectClass(classFullName) {
247
- this._exportElementAspectsStrategy.excludeElementAspectClass(classFullName);
290
+ this._elementAspectExportProcessor.excludeElementAspectClass(classFullName);
248
291
  }
249
292
  /** Add a rule to exclude all Relationships of a specified class. */
250
293
  excludeRelationshipClass(classFullName) {
@@ -257,11 +300,12 @@ class IModelExporter {
257
300
  await this.initialize({});
258
301
  await this.exportCodeSpecs();
259
302
  await this.exportFonts();
260
- await this.exportModel(core_common_1.IModel.repositoryModelId);
303
+ await this._elementAspectExportCoordinator.run(async () => this.exportModel(core_common_1.IModel.repositoryModelId));
261
304
  await this.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
262
305
  }
263
306
  /** Export changes from the source iModel.
264
307
  * Inserts, updates, and deletes are determined by inspecting the changeset(s).
308
+ * @throws [[ITwinError]] with scope `@itwin/imodel-transformer` and key `no-changesets` if the source iModel has no changesets and no custom changes. Call [[exportAll]] to export all content.
265
309
  * @note To form a range of versions to process, set `startChangesetId` for the start (inclusive) of the desired
266
310
  * range and open the source iModel as of the end (inclusive) of the desired range.
267
311
  * @note the changedInstanceIds are just for this call to exportChanges, so you must continue to pass it in
@@ -269,29 +313,68 @@ class IModelExporter {
269
313
  */
270
314
  async exportChanges(args) {
271
315
  if (!this.sourceDb.isBriefcaseDb())
272
- throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Must be a briefcase to export changes");
273
- if ("" === this.sourceDb.changeset.id &&
274
- !this.sourceDbChanges?.hasChanges) {
275
- await this.exportAll(); // no changesets or custom changes, so revert to exportAll
276
- return;
316
+ core_bentley_1.ITwinError.throwError({
317
+ iTwinErrorId: {
318
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
319
+ key: IModelTransformerError_1.IModelTransformerError.ExportChangesRequiresBriefcase,
320
+ },
321
+ message: "Must be a briefcase to export changes",
322
+ });
323
+ let initOpts = args ?? {};
324
+ const hasExplicitChangeSource = "csFileProps" in initOpts ||
325
+ "changedInstanceIds" in initOpts ||
326
+ "changesetRanges" in initOpts ||
327
+ "startChangeset" in initOpts;
328
+ const currentChangesetId = this.sourceDb.changeset.id;
329
+ if (!hasExplicitChangeSource && currentChangesetId !== "") {
330
+ initOpts = {
331
+ ...initOpts,
332
+ startChangeset: { id: currentChangesetId },
333
+ };
277
334
  }
278
- const startChangeset = args && "startChangeset" in args ? args.startChangeset : undefined;
279
- const initOpts = {
280
- startChangeset: { id: startChangeset?.id },
281
- };
282
335
  await this.initialize(initOpts);
336
+ if (currentChangesetId === "" && !this.sourceDbChanges?.hasChanges) {
337
+ core_bentley_1.ITwinError.throwError({
338
+ iTwinErrorId: {
339
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
340
+ key: IModelTransformerError_1.IModelTransformerError.NoChangesets,
341
+ },
342
+ message: "Cannot export changes because the source iModel has no changesets or custom changes. Call exportAll() to export all content.",
343
+ });
344
+ }
345
+ this._skipPropagateChangesToRootElements =
346
+ initOpts.skipPropagateChangesToRootElements ?? false;
283
347
  // _sourceDbChanges are initialized in this.initialize
284
- nodeAssert(this._sourceDbChanges !== undefined, "sourceDbChanges must be initialized.");
348
+ (0, node_assert_1.strict)(this._sourceDbChanges !== undefined, "sourceDbChanges must be initialized.");
285
349
  await this.exportCodeSpecs();
286
350
  await this.exportFonts();
287
- if (initOpts.skipPropagateChangesToRootElements) {
288
- await this.exportModelContents(core_common_1.IModel.repositoryModelId);
289
- await this.exportSubModels(core_common_1.IModel.repositoryModelId);
351
+ (0, node_assert_1.strict)(this.beginChangedElementTraversalScope(), "exportChanges must own its changed-element traversal scope");
352
+ try {
353
+ await this._elementAspectExportCoordinator.run(async () => {
354
+ if (initOpts.skipPropagateChangesToRootElements) {
355
+ // The root Subject is in the RepositoryModel. Traverse its children
356
+ // separately, then export other top-level repository elements while
357
+ // excluding the root so no element is visited twice.
358
+ await this.exportChildElements(core_common_1.IModel.rootSubjectId);
359
+ await this.exportModelContents(core_common_1.IModel.repositoryModelId, core_backend_1.Element.classFullName, true);
360
+ await this.exportSubModels(core_common_1.IModel.repositoryModelId);
361
+ }
362
+ else {
363
+ await this.exportModel(core_common_1.IModel.repositoryModelId);
364
+ }
365
+ });
366
+ const aspectOnlyOwnerElementIds = new Set(this._sourceDbChanges.aspectOwnerElementIds);
367
+ for (const elementId of this._sourceDbChanges.element.insertIds) {
368
+ aspectOnlyOwnerElementIds.delete(elementId);
369
+ }
370
+ for (const elementId of this._sourceDbChanges.element.updateIds) {
371
+ aspectOnlyOwnerElementIds.delete(elementId);
372
+ }
373
+ await this.exportAspectsForOwners(await this.filterOwnerElementIdsForAspectExport(aspectOnlyOwnerElementIds));
290
374
  }
291
- else {
292
- await this.exportModel(core_common_1.IModel.repositoryModelId);
375
+ finally {
376
+ this.endChangedElementTraversalScope();
293
377
  }
294
- await this.exportAllAspects();
295
378
  await this.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
296
379
  // handle deletes
297
380
  if (this.visitElements) {
@@ -299,42 +382,31 @@ class IModelExporter {
299
382
  for (const modelId of this._sourceDbChanges.model.deleteIds) {
300
383
  await this.handler.onDeleteModel(modelId);
301
384
  }
302
- for (const elementId of this._sourceDbChanges.element.deleteIds) {
303
- // We don't know how the handler wants to handle deletions, and we don't have enough information
304
- // to know if deleted entities were related, so when processing changes, ignore errors from deletion.
305
- // Technically, to keep the ignored error scope small, we ignore only the error of looking up a missing element,
306
- // that approach works at least for the IModelTransformer.
307
- // In the future, the handler may be responsible for doing the work of finding out which elements were cascade deleted,
308
- // and returning them for the exporter to use to avoid double-deleting with error ignoring
309
- try {
310
- this.handler.onDeleteElement(elementId);
311
- }
312
- catch (err) {
313
- const isMissingErr = err instanceof core_common_1.IModelError &&
314
- err.errorNumber === core_bentley_1.IModelStatus.NotFound;
315
- if (!isMissingErr)
316
- throw err;
317
- }
385
+ if (this._sourceDbChanges.element.deleteIds.size > 0) {
386
+ await this.handler.onDeleteElements(new Set(this._sourceDbChanges.element.deleteIds));
318
387
  }
319
388
  }
320
389
  if (this.visitRelationships) {
321
390
  for (const relInstanceId of this._sourceDbChanges.relationship
322
391
  .deleteIds) {
323
- this.handler.onDeleteRelationship(relInstanceId);
392
+ await this.handler.onDeleteRelationship(relInstanceId);
324
393
  }
325
394
  }
326
395
  // Enable consecutive exportChanges runs without the need to re-instantiate the exporter.
327
396
  // You can counteract the obvious impact of losing this expensive data by always calling
328
397
  // exportChanges with the [[ExportChangesOptions.changedInstanceIds]] option set to
329
398
  // whatever you want
330
- if (this._resetChangeDataOnExport)
399
+ if (this._resetChangeDataOnExport) {
331
400
  this._sourceDbChanges = undefined;
401
+ this._elementAspectExportProcessor.setAspectChanges(undefined);
402
+ }
332
403
  }
333
404
  _resetChangeDataOnExport = true;
334
- /** Export schemas from the source iModel.
335
- * @note This must be called separately from [[exportAll]] or [[exportChanges]].
405
+ /** Enumerate schemas from the source iModel in database order.
406
+ * @note Override this method to customize schema discovery for both direct exports and transformer schema processing.
407
+ * @beta
336
408
  */
337
- async exportSchemas() {
409
+ async *enumerateSchemas() {
338
410
  const sql = `
339
411
  SELECT s.Name, s.VersionMajor, s.VersionWrite, s.VersionMinor
340
412
  FROM ECDbMeta.ECSchemaDef s
@@ -345,27 +417,35 @@ class IModelExporter {
345
417
  `}
346
418
  ORDER BY ECInstanceId
347
419
  `;
348
- const schemaKeysToExport = [];
349
420
  for await (const row of this.sourceDb.createQueryReader(sql, undefined, {
350
421
  usePrimaryConn: true,
351
422
  })) {
352
- const schemaName = row[0];
353
- const versionMajor = row[1];
354
- const versionWrite = row[2];
355
- const versionMinor = row[3];
356
- const schemaKey = new ecschema_metadata_1.SchemaKey(schemaName, new ecschema_metadata_1.ECVersion(versionMajor, versionWrite, versionMinor));
357
- if (this.handler.shouldExportSchema(schemaKey)) {
358
- schemaKeysToExport.push(schemaKey);
359
- }
360
- }
361
- if (schemaKeysToExport.length === 0)
362
- return;
363
- await Promise.all(schemaKeysToExport.map(async (schemaKey) => {
423
+ const schemaKey = new ecschema_metadata_1.SchemaKey(row[0], new ecschema_metadata_1.ECVersion(row[1], row[2], row[3]));
364
424
  const schema = await this.sourceDb.schemaContext.getSchema(schemaKey);
365
425
  if (!schema) {
366
- throw new Error(`Failed to load schema: ${schemaKey.name}`);
426
+ core_bentley_1.ITwinError.throwError({
427
+ iTwinErrorId: {
428
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
429
+ key: IModelTransformerError_1.IModelTransformerError.SchemaLoadFailed,
430
+ },
431
+ message: `Failed to load schema: ${schemaKey.name}`,
432
+ });
433
+ }
434
+ yield schema;
435
+ }
436
+ }
437
+ /** Export schemas from the source iModel.
438
+ * @note This must be called separately from [[exportAll]] or [[exportChanges]].
439
+ */
440
+ async exportSchemas() {
441
+ const schemasToExport = [];
442
+ for await (const schema of this.enumerateSchemas()) {
443
+ if (await this.handler.shouldExportSchema(schema.schemaKey)) {
444
+ schemasToExport.push(schema);
367
445
  }
368
- core_bentley_1.Logger.logTrace(loggerCategory, `exportSchema(${schemaKey.name})`);
446
+ }
447
+ await Promise.all(schemasToExport.map(async (schema) => {
448
+ core_bentley_1.Logger.logTrace(loggerCategory, `exportSchema(${schema.name})`);
369
449
  return this.handler.onExportSchema(schema);
370
450
  }));
371
451
  }
@@ -409,9 +489,9 @@ class IModelExporter {
409
489
  return;
410
490
  }
411
491
  // CodeSpec has passed standard exclusion rules, now give handler a chance to accept/reject export
412
- if (this.handler.shouldExportCodeSpec(codeSpec)) {
492
+ if (await this.handler.shouldExportCodeSpec(codeSpec)) {
413
493
  core_bentley_1.Logger.logTrace(loggerCategory, `exportCodeSpec(${codeSpecName})${this.getChangeOpSuffix(isUpdate)}`);
414
- this.handler.onExportCodeSpec(codeSpec, isUpdate);
494
+ await this.handler.onExportCodeSpec(codeSpec, isUpdate);
415
495
  return this.trackProgress();
416
496
  }
417
497
  }
@@ -449,7 +529,7 @@ class IModelExporter {
449
529
  */
450
530
  async exportFontByFontProps(fontProps) {
451
531
  core_bentley_1.Logger.logTrace(loggerCategory, `exportFontByFamily(${fontProps.name}, ${fontProps.type})`);
452
- this.handler.onExportFont(fontProps, true);
532
+ await this.handler.onExportFont(fontProps, true);
453
533
  return this.trackProgress();
454
534
  }
455
535
  /** Export a single font from the source iModel.
@@ -466,8 +546,8 @@ class IModelExporter {
466
546
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
467
547
  */
468
548
  async exportFontByNumber(fontNumber) {
469
- /** sourceDbChanges now works by using TS ChangesetECAdaptor which doesn't pick up changes to fonts since fonts is not an ec table.
470
- * So lets always export fonts for the time being by always setting isUpdate = true.
549
+ /** ChangedInstanceIds contains EC-mapped changes, but the font table is not EC-mapped.
550
+ * Always export fonts because their changes are therefore absent from sourceDbChanges.
471
551
  * It is very rare and even problematic for the font table to reach a large size, so it is not a bottleneck in transforming changes.
472
552
  * See https://github.com/iTwin/imodel-transformer/pull/135 for removed code.
473
553
  */
@@ -481,6 +561,9 @@ class IModelExporter {
481
561
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
482
562
  */
483
563
  async exportModel(modeledElementId) {
564
+ return this.runScopedElementExport(async () => this.exportModelImpl(modeledElementId));
565
+ }
566
+ async exportModelImpl(modeledElementId) {
484
567
  const model = this.sourceDb.models.getModel(modeledElementId);
485
568
  if (model.isTemplate && !this.wantTemplateModels) {
486
569
  return;
@@ -491,7 +574,7 @@ class IModelExporter {
491
574
  wantBRepData: this.wantGeometry,
492
575
  });
493
576
  core_bentley_1.Logger.logTrace(loggerCategory, `exportModel(${modeledElementId})`);
494
- if (this.shouldExportElement(modeledElement)) {
577
+ if (await this.shouldExportElement(modeledElement)) {
495
578
  await this.exportModelContainer(model);
496
579
  if (this.visitElements) {
497
580
  await this.exportModelContents(modeledElementId);
@@ -514,7 +597,7 @@ class IModelExporter {
514
597
  return; // not in changeset, don't export
515
598
  }
516
599
  }
517
- this.handler.onExportModel(model, isUpdate);
600
+ await this.handler.onExportModel(model, isUpdate);
518
601
  return this.trackProgress();
519
602
  }
520
603
  _yieldManager = new core_bentley_1.YieldManager();
@@ -525,6 +608,9 @@ class IModelExporter {
525
608
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
526
609
  */
527
610
  async exportModelContents(modelId, elementClassFullName = core_backend_1.Element.classFullName, skipRootSubject) {
611
+ return this.runScopedElementExport(async () => this.exportModelContentsImpl(modelId, elementClassFullName, skipRootSubject));
612
+ }
613
+ async exportModelContentsImpl(modelId, elementClassFullName, skipRootSubject) {
528
614
  if (skipRootSubject) {
529
615
  // NOTE: IModelTransformer.processAll should skip the root Subject since it is specific to the individual iModel and is not part of the changes that need to be synchronized
530
616
  // NOTE: IModelExporter.exportAll should not skip the root Subject since the goal is to export everything
@@ -540,19 +626,33 @@ class IModelExporter {
540
626
  !this._sourceDbChanges.model.updateIds.has(modelId)) {
541
627
  return; // this optimization assumes that the Model changes (LastMod) any time an Element in the Model changes
542
628
  }
629
+ if (this.canUseChangedElementForest() &&
630
+ elementClassFullName === core_backend_1.Element.classFullName) {
631
+ const forest = await this.getChangedElementForest();
632
+ if (forest !== undefined) {
633
+ core_bentley_1.Logger.logTrace(loggerCategory, `exportModelContents(${modelId}) via changed-element index`);
634
+ for (const rootId of forest.getModelRoots(modelId)) {
635
+ if (skipRootSubject && rootId === core_common_1.IModel.rootSubjectId)
636
+ continue;
637
+ await this.exportElement(rootId);
638
+ await this._yieldManager.allowYield();
639
+ }
640
+ return;
641
+ }
642
+ }
543
643
  }
544
644
  core_bentley_1.Logger.logTrace(loggerCategory, `exportModelContents(${modelId})`);
545
- let sql;
546
- if (skipRootSubject) {
547
- sql = `SELECT ECInstanceId FROM ${elementClassFullName} WHERE Parent.Id IS NULL AND Model.Id=:modelId AND ECInstanceId!=:rootSubjectId ORDER BY ECInstanceId`;
548
- }
549
- else {
550
- sql = `SELECT ECInstanceId FROM ${elementClassFullName} WHERE Parent.Id IS NULL AND Model.Id=:modelId ORDER BY ECInstanceId`;
551
- }
645
+ const rootFilter = skipRootSubject
646
+ ? "e.Parent.Id IS NULL AND e.Model.Id=:modelId AND e.ECInstanceId!=:rootSubjectId"
647
+ : "e.Parent.Id IS NULL AND e.Model.Id=:modelId";
552
648
  const params = new core_common_1.QueryBinder().bindId("modelId", modelId);
553
649
  if (skipRootSubject) {
554
650
  params.bindId("rootSubjectId", core_common_1.IModel.rootSubjectId);
555
651
  }
652
+ if (this.canUseSetBasedTraversal()) {
653
+ return this.exportElementTreesStreamed(`SELECT e.ECInstanceId, 0 FROM ${elementClassFullName} e WHERE ${rootFilter}`, params);
654
+ }
655
+ const sql = `SELECT e.ECInstanceId FROM ${elementClassFullName} e WHERE ${rootFilter} ORDER BY e.ECInstanceId`;
556
656
  for await (const row of this.sourceDb.createQueryReader(sql, params, {
557
657
  usePrimaryConn: true,
558
658
  })) {
@@ -560,10 +660,61 @@ class IModelExporter {
560
660
  await this._yieldManager.allowYield();
561
661
  }
562
662
  }
663
+ /** Whether the public element traversal methods still have their base implementations. */
664
+ hasDefaultElementTraversal() {
665
+ // The optimized helpers bypass these public overridable methods. Compare function
666
+ // identity so subclass, bound, wrapped, or instrumented methods retain legacy dispatch.
667
+ return (this.exportElement === IModelExporter.prototype.exportElement &&
668
+ this.exportChildElements === IModelExporter.prototype.exportChildElements);
669
+ }
670
+ /** Whether hierarchy traversal can use the streamed query.
671
+ * Changes mode and overrides of the public traversal methods require legacy dispatch.
672
+ */
673
+ canUseSetBasedTraversal() {
674
+ return (this._sourceDbChanges === undefined && this.hasDefaultElementTraversal());
675
+ }
676
+ /** Stream element trees in the legacy depth-first order with one recursive ECSQL query.
677
+ * Rejected subtrees are skipped in the exporter without loading their descendants.
678
+ * @param anchorSelect a SELECT producing `(ECInstanceId, 0)` rows for the tree roots
679
+ */
680
+ async exportElementTreesStreamed(anchorSelect, params) {
681
+ // The anchor seeds roots at depth 0; the recursive term follows parent links.
682
+ // SQLite's documented recursive-CTE queue behavior makes this ORDER BY a
683
+ // depth-first priority queue, with ascending IDs breaking ties. Supported
684
+ // iModel databases use SQLite, so the outer SELECT intentionally consumes
685
+ // the CTE rows in this queue order.
686
+ const sql = `
687
+ WITH RECURSIVE ElementTree (ECInstanceId, Depth) AS (
688
+ ${anchorSelect}
689
+ UNION ALL
690
+ SELECT c.ECInstanceId, t.Depth + 1 FROM ${core_backend_1.Element.classFullName} c
691
+ JOIN ElementTree t ON c.Parent.Id = t.ECInstanceId
692
+ ORDER BY 2 DESC, 1 ASC
693
+ )
694
+ SELECT ECInstanceId, Depth FROM ElementTree`;
695
+ let pruneDepth;
696
+ for await (const row of this.sourceDb.createQueryReader(sql, params, {
697
+ usePrimaryConn: true,
698
+ })) {
699
+ const elementId = row[0];
700
+ const depth = row[1];
701
+ const isPruned = pruneDepth !== undefined && depth > pruneDepth;
702
+ if (!isPruned) {
703
+ pruneDepth = undefined;
704
+ if ((await this.exportElementShallow(elementId)) === "skip")
705
+ pruneDepth = depth;
706
+ }
707
+ // Keep the streamed loop responsive, including while consuming a rejected subtree.
708
+ await this._yieldManager.allowYield();
709
+ }
710
+ }
563
711
  /** Export the sub-models directly below the specified model.
564
712
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
565
713
  */
566
714
  async exportSubModels(parentModelId) {
715
+ return this.runScopedElementExport(async () => this.exportSubModelsImpl(parentModelId));
716
+ }
717
+ async exportSubModelsImpl(parentModelId) {
567
718
  core_bentley_1.Logger.logTrace(loggerCategory, `exportSubModels(${parentModelId})`);
568
719
  const definitionModelIds = [];
569
720
  const otherModelIds = [];
@@ -593,16 +744,15 @@ class IModelExporter {
593
744
  * This considers the standard IModelExporter exclusion rules plus calls [IModelExportHandler.shouldExportElement]($transformer) for any custom exclusion rules.
594
745
  * @note This method is called from within [[exportChanges]] and [[exportAll]], so usually does not need to be called directly.
595
746
  */
596
- shouldExportElement(element) {
747
+ async shouldExportElement(element) {
597
748
  if (this._excludedElementIds.has(element.id)) {
598
749
  core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${element.id} by Id`);
599
750
  return false;
600
751
  }
601
- if (element instanceof core_backend_1.GeometricElement) {
602
- if (this._excludedElementCategoryIds.has(element.category)) {
603
- core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${element.id} by Category`);
604
- return false;
605
- }
752
+ if (element instanceof core_backend_1.GeometricElement &&
753
+ this._excludedElementCategoryIds.has(element.category)) {
754
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${element.id} by Category`);
755
+ return false;
606
756
  }
607
757
  if (!this.wantTemplateModels &&
608
758
  element instanceof core_backend_1.RecipeDefinitionElement) {
@@ -616,28 +766,86 @@ class IModelExporter {
616
766
  }
617
767
  }
618
768
  // element has passed standard exclusion rules, now give handler a chance to accept/reject
619
- return this.handler.shouldExportElement(element);
769
+ if (!(await this.handler.shouldExportElement(element))) {
770
+ return false;
771
+ }
772
+ return true;
620
773
  }
621
- /** Export the specified element, its child elements (if applicable), and any owned ElementAspects.
774
+ /** Export the specified element and its child elements (if applicable).
622
775
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
776
+ * @note ElementAspects are exported separately in owner-scoped groups, so their callbacks are not guaranteed to follow this element callback.
623
777
  */
624
778
  async exportElement(elementId) {
779
+ return this.runScopedElementExport(async () => this.exportElementImpl(elementId));
780
+ }
781
+ async exportElementImpl(elementId) {
625
782
  if (!this.visitElements) {
626
783
  core_bentley_1.Logger.logTrace(loggerCategory, `visitElements=false, skipping exportElement(${elementId})`);
627
784
  return;
628
785
  }
629
- // Return early if the elementId is already in the excludedElementIds, that way we don't need to load the element from the db.
786
+ const childVisit = await this.exportElementShallow(elementId);
787
+ if (childVisit === "visit")
788
+ return this.exportChildElements(elementId);
789
+ if (childVisit === "passThrough") {
790
+ // The element's own filter result is decided later, if a changed descendant is reached.
791
+ this._unchangedAncestors.push({ elementId });
792
+ try {
793
+ await this.exportChildElements(elementId);
794
+ }
795
+ finally {
796
+ this._unchangedAncestors.pop();
797
+ }
798
+ }
799
+ }
800
+ /** Returns the filter result for an unchanged ancestor on the current change-processing path, or `undefined` if it has not been filtered.
801
+ * @internal
802
+ */
803
+ getUnchangedAncestorFilterResult(elementId) {
804
+ return this._unchangedAncestors.find((ancestor) => ancestor.elementId === elementId)?.accepted;
805
+ }
806
+ /** Filters the unchanged ancestors of a changed element, top-down, once each.
807
+ * @returns `false` if an ancestor is rejected.
808
+ */
809
+ async acceptUnchangedAncestors() {
810
+ for (const ancestor of this._unchangedAncestors) {
811
+ if (ancestor.accepted === undefined) {
812
+ const element = this.sourceDb.elements.getElement({
813
+ id: ancestor.elementId,
814
+ wantGeometry: this.wantGeometry,
815
+ wantBRepData: this.wantGeometry,
816
+ });
817
+ ancestor.accepted = await this.shouldExportElement(element);
818
+ if (!ancestor.accepted)
819
+ await this.handler.onSkipElement(ancestor.elementId);
820
+ }
821
+ if (!ancestor.accepted)
822
+ return false;
823
+ }
824
+ return true;
825
+ }
826
+ /** Runs the export callbacks for a single element without visiting its children.
827
+ * @returns how to continue with the element's children: skip them, visit them, or pass through an unchanged element to reach changed descendants.
828
+ */
829
+ async exportElementShallow(elementId) {
830
+ // Descendants of a rejected unchanged ancestor are skipped, as in a full export.
831
+ if (this._unchangedAncestors.some((ancestor) => ancestor.accepted === false))
832
+ return "skip";
833
+ // Return early if the elementId is already excluded so it does not need to be loaded.
630
834
  if (this._excludedElementIds.has(elementId)) {
631
835
  core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementId} by Id`);
632
- this.handler.onSkipElement(elementId);
633
- return;
836
+ await this.handler.onSkipElement(elementId);
837
+ return "skip";
634
838
  }
635
- // are we processing changes?
636
839
  const isUpdate = this._sourceDbChanges?.element.insertIds.has(elementId)
637
840
  ? false
638
841
  : this._sourceDbChanges?.element.updateIds.has(elementId)
639
842
  ? true
640
843
  : undefined;
844
+ // An unchanged element may still connect a changed descendant to its model root.
845
+ if (this._sourceDbChanges !== undefined && isUpdate === undefined)
846
+ return "passThrough";
847
+ if (!(await this.acceptUnchangedAncestors()))
848
+ return "skip";
641
849
  const element = this.sourceDb.elements.getElement({
642
850
  id: elementId,
643
851
  wantGeometry: this.wantGeometry,
@@ -645,26 +853,41 @@ class IModelExporter {
645
853
  });
646
854
  core_bentley_1.Logger.logTrace(loggerCategory, `exportElement(${element.id}, "${element.getDisplayLabel()}")${this.getChangeOpSuffix(isUpdate)}`);
647
855
  // the order and `await`ing of calls beyond here is depended upon by the IModelTransformer for a current bug workaround
648
- if (this.shouldExportElement(element)) {
856
+ if (await this.shouldExportElement(element)) {
649
857
  await this.handler.preExportElement(element);
650
858
  await this.handler.onExportElement(element, isUpdate);
651
859
  await this.trackProgress();
652
- await this._exportElementAspectsStrategy.exportElementAspectsForElement(elementId);
653
- return this.exportChildElements(elementId);
654
- }
655
- else {
656
- this.handler.onSkipElement(element.id);
860
+ await this._elementAspectExportCoordinator.addAcceptedOwner(elementId);
861
+ return "visit";
657
862
  }
863
+ await this.handler.onSkipElement(element.id);
864
+ return "skip";
658
865
  }
659
866
  /** Export the child elements of the specified element from the source iModel.
660
867
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
661
868
  */
662
869
  async exportChildElements(elementId) {
870
+ return this.runScopedElementExport(async () => this.exportChildElementsImpl(elementId));
871
+ }
872
+ async exportChildElementsImpl(elementId) {
663
873
  if (!this.visitElements) {
664
874
  core_bentley_1.Logger.logTrace(loggerCategory, `visitElements=false, skipping exportChildElements(${elementId})`);
665
875
  return;
666
876
  }
667
- const childElementIds = this.sourceDb.elements.queryChildren(elementId);
877
+ if (this.canUseSetBasedTraversal()) {
878
+ core_bentley_1.Logger.logTrace(loggerCategory, `exportChildElements(${elementId})`);
879
+ // Seed the stream with direct children; the helper adds their descendants.
880
+ return this.exportElementTreesStreamed(`SELECT e.ECInstanceId, 0 FROM ${core_backend_1.Element.classFullName} e WHERE e.Parent.Id=:parentId`, new core_common_1.QueryBinder().bindId("parentId", elementId));
881
+ }
882
+ let childElementIds;
883
+ if (this.canUseChangedElementForest()) {
884
+ const forest = await this.getChangedElementForest();
885
+ if (forest !== undefined) {
886
+ core_bentley_1.Logger.logTrace(loggerCategory, `exportChildElements(${elementId}) via changed-element index`);
887
+ childElementIds = forest.getChildren(elementId);
888
+ }
889
+ }
890
+ childElementIds ??= this.sourceDb.elements.queryChildren(elementId);
668
891
  if (childElementIds.length > 0) {
669
892
  core_bentley_1.Logger.logTrace(loggerCategory, `exportChildElements(${elementId})`);
670
893
  for (const childElementId of childElementIds) {
@@ -672,13 +895,282 @@ class IModelExporter {
672
895
  }
673
896
  }
674
897
  }
675
- /** Exports all aspects present in the iModel.
898
+ /** Whether change processing may use the sparse hierarchy as the child-id source.
899
+ * Custom traversal overrides retain the previous full element dispatch.
676
900
  */
677
- async exportAllAspects() {
678
- return this._exportElementAspectsStrategy.exportAllElementAspects();
901
+ canUseChangedElementForest() {
902
+ return (this._sourceDbChanges !== undefined &&
903
+ this._changedElementTraversal?.useForest === true &&
904
+ this.hasDefaultElementTraversal());
905
+ }
906
+ getOverriddenElementTraversalMethods() {
907
+ const methods = [];
908
+ if (this.exportElement !== IModelExporter.prototype.exportElement)
909
+ methods.push("exportElement");
910
+ if (this.exportChildElements !== IModelExporter.prototype.exportChildElements)
911
+ methods.push("exportChildElements");
912
+ return methods;
913
+ }
914
+ disableChangedElementForest(reason) {
915
+ if (!this._changedElementTraversal?.useForest)
916
+ return;
917
+ this._changedElementTraversal.useForest = false;
918
+ core_bentley_1.Logger.logInfo(loggerCategory, `Using full element traversal for change processing because ${reason}.`);
919
+ }
920
+ async createChangedElementForest() {
921
+ (0, node_assert_1.strict)(this._sourceDbChanges !== undefined, "changed-element traversal requires sourceDbChanges");
922
+ const excludedElementIds = this._excludedElementIds;
923
+ const candidateCountUpperBound = this._sourceDbChanges.element.insertIds.size +
924
+ this._sourceDbChanges.element.updateIds.size +
925
+ excludedElementIds.size;
926
+ if (candidateCountUpperBound > this._changedElementForestElementLimit) {
927
+ this.disableChangedElementForest(`${candidateCountUpperBound} candidate element references exceed the ${this._changedElementForestElementLimit}-element in-memory index limit`);
928
+ return undefined;
929
+ }
930
+ const forest = await ChangedElementForest_1.ChangedElementForest.create(this.sourceDb, [
931
+ ...this._sourceDbChanges.element.insertIds,
932
+ ...this._sourceDbChanges.element.updateIds,
933
+ ...excludedElementIds,
934
+ ], this._changedElementForestElementLimit);
935
+ if (forest === undefined) {
936
+ this.disableChangedElementForest(`the required parent hierarchy exceeds the ${this._changedElementForestElementLimit}-element in-memory index limit`);
937
+ }
938
+ return forest;
939
+ }
940
+ async getChangedElementForest() {
941
+ const traversal = this._changedElementTraversal;
942
+ (0, node_assert_1.strict)(traversal !== undefined, "changed-element index requires an active traversal scope");
943
+ return (traversal.forest ??= this.createChangedElementForest());
944
+ }
945
+ /** Exports all aspects owned by the supplied elements. */
946
+ async exportAspectsForOwners(ownerElementIds) {
947
+ return this._elementAspectExportCoordinator.exportOwners(ownerElementIds);
948
+ }
949
+ async filterOwnerElementIdsForAspectExport(ownerElementIds) {
950
+ if (ownerElementIds.size === 0)
951
+ return new Set();
952
+ const elementFacts = await this.queryAspectElementHierarchyFacts(ownerElementIds);
953
+ const modelFacts = await this.queryAspectModelHierarchyFacts(new Set([...elementFacts.values()].map((elementFact) => elementFact.modelId)));
954
+ await this.queryAspectElementFacts(new Set(modelFacts.keys()), elementFacts);
955
+ const elementIds = new Set();
956
+ for (const elementId of ownerElementIds) {
957
+ if (this._skipPropagateChangesToRootElements &&
958
+ this._rootElementIds.has(elementId)) {
959
+ continue;
960
+ }
961
+ if (await this.shouldExportElementHierarchyForAspect(elementId, elementFacts, modelFacts)) {
962
+ elementIds.add(elementId);
963
+ }
964
+ }
965
+ return elementIds;
966
+ }
967
+ async shouldExportElementHierarchyForAspect(elementId, elementFacts, modelFacts) {
968
+ let elementFact = elementFacts.get(elementId);
969
+ while (elementFact !== undefined) {
970
+ if (!(await this.shouldExportElementForAspect(elementFact.id, elementFact)))
971
+ return false;
972
+ if (!(await this.shouldExportModelForAspect(elementFact.modelId, elementFacts, modelFacts))) {
973
+ return false;
974
+ }
975
+ const parentId = elementFact.parentId;
976
+ if (parentId === undefined ||
977
+ (this._skipPropagateChangesToRootElements &&
978
+ parentId === core_common_1.IModel.rootSubjectId)) {
979
+ return true;
980
+ }
981
+ elementFact = elementFacts.get(parentId);
982
+ }
983
+ return false;
984
+ }
985
+ async shouldExportModelForAspect(modelId, elementFacts, modelFacts) {
986
+ const modelFact = modelFacts.get(modelId);
987
+ if (modelFact === undefined) {
988
+ this.sourceDb.models.getModel(modelId);
989
+ return false;
990
+ }
991
+ if (modelFact.isTemplate && !this.wantTemplateModels)
992
+ return false;
993
+ if (modelFact.id !== core_common_1.IModel.repositoryModelId &&
994
+ modelFact.id !== core_common_1.IModel.dictionaryId &&
995
+ modelFact.id !== "0xe" &&
996
+ !(await this.shouldExportElementForAspect(modelFact.id, elementFacts.get(modelFact.id)))) {
997
+ return false;
998
+ }
999
+ if (modelFact.parentModelId === undefined ||
1000
+ modelFact.parentModelId === modelFact.id) {
1001
+ return true;
1002
+ }
1003
+ return this.shouldExportModelForAspect(modelFact.parentModelId, elementFacts, modelFacts);
1004
+ }
1005
+ beginChangedElementTraversalScope() {
1006
+ if (this._sourceDbChanges === undefined ||
1007
+ this._changedElementTraversal !== undefined) {
1008
+ return false;
1009
+ }
1010
+ this._changedElementTraversal = { useForest: true };
1011
+ const overriddenMethods = this.getOverriddenElementTraversalMethods();
1012
+ if (this.visitElements && overriddenMethods.length > 0) {
1013
+ this.disableChangedElementForest(`${overriddenMethods.join(" and ")} ${overriddenMethods.length === 1 ? "is" : "are"} overridden by the exporter subclass`);
1014
+ }
1015
+ return true;
1016
+ }
1017
+ endChangedElementTraversalScope() {
1018
+ this._changedElementTraversal = undefined;
1019
+ }
1020
+ async runScopedElementExport(exportElements) {
1021
+ const ownsChangedElementTraversal = this.beginChangedElementTraversalScope();
1022
+ try {
1023
+ await this._elementAspectExportCoordinator.run(exportElements);
1024
+ }
1025
+ finally {
1026
+ if (ownsChangedElementTraversal)
1027
+ this.endChangedElementTraversalScope();
1028
+ }
1029
+ }
1030
+ /** Apply the element export filter when deciding whether to process an aspect owner. @internal */
1031
+ async shouldExportElementForAspect(elementId, elementFact) {
1032
+ if (!this.visitElements)
1033
+ return false;
1034
+ if (this.shouldExportElement ===
1035
+ IModelExporter.prototype.shouldExportElement &&
1036
+ elementFact !== undefined &&
1037
+ !this.shouldExportElementFact(elementFact)) {
1038
+ return false;
1039
+ }
1040
+ const element = this.sourceDb.elements.getElement({
1041
+ id: elementId,
1042
+ wantGeometry: false,
1043
+ wantBRepData: false,
1044
+ });
1045
+ return this.shouldExportElement(element);
1046
+ }
1047
+ shouldExportElementFact(elementFact) {
1048
+ if (this._excludedElementIds.has(elementFact.id)) {
1049
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementFact.id} by Id`);
1050
+ return false;
1051
+ }
1052
+ const elementClass = this.sourceDb.getJsClass(elementFact.classFullName);
1053
+ if ((elementFact.classFullName === core_backend_1.GeometricElement.classFullName ||
1054
+ elementClass.prototype instanceof core_backend_1.GeometricElement) &&
1055
+ elementFact.categoryId !== undefined &&
1056
+ this._excludedElementCategoryIds.has(elementFact.categoryId)) {
1057
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementFact.id} by Category`);
1058
+ return false;
1059
+ }
1060
+ if (!this.wantTemplateModels &&
1061
+ (elementFact.classFullName === core_backend_1.RecipeDefinitionElement.classFullName ||
1062
+ elementClass.prototype instanceof core_backend_1.RecipeDefinitionElement)) {
1063
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded RecipeDefinitionElement ${elementFact.id} because wantTemplate=false`);
1064
+ return false;
1065
+ }
1066
+ for (const excludedElementClass of this._excludedElementClasses) {
1067
+ if (elementFact.classFullName === excludedElementClass.classFullName ||
1068
+ elementClass.prototype instanceof excludedElementClass) {
1069
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementFact.id} by class: ${excludedElementClass.classFullName}`);
1070
+ return false;
1071
+ }
1072
+ }
1073
+ return true;
1074
+ }
1075
+ async queryAspectElementHierarchyFacts(elementIds) {
1076
+ const elementFacts = new Map();
1077
+ const queriedElementIds = new Set();
1078
+ let pendingElementIds = new Set(elementIds);
1079
+ while (pendingElementIds.size > 0) {
1080
+ const currentElementIds = pendingElementIds;
1081
+ pendingElementIds = new Set();
1082
+ await this.queryAspectElementFacts(currentElementIds, elementFacts);
1083
+ for (const elementId of currentElementIds)
1084
+ queriedElementIds.add(elementId);
1085
+ for (const elementId of currentElementIds) {
1086
+ const parentId = elementFacts.get(elementId)?.parentId;
1087
+ if (parentId !== undefined &&
1088
+ !queriedElementIds.has(parentId) &&
1089
+ !elementFacts.has(parentId)) {
1090
+ pendingElementIds.add(parentId);
1091
+ }
1092
+ }
1093
+ }
1094
+ return elementFacts;
1095
+ }
1096
+ async queryAspectElementFacts(elementIds, elementFacts) {
1097
+ const unqueriedElementIds = new Set([...elementIds].filter((elementId) => !elementFacts.has(elementId)));
1098
+ if (unqueriedElementIds.size === 0)
1099
+ return;
1100
+ for (const elementIdBatch of this.getAspectOwnerIdBatches(unqueriedElementIds)) {
1101
+ const queryParams = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIdBatch);
1102
+ for await (const row of this.sourceDb.createQueryReader(`SELECT e.ECInstanceId id, e.Parent.Id parentId, e.Model.Id modelId,
1103
+ ec_className(e.ECClassId, 's') schemaName,
1104
+ ec_className(e.ECClassId, 'c') className
1105
+ FROM bis.Element e
1106
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = e.ECInstanceId`, queryParams, { usePrimaryConn: true })) {
1107
+ elementFacts.set(row.id, {
1108
+ id: row.id,
1109
+ parentId: row.parentId ?? undefined,
1110
+ modelId: row.modelId,
1111
+ classFullName: `${row.schemaName}:${row.className}`,
1112
+ });
1113
+ }
1114
+ if (this._excludedElementCategoryIds.size > 0) {
1115
+ const categoryQueryParams = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIdBatch);
1116
+ for await (const row of this.sourceDb.createQueryReader(`SELECT g.ECInstanceId id, g.Category.Id categoryId
1117
+ FROM bis.GeometricElement g
1118
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = g.ECInstanceId`, categoryQueryParams, { usePrimaryConn: true })) {
1119
+ const elementFact = elementFacts.get(row.id);
1120
+ if (elementFact !== undefined) {
1121
+ elementFact.categoryId = row.categoryId;
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1126
+ }
1127
+ async queryAspectModelHierarchyFacts(modelIds) {
1128
+ const modelFacts = new Map();
1129
+ const queriedModelIds = new Set();
1130
+ let pendingModelIds = new Set(modelIds);
1131
+ while (pendingModelIds.size > 0) {
1132
+ const currentModelIds = pendingModelIds;
1133
+ pendingModelIds = new Set();
1134
+ for (const modelIdBatch of this.getAspectOwnerIdBatches(currentModelIds)) {
1135
+ const queryParams = new core_common_1.QueryBinder().bindIdSet("modelIds", modelIdBatch);
1136
+ for await (const row of this.sourceDb.createQueryReader(`SELECT model.ECInstanceId id, model.ParentModel.Id parentModelId, model.IsTemplate isTemplate
1137
+ FROM bis.Model model
1138
+ INNER JOIN IdSet(:modelIds) ids ON ids.id = model.ECInstanceId`, queryParams, { usePrimaryConn: true })) {
1139
+ modelFacts.set(row.id, {
1140
+ id: row.id,
1141
+ parentModelId: row.parentModelId ?? undefined,
1142
+ isTemplate: row.isTemplate === true || row.isTemplate === 1,
1143
+ });
1144
+ }
1145
+ }
1146
+ for (const modelId of currentModelIds)
1147
+ queriedModelIds.add(modelId);
1148
+ for (const modelId of currentModelIds) {
1149
+ const parentModelId = modelFacts.get(modelId)?.parentModelId;
1150
+ if (parentModelId !== undefined &&
1151
+ !queriedModelIds.has(parentModelId) &&
1152
+ !modelFacts.has(parentModelId)) {
1153
+ pendingModelIds.add(parentModelId);
1154
+ }
1155
+ }
1156
+ }
1157
+ return modelFacts;
1158
+ }
1159
+ *getAspectOwnerIdBatches(ids) {
1160
+ let batch = new Set();
1161
+ for (const id of ids) {
1162
+ batch.add(id);
1163
+ if (batch.size === IModelExporter._elementAspectOwnerBatchSize) {
1164
+ yield batch;
1165
+ batch = new Set();
1166
+ }
1167
+ }
1168
+ if (batch.size > 0)
1169
+ yield batch;
679
1170
  }
680
1171
  /** Exports all relationships that subclass from the specified base class.
681
1172
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
1173
+ * @note This is the bulk traversal path. It hydrates relationships with one streaming query and calls [[exportRelationshipInstance]] directly; it does not call [[exportRelationship]] once per row.
682
1174
  */
683
1175
  async exportRelationships(baseRelClassFullName) {
684
1176
  if (!this.visitRelationships) {
@@ -686,41 +1178,154 @@ class IModelExporter {
686
1178
  return;
687
1179
  }
688
1180
  core_bentley_1.Logger.logTrace(loggerCategory, `exportRelationships(${baseRelClassFullName})`);
689
- const sql = `SELECT r.ECInstanceId, ec_className(r.ECClassId, 's.c') as className FROM ${baseRelClassFullName} r
690
- JOIN bis.Element s ON s.ECInstanceId = r.SourceECInstanceId
691
- JOIN bis.Element t ON t.ECInstanceId = r.TargetECInstanceId
692
- WHERE s.ECInstanceId IS NOT NULL AND t.ECInstanceId IS NOT NULL`;
693
- for await (const row of this.sourceDb.createQueryReader(sql, undefined, {
694
- usePrimaryConn: true,
695
- })) {
696
- const relationshipId = row.id;
697
- const relationshipClass = row.className;
698
- await this.exportRelationship(relationshipClass, relationshipId); // must call exportRelationship using the actual classFullName, not baseRelClassFullName
699
- await this._yieldManager.allowYield();
1181
+ const changedRelationshipIds = this._sourceDbChanges === undefined
1182
+ ? undefined
1183
+ : new Set([
1184
+ ...this._sourceDbChanges.relationship.insertIds,
1185
+ ...this._sourceDbChanges.relationship.updateIds,
1186
+ ]);
1187
+ if (changedRelationshipIds?.size === 0) {
1188
+ // Deleted relationships are handled by exportChanges after this traversal; they are no
1189
+ // longer present in the source iModel and must not cause a full relationship scan here.
1190
+ core_bentley_1.Logger.logTrace(loggerCategory, "No inserted or updated relationships, skipping exportRelationships()");
1191
+ return;
1192
+ }
1193
+ const changedRelationshipJoin = changedRelationshipIds === undefined
1194
+ ? undefined
1195
+ : "INNER JOIN IdSet(:changedRelationshipIds) changed ON changed.id = r.ECInstanceId";
1196
+ const relationshipWhereClause = "s.ECInstanceId IS NOT NULL AND t.ECInstanceId IS NOT NULL";
1197
+ // Bound the async reader's retained raw rows while preserving batched query throughput.
1198
+ // Keyset pagination avoids rescanning rows returned by earlier batches.
1199
+ const relationshipQueryBatchSize = 1000;
1200
+ let lastRelationshipId;
1201
+ while (true) {
1202
+ let rowsRead = 0;
1203
+ const whereClause = lastRelationshipId === undefined
1204
+ ? relationshipWhereClause
1205
+ : `${relationshipWhereClause} AND r.ECInstanceId > :lastRelationshipId`;
1206
+ // QueryBinder parameters are immutable after binding a name. Recreate the binder for each
1207
+ // keyset page so the changing boundary can be bound without redefining a property.
1208
+ const queryParams = changedRelationshipIds === undefined && lastRelationshipId === undefined
1209
+ ? undefined
1210
+ : new core_common_1.QueryBinder();
1211
+ if (queryParams !== undefined) {
1212
+ if (changedRelationshipIds !== undefined) {
1213
+ queryParams.bindIdSet("changedRelationshipIds", changedRelationshipIds);
1214
+ }
1215
+ if (lastRelationshipId !== undefined) {
1216
+ queryParams.bindId("lastRelationshipId", lastRelationshipId);
1217
+ }
1218
+ }
1219
+ const reader = this.sourceDb.createQueryReader(this.getRelationshipQuery(baseRelClassFullName, "JOIN", whereClause, changedRelationshipJoin), queryParams, {
1220
+ usePrimaryConn: true,
1221
+ limit: { count: relationshipQueryBatchSize },
1222
+ });
1223
+ for await (const row of reader) {
1224
+ rowsRead++;
1225
+ const relInstanceId = row.relInstanceId;
1226
+ lastRelationshipId = relInstanceId;
1227
+ const changesetFilter = this.checkRelationshipChangesetFilter(relInstanceId);
1228
+ if (changesetFilter.skip) {
1229
+ await this._yieldManager.allowYield();
1230
+ continue;
1231
+ }
1232
+ await this.exportHydratedRelationship(row.rawInstance, row.sourceFedGuid, row.targetFedGuid, changesetFilter.isUpdate);
1233
+ await this._yieldManager.allowYield();
1234
+ }
1235
+ if (rowsRead < relationshipQueryBatchSize)
1236
+ break;
1237
+ }
1238
+ }
1239
+ /** Builds the raw relationship query used by the bulk and targeted export paths. */
1240
+ getRelationshipQuery(relClassFullName, endpointJoin, whereClause, relationshipJoin) {
1241
+ return `SELECT r.$ AS rawInstance, r.ECInstanceId AS relInstanceId, s.FederationGuid AS sourceFedGuid, t.FederationGuid AS targetFedGuid FROM ${relClassFullName} r
1242
+ ${relationshipJoin ?? ""}
1243
+ ${endpointJoin} bis.Element s ON s.ECInstanceId = r.SourceECInstanceId
1244
+ ${endpointJoin} bis.Element t ON t.ECInstanceId = r.TargetECInstanceId
1245
+ WHERE ${whereClause}
1246
+ ORDER BY r.ECInstanceId
1247
+ OPTIONS USE_JS_PROP_NAMES DO_NOT_TRUNCATE_BLOB`;
1248
+ }
1249
+ /** Applies the changeset filter (when exporting changes) to a relationship instance id.
1250
+ * @returns whether to skip the relationship, and whether the export is an update.
1251
+ */
1252
+ checkRelationshipChangesetFilter(relInstanceId) {
1253
+ if (undefined === this._sourceDbChanges)
1254
+ return { skip: false, isUpdate: undefined };
1255
+ if (this._sourceDbChanges.relationship.insertIds.has(relInstanceId))
1256
+ return { skip: false, isUpdate: false };
1257
+ if (this._sourceDbChanges.relationship.updateIds.has(relInstanceId))
1258
+ return { skip: false, isUpdate: true };
1259
+ return { skip: true, isUpdate: undefined }; // not in changeset, don't export
1260
+ }
1261
+ /** Converts a raw `$` instance-access row into [RelationshipProps]($common). */
1262
+ getRelationshipProps(rawInstance) {
1263
+ const parsedRow = typeof rawInstance === "string"
1264
+ ? JSON.parse(rawInstance, core_common_1.Base64EncodedString.reviver)
1265
+ : rawInstance;
1266
+ if (!core_bentley_1.JsonUtils.isObject(parsedRow))
1267
+ core_bentley_1.ITwinError.throwError({
1268
+ iTwinErrorId: {
1269
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1270
+ key: IModelTransformerError_1.IModelTransformerError.InvalidRelationshipData,
1271
+ },
1272
+ message: "Expected a Relationship instance query to return an object",
1273
+ });
1274
+ const row = {};
1275
+ for (const [key, value] of Object.entries(parsedRow)) {
1276
+ const ecPropertyName = key.length === 0 ? key : key[0].toUpperCase() + key.substring(1);
1277
+ row[core_common_1.ECJsNames.toJsName(ecPropertyName)] = value;
700
1278
  }
1279
+ const className = row.className;
1280
+ if (typeof className !== "string")
1281
+ core_bentley_1.ITwinError.throwError({
1282
+ iTwinErrorId: {
1283
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1284
+ key: IModelTransformerError_1.IModelTransformerError.InvalidRelationshipData,
1285
+ },
1286
+ message: "Expected a Relationship instance query to return a className",
1287
+ });
1288
+ row.classFullName = className.replace(".", ":");
1289
+ delete row.className;
1290
+ delete row.sourceClassName;
1291
+ delete row.targetClassName;
1292
+ return row;
701
1293
  }
702
- /** Export a relationship from the source iModel. */
1294
+ /** Export one specified relationship from the source iModel.
1295
+ * @note This targeted path is separate from [[exportRelationships]], which traverses a class hierarchy. It uses the same raw instance query and passes endpoint FederationGuids to the handler.
1296
+ */
703
1297
  async exportRelationship(relClassFullName, relInstanceId) {
704
1298
  if (!this.visitRelationships) {
705
1299
  core_bentley_1.Logger.logTrace(loggerCategory, `visitRelationships=false, skipping exportRelationship(${relClassFullName}, ${relInstanceId})`);
706
1300
  return;
707
1301
  }
708
- let isUpdate;
709
- if (undefined !== this._sourceDbChanges) {
710
- // is changeset information available?
711
- if (this._sourceDbChanges.relationship.insertIds.has(relInstanceId)) {
712
- isUpdate = false;
713
- }
714
- else if (this._sourceDbChanges.relationship.updateIds.has(relInstanceId)) {
715
- isUpdate = true;
716
- }
717
- else {
718
- return; // not in changeset, don't export
719
- }
1302
+ const changesetFilter = this.checkRelationshipChangesetFilter(relInstanceId);
1303
+ if (changesetFilter.skip)
1304
+ return;
1305
+ const queryParams = new core_common_1.QueryBinder().bindId("relInstanceId", relInstanceId);
1306
+ const reader = this.sourceDb.createQueryReader(this.getRelationshipQuery(relClassFullName, "LEFT JOIN", "r.ECInstanceId = :relInstanceId"), queryParams, { usePrimaryConn: true });
1307
+ if (await reader.step()) {
1308
+ const row = reader.current;
1309
+ await this.exportHydratedRelationship(row.rawInstance, row.sourceFedGuid, row.targetFedGuid, changesetFilter.isUpdate);
1310
+ return;
720
1311
  }
721
- // passed changeset test, now apply standard exclusion rules
722
- core_bentley_1.Logger.logTrace(loggerCategory, `exportRelationship(${relClassFullName}, ${relInstanceId})`);
1312
+ // Preserve the previous not-found behavior if the targeted query returned no row.
723
1313
  const relationship = this.sourceDb.relationships.getInstance(relClassFullName, relInstanceId);
1314
+ await this.exportRelationshipInstance(relationship, changesetFilter.isUpdate, this.sourceDb.elements.getFederationGuidFromId(relationship.sourceId), this.sourceDb.elements.getFederationGuidFromId(relationship.targetId));
1315
+ }
1316
+ /** Hydrates a relationship query row and runs the common export pipeline. */
1317
+ async exportHydratedRelationship(rawInstance, sourceFedGuid, targetFedGuid, isUpdate) {
1318
+ const relationship = this.sourceDb.constructEntity(this.getRelationshipProps(rawInstance));
1319
+ await this.exportRelationshipInstance(relationship, isUpdate, sourceFedGuid ?? undefined, targetFedGuid ?? undefined);
1320
+ }
1321
+ /** Applies exclusion rules and handler callbacks to an already-hydrated relationship instance.
1322
+ * @param sourceFedGuid The FederationGuid of the relationship's source endpoint, if available.
1323
+ * @param targetFedGuid The FederationGuid of the relationship's target endpoint, if available.
1324
+ * @note This is the common protected hook used by both [[exportRelationship]] and [[exportRelationships]]. Most consumers should customize [[IModelExportHandler]] instead.
1325
+ */
1326
+ async exportRelationshipInstance(relationship, isUpdate, sourceFedGuid, targetFedGuid) {
1327
+ // passed changeset test, now apply standard exclusion rules
1328
+ core_bentley_1.Logger.logTrace(loggerCategory, `exportRelationship(${relationship.classFullName}, ${relationship.id})`);
724
1329
  for (const excludedRelationshipClass of this._excludedRelationshipClasses) {
725
1330
  if (relationship instanceof excludedRelationshipClass) {
726
1331
  core_bentley_1.Logger.logInfo(loggerCategory, `Excluded relationship by class: ${excludedRelationshipClass.classFullName}`);
@@ -728,8 +1333,8 @@ class IModelExporter {
728
1333
  }
729
1334
  }
730
1335
  // relationship has passed standard exclusion rules, now give handler a chance to accept/reject export
731
- if (this.handler.shouldExportRelationship(relationship)) {
732
- await this.handler.onExportRelationship(relationship, isUpdate);
1336
+ if (await this.handler.shouldExportRelationship(relationship)) {
1337
+ await this.handler.onExportRelationship(relationship, isUpdate, sourceFedGuid, targetFedGuid);
733
1338
  await this.trackProgress();
734
1339
  }
735
1340
  }
@@ -788,6 +1393,13 @@ class ChangedInstanceIds {
788
1393
  _aspectSubclassIds;
789
1394
  _relationshipSubclassIds;
790
1395
  _relationshipSubclassIdsToSkip;
1396
+ _aspectOwnerElementIds = new Set();
1397
+ /** Element IDs that own the aspects represented by `aspect` changes.
1398
+ * @internal
1399
+ */
1400
+ get aspectOwnerElementIds() {
1401
+ return this._aspectOwnerElementIds;
1402
+ }
791
1403
  _db;
792
1404
  constructor(db) {
793
1405
  this._db = db;
@@ -850,33 +1462,71 @@ class ChangedInstanceIds {
850
1462
  !this.font.isEmpty);
851
1463
  }
852
1464
  /**
853
- * Adds the provided [[ChangedECInstance]] to the appropriate set of changes by class type (codeSpec, model, element, aspect, or relationship) maintained by this instance of ChangedInstanceIds.
1465
+ * Adds the provided [[ChangeInstance]] to the appropriate set of changes by class type (codeSpec, model, element, aspect, or relationship) maintained by this instance of ChangedInstanceIds.
854
1466
  * If the same ECInstanceId is seen multiple times, the changedInstanceIds will be modified accordingly, i.e. if an id 'x' was updated but now we see 'x' was deleted, we will remove 'x'
855
1467
  * from the set of updatedIds and add it to the set of deletedIds for the appropriate class type.
856
- * @param change ChangedECInstance which has the ECInstanceId, changeType (insert, update, delete) and ECClassId of the changed entity
1468
+ * @param change Changed EC instance with the ID, operation, and EC class ID of the changed entity.
857
1469
  */
858
1470
  async addChange(change) {
1471
+ return this.recordChange(change, undefined);
1472
+ }
1473
+ async recordChange(change, unresolvedAspectIds) {
859
1474
  if (!this._ecClassIdsInitialized)
860
1475
  await this.setupECClassIds();
861
- const ecClassId = change.ECClassId ?? change.$meta?.fallbackClassId;
1476
+ const ecClassId = change.ECClassId;
862
1477
  if (ecClassId === undefined)
863
- throw new Error(`ECClassId was not found for id: ${change.ECInstanceId}! Table is : ${change?.$meta?.tables}`);
864
- const changeType = change.$meta?.op;
1478
+ core_bentley_1.ITwinError.throwError({
1479
+ iTwinErrorId: {
1480
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1481
+ key: IModelTransformerError_1.IModelTransformerError.ChangedInstanceMetadataMissing,
1482
+ },
1483
+ message: `ECClassId was not found for id: ${change.ECInstanceId}! Table is : ${change.$meta.tables}`,
1484
+ });
1485
+ const changeType = change.$meta.op;
865
1486
  if (changeType === undefined)
866
- throw new Error(`ChangeType was undefined for id: ${change.ECInstanceId}.`);
1487
+ core_bentley_1.ITwinError.throwError({
1488
+ iTwinErrorId: {
1489
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1490
+ key: IModelTransformerError_1.IModelTransformerError.ChangedInstanceMetadataMissing,
1491
+ },
1492
+ message: `ChangeType was undefined for id: ${change.ECInstanceId}.`,
1493
+ });
867
1494
  if (this._relationshipSubclassIdsToSkip?.has(ecClassId))
868
1495
  return;
869
1496
  if (this.isRelationship(ecClassId))
870
1497
  this.handleChange(this.relationship, changeType, change.ECInstanceId);
871
1498
  else if (this.isCodeSpec(ecClassId))
872
1499
  this.handleChange(this.codeSpec, changeType, change.ECInstanceId);
873
- else if (this.isAspect(ecClassId))
1500
+ else if (this.isAspect(ecClassId)) {
1501
+ let ownerElementId = change.Element?.Id;
1502
+ if (ownerElementId === undefined) {
1503
+ if (unresolvedAspectIds !== undefined)
1504
+ unresolvedAspectIds.add(change.ECInstanceId);
1505
+ else
1506
+ ownerElementId = this.tryGetAspectOwnerElementId(change.ECInstanceId);
1507
+ }
1508
+ if (ownerElementId !== undefined) {
1509
+ this._aspectOwnerElementIds.add(ownerElementId);
1510
+ }
874
1511
  this.handleChange(this.aspect, changeType, change.ECInstanceId);
1512
+ }
875
1513
  else if (this.isModel(ecClassId))
876
1514
  this.handleChange(this.model, changeType, change.ECInstanceId);
877
1515
  else if (this.isElement(ecClassId))
878
1516
  this.handleChange(this.element, changeType, change.ECInstanceId);
879
1517
  }
1518
+ tryGetAspectOwnerElementId(aspectId) {
1519
+ try {
1520
+ return this._db.elements.getAspect(aspectId).element.id;
1521
+ }
1522
+ catch (error) {
1523
+ if (error instanceof core_common_1.IModelError &&
1524
+ error.errorNumber === core_bentley_1.IModelStatus.NotFound) {
1525
+ return undefined;
1526
+ }
1527
+ throw error;
1528
+ }
1529
+ }
880
1530
  /**
881
1531
  * This method should only be called inside [[IModelTransformer.addCustomChanges]].
882
1532
  * It adds the provided change to the element changes maintained by this instance of ChangedInstanceIds.
@@ -938,14 +1588,47 @@ class ChangedInstanceIds {
938
1588
  * If the same ECInstanceId is seen multiple times, the changedInstanceIds will be modified accordingly, i.e. if an id 'x' was updated but now we see 'x' was deleted, we will remove 'x'
939
1589
  * from the set of updatedIds and add it to the set of deletedIds for the appropriate class type.
940
1590
  * @note It is the responsibility of the caller to ensure that the provided id is, in fact an aspect.
1591
+ * @param elementIds Owning element IDs. Required when `changeType` is
1592
+ * `Deleted` or the source aspect row is unavailable.
941
1593
  * @note In most cases, this method does not need to be called. Its only for consumers to mimic changes as if they were found in a changeset, which should only be useful in certain cases such as the changing of filter criteria for a preexisting master branch relationship.
942
1594
  * @beta
943
1595
  */
944
- addCustomAspectChange(changeType, ids) {
1596
+ addCustomAspectChange(changeType, ids, elementIds) {
1597
+ if (elementIds !== undefined) {
1598
+ for (const elementId of core_bentley_1.Id64.iterable(elementIds)) {
1599
+ this._aspectOwnerElementIds.add(elementId);
1600
+ }
1601
+ }
945
1602
  for (const id of core_bentley_1.Id64.iterable(ids)) {
1603
+ if (elementIds === undefined && changeType === "Deleted") {
1604
+ core_bentley_1.ITwinError.throwError({
1605
+ iTwinErrorId: {
1606
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1607
+ key: IModelTransformerError_1.IModelTransformerError.AspectOwnerRequired,
1608
+ },
1609
+ message: "Custom deleted ElementAspect changes require the owning element ID.",
1610
+ });
1611
+ }
1612
+ if (elementIds === undefined && changeType !== "Deleted") {
1613
+ const ownerElementId = this.tryGetAspectOwnerElementId(id);
1614
+ if (ownerElementId === undefined) {
1615
+ core_bentley_1.ITwinError.throwError({
1616
+ iTwinErrorId: {
1617
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1618
+ key: IModelTransformerError_1.IModelTransformerError.AspectOwnerRequired,
1619
+ },
1620
+ message: "Custom ElementAspect changes require the owning element ID when the source aspect is unavailable.",
1621
+ });
1622
+ }
1623
+ this._aspectOwnerElementIds.add(ownerElementId);
1624
+ }
946
1625
  this.handleChange(this.aspect, changeType, id);
947
1626
  }
948
1627
  }
1628
+ recordCustomAspectChange(changeType, aspectId, ownerElementId) {
1629
+ this._aspectOwnerElementIds.add(ownerElementId);
1630
+ this.handleChange(this.aspect, changeType, aspectId);
1631
+ }
949
1632
  /**
950
1633
  * There is an optimization in [IModelExporter.exportModelContents] which doesn't try to export elements within a model unless the model itself is marked as `Updated` or 'Inserted' in sourceDbChanges. This method is used in [[addCustomElementChange]] and [[addCustomModelChange]] to add the parent model hierarchy to the 'updatedIds' so that the custom element changes are exported.
951
1634
  * Transformer will insert 'Updated' model to target if it does not exist there already. To handle such case, modeled elements of parent models are also marked as updated. This is done, because model can not be inserted without it's modeled element.
@@ -954,7 +1637,9 @@ class ChangedInstanceIds {
954
1637
  const params = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIds);
955
1638
  const ecQuery = `
956
1639
  WITH RECURSIVE hierarchy (parentId) AS (
957
- SELECT Model.Id FROM bis.Element WHERE InVirtualSet(:elementIds, ECInstanceId)
1640
+ SELECT element.Model.Id
1641
+ FROM bis.Element element
1642
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = element.ECInstanceId
958
1643
  UNION
959
1644
  SELECT ParentModel.id
960
1645
  FROM bis.Model e
@@ -979,9 +1664,15 @@ class ChangedInstanceIds {
979
1664
  return parentModelIds;
980
1665
  }
981
1666
  async markElementRelationshipsAsInserted(relationshipClassName, elementIds) {
982
- const ecQuery = `SELECT ECInstanceId FROM ${relationshipClassName}
983
- WHERE InVirtualSet(:elementIds, TargetECInstanceId)
984
- OR InVirtualSet(:elementIds, SourceECInstanceId)`;
1667
+ const ecQuery = `SELECT relationship.ECInstanceId
1668
+ FROM ${relationshipClassName} relationship
1669
+ INNER JOIN IdSet(:elementIds) ids
1670
+ ON ids.id = relationship.TargetECInstanceId
1671
+ UNION
1672
+ SELECT relationship.ECInstanceId
1673
+ FROM ${relationshipClassName} relationship
1674
+ INNER JOIN IdSet(:elementIds) ids
1675
+ ON ids.id = relationship.SourceECInstanceId`;
985
1676
  const queryBinder = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIds);
986
1677
  const queryReader = this._db.createQueryReader(ecQuery, queryBinder, {
987
1678
  usePrimaryConn: true,
@@ -995,13 +1686,16 @@ class ChangedInstanceIds {
995
1686
  core_backend_1.ElementUniqueAspect.classFullName,
996
1687
  core_backend_1.ElementMultiAspect.classFullName,
997
1688
  ]) {
998
- const ecQuery = `Select ECInstanceId from ${aspectClassName} where InVirtualSet(:elementIds, Element.Id)`;
1689
+ const ecQuery = `SELECT aspect.ECInstanceId, aspect.Element.Id
1690
+ FROM ${aspectClassName} aspect
1691
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = aspect.Element.Id`;
999
1692
  const queryBinder = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIds);
1000
1693
  const queryReader = this._db.createQueryReader(ecQuery, queryBinder, {
1001
1694
  usePrimaryConn: true,
1002
1695
  });
1003
1696
  for await (const row of queryReader) {
1004
- this.addCustomAspectChange("Inserted", row.toArray()[0]);
1697
+ const [aspectId, elementId] = row.toArray();
1698
+ this.recordCustomAspectChange("Inserted", aspectId, elementId);
1005
1699
  }
1006
1700
  }
1007
1701
  }
@@ -1068,25 +1762,30 @@ class ChangedInstanceIds {
1068
1762
  if (csFileProps === undefined)
1069
1763
  return undefined;
1070
1764
  const changedInstanceIds = new ChangedInstanceIds(opts.iModel);
1071
- for (const csFile of csFileProps) {
1072
- const csReader = core_backend_1.SqliteChangesetReader.openFile({
1073
- fileName: csFile.pathname,
1074
- db: opts.iModel,
1075
- disableSchemaCheck: true,
1076
- });
1077
- const csAdaptor = new core_backend_1.ChangesetECAdaptor(csReader);
1078
- const ecChangeUnifier = new core_backend_1.PartialECChangeUnifier(opts.iModel);
1079
- while (csAdaptor.step()) {
1080
- ecChangeUnifier.appendFrom(csAdaptor);
1081
- }
1082
- const changes = [...ecChangeUnifier.instances];
1083
- for (const change of changes) {
1084
- await changedInstanceIds.addChange(change);
1085
- }
1086
- csReader.close();
1087
- }
1765
+ await ChangesetScanner_1.ChangesetScanner.scan(opts.iModel, csFileProps, changedInstanceIds);
1088
1766
  return changedInstanceIds;
1089
1767
  }
1768
+ /** Record an ordered batch, then resolve missing aspect owners against current source state.
1769
+ * @internal
1770
+ */
1771
+ async addChanges(changes) {
1772
+ const unresolvedAspectIds = new Set();
1773
+ for (const change of changes)
1774
+ await this.recordChange(change, unresolvedAspectIds);
1775
+ // Only resolve missing owners from current source state. Changeset-provided
1776
+ // owners, including historical owners, were retained while recording changes.
1777
+ if (unresolvedAspectIds.size > 0) {
1778
+ this._db.withQueryReader(`WITH AspectIds AS (SELECT id FROM IdSet(:aspectIds))
1779
+ SELECT Element.Id FROM BisCore.ElementMultiAspect
1780
+ WHERE ECInstanceId IN (SELECT id FROM AspectIds)
1781
+ UNION ALL
1782
+ SELECT Element.Id FROM BisCore.ElementUniqueAspect
1783
+ WHERE ECInstanceId IN (SELECT id FROM AspectIds)`, (reader) => {
1784
+ while (reader.step())
1785
+ this._aspectOwnerElementIds.add(reader.current[0]);
1786
+ }, new core_common_1.QueryBinder().bindIdSet("aspectIds", unresolvedAspectIds));
1787
+ }
1788
+ }
1090
1789
  }
1091
1790
  exports.ChangedInstanceIds = ChangedInstanceIds;
1092
1791
  //# sourceMappingURL=IModelExporter.js.map