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