@itwin/imodel-transformer 2.0.0-dev.5 → 2.0.0-dev.51

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 (119) hide show
  1. package/README.md +13 -27
  2. package/lib/cjs/Algo.js.map +1 -1
  3. package/lib/cjs/BigMap.d.ts.map +1 -1
  4. package/lib/cjs/BigMap.js.map +1 -1
  5. package/lib/cjs/BranchProvenanceInitializer.d.ts.map +1 -1
  6. package/lib/cjs/BranchProvenanceInitializer.js +34 -36
  7. package/lib/cjs/BranchProvenanceInitializer.js.map +1 -1
  8. package/lib/cjs/ChangesetScanner.d.ts +60 -0
  9. package/lib/cjs/ChangesetScanner.d.ts.map +1 -0
  10. package/lib/cjs/ChangesetScanner.js +101 -0
  11. package/lib/cjs/ChangesetScanner.js.map +1 -0
  12. package/lib/cjs/ECReferenceTypesCache.d.ts +0 -3
  13. package/lib/cjs/ECReferenceTypesCache.d.ts.map +1 -1
  14. package/lib/cjs/ECReferenceTypesCache.js +61 -103
  15. package/lib/cjs/ECReferenceTypesCache.js.map +1 -1
  16. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.d.ts.map +1 -1
  17. package/lib/cjs/ECSqlReaderAsyncIterableIteratorAdapter.js.map +1 -1
  18. package/lib/cjs/ElementAspectCleanup.d.ts +16 -0
  19. package/lib/cjs/ElementAspectCleanup.d.ts.map +1 -0
  20. package/lib/cjs/ElementAspectCleanup.js +82 -0
  21. package/lib/cjs/ElementAspectCleanup.js.map +1 -0
  22. package/lib/cjs/ElementAspectExportCoordinator.d.ts +44 -0
  23. package/lib/cjs/ElementAspectExportCoordinator.d.ts.map +1 -0
  24. package/lib/cjs/ElementAspectExportCoordinator.js +148 -0
  25. package/lib/cjs/ElementAspectExportCoordinator.js.map +1 -0
  26. package/lib/cjs/ElementAspectExportProcessor.d.ts +43 -0
  27. package/lib/cjs/ElementAspectExportProcessor.d.ts.map +1 -0
  28. package/lib/cjs/ElementAspectExportProcessor.js +167 -0
  29. package/lib/cjs/ElementAspectExportProcessor.js.map +1 -0
  30. package/lib/cjs/ElementBulkDelete.d.ts +11 -0
  31. package/lib/cjs/ElementBulkDelete.d.ts.map +1 -0
  32. package/lib/cjs/ElementBulkDelete.js +55 -0
  33. package/lib/cjs/ElementBulkDelete.js.map +1 -0
  34. package/lib/cjs/EntityExistenceCache.d.ts +43 -0
  35. package/lib/cjs/EntityExistenceCache.d.ts.map +1 -0
  36. package/lib/cjs/EntityExistenceCache.js +91 -0
  37. package/lib/cjs/EntityExistenceCache.js.map +1 -0
  38. package/lib/cjs/EntityMap.d.ts.map +1 -1
  39. package/lib/cjs/EntityMap.js.map +1 -1
  40. package/lib/cjs/EntityUnifier.d.ts +8 -8
  41. package/lib/cjs/EntityUnifier.d.ts.map +1 -1
  42. package/lib/cjs/EntityUnifier.js +55 -32
  43. package/lib/cjs/EntityUnifier.js.map +1 -1
  44. package/lib/cjs/IModelCloneContext.d.ts +11 -5
  45. package/lib/cjs/IModelCloneContext.d.ts.map +1 -1
  46. package/lib/cjs/IModelCloneContext.js +94 -58
  47. package/lib/cjs/IModelCloneContext.js.map +1 -1
  48. package/lib/cjs/IModelExporter.d.ts +109 -35
  49. package/lib/cjs/IModelExporter.d.ts.map +1 -1
  50. package/lib/cjs/IModelExporter.js +698 -224
  51. package/lib/cjs/IModelExporter.js.map +1 -1
  52. package/lib/cjs/IModelImporter.d.ts +73 -32
  53. package/lib/cjs/IModelImporter.d.ts.map +1 -1
  54. package/lib/cjs/IModelImporter.js +278 -144
  55. package/lib/cjs/IModelImporter.js.map +1 -1
  56. package/lib/cjs/IModelTransformContext.d.ts +54 -0
  57. package/lib/cjs/IModelTransformContext.d.ts.map +1 -0
  58. package/lib/cjs/IModelTransformContext.js +3 -0
  59. package/lib/cjs/IModelTransformContext.js.map +1 -0
  60. package/lib/cjs/IModelTransformer.d.ts +126 -206
  61. package/lib/cjs/IModelTransformer.d.ts.map +1 -1
  62. package/lib/cjs/IModelTransformer.js +652 -1198
  63. package/lib/cjs/IModelTransformer.js.map +1 -1
  64. package/lib/cjs/IModelTransformerError.d.ts +84 -0
  65. package/lib/cjs/IModelTransformerError.d.ts.map +1 -0
  66. package/lib/cjs/IModelTransformerError.js +93 -0
  67. package/lib/cjs/IModelTransformerError.js.map +1 -0
  68. package/lib/cjs/ProvenanceManager.d.ts +182 -0
  69. package/lib/cjs/ProvenanceManager.d.ts.map +1 -0
  70. package/lib/cjs/ProvenanceManager.js +743 -0
  71. package/lib/cjs/ProvenanceManager.js.map +1 -0
  72. package/lib/cjs/SyncTypeResolver.d.ts +34 -0
  73. package/lib/cjs/SyncTypeResolver.d.ts.map +1 -0
  74. package/lib/cjs/SyncTypeResolver.js +98 -0
  75. package/lib/cjs/SyncTypeResolver.js.map +1 -0
  76. package/lib/cjs/TransformerLoggerCategory.d.ts +1 -1
  77. package/lib/cjs/TransformerLoggerCategory.js +1 -1
  78. package/lib/cjs/TransformerLoggerCategory.js.map +1 -1
  79. package/lib/cjs/TransformerPackageMetadata.d.ts +8 -0
  80. package/lib/cjs/TransformerPackageMetadata.d.ts.map +1 -0
  81. package/lib/cjs/TransformerPackageMetadata.js +47 -0
  82. package/lib/cjs/TransformerPackageMetadata.js.map +1 -0
  83. package/lib/cjs/imodel-transformer.d.ts +19 -0
  84. package/lib/cjs/imodel-transformer.d.ts.map +1 -1
  85. package/lib/cjs/imodel-transformer.js +53 -34
  86. package/lib/cjs/imodel-transformer.js.map +1 -1
  87. package/lib/cjs/schema-processing/DynamicSchemaUnionStrategy.d.ts +29 -0
  88. package/lib/cjs/schema-processing/DynamicSchemaUnionStrategy.d.ts.map +1 -0
  89. package/lib/cjs/schema-processing/DynamicSchemaUnionStrategy.js +212 -0
  90. package/lib/cjs/schema-processing/DynamicSchemaUnionStrategy.js.map +1 -0
  91. package/lib/cjs/schema-processing/SchemaProcessingCoordinator.d.ts +25 -0
  92. package/lib/cjs/schema-processing/SchemaProcessingCoordinator.d.ts.map +1 -0
  93. package/lib/cjs/schema-processing/SchemaProcessingCoordinator.js +196 -0
  94. package/lib/cjs/schema-processing/SchemaProcessingCoordinator.js.map +1 -0
  95. package/lib/cjs/schema-processing/SchemaProcessingStrategy.d.ts +59 -0
  96. package/lib/cjs/schema-processing/SchemaProcessingStrategy.d.ts.map +1 -0
  97. package/lib/cjs/schema-processing/SchemaProcessingStrategy.js +28 -0
  98. package/lib/cjs/schema-processing/SchemaProcessingStrategy.js.map +1 -0
  99. package/lib/cjs/schema-processing.d.ts +7 -0
  100. package/lib/cjs/schema-processing.d.ts.map +1 -0
  101. package/lib/cjs/schema-processing.js +27 -0
  102. package/lib/cjs/schema-processing.js.map +1 -0
  103. package/package.json +65 -43
  104. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts +0 -16
  105. package/lib/cjs/DetachedExportElementAspectsStrategy.d.ts.map +0 -1
  106. package/lib/cjs/DetachedExportElementAspectsStrategy.js +0 -106
  107. package/lib/cjs/DetachedExportElementAspectsStrategy.js.map +0 -1
  108. package/lib/cjs/ElementCascadingDeleter.d.ts +0 -22
  109. package/lib/cjs/ElementCascadingDeleter.d.ts.map +0 -1
  110. package/lib/cjs/ElementCascadingDeleter.js +0 -57
  111. package/lib/cjs/ElementCascadingDeleter.js.map +0 -1
  112. package/lib/cjs/ExportElementAspectsStrategy.d.ts +0 -35
  113. package/lib/cjs/ExportElementAspectsStrategy.d.ts.map +0 -1
  114. package/lib/cjs/ExportElementAspectsStrategy.js +0 -52
  115. package/lib/cjs/ExportElementAspectsStrategy.js.map +0 -1
  116. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts +0 -12
  117. package/lib/cjs/ExportElementAspectsWithElementsStrategy.d.ts.map +0 -1
  118. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js +0 -47
  119. package/lib/cjs/ExportElementAspectsWithElementsStrategy.js.map +0 -1
@@ -13,8 +13,11 @@ 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");
18
21
  const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelExporter;
19
22
  /** Handles the events generated by IModelExporter.
20
23
  * @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 +29,7 @@ class IModelExportHandler {
26
29
  /** If `true` is returned, then the CodeSpec will be exported.
27
30
  * @note This method can optionally be overridden to exclude an individual CodeSpec from the export. The base implementation always returns `true`.
28
31
  */
29
- shouldExportCodeSpec(_codeSpec) {
32
+ async shouldExportCodeSpec(_codeSpec) {
30
33
  return true;
31
34
  }
32
35
  /** Called when a CodeSpec should be exported.
@@ -34,37 +37,37 @@ class IModelExportHandler {
34
37
  * @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
38
  * @note This should be overridden to actually do the export.
36
39
  */
37
- onExportCodeSpec(_codeSpec, _isUpdate) { }
40
+ async onExportCodeSpec(_codeSpec, _isUpdate) { }
38
41
  /** Called when a font should be exported.
39
42
  * @param font The font to export
40
43
  * @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
44
  * @note This should be overridden to actually do the export.
42
45
  */
43
- onExportFont(_font, _isUpdate) { }
46
+ async onExportFont(_font, _isUpdate) { }
44
47
  /** Called when a model should be exported.
45
48
  * @param model The model to export
46
49
  * @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
50
  * @note This should be overridden to actually do the export.
48
51
  */
49
- onExportModel(_model, _isUpdate) { }
52
+ async onExportModel(_model, _isUpdate) { }
50
53
  /** Called when a model should be deleted. */
51
- onDeleteModel(_modelId) { }
54
+ async onDeleteModel(_modelId) { }
52
55
  /** If `true` is returned, then the element will be exported.
53
56
  * @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`.
54
57
  */
55
- shouldExportElement(_element) {
58
+ async shouldExportElement(_element) {
56
59
  return true;
57
60
  }
58
61
  /** Called when element is skipped instead of exported.
59
62
  * @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.
60
63
  */
61
- onSkipElement(_elementId) { }
64
+ async onSkipElement(_elementId) { }
62
65
  /** Called when an element should be exported.
63
66
  * @param element The element to export
64
67
  * @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.
65
68
  * @note This should be overridden to actually do the export.
66
69
  */
67
- onExportElement(_element, _isUpdate) { }
70
+ async onExportElement(_element, _isUpdate) { }
68
71
  /**
69
72
  * Do any asynchronous actions before exporting an element
70
73
  * @note Do not implement this handler manually, it is internal, it will be removed.
@@ -72,12 +75,12 @@ class IModelExportHandler {
72
75
  * @internal
73
76
  */
74
77
  async preExportElement(_element) { }
75
- /** Called when an element should be deleted. */
76
- onDeleteElement(_elementId) { }
78
+ /** Called once with the source IDs of all elements deleted by the exported changes. */
79
+ async onDeleteElements(_elementIds) { }
77
80
  /** If `true` is returned, then the ElementAspect will be exported.
78
81
  * @note This method can optionally be overridden to exclude an individual ElementAspect from the export. The base implementation always returns `true`.
79
82
  */
80
- shouldExportElementAspect(_aspect) {
83
+ async shouldExportElementAspect(_aspect) {
81
84
  return true;
82
85
  }
83
86
  /** Called when an ElementUniqueAspect should be exported.
@@ -85,29 +88,31 @@ class IModelExportHandler {
85
88
  * @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.
86
89
  * @note This should be overridden to actually do the export.
87
90
  */
88
- onExportElementUniqueAspect(_aspect, _isUpdate) { }
91
+ async onExportElementUniqueAspect(_aspect, _isUpdate) { }
89
92
  /** Called when ElementMultiAspects should be exported.
90
93
  * @note This should be overridden to actually do the export.
91
94
  */
92
- onExportElementMultiAspects(_aspects) { }
95
+ async onExportElementMultiAspects(_aspects) { }
93
96
  /** If `true` is returned, then the relationship will be exported.
94
97
  * @note This method can optionally be overridden to exclude an individual CodeSpec from the export. The base implementation always returns `true`.
95
98
  */
96
- shouldExportRelationship(_relationship) {
99
+ async shouldExportRelationship(_relationship) {
97
100
  return true;
98
101
  }
99
102
  /** Called when a Relationship should be exported.
100
103
  * @param relationship The Relationship to export
101
104
  * @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.
105
+ * @param sourceFedGuid The FederationGuid of the relationship's source endpoint, if available.
106
+ * @param targetFedGuid The FederationGuid of the relationship's target endpoint, if available.
102
107
  * @note This should be overridden to actually do the export.
103
108
  */
104
- onExportRelationship(_relationship, _isUpdate) { }
109
+ async onExportRelationship(_relationship, _isUpdate, _sourceFedGuid, _targetFedGuid) { }
105
110
  /** Called when a relationship should be deleted. */
106
- onDeleteRelationship(_relInstanceId) { }
111
+ async onDeleteRelationship(_relInstanceId) { }
107
112
  /** If `true` is returned, then the schema will be exported.
108
113
  * @note This method can optionally be overridden to exclude an individual schema from the export. The base implementation always returns `true`.
109
114
  */
110
- shouldExportSchema(_schemaKey) {
115
+ async shouldExportSchema(_schemaKey) {
111
116
  return true;
112
117
  }
113
118
  /** Called when a schema should be exported.
@@ -126,10 +131,14 @@ class IModelExportHandler {
126
131
  exports.IModelExportHandler = IModelExportHandler;
127
132
  /** Base class for exporting data from an iModel.
128
133
  * @note Most uses cases will not require a custom subclass of `IModelExporter`. Instead, it is more typical to subclass/customize [IModelExportHandler]($transformer).
134
+ * ElementAspects are exported separately from element callbacks in bounded,
135
+ * owner-scoped groups. Aspect callbacks are not guaranteed to be adjacent to
136
+ * callbacks for their owners.
129
137
  * @see [iModel Transformation and Data Exchange]($docs/learning/transformer/index.md), [[registerHandler]], [IModelTransformer]($transformer), [IModelImporter]($transformer)
130
138
  * @beta
131
139
  */
132
140
  class IModelExporter {
141
+ static _elementAspectOwnerBatchSize = 1000;
133
142
  /** The read-only source iModel. */
134
143
  sourceDb;
135
144
  /** A flag that indicates whether element GeometryStreams are loaded or not.
@@ -175,7 +184,13 @@ class IModelExporter {
175
184
  /** The handler called by this IModelExporter. */
176
185
  get handler() {
177
186
  if (undefined === this._handler) {
178
- throw new Error("IModelExportHandler not registered");
187
+ core_bentley_1.ITwinError.throwError({
188
+ iTwinErrorId: {
189
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
190
+ key: IModelTransformerError_1.IModelTransformerError.ExportHandlerNotRegistered,
191
+ },
192
+ message: "IModelExportHandler not registered",
193
+ });
179
194
  }
180
195
  return this._handler;
181
196
  }
@@ -189,20 +204,37 @@ class IModelExporter {
189
204
  _excludedElementClasses = new Set();
190
205
  /** The set of classes of Relationships that will be excluded (polymorphically) from transformation to the target iModel. */
191
206
  _excludedRelationshipClasses = new Set();
192
- /** Strategy for how ElementAspects are exported */
193
- _exportElementAspectsStrategy;
207
+ /** Exports ElementAspects for accepted owner groups. */
208
+ _elementAspectExportProcessor;
209
+ /** Coordinates accepted-owner scopes and bounded group processing. */
210
+ _elementAspectExportCoordinator;
211
+ /** Coordinates accepted ElementAspect owners and bounded group processing.
212
+ * @internal
213
+ */
214
+ get elementAspectExportCoordinator() {
215
+ return this._elementAspectExportCoordinator;
216
+ }
217
+ /** Whether change traversal skips root-owned entities. */
218
+ _skipPropagateChangesToRootElements = false;
219
+ _rootElementIds = new Set([
220
+ core_common_1.IModel.rootSubjectId,
221
+ core_common_1.IModel.repositoryModelId,
222
+ core_common_1.IModel.dictionaryId,
223
+ "0xe", // The reality-data-sources modelId, skipped with root propagation.
224
+ ]);
194
225
  /** Construct a new IModelExporter
195
226
  * @param sourceDb The source IModelDb
196
227
  * @see registerHandler
197
228
  */
198
- constructor(sourceDb, elementAspectsStrategy = ExportElementAspectsWithElementsStrategy_1.ExportElementAspectsWithElementsStrategy) {
229
+ constructor(sourceDb) {
199
230
  this.sourceDb = sourceDb;
200
- this._exportElementAspectsStrategy = new elementAspectsStrategy(this.sourceDb, {
201
- onExportElementMultiAspects: (aspects) => this.handler.onExportElementMultiAspects(aspects),
202
- onExportElementUniqueAspect: (aspect, isUpdate) => this.handler.onExportElementUniqueAspect(aspect, isUpdate),
203
- shouldExportElementAspect: (aspect) => this.handler.shouldExportElementAspect(aspect),
231
+ this._elementAspectExportProcessor = new ElementAspectExportProcessor_1.ElementAspectExportProcessor(this.sourceDb, {
232
+ onExportElementMultiAspects: async (aspects) => this.handler.onExportElementMultiAspects(aspects),
233
+ onExportElementUniqueAspect: async (aspect, isUpdate) => this.handler.onExportElementUniqueAspect(aspect, isUpdate),
234
+ shouldExportElementAspect: async (aspect) => this.handler.shouldExportElementAspect(aspect),
204
235
  trackProgress: async () => this.trackProgress(),
205
236
  });
237
+ this._elementAspectExportCoordinator = new ElementAspectExportCoordinator_1.ElementAspectExportCoordinator(IModelExporter._elementAspectOwnerBatchSize, () => this._elementAspectExportProcessor.excludedElementAspectClassFullNames, async (elementIds) => this._elementAspectExportProcessor.exportAllElementAspects(elementIds), () => this._elementAspectExportProcessor.resetCaches());
206
238
  }
207
239
  /**
208
240
  * Initialize prerequisites of exporting. This is implicitly done by any `export*` calls that need initialization
@@ -220,7 +252,7 @@ class IModelExporter {
220
252
  });
221
253
  if (this._sourceDbChanges === undefined)
222
254
  return;
223
- this._exportElementAspectsStrategy.setAspectChanges(this._sourceDbChanges.aspect);
255
+ this._elementAspectExportProcessor.setAspectChanges(this._sourceDbChanges.aspect);
224
256
  }
225
257
  /** Register the handler that will be called by IModelExporter. */
226
258
  registerHandler(handler) {
@@ -244,7 +276,7 @@ class IModelExporter {
244
276
  }
245
277
  /** Add a rule to exclude all ElementAspects of a specified class. */
246
278
  excludeElementAspectClass(classFullName) {
247
- this._exportElementAspectsStrategy.excludeElementAspectClass(classFullName);
279
+ this._elementAspectExportProcessor.excludeElementAspectClass(classFullName);
248
280
  }
249
281
  /** Add a rule to exclude all Relationships of a specified class. */
250
282
  excludeRelationshipClass(classFullName) {
@@ -257,11 +289,12 @@ class IModelExporter {
257
289
  await this.initialize({});
258
290
  await this.exportCodeSpecs();
259
291
  await this.exportFonts();
260
- await this.exportModel(core_common_1.IModel.repositoryModelId);
292
+ await this._elementAspectExportCoordinator.run(async () => this.exportModel(core_common_1.IModel.repositoryModelId));
261
293
  await this.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
262
294
  }
263
295
  /** Export changes from the source iModel.
264
296
  * Inserts, updates, and deletes are determined by inspecting the changeset(s).
297
+ * @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
298
  * @note To form a range of versions to process, set `startChangesetId` for the start (inclusive) of the desired
266
299
  * range and open the source iModel as of the end (inclusive) of the desired range.
267
300
  * @note the changedInstanceIds are just for this call to exportChanges, so you must continue to pass it in
@@ -269,72 +302,94 @@ class IModelExporter {
269
302
  */
270
303
  async exportChanges(args) {
271
304
  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;
305
+ core_bentley_1.ITwinError.throwError({
306
+ iTwinErrorId: {
307
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
308
+ key: IModelTransformerError_1.IModelTransformerError.ExportChangesRequiresBriefcase,
309
+ },
310
+ message: "Must be a briefcase to export changes",
311
+ });
312
+ let initOpts = args ?? {};
313
+ const hasExplicitChangeSource = "csFileProps" in initOpts ||
314
+ "changedInstanceIds" in initOpts ||
315
+ "changesetRanges" in initOpts ||
316
+ "startChangeset" in initOpts;
317
+ const currentChangesetId = this.sourceDb.changeset.id;
318
+ if (!hasExplicitChangeSource && currentChangesetId !== "") {
319
+ initOpts = {
320
+ ...initOpts,
321
+ startChangeset: { id: currentChangesetId },
322
+ };
277
323
  }
278
- const startChangeset = args && "startChangeset" in args ? args.startChangeset : undefined;
279
- const initOpts = {
280
- startChangeset: { id: startChangeset?.id },
281
- };
282
324
  await this.initialize(initOpts);
325
+ if (currentChangesetId === "" && !this.sourceDbChanges?.hasChanges) {
326
+ core_bentley_1.ITwinError.throwError({
327
+ iTwinErrorId: {
328
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
329
+ key: IModelTransformerError_1.IModelTransformerError.NoChangesets,
330
+ },
331
+ message: "Cannot export changes because the source iModel has no changesets or custom changes. Call exportAll() to export all content.",
332
+ });
333
+ }
334
+ this._skipPropagateChangesToRootElements =
335
+ initOpts.skipPropagateChangesToRootElements ?? false;
283
336
  // _sourceDbChanges are initialized in this.initialize
284
- nodeAssert(this._sourceDbChanges !== undefined, "sourceDbChanges must be initialized.");
337
+ (0, node_assert_1.strict)(this._sourceDbChanges !== undefined, "sourceDbChanges must be initialized.");
285
338
  await this.exportCodeSpecs();
286
339
  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);
340
+ await this._elementAspectExportCoordinator.run(async () => {
341
+ if (initOpts.skipPropagateChangesToRootElements) {
342
+ // The root Subject is in the RepositoryModel. Traverse its children
343
+ // separately, then export other top-level repository elements while
344
+ // excluding the root so no element is visited twice.
345
+ await this.exportChildElements(core_common_1.IModel.rootSubjectId);
346
+ await this.exportModelContents(core_common_1.IModel.repositoryModelId, core_backend_1.Element.classFullName, true);
347
+ await this.exportSubModels(core_common_1.IModel.repositoryModelId);
348
+ }
349
+ else {
350
+ await this.exportModel(core_common_1.IModel.repositoryModelId);
351
+ }
352
+ });
353
+ const aspectOnlyOwnerElementIds = new Set(this._sourceDbChanges.aspectOwnerElementIds);
354
+ for (const elementId of this._sourceDbChanges.element.insertIds) {
355
+ aspectOnlyOwnerElementIds.delete(elementId);
290
356
  }
291
- else {
292
- await this.exportModel(core_common_1.IModel.repositoryModelId);
357
+ for (const elementId of this._sourceDbChanges.element.updateIds) {
358
+ aspectOnlyOwnerElementIds.delete(elementId);
293
359
  }
294
- await this.exportAllAspects();
360
+ await this.exportAspectsForOwners(await this.filterOwnerElementIdsForAspectExport(aspectOnlyOwnerElementIds));
295
361
  await this.exportRelationships(core_backend_1.ElementRefersToElements.classFullName);
296
362
  // handle deletes
297
363
  if (this.visitElements) {
298
364
  // must delete models first since they have a constraint on the submodeling element which may also be deleted
299
365
  for (const modelId of this._sourceDbChanges.model.deleteIds) {
300
- this.handler.onDeleteModel(modelId);
366
+ await this.handler.onDeleteModel(modelId);
301
367
  }
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
- }
368
+ if (this._sourceDbChanges.element.deleteIds.size > 0) {
369
+ await this.handler.onDeleteElements(new Set(this._sourceDbChanges.element.deleteIds));
318
370
  }
319
371
  }
320
372
  if (this.visitRelationships) {
321
373
  for (const relInstanceId of this._sourceDbChanges.relationship
322
374
  .deleteIds) {
323
- this.handler.onDeleteRelationship(relInstanceId);
375
+ await this.handler.onDeleteRelationship(relInstanceId);
324
376
  }
325
377
  }
326
378
  // Enable consecutive exportChanges runs without the need to re-instantiate the exporter.
327
379
  // You can counteract the obvious impact of losing this expensive data by always calling
328
380
  // exportChanges with the [[ExportChangesOptions.changedInstanceIds]] option set to
329
381
  // whatever you want
330
- if (this._resetChangeDataOnExport)
382
+ if (this._resetChangeDataOnExport) {
331
383
  this._sourceDbChanges = undefined;
384
+ this._elementAspectExportProcessor.setAspectChanges(undefined);
385
+ }
332
386
  }
333
387
  _resetChangeDataOnExport = true;
334
- /** Export schemas from the source iModel.
335
- * @note This must be called separately from [[exportAll]] or [[exportChanges]].
388
+ /** Enumerate schemas from the source iModel in database order.
389
+ * @note Override this method to customize schema discovery for both direct exports and transformer schema processing.
390
+ * @beta
336
391
  */
337
- async exportSchemas() {
392
+ async *enumerateSchemas() {
338
393
  const sql = `
339
394
  SELECT s.Name, s.VersionMajor, s.VersionWrite, s.VersionMinor
340
395
  FROM ECDbMeta.ECSchemaDef s
@@ -345,26 +400,35 @@ class IModelExporter {
345
400
  `}
346
401
  ORDER BY ECInstanceId
347
402
  `;
348
- const schemaNamesToExport = [];
349
- // eslint-disable-next-line @itwin/no-internal, @typescript-eslint/no-deprecated
350
- this.sourceDb.withPreparedStatement(sql, (statement) => {
351
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
352
- const schemaName = statement.getValue(0).getString();
353
- const versionMajor = statement.getValue(1).getInteger();
354
- const versionWrite = statement.getValue(2).getInteger();
355
- const versionMinor = statement.getValue(3).getInteger();
356
- const schemaKey = new ecschema_metadata_1.SchemaKey(schemaName, new ecschema_metadata_1.ECVersion(versionMajor, versionWrite, versionMinor));
357
- if (this.handler.shouldExportSchema(schemaKey)) {
358
- schemaNamesToExport.push(schemaName);
359
- }
403
+ for await (const row of this.sourceDb.createQueryReader(sql, undefined, {
404
+ usePrimaryConn: true,
405
+ })) {
406
+ const schemaKey = new ecschema_metadata_1.SchemaKey(row[0], new ecschema_metadata_1.ECVersion(row[1], row[2], row[3]));
407
+ const schema = await this.sourceDb.schemaContext.getSchema(schemaKey);
408
+ if (!schema) {
409
+ core_bentley_1.ITwinError.throwError({
410
+ iTwinErrorId: {
411
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
412
+ key: IModelTransformerError_1.IModelTransformerError.SchemaLoadFailed,
413
+ },
414
+ message: `Failed to load schema: ${schemaKey.name}`,
415
+ });
360
416
  }
361
- });
362
- if (schemaNamesToExport.length === 0)
363
- return;
364
- const schemaLoader = new ecschema_metadata_1.SchemaLoader((name) => this.sourceDb.getSchemaProps(name));
365
- await Promise.all(schemaNamesToExport.map(async (schemaName) => {
366
- const schema = schemaLoader.getSchema(schemaName);
367
- core_bentley_1.Logger.logTrace(loggerCategory, `exportSchema(${schemaName})`);
417
+ yield schema;
418
+ }
419
+ }
420
+ /** Export schemas from the source iModel.
421
+ * @note This must be called separately from [[exportAll]] or [[exportChanges]].
422
+ */
423
+ async exportSchemas() {
424
+ const schemasToExport = [];
425
+ for await (const schema of this.enumerateSchemas()) {
426
+ if (await this.handler.shouldExportSchema(schema.schemaKey)) {
427
+ schemasToExport.push(schema);
428
+ }
429
+ }
430
+ await Promise.all(schemasToExport.map(async (schema) => {
431
+ core_bentley_1.Logger.logTrace(loggerCategory, `exportSchema(${schema.name})`);
368
432
  return this.handler.onExportSchema(schema);
369
433
  }));
370
434
  }
@@ -378,15 +442,11 @@ class IModelExporter {
378
442
  async exportCodeSpecs() {
379
443
  core_bentley_1.Logger.logTrace(loggerCategory, "exportCodeSpecs()");
380
444
  const sql = "SELECT Name FROM BisCore:CodeSpec ORDER BY ECInstanceId";
381
- // eslint-disable-next-line @itwin/no-internal, @typescript-eslint/no-deprecated
382
- await this.sourceDb.withPreparedStatement(sql,
383
- // eslint-disable-next-line @itwin/no-internal, @typescript-eslint/no-deprecated
384
- async (statement) => {
385
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
386
- const codeSpecName = statement.getValue(0).getString();
387
- await this.exportCodeSpecByName(codeSpecName);
388
- }
389
- });
445
+ for await (const row of this.sourceDb.createQueryReader(sql, undefined, {
446
+ usePrimaryConn: true,
447
+ })) {
448
+ await this.exportCodeSpecByName(row[0]);
449
+ }
390
450
  }
391
451
  /** Export a single CodeSpec from the source iModel.
392
452
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
@@ -412,9 +472,9 @@ class IModelExporter {
412
472
  return;
413
473
  }
414
474
  // CodeSpec has passed standard exclusion rules, now give handler a chance to accept/reject export
415
- if (this.handler.shouldExportCodeSpec(codeSpec)) {
475
+ if (await this.handler.shouldExportCodeSpec(codeSpec)) {
416
476
  core_bentley_1.Logger.logTrace(loggerCategory, `exportCodeSpec(${codeSpecName})${this.getChangeOpSuffix(isUpdate)}`);
417
- this.handler.onExportCodeSpec(codeSpec, isUpdate);
477
+ await this.handler.onExportCodeSpec(codeSpec, isUpdate);
418
478
  return this.trackProgress();
419
479
  }
420
480
  }
@@ -430,41 +490,63 @@ class IModelExporter {
430
490
  */
431
491
  async exportFonts() {
432
492
  core_bentley_1.Logger.logTrace(loggerCategory, "exportFonts()");
433
- for (const font of this.sourceDb.fontMap.fonts.values()) {
434
- await this.exportFontByNumber(font.id);
493
+ for (const font of this.sourceDb.fonts.queryMappedFamilies()) {
494
+ await this.exportFontByFontProps(font);
435
495
  }
436
496
  }
437
497
  /** Export a single font from the source iModel.
438
- * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
498
+ * @note multiple fonts can have the same font name, if a font with a specific type is needed use exportFontByFontFamilyDescriptor.
499
+ * If not this will only export the first font with this type in the db.
439
500
  */
440
501
  async exportFontByName(fontName) {
441
502
  core_bentley_1.Logger.logTrace(loggerCategory, `exportFontByName(${fontName})`);
442
- const font = this.sourceDb.fontMap.getFont(fontName);
443
- if (undefined !== font) {
444
- await this.exportFontByNumber(font.id);
503
+ const fontId = this.sourceDb.fonts.findId({
504
+ name: fontName,
505
+ });
506
+ if (undefined !== fontId) {
507
+ await this.exportFontByNumber(fontId);
508
+ }
509
+ }
510
+ /** Export a single font from the source iModel.
511
+ * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
512
+ */
513
+ async exportFontByFontProps(fontProps) {
514
+ core_bentley_1.Logger.logTrace(loggerCategory, `exportFontByFamily(${fontProps.name}, ${fontProps.type})`);
515
+ await this.handler.onExportFont(fontProps, true);
516
+ return this.trackProgress();
517
+ }
518
+ /** Export a single font from the source iModel.
519
+ * @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
+ */
521
+ async exportFontByFontFamilyDescriptor(fontFamily) {
522
+ core_bentley_1.Logger.logTrace(loggerCategory, `exportFontByFamilyDescriptor(${fontFamily.name}, ${fontFamily.type})`);
523
+ const fontId = await this.sourceDb.fonts.acquireId(fontFamily);
524
+ if (undefined !== fontId) {
525
+ await this.exportFontByFontProps({ ...fontFamily, id: fontId });
445
526
  }
446
527
  }
447
528
  /** Export a single font from the source iModel.
448
529
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
449
530
  */
450
531
  async exportFontByNumber(fontNumber) {
451
- /** sourceDbChanges now works by using TS ChangesetECAdaptor which doesn't pick up changes to fonts since fonts is not an ec table.
452
- * So lets always export fonts for the time being by always setting isUpdate = true.
532
+ /** ChangedInstanceIds contains EC-mapped changes, but the font table is not EC-mapped.
533
+ * Always export fonts because their changes are therefore absent from sourceDbChanges.
453
534
  * 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.
454
535
  * See https://github.com/iTwin/imodel-transformer/pull/135 for removed code.
455
536
  */
456
- const isUpdate = true;
457
537
  core_bentley_1.Logger.logTrace(loggerCategory, `exportFontById(${fontNumber})`);
458
- const font = this.sourceDb.fontMap.getFont(fontNumber);
538
+ const font = this.sourceDb.fonts.findDescriptor(fontNumber);
459
539
  if (undefined !== font) {
460
- this.handler.onExportFont(font, isUpdate);
461
- return this.trackProgress();
540
+ await this.exportFontByFontFamilyDescriptor(font);
462
541
  }
463
542
  }
464
543
  /** Export the model container, contents, and sub-models from the source iModel.
465
544
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
466
545
  */
467
546
  async exportModel(modeledElementId) {
547
+ return this.runScopedElementExport(async () => this.exportModelImpl(modeledElementId));
548
+ }
549
+ async exportModelImpl(modeledElementId) {
468
550
  const model = this.sourceDb.models.getModel(modeledElementId);
469
551
  if (model.isTemplate && !this.wantTemplateModels) {
470
552
  return;
@@ -475,7 +557,7 @@ class IModelExporter {
475
557
  wantBRepData: this.wantGeometry,
476
558
  });
477
559
  core_bentley_1.Logger.logTrace(loggerCategory, `exportModel(${modeledElementId})`);
478
- if (this.shouldExportElement(modeledElement)) {
560
+ if (await this.shouldExportElement(modeledElement)) {
479
561
  await this.exportModelContainer(model);
480
562
  if (this.visitElements) {
481
563
  await this.exportModelContents(modeledElementId);
@@ -498,7 +580,7 @@ class IModelExporter {
498
580
  return; // not in changeset, don't export
499
581
  }
500
582
  }
501
- this.handler.onExportModel(model, isUpdate);
583
+ await this.handler.onExportModel(model, isUpdate);
502
584
  return this.trackProgress();
503
585
  }
504
586
  _yieldManager = new core_bentley_1.YieldManager();
@@ -509,6 +591,9 @@ class IModelExporter {
509
591
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
510
592
  */
511
593
  async exportModelContents(modelId, elementClassFullName = core_backend_1.Element.classFullName, skipRootSubject) {
594
+ return this.runScopedElementExport(async () => this.exportModelContentsImpl(modelId, elementClassFullName, skipRootSubject));
595
+ }
596
+ async exportModelContentsImpl(modelId, elementClassFullName, skipRootSubject) {
512
597
  if (skipRootSubject) {
513
598
  // 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
514
599
  // NOTE: IModelExporter.exportAll should not skip the root Subject since the goal is to export everything
@@ -533,44 +618,41 @@ class IModelExporter {
533
618
  else {
534
619
  sql = `SELECT ECInstanceId FROM ${elementClassFullName} WHERE Parent.Id IS NULL AND Model.Id=:modelId ORDER BY ECInstanceId`;
535
620
  }
536
- // eslint-disable-next-line @itwin/no-internal, @typescript-eslint/no-deprecated
537
- await this.sourceDb.withPreparedStatement(sql,
538
- // eslint-disable-next-line @itwin/no-internal, @typescript-eslint/no-deprecated
539
- async (statement) => {
540
- statement.bindId("modelId", modelId);
541
- if (skipRootSubject) {
542
- statement.bindId("rootSubjectId", core_common_1.IModel.rootSubjectId);
543
- }
544
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
545
- await this.exportElement(statement.getValue(0).getId());
546
- await this._yieldManager.allowYield();
547
- }
548
- });
621
+ const params = new core_common_1.QueryBinder().bindId("modelId", modelId);
622
+ if (skipRootSubject) {
623
+ params.bindId("rootSubjectId", core_common_1.IModel.rootSubjectId);
624
+ }
625
+ for await (const row of this.sourceDb.createQueryReader(sql, params, {
626
+ usePrimaryConn: true,
627
+ })) {
628
+ await this.exportElement(row.id);
629
+ await this._yieldManager.allowYield();
630
+ }
549
631
  }
550
632
  /** Export the sub-models directly below the specified model.
551
633
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
552
634
  */
553
635
  async exportSubModels(parentModelId) {
636
+ return this.runScopedElementExport(async () => this.exportSubModelsImpl(parentModelId));
637
+ }
638
+ async exportSubModelsImpl(parentModelId) {
554
639
  core_bentley_1.Logger.logTrace(loggerCategory, `exportSubModels(${parentModelId})`);
555
640
  const definitionModelIds = [];
556
641
  const otherModelIds = [];
557
642
  const sql = `SELECT ECInstanceId FROM ${core_backend_1.Model.classFullName} WHERE ParentModel.Id=:parentModelId ORDER BY ECInstanceId`;
558
- // eslint-disable-next-line @itwin/no-internal, @typescript-eslint/no-deprecated
559
- this.sourceDb.withPreparedStatement(sql,
560
- // eslint-disable-next-line @itwin/no-internal, @typescript-eslint/no-deprecated
561
- (statement) => {
562
- statement.bindId("parentModelId", parentModelId);
563
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
564
- const modelId = statement.getValue(0).getId();
565
- const model = this.sourceDb.models.getModel(modelId);
566
- if (model instanceof core_backend_1.DefinitionModel) {
567
- definitionModelIds.push(modelId);
568
- }
569
- else {
570
- otherModelIds.push(modelId);
571
- }
643
+ const params = new core_common_1.QueryBinder().bindId("parentModelId", parentModelId);
644
+ for await (const row of this.sourceDb.createQueryReader(sql, params, {
645
+ usePrimaryConn: true,
646
+ })) {
647
+ const modelId = row.id;
648
+ const model = this.sourceDb.models.getModel(modelId);
649
+ if (model instanceof core_backend_1.DefinitionModel) {
650
+ definitionModelIds.push(modelId);
572
651
  }
573
- });
652
+ else {
653
+ otherModelIds.push(modelId);
654
+ }
655
+ }
574
656
  // export DefinitionModels before other types of Models
575
657
  for (const definitionModelId of definitionModelIds) {
576
658
  await this.exportModel(definitionModelId);
@@ -583,16 +665,15 @@ class IModelExporter {
583
665
  * This considers the standard IModelExporter exclusion rules plus calls [IModelExportHandler.shouldExportElement]($transformer) for any custom exclusion rules.
584
666
  * @note This method is called from within [[exportChanges]] and [[exportAll]], so usually does not need to be called directly.
585
667
  */
586
- shouldExportElement(element) {
668
+ async shouldExportElement(element) {
587
669
  if (this._excludedElementIds.has(element.id)) {
588
670
  core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${element.id} by Id`);
589
671
  return false;
590
672
  }
591
- if (element instanceof core_backend_1.GeometricElement) {
592
- if (this._excludedElementCategoryIds.has(element.category)) {
593
- core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${element.id} by Category`);
594
- return false;
595
- }
673
+ if (element instanceof core_backend_1.GeometricElement &&
674
+ this._excludedElementCategoryIds.has(element.category)) {
675
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${element.id} by Category`);
676
+ return false;
596
677
  }
597
678
  if (!this.wantTemplateModels &&
598
679
  element instanceof core_backend_1.RecipeDefinitionElement) {
@@ -606,12 +687,19 @@ class IModelExporter {
606
687
  }
607
688
  }
608
689
  // element has passed standard exclusion rules, now give handler a chance to accept/reject
609
- return this.handler.shouldExportElement(element);
690
+ if (!(await this.handler.shouldExportElement(element))) {
691
+ return false;
692
+ }
693
+ return true;
610
694
  }
611
- /** Export the specified element, its child elements (if applicable), and any owned ElementAspects.
695
+ /** Export the specified element and its child elements (if applicable).
612
696
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
697
+ * @note ElementAspects are exported separately in owner-scoped groups, so their callbacks are not guaranteed to follow this element callback.
613
698
  */
614
699
  async exportElement(elementId) {
700
+ return this.runScopedElementExport(async () => this.exportElementImpl(elementId));
701
+ }
702
+ async exportElementImpl(elementId) {
615
703
  if (!this.visitElements) {
616
704
  core_bentley_1.Logger.logTrace(loggerCategory, `visitElements=false, skipping exportElement(${elementId})`);
617
705
  return;
@@ -619,7 +707,7 @@ class IModelExporter {
619
707
  // Return early if the elementId is already in the excludedElementIds, that way we don't need to load the element from the db.
620
708
  if (this._excludedElementIds.has(elementId)) {
621
709
  core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementId} by Id`);
622
- this.handler.onSkipElement(elementId);
710
+ await this.handler.onSkipElement(elementId);
623
711
  return;
624
712
  }
625
713
  // are we processing changes?
@@ -628,6 +716,10 @@ class IModelExporter {
628
716
  : this._sourceDbChanges?.element.updateIds.has(elementId)
629
717
  ? true
630
718
  : undefined;
719
+ // Short-circuit: element is not in the changeset, skip its own export but still visit children
720
+ if (undefined !== this._sourceDbChanges && undefined === isUpdate) {
721
+ return this.exportChildElements(elementId);
722
+ }
631
723
  const element = this.sourceDb.elements.getElement({
632
724
  id: elementId,
633
725
  wantGeometry: this.wantGeometry,
@@ -635,21 +727,24 @@ class IModelExporter {
635
727
  });
636
728
  core_bentley_1.Logger.logTrace(loggerCategory, `exportElement(${element.id}, "${element.getDisplayLabel()}")${this.getChangeOpSuffix(isUpdate)}`);
637
729
  // the order and `await`ing of calls beyond here is depended upon by the IModelTransformer for a current bug workaround
638
- if (this.shouldExportElement(element)) {
730
+ if (await this.shouldExportElement(element)) {
639
731
  await this.handler.preExportElement(element);
640
- this.handler.onExportElement(element, isUpdate);
732
+ await this.handler.onExportElement(element, isUpdate);
641
733
  await this.trackProgress();
642
- await this._exportElementAspectsStrategy.exportElementAspectsForElement(elementId);
734
+ await this._elementAspectExportCoordinator.addAcceptedOwner(elementId);
643
735
  return this.exportChildElements(elementId);
644
736
  }
645
737
  else {
646
- this.handler.onSkipElement(element.id);
738
+ await this.handler.onSkipElement(element.id);
647
739
  }
648
740
  }
649
741
  /** Export the child elements of the specified element from the source iModel.
650
742
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
651
743
  */
652
744
  async exportChildElements(elementId) {
745
+ return this.runScopedElementExport(async () => this.exportChildElementsImpl(elementId));
746
+ }
747
+ async exportChildElementsImpl(elementId) {
653
748
  if (!this.visitElements) {
654
749
  core_bentley_1.Logger.logTrace(loggerCategory, `visitElements=false, skipping exportChildElements(${elementId})`);
655
750
  return;
@@ -662,13 +757,213 @@ class IModelExporter {
662
757
  }
663
758
  }
664
759
  }
665
- /** Exports all aspects present in the iModel.
666
- */
667
- async exportAllAspects() {
668
- return this._exportElementAspectsStrategy.exportAllElementAspects();
760
+ /** Exports all aspects owned by the supplied elements. */
761
+ async exportAspectsForOwners(ownerElementIds) {
762
+ return this._elementAspectExportCoordinator.exportOwners(ownerElementIds);
763
+ }
764
+ async filterOwnerElementIdsForAspectExport(ownerElementIds) {
765
+ if (ownerElementIds.size === 0)
766
+ return new Set();
767
+ const elementFacts = await this.queryAspectElementHierarchyFacts(ownerElementIds);
768
+ const modelFacts = await this.queryAspectModelHierarchyFacts(new Set([...elementFacts.values()].map((elementFact) => elementFact.modelId)));
769
+ await this.queryAspectElementFacts(new Set(modelFacts.keys()), elementFacts);
770
+ const elementIds = new Set();
771
+ for (const elementId of ownerElementIds) {
772
+ if (this._skipPropagateChangesToRootElements &&
773
+ this._rootElementIds.has(elementId)) {
774
+ continue;
775
+ }
776
+ if (await this.shouldExportElementHierarchyForAspect(elementId, elementFacts, modelFacts)) {
777
+ elementIds.add(elementId);
778
+ }
779
+ }
780
+ return elementIds;
781
+ }
782
+ async shouldExportElementHierarchyForAspect(elementId, elementFacts, modelFacts) {
783
+ let elementFact = elementFacts.get(elementId);
784
+ while (elementFact !== undefined) {
785
+ if (!(await this.shouldExportElementForAspect(elementFact.id, elementFact)))
786
+ return false;
787
+ if (!(await this.shouldExportModelForAspect(elementFact.modelId, elementFacts, modelFacts))) {
788
+ return false;
789
+ }
790
+ const parentId = elementFact.parentId;
791
+ if (parentId === undefined ||
792
+ (this._skipPropagateChangesToRootElements &&
793
+ parentId === core_common_1.IModel.rootSubjectId)) {
794
+ return true;
795
+ }
796
+ elementFact = elementFacts.get(parentId);
797
+ }
798
+ return false;
799
+ }
800
+ async shouldExportModelForAspect(modelId, elementFacts, modelFacts) {
801
+ const modelFact = modelFacts.get(modelId);
802
+ if (modelFact === undefined) {
803
+ this.sourceDb.models.getModel(modelId);
804
+ return false;
805
+ }
806
+ if (modelFact.isTemplate && !this.wantTemplateModels)
807
+ return false;
808
+ if (modelFact.id !== core_common_1.IModel.repositoryModelId &&
809
+ modelFact.id !== core_common_1.IModel.dictionaryId &&
810
+ modelFact.id !== "0xe" &&
811
+ !(await this.shouldExportElementForAspect(modelFact.id, elementFacts.get(modelFact.id)))) {
812
+ return false;
813
+ }
814
+ if (modelFact.parentModelId === undefined ||
815
+ modelFact.parentModelId === modelFact.id) {
816
+ return true;
817
+ }
818
+ return this.shouldExportModelForAspect(modelFact.parentModelId, elementFacts, modelFacts);
819
+ }
820
+ async runScopedElementExport(exportElements) {
821
+ await this._elementAspectExportCoordinator.run(exportElements);
822
+ }
823
+ /** Apply the element export filter when deciding whether to process an aspect owner. @internal */
824
+ async shouldExportElementForAspect(elementId, elementFact) {
825
+ if (!this.visitElements)
826
+ return false;
827
+ if (this.shouldExportElement ===
828
+ IModelExporter.prototype.shouldExportElement &&
829
+ elementFact !== undefined &&
830
+ !this.shouldExportElementFact(elementFact)) {
831
+ return false;
832
+ }
833
+ const element = this.sourceDb.elements.getElement({
834
+ id: elementId,
835
+ wantGeometry: false,
836
+ wantBRepData: false,
837
+ });
838
+ return this.shouldExportElement(element);
839
+ }
840
+ shouldExportElementFact(elementFact) {
841
+ if (this._excludedElementIds.has(elementFact.id)) {
842
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementFact.id} by Id`);
843
+ return false;
844
+ }
845
+ const elementClass = this.sourceDb.getJsClass(elementFact.classFullName);
846
+ if ((elementFact.classFullName === core_backend_1.GeometricElement.classFullName ||
847
+ elementClass.prototype instanceof core_backend_1.GeometricElement) &&
848
+ elementFact.categoryId !== undefined &&
849
+ this._excludedElementCategoryIds.has(elementFact.categoryId)) {
850
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementFact.id} by Category`);
851
+ return false;
852
+ }
853
+ if (!this.wantTemplateModels &&
854
+ (elementFact.classFullName === core_backend_1.RecipeDefinitionElement.classFullName ||
855
+ elementClass.prototype instanceof core_backend_1.RecipeDefinitionElement)) {
856
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded RecipeDefinitionElement ${elementFact.id} because wantTemplate=false`);
857
+ return false;
858
+ }
859
+ for (const excludedElementClass of this._excludedElementClasses) {
860
+ if (elementFact.classFullName === excludedElementClass.classFullName ||
861
+ elementClass.prototype instanceof excludedElementClass) {
862
+ core_bentley_1.Logger.logInfo(loggerCategory, `Excluded element ${elementFact.id} by class: ${excludedElementClass.classFullName}`);
863
+ return false;
864
+ }
865
+ }
866
+ return true;
867
+ }
868
+ async queryAspectElementHierarchyFacts(elementIds) {
869
+ const elementFacts = new Map();
870
+ const queriedElementIds = new Set();
871
+ let pendingElementIds = new Set(elementIds);
872
+ while (pendingElementIds.size > 0) {
873
+ const currentElementIds = pendingElementIds;
874
+ pendingElementIds = new Set();
875
+ await this.queryAspectElementFacts(currentElementIds, elementFacts);
876
+ for (const elementId of currentElementIds)
877
+ queriedElementIds.add(elementId);
878
+ for (const elementId of currentElementIds) {
879
+ const parentId = elementFacts.get(elementId)?.parentId;
880
+ if (parentId !== undefined &&
881
+ !queriedElementIds.has(parentId) &&
882
+ !elementFacts.has(parentId)) {
883
+ pendingElementIds.add(parentId);
884
+ }
885
+ }
886
+ }
887
+ return elementFacts;
888
+ }
889
+ async queryAspectElementFacts(elementIds, elementFacts) {
890
+ const unqueriedElementIds = new Set([...elementIds].filter((elementId) => !elementFacts.has(elementId)));
891
+ if (unqueriedElementIds.size === 0)
892
+ return;
893
+ for (const elementIdBatch of this.getAspectOwnerIdBatches(unqueriedElementIds)) {
894
+ const queryParams = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIdBatch);
895
+ for await (const row of this.sourceDb.createQueryReader(`SELECT e.ECInstanceId id, e.Parent.Id parentId, e.Model.Id modelId,
896
+ ec_className(e.ECClassId, 's') schemaName,
897
+ ec_className(e.ECClassId, 'c') className
898
+ FROM bis.Element e
899
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = e.ECInstanceId`, queryParams, { usePrimaryConn: true })) {
900
+ elementFacts.set(row.id, {
901
+ id: row.id,
902
+ parentId: row.parentId ?? undefined,
903
+ modelId: row.modelId,
904
+ classFullName: `${row.schemaName}:${row.className}`,
905
+ });
906
+ }
907
+ if (this._excludedElementCategoryIds.size > 0) {
908
+ const categoryQueryParams = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIdBatch);
909
+ for await (const row of this.sourceDb.createQueryReader(`SELECT g.ECInstanceId id, g.Category.Id categoryId
910
+ FROM bis.GeometricElement g
911
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = g.ECInstanceId`, categoryQueryParams, { usePrimaryConn: true })) {
912
+ const elementFact = elementFacts.get(row.id);
913
+ if (elementFact !== undefined) {
914
+ elementFact.categoryId = row.categoryId;
915
+ }
916
+ }
917
+ }
918
+ }
919
+ }
920
+ async queryAspectModelHierarchyFacts(modelIds) {
921
+ const modelFacts = new Map();
922
+ const queriedModelIds = new Set();
923
+ let pendingModelIds = new Set(modelIds);
924
+ while (pendingModelIds.size > 0) {
925
+ const currentModelIds = pendingModelIds;
926
+ pendingModelIds = new Set();
927
+ for (const modelIdBatch of this.getAspectOwnerIdBatches(currentModelIds)) {
928
+ const queryParams = new core_common_1.QueryBinder().bindIdSet("modelIds", modelIdBatch);
929
+ for await (const row of this.sourceDb.createQueryReader(`SELECT model.ECInstanceId id, model.ParentModel.Id parentModelId, model.IsTemplate isTemplate
930
+ FROM bis.Model model
931
+ INNER JOIN IdSet(:modelIds) ids ON ids.id = model.ECInstanceId`, queryParams, { usePrimaryConn: true })) {
932
+ modelFacts.set(row.id, {
933
+ id: row.id,
934
+ parentModelId: row.parentModelId ?? undefined,
935
+ isTemplate: row.isTemplate === true || row.isTemplate === 1,
936
+ });
937
+ }
938
+ }
939
+ for (const modelId of currentModelIds)
940
+ queriedModelIds.add(modelId);
941
+ for (const modelId of currentModelIds) {
942
+ const parentModelId = modelFacts.get(modelId)?.parentModelId;
943
+ if (parentModelId !== undefined &&
944
+ !queriedModelIds.has(parentModelId) &&
945
+ !modelFacts.has(parentModelId)) {
946
+ pendingModelIds.add(parentModelId);
947
+ }
948
+ }
949
+ }
950
+ return modelFacts;
951
+ }
952
+ *getAspectOwnerIdBatches(ids) {
953
+ let batch = new Set();
954
+ for (const id of ids) {
955
+ batch.add(id);
956
+ if (batch.size === IModelExporter._elementAspectOwnerBatchSize) {
957
+ yield batch;
958
+ batch = new Set();
959
+ }
960
+ }
961
+ if (batch.size > 0)
962
+ yield batch;
669
963
  }
670
964
  /** Exports all relationships that subclass from the specified base class.
671
965
  * @note This method is called from [[exportChanges]] and [[exportAll]], so it only needs to be called directly when exporting a subset of an iModel.
966
+ * @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.
672
967
  */
673
968
  async exportRelationships(baseRelClassFullName) {
674
969
  if (!this.visitRelationships) {
@@ -676,46 +971,154 @@ class IModelExporter {
676
971
  return;
677
972
  }
678
973
  core_bentley_1.Logger.logTrace(loggerCategory, `exportRelationships(${baseRelClassFullName})`);
679
- const sql = `SELECT r.ECInstanceId, r.ECClassId FROM ${baseRelClassFullName} r
680
- JOIN bis.Element s ON s.ECInstanceId = r.SourceECInstanceId
681
- JOIN bis.Element t ON t.ECInstanceId = r.TargetECInstanceId
682
- WHERE s.ECInstanceId IS NOT NULL AND t.ECInstanceId IS NOT NULL`;
683
- // eslint-disable-next-line @itwin/no-internal, @typescript-eslint/no-deprecated
684
- await this.sourceDb.withPreparedStatement(sql,
685
- // eslint-disable-next-line @itwin/no-internal, @typescript-eslint/no-deprecated
686
- async (statement) => {
687
- while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
688
- const relationshipId = statement.getValue(0).getId();
689
- const relationshipClass = statement
690
- .getValue(1)
691
- .getClassNameForClassId();
692
- await this.exportRelationship(relationshipClass, relationshipId); // must call exportRelationship using the actual classFullName, not baseRelClassFullName
974
+ const changedRelationshipIds = this._sourceDbChanges === undefined
975
+ ? undefined
976
+ : new Set([
977
+ ...this._sourceDbChanges.relationship.insertIds,
978
+ ...this._sourceDbChanges.relationship.updateIds,
979
+ ]);
980
+ if (changedRelationshipIds?.size === 0) {
981
+ // Deleted relationships are handled by exportChanges after this traversal; they are no
982
+ // longer present in the source iModel and must not cause a full relationship scan here.
983
+ core_bentley_1.Logger.logTrace(loggerCategory, "No inserted or updated relationships, skipping exportRelationships()");
984
+ return;
985
+ }
986
+ const changedRelationshipJoin = changedRelationshipIds === undefined
987
+ ? undefined
988
+ : "INNER JOIN IdSet(:changedRelationshipIds) changed ON changed.id = r.ECInstanceId";
989
+ const relationshipWhereClause = "s.ECInstanceId IS NOT NULL AND t.ECInstanceId IS NOT NULL";
990
+ // Bound the async reader's retained raw rows while preserving batched query throughput.
991
+ // Keyset pagination avoids rescanning rows returned by earlier batches.
992
+ const relationshipQueryBatchSize = 1000;
993
+ let lastRelationshipId;
994
+ while (true) {
995
+ let rowsRead = 0;
996
+ const whereClause = lastRelationshipId === undefined
997
+ ? relationshipWhereClause
998
+ : `${relationshipWhereClause} AND r.ECInstanceId > :lastRelationshipId`;
999
+ // QueryBinder parameters are immutable after binding a name. Recreate the binder for each
1000
+ // keyset page so the changing boundary can be bound without redefining a property.
1001
+ const queryParams = changedRelationshipIds === undefined && lastRelationshipId === undefined
1002
+ ? undefined
1003
+ : new core_common_1.QueryBinder();
1004
+ if (queryParams !== undefined) {
1005
+ if (changedRelationshipIds !== undefined) {
1006
+ queryParams.bindIdSet("changedRelationshipIds", changedRelationshipIds);
1007
+ }
1008
+ if (lastRelationshipId !== undefined) {
1009
+ queryParams.bindId("lastRelationshipId", lastRelationshipId);
1010
+ }
1011
+ }
1012
+ const reader = this.sourceDb.createQueryReader(this.getRelationshipQuery(baseRelClassFullName, "JOIN", whereClause, changedRelationshipJoin), queryParams, {
1013
+ usePrimaryConn: true,
1014
+ limit: { count: relationshipQueryBatchSize },
1015
+ });
1016
+ for await (const row of reader) {
1017
+ rowsRead++;
1018
+ const relInstanceId = row.relInstanceId;
1019
+ lastRelationshipId = relInstanceId;
1020
+ const changesetFilter = this.checkRelationshipChangesetFilter(relInstanceId);
1021
+ if (changesetFilter.skip) {
1022
+ await this._yieldManager.allowYield();
1023
+ continue;
1024
+ }
1025
+ await this.exportHydratedRelationship(row.rawInstance, row.sourceFedGuid, row.targetFedGuid, changesetFilter.isUpdate);
693
1026
  await this._yieldManager.allowYield();
694
1027
  }
695
- });
1028
+ if (rowsRead < relationshipQueryBatchSize)
1029
+ break;
1030
+ }
1031
+ }
1032
+ /** Builds the raw relationship query used by the bulk and targeted export paths. */
1033
+ getRelationshipQuery(relClassFullName, endpointJoin, whereClause, relationshipJoin) {
1034
+ return `SELECT r.$ AS rawInstance, r.ECInstanceId AS relInstanceId, s.FederationGuid AS sourceFedGuid, t.FederationGuid AS targetFedGuid FROM ${relClassFullName} r
1035
+ ${relationshipJoin ?? ""}
1036
+ ${endpointJoin} bis.Element s ON s.ECInstanceId = r.SourceECInstanceId
1037
+ ${endpointJoin} bis.Element t ON t.ECInstanceId = r.TargetECInstanceId
1038
+ WHERE ${whereClause}
1039
+ ORDER BY r.ECInstanceId
1040
+ OPTIONS USE_JS_PROP_NAMES DO_NOT_TRUNCATE_BLOB`;
696
1041
  }
697
- /** Export a relationship from the source iModel. */
1042
+ /** Applies the changeset filter (when exporting changes) to a relationship instance id.
1043
+ * @returns whether to skip the relationship, and whether the export is an update.
1044
+ */
1045
+ checkRelationshipChangesetFilter(relInstanceId) {
1046
+ if (undefined === this._sourceDbChanges)
1047
+ return { skip: false, isUpdate: undefined };
1048
+ if (this._sourceDbChanges.relationship.insertIds.has(relInstanceId))
1049
+ return { skip: false, isUpdate: false };
1050
+ if (this._sourceDbChanges.relationship.updateIds.has(relInstanceId))
1051
+ return { skip: false, isUpdate: true };
1052
+ return { skip: true, isUpdate: undefined }; // not in changeset, don't export
1053
+ }
1054
+ /** Converts a raw `$` instance-access row into [RelationshipProps]($common). */
1055
+ getRelationshipProps(rawInstance) {
1056
+ const parsedRow = typeof rawInstance === "string"
1057
+ ? JSON.parse(rawInstance, core_common_1.Base64EncodedString.reviver)
1058
+ : rawInstance;
1059
+ if (!core_bentley_1.JsonUtils.isObject(parsedRow))
1060
+ core_bentley_1.ITwinError.throwError({
1061
+ iTwinErrorId: {
1062
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1063
+ key: IModelTransformerError_1.IModelTransformerError.InvalidRelationshipData,
1064
+ },
1065
+ message: "Expected a Relationship instance query to return an object",
1066
+ });
1067
+ const row = {};
1068
+ for (const [key, value] of Object.entries(parsedRow)) {
1069
+ const ecPropertyName = key.length === 0 ? key : key[0].toUpperCase() + key.substring(1);
1070
+ row[core_common_1.ECJsNames.toJsName(ecPropertyName)] = value;
1071
+ }
1072
+ const className = row.className;
1073
+ if (typeof className !== "string")
1074
+ core_bentley_1.ITwinError.throwError({
1075
+ iTwinErrorId: {
1076
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1077
+ key: IModelTransformerError_1.IModelTransformerError.InvalidRelationshipData,
1078
+ },
1079
+ message: "Expected a Relationship instance query to return a className",
1080
+ });
1081
+ row.classFullName = className.replace(".", ":");
1082
+ delete row.className;
1083
+ delete row.sourceClassName;
1084
+ delete row.targetClassName;
1085
+ return row;
1086
+ }
1087
+ /** Export one specified relationship from the source iModel.
1088
+ * @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.
1089
+ */
698
1090
  async exportRelationship(relClassFullName, relInstanceId) {
699
1091
  if (!this.visitRelationships) {
700
1092
  core_bentley_1.Logger.logTrace(loggerCategory, `visitRelationships=false, skipping exportRelationship(${relClassFullName}, ${relInstanceId})`);
701
1093
  return;
702
1094
  }
703
- let isUpdate;
704
- if (undefined !== this._sourceDbChanges) {
705
- // is changeset information available?
706
- if (this._sourceDbChanges.relationship.insertIds.has(relInstanceId)) {
707
- isUpdate = false;
708
- }
709
- else if (this._sourceDbChanges.relationship.updateIds.has(relInstanceId)) {
710
- isUpdate = true;
711
- }
712
- else {
713
- return; // not in changeset, don't export
714
- }
1095
+ const changesetFilter = this.checkRelationshipChangesetFilter(relInstanceId);
1096
+ if (changesetFilter.skip)
1097
+ return;
1098
+ const queryParams = new core_common_1.QueryBinder().bindId("relInstanceId", relInstanceId);
1099
+ const reader = this.sourceDb.createQueryReader(this.getRelationshipQuery(relClassFullName, "LEFT JOIN", "r.ECInstanceId = :relInstanceId"), queryParams, { usePrimaryConn: true });
1100
+ if (await reader.step()) {
1101
+ const row = reader.current;
1102
+ await this.exportHydratedRelationship(row.rawInstance, row.sourceFedGuid, row.targetFedGuid, changesetFilter.isUpdate);
1103
+ return;
715
1104
  }
716
- // passed changeset test, now apply standard exclusion rules
717
- core_bentley_1.Logger.logTrace(loggerCategory, `exportRelationship(${relClassFullName}, ${relInstanceId})`);
1105
+ // Preserve the previous not-found behavior if the targeted query returned no row.
718
1106
  const relationship = this.sourceDb.relationships.getInstance(relClassFullName, relInstanceId);
1107
+ await this.exportRelationshipInstance(relationship, changesetFilter.isUpdate, this.sourceDb.elements.getFederationGuidFromId(relationship.sourceId), this.sourceDb.elements.getFederationGuidFromId(relationship.targetId));
1108
+ }
1109
+ /** Hydrates a relationship query row and runs the common export pipeline. */
1110
+ async exportHydratedRelationship(rawInstance, sourceFedGuid, targetFedGuid, isUpdate) {
1111
+ const relationship = this.sourceDb.constructEntity(this.getRelationshipProps(rawInstance));
1112
+ await this.exportRelationshipInstance(relationship, isUpdate, sourceFedGuid ?? undefined, targetFedGuid ?? undefined);
1113
+ }
1114
+ /** Applies exclusion rules and handler callbacks to an already-hydrated relationship instance.
1115
+ * @param sourceFedGuid The FederationGuid of the relationship's source endpoint, if available.
1116
+ * @param targetFedGuid The FederationGuid of the relationship's target endpoint, if available.
1117
+ * @note This is the common protected hook used by both [[exportRelationship]] and [[exportRelationships]]. Most consumers should customize [[IModelExportHandler]] instead.
1118
+ */
1119
+ async exportRelationshipInstance(relationship, isUpdate, sourceFedGuid, targetFedGuid) {
1120
+ // passed changeset test, now apply standard exclusion rules
1121
+ core_bentley_1.Logger.logTrace(loggerCategory, `exportRelationship(${relationship.classFullName}, ${relationship.id})`);
719
1122
  for (const excludedRelationshipClass of this._excludedRelationshipClasses) {
720
1123
  if (relationship instanceof excludedRelationshipClass) {
721
1124
  core_bentley_1.Logger.logInfo(loggerCategory, `Excluded relationship by class: ${excludedRelationshipClass.classFullName}`);
@@ -723,8 +1126,8 @@ class IModelExporter {
723
1126
  }
724
1127
  }
725
1128
  // relationship has passed standard exclusion rules, now give handler a chance to accept/reject export
726
- if (this.handler.shouldExportRelationship(relationship)) {
727
- this.handler.onExportRelationship(relationship, isUpdate);
1129
+ if (await this.handler.shouldExportRelationship(relationship)) {
1130
+ await this.handler.onExportRelationship(relationship, isUpdate, sourceFedGuid, targetFedGuid);
728
1131
  await this.trackProgress();
729
1132
  }
730
1133
  }
@@ -783,6 +1186,13 @@ class ChangedInstanceIds {
783
1186
  _aspectSubclassIds;
784
1187
  _relationshipSubclassIds;
785
1188
  _relationshipSubclassIdsToSkip;
1189
+ _aspectOwnerElementIds = new Set();
1190
+ /** Element IDs that own the aspects represented by `aspect` changes.
1191
+ * @internal
1192
+ */
1193
+ get aspectOwnerElementIds() {
1194
+ return this._aspectOwnerElementIds;
1195
+ }
786
1196
  _db;
787
1197
  constructor(db) {
788
1198
  this._db = db;
@@ -795,7 +1205,7 @@ class ChangedInstanceIds {
795
1205
  this._relationshipSubclassIds = new Set();
796
1206
  this._relationshipSubclassIdsToSkip = new Set();
797
1207
  const addECClassIdsToSet = async (setToModify, baseClass) => {
798
- for await (const row of this._db.createQueryReader(`SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (${baseClass})`)) {
1208
+ for await (const row of this._db.createQueryReader(`SELECT ECInstanceId FROM ECDbMeta.ECClassDef where ECInstanceId IS (${baseClass})`, undefined, { usePrimaryConn: true })) {
799
1209
  setToModify.add(row.ECInstanceId);
800
1210
  }
801
1211
  };
@@ -845,33 +1255,63 @@ class ChangedInstanceIds {
845
1255
  !this.font.isEmpty);
846
1256
  }
847
1257
  /**
848
- * 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.
1258
+ * 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.
849
1259
  * 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'
850
1260
  * from the set of updatedIds and add it to the set of deletedIds for the appropriate class type.
851
- * @param change ChangedECInstance which has the ECInstanceId, changeType (insert, update, delete) and ECClassId of the changed entity
1261
+ * @param change Changed EC instance with the ID, operation, and EC class ID of the changed entity.
852
1262
  */
853
1263
  async addChange(change) {
854
1264
  if (!this._ecClassIdsInitialized)
855
1265
  await this.setupECClassIds();
856
- const ecClassId = change.ECClassId ?? change.$meta?.fallbackClassId;
1266
+ const ecClassId = change.ECClassId;
857
1267
  if (ecClassId === undefined)
858
- throw new Error(`ECClassId was not found for id: ${change.ECInstanceId}! Table is : ${change?.$meta?.tables}`);
859
- const changeType = change.$meta?.op;
1268
+ core_bentley_1.ITwinError.throwError({
1269
+ iTwinErrorId: {
1270
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1271
+ key: IModelTransformerError_1.IModelTransformerError.ChangedInstanceMetadataMissing,
1272
+ },
1273
+ message: `ECClassId was not found for id: ${change.ECInstanceId}! Table is : ${change.$meta.tables}`,
1274
+ });
1275
+ const changeType = change.$meta.op;
860
1276
  if (changeType === undefined)
861
- throw new Error(`ChangeType was undefined for id: ${change.ECInstanceId}.`);
1277
+ core_bentley_1.ITwinError.throwError({
1278
+ iTwinErrorId: {
1279
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1280
+ key: IModelTransformerError_1.IModelTransformerError.ChangedInstanceMetadataMissing,
1281
+ },
1282
+ message: `ChangeType was undefined for id: ${change.ECInstanceId}.`,
1283
+ });
862
1284
  if (this._relationshipSubclassIdsToSkip?.has(ecClassId))
863
1285
  return;
864
1286
  if (this.isRelationship(ecClassId))
865
1287
  this.handleChange(this.relationship, changeType, change.ECInstanceId);
866
1288
  else if (this.isCodeSpec(ecClassId))
867
1289
  this.handleChange(this.codeSpec, changeType, change.ECInstanceId);
868
- else if (this.isAspect(ecClassId))
1290
+ else if (this.isAspect(ecClassId)) {
1291
+ const ownerElementId = change.Element?.Id ??
1292
+ this.tryGetAspectOwnerElementId(change.ECInstanceId);
1293
+ if (ownerElementId !== undefined) {
1294
+ this._aspectOwnerElementIds.add(ownerElementId);
1295
+ }
869
1296
  this.handleChange(this.aspect, changeType, change.ECInstanceId);
1297
+ }
870
1298
  else if (this.isModel(ecClassId))
871
1299
  this.handleChange(this.model, changeType, change.ECInstanceId);
872
1300
  else if (this.isElement(ecClassId))
873
1301
  this.handleChange(this.element, changeType, change.ECInstanceId);
874
1302
  }
1303
+ tryGetAspectOwnerElementId(aspectId) {
1304
+ try {
1305
+ return this._db.elements.getAspect(aspectId).element.id;
1306
+ }
1307
+ catch (error) {
1308
+ if (error instanceof core_common_1.IModelError &&
1309
+ error.errorNumber === core_bentley_1.IModelStatus.NotFound) {
1310
+ return undefined;
1311
+ }
1312
+ throw error;
1313
+ }
1314
+ }
875
1315
  /**
876
1316
  * This method should only be called inside [[IModelTransformer.addCustomChanges]].
877
1317
  * It adds the provided change to the element changes maintained by this instance of ChangedInstanceIds.
@@ -933,14 +1373,47 @@ class ChangedInstanceIds {
933
1373
  * 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'
934
1374
  * from the set of updatedIds and add it to the set of deletedIds for the appropriate class type.
935
1375
  * @note It is the responsibility of the caller to ensure that the provided id is, in fact an aspect.
1376
+ * @param elementIds Owning element IDs. Required when `changeType` is
1377
+ * `Deleted` or the source aspect row is unavailable.
936
1378
  * @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.
937
1379
  * @beta
938
1380
  */
939
- addCustomAspectChange(changeType, ids) {
1381
+ addCustomAspectChange(changeType, ids, elementIds) {
1382
+ if (elementIds !== undefined) {
1383
+ for (const elementId of core_bentley_1.Id64.iterable(elementIds)) {
1384
+ this._aspectOwnerElementIds.add(elementId);
1385
+ }
1386
+ }
940
1387
  for (const id of core_bentley_1.Id64.iterable(ids)) {
1388
+ if (elementIds === undefined && changeType === "Deleted") {
1389
+ core_bentley_1.ITwinError.throwError({
1390
+ iTwinErrorId: {
1391
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1392
+ key: IModelTransformerError_1.IModelTransformerError.AspectOwnerRequired,
1393
+ },
1394
+ message: "Custom deleted ElementAspect changes require the owning element ID.",
1395
+ });
1396
+ }
1397
+ if (elementIds === undefined && changeType !== "Deleted") {
1398
+ const ownerElementId = this.tryGetAspectOwnerElementId(id);
1399
+ if (ownerElementId === undefined) {
1400
+ core_bentley_1.ITwinError.throwError({
1401
+ iTwinErrorId: {
1402
+ scope: IModelTransformerError_1.IModelTransformerErrorScope,
1403
+ key: IModelTransformerError_1.IModelTransformerError.AspectOwnerRequired,
1404
+ },
1405
+ message: "Custom ElementAspect changes require the owning element ID when the source aspect is unavailable.",
1406
+ });
1407
+ }
1408
+ this._aspectOwnerElementIds.add(ownerElementId);
1409
+ }
941
1410
  this.handleChange(this.aspect, changeType, id);
942
1411
  }
943
1412
  }
1413
+ recordCustomAspectChange(changeType, aspectId, ownerElementId) {
1414
+ this._aspectOwnerElementIds.add(ownerElementId);
1415
+ this.handleChange(this.aspect, changeType, aspectId);
1416
+ }
944
1417
  /**
945
1418
  * 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.
946
1419
  * 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.
@@ -949,7 +1422,9 @@ class ChangedInstanceIds {
949
1422
  const params = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIds);
950
1423
  const ecQuery = `
951
1424
  WITH RECURSIVE hierarchy (parentId) AS (
952
- SELECT Model.Id FROM bis.Element WHERE InVirtualSet(:elementIds, ECInstanceId)
1425
+ SELECT element.Model.Id
1426
+ FROM bis.Element element
1427
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = element.ECInstanceId
953
1428
  UNION
954
1429
  SELECT ParentModel.id
955
1430
  FROM bis.Model e
@@ -958,7 +1433,9 @@ class ChangedInstanceIds {
958
1433
  SELECT parentId FROM hierarchy where parentId is not null
959
1434
  `;
960
1435
  const parentModelIds = new Set();
961
- for await (const row of this._db.createQueryReader(ecQuery, params)) {
1436
+ for await (const row of this._db.createQueryReader(ecQuery, params, {
1437
+ usePrimaryConn: true,
1438
+ })) {
962
1439
  // Transformer handles update as insert when element does not exist in target.
963
1440
  // Which means that in scenario where child and parent model are filtered out from target,
964
1441
  // and child element is inserted trough custom change, its parent model will be marked as updated.
@@ -972,11 +1449,19 @@ class ChangedInstanceIds {
972
1449
  return parentModelIds;
973
1450
  }
974
1451
  async markElementRelationshipsAsInserted(relationshipClassName, elementIds) {
975
- const ecQuery = `SELECT ECInstanceId FROM ${relationshipClassName}
976
- WHERE InVirtualSet(:elementIds, TargetECInstanceId)
977
- OR InVirtualSet(:elementIds, SourceECInstanceId)`;
1452
+ const ecQuery = `SELECT relationship.ECInstanceId
1453
+ FROM ${relationshipClassName} relationship
1454
+ INNER JOIN IdSet(:elementIds) ids
1455
+ ON ids.id = relationship.TargetECInstanceId
1456
+ UNION
1457
+ SELECT relationship.ECInstanceId
1458
+ FROM ${relationshipClassName} relationship
1459
+ INNER JOIN IdSet(:elementIds) ids
1460
+ ON ids.id = relationship.SourceECInstanceId`;
978
1461
  const queryBinder = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIds);
979
- const queryReader = this._db.createQueryReader(ecQuery, queryBinder);
1462
+ const queryReader = this._db.createQueryReader(ecQuery, queryBinder, {
1463
+ usePrimaryConn: true,
1464
+ });
980
1465
  for await (const row of queryReader) {
981
1466
  this.handleChange(this.relationship, "Inserted", row.ECInstanceId);
982
1467
  }
@@ -986,11 +1471,16 @@ class ChangedInstanceIds {
986
1471
  core_backend_1.ElementUniqueAspect.classFullName,
987
1472
  core_backend_1.ElementMultiAspect.classFullName,
988
1473
  ]) {
989
- const ecQuery = `Select ECInstanceId from ${aspectClassName} where InVirtualSet(:elementIds, Element.Id)`;
1474
+ const ecQuery = `SELECT aspect.ECInstanceId, aspect.Element.Id
1475
+ FROM ${aspectClassName} aspect
1476
+ INNER JOIN IdSet(:elementIds) ids ON ids.id = aspect.Element.Id`;
990
1477
  const queryBinder = new core_common_1.QueryBinder().bindIdSet("elementIds", elementIds);
991
- const queryReader = this._db.createQueryReader(ecQuery, queryBinder);
1478
+ const queryReader = this._db.createQueryReader(ecQuery, queryBinder, {
1479
+ usePrimaryConn: true,
1480
+ });
992
1481
  for await (const row of queryReader) {
993
- this.addCustomAspectChange("Inserted", row.toArray()[0]);
1482
+ const [aspectId, elementId] = row.toArray();
1483
+ this.recordCustomAspectChange("Inserted", aspectId, elementId);
994
1484
  }
995
1485
  }
996
1486
  }
@@ -1057,23 +1547,7 @@ class ChangedInstanceIds {
1057
1547
  if (csFileProps === undefined)
1058
1548
  return undefined;
1059
1549
  const changedInstanceIds = new ChangedInstanceIds(opts.iModel);
1060
- for (const csFile of csFileProps) {
1061
- const csReader = core_backend_1.SqliteChangesetReader.openFile({
1062
- fileName: csFile.pathname,
1063
- db: opts.iModel,
1064
- disableSchemaCheck: true,
1065
- });
1066
- const csAdaptor = new core_backend_1.ChangesetECAdaptor(csReader);
1067
- const ecChangeUnifier = new core_backend_1.PartialECChangeUnifier(opts.iModel);
1068
- while (csAdaptor.step()) {
1069
- ecChangeUnifier.appendFrom(csAdaptor);
1070
- }
1071
- const changes = [...ecChangeUnifier.instances];
1072
- for (const change of changes) {
1073
- await changedInstanceIds.addChange(change);
1074
- }
1075
- csReader.close();
1076
- }
1550
+ await ChangesetScanner_1.ChangesetScanner.scan(opts.iModel, csFileProps, changedInstanceIds);
1077
1551
  return changedInstanceIds;
1078
1552
  }
1079
1553
  }