@itwin/imodel-transformer 2.0.0-dev.7 → 2.0.0-dev.70

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