@itwin/imodel-transformer 0.1.17-dev.3 → 0.1.17-fedguidopt.3
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/lib/cjs/Algo.d.ts +7 -0
- package/lib/cjs/Algo.d.ts.map +1 -0
- package/lib/cjs/Algo.js +50 -0
- package/lib/cjs/Algo.js.map +1 -0
- package/lib/cjs/ECReferenceTypesCache.js +1 -1
- package/lib/cjs/ECReferenceTypesCache.js.map +1 -1
- package/lib/cjs/EntityUnifier.js +1 -1
- package/lib/cjs/EntityUnifier.js.map +1 -1
- package/lib/cjs/IModelCloneContext.js +4 -4
- package/lib/cjs/IModelCloneContext.js.map +1 -1
- package/lib/cjs/IModelExporter.d.ts +14 -8
- package/lib/cjs/IModelExporter.d.ts.map +1 -1
- package/lib/cjs/IModelExporter.js +32 -19
- package/lib/cjs/IModelExporter.js.map +1 -1
- package/lib/cjs/IModelImporter.js +1 -1
- package/lib/cjs/IModelImporter.js.map +1 -1
- package/lib/cjs/IModelTransformer.d.ts +135 -27
- package/lib/cjs/IModelTransformer.d.ts.map +1 -1
- package/lib/cjs/IModelTransformer.js +842 -218
- package/lib/cjs/IModelTransformer.js.map +1 -1
- package/lib/cjs/PendingReferenceMap.js +1 -1
- package/lib/cjs/PendingReferenceMap.js.map +1 -1
- package/lib/cjs/TransformerLoggerCategory.js +1 -1
- package/lib/cjs/TransformerLoggerCategory.js.map +1 -1
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@ const PendingReferenceMap_1 = require("./PendingReferenceMap");
|
|
|
22
22
|
const EntityMap_1 = require("./EntityMap");
|
|
23
23
|
const IModelCloneContext_1 = require("./IModelCloneContext");
|
|
24
24
|
const EntityUnifier_1 = require("./EntityUnifier");
|
|
25
|
+
const Algo_1 = require("./Algo");
|
|
25
26
|
const loggerCategory = TransformerLoggerCategory_1.TransformerLoggerCategory.IModelTransformer;
|
|
26
27
|
const nullLastProvenanceEntityInfo = {
|
|
27
28
|
entityId: core_bentley_1.Id64.invalid,
|
|
@@ -94,6 +95,12 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
94
95
|
get targetScopeElementId() {
|
|
95
96
|
return this._options.targetScopeElementId;
|
|
96
97
|
}
|
|
98
|
+
get _isReverseSynchronization() {
|
|
99
|
+
return this._isSynchronization && this._options.isReverseSynchronization;
|
|
100
|
+
}
|
|
101
|
+
get _isForwardSynchronization() {
|
|
102
|
+
return this._isSynchronization && !this._options.isReverseSynchronization;
|
|
103
|
+
}
|
|
97
104
|
/** The element classes that are considered to define provenance in the iModel */
|
|
98
105
|
static get provenanceElementClasses() {
|
|
99
106
|
return [core_backend_1.FolderLink, core_backend_1.SynchronizationConfigLink, core_backend_1.ExternalSource, core_backend_1.ExternalSourceAttachment];
|
|
@@ -112,14 +119,42 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
112
119
|
/** map of (unprocessed element, referencing processed element) pairs to the partially committed element that needs the reference resolved
|
|
113
120
|
* and have some helper methods below for now */
|
|
114
121
|
this._pendingReferences = new PendingReferenceMap_1.PendingReferenceMap();
|
|
122
|
+
/** a set of elements for which source provenance will be explicitly tracked by ExternalSourceAspects */
|
|
123
|
+
this._elementsWithExplicitlyTrackedProvenance = new Set();
|
|
115
124
|
/** map of partially committed entities to their partial commit progress */
|
|
116
125
|
this._partiallyCommittedEntities = new EntityMap_1.EntityMap();
|
|
126
|
+
this._isSynchronization = false;
|
|
127
|
+
this._changesetRanges = undefined;
|
|
128
|
+
// FIXME: add test using this
|
|
129
|
+
/**
|
|
130
|
+
* Previously the transformer would insert provenance always pointing to the "target" relationship.
|
|
131
|
+
* It should (and now by default does) instead insert provenance pointing to the provenanceSource
|
|
132
|
+
* SEE: https://github.com/iTwin/imodel-transformer/issues/54
|
|
133
|
+
* This exists only to facilitate testing that the transformer can handle the older, flawed method
|
|
134
|
+
*/
|
|
135
|
+
this._forceOldRelationshipProvenanceMethod = false;
|
|
136
|
+
/** NOTE: the json properties must be converted to string before insertion */
|
|
137
|
+
this._targetScopeProvenanceProps = undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Index of the changeset that the transformer was at when the transformation begins (was constructed).
|
|
140
|
+
* Used to determine at the end which changesets were part of a synchronization.
|
|
141
|
+
*/
|
|
142
|
+
this._startingTargetChangesetIndex = undefined;
|
|
143
|
+
this._cachedSynchronizationVersion = undefined;
|
|
144
|
+
this._targetClassNameToClassIdCache = new Map();
|
|
145
|
+
// if undefined, it can be initialized by calling [[this._cacheSourceChanges]]
|
|
146
|
+
this._hasElementChangedCache = undefined;
|
|
147
|
+
this._deletedSourceRelationshipData = undefined;
|
|
117
148
|
this._yieldManager = new core_bentley_1.YieldManager();
|
|
118
149
|
/** The directory where schemas will be exported, a random temporary directory */
|
|
119
150
|
this._schemaExportDir = path.join(core_backend_1.KnownLocations.tmpdir, core_bentley_1.Guid.createValue());
|
|
120
151
|
this._longNamedSchemasMap = new Map();
|
|
121
152
|
/** state to prevent reinitialization, @see [[initialize]] */
|
|
122
153
|
this._initialized = false;
|
|
154
|
+
/** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
|
|
155
|
+
this._changeSummaryIds = undefined;
|
|
156
|
+
this._sourceChangeDataState = "uninited";
|
|
157
|
+
/** previous provenance, either a federation guid, a `${sourceFedGuid}/${targetFedGuid}` pair, or required aspect props */
|
|
123
158
|
this._lastProvenanceEntityInfo = nullLastProvenanceEntityInfo;
|
|
124
159
|
// initialize IModelTransformOptions
|
|
125
160
|
this._options = {
|
|
@@ -167,6 +202,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
167
202
|
this.targetDb = this.importer.targetDb;
|
|
168
203
|
// create the IModelCloneContext, it must be initialized later
|
|
169
204
|
this.context = new IModelCloneContext_1.IModelCloneContext(this.sourceDb, this.targetDb);
|
|
205
|
+
this._startingTargetChangesetIndex = this.targetDb?.changeset.index;
|
|
170
206
|
}
|
|
171
207
|
/** Dispose any native resources associated with this IModelTransformer. */
|
|
172
208
|
dispose() {
|
|
@@ -195,26 +231,23 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
195
231
|
get provenanceDb() {
|
|
196
232
|
return this._options.isReverseSynchronization ? this.sourceDb : this.targetDb;
|
|
197
233
|
}
|
|
198
|
-
/** Return the IModelDb where IModelTransformer
|
|
234
|
+
/** Return the IModelDb where IModelTransformer will NOT store its provenance.
|
|
199
235
|
* @note This will be [[sourceDb]] except when it is a reverse synchronization. In that case it be [[targetDb]].
|
|
200
236
|
*/
|
|
201
237
|
get provenanceSourceDb() {
|
|
202
238
|
return this._options.isReverseSynchronization ? this.targetDb : this.sourceDb;
|
|
203
239
|
}
|
|
204
|
-
/** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
|
|
205
240
|
initElementProvenance(sourceElementId, targetElementId) {
|
|
241
|
+
// FIXME: deprecate isReverseSync option and instead detect from targetScopeElement provenance
|
|
206
242
|
const elementId = this._options.isReverseSynchronization ? sourceElementId : targetElementId;
|
|
207
243
|
const aspectIdentifier = this._options.isReverseSynchronization ? targetElementId : sourceElementId;
|
|
208
|
-
const version = this._options.isReverseSynchronization
|
|
209
|
-
? this.targetDb.elements.queryLastModifiedTime(targetElementId)
|
|
210
|
-
: this.sourceDb.elements.queryLastModifiedTime(sourceElementId);
|
|
211
244
|
const aspectProps = {
|
|
212
245
|
classFullName: core_backend_1.ExternalSourceAspect.classFullName,
|
|
213
246
|
element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
|
|
214
247
|
scope: { id: this.targetScopeElementId },
|
|
215
248
|
identifier: aspectIdentifier,
|
|
216
249
|
kind: core_backend_1.ExternalSourceAspect.Kind.Element,
|
|
217
|
-
version,
|
|
250
|
+
version: this.sourceDb.elements.queryLastModifiedTime(sourceElementId),
|
|
218
251
|
};
|
|
219
252
|
return aspectProps;
|
|
220
253
|
}
|
|
@@ -224,32 +257,89 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
224
257
|
* The ECInstanceId of the relationship in the target iModel will be stored in the JsonProperties of the ExternalSourceAspect.
|
|
225
258
|
*/
|
|
226
259
|
initRelationshipProvenance(sourceRelationship, targetRelInstanceId) {
|
|
227
|
-
const
|
|
228
|
-
|
|
260
|
+
const elementId = this._options.isReverseSynchronization
|
|
261
|
+
? sourceRelationship.sourceId
|
|
262
|
+
: this.targetDb.withPreparedStatement("SELECT SourceECInstanceId FROM Bis.ElementRefersToElements WHERE ECInstanceId=?", (stmt) => {
|
|
263
|
+
stmt.bindId(1, targetRelInstanceId);
|
|
264
|
+
nodeAssert(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
|
|
265
|
+
return stmt.getValue(0).getId();
|
|
266
|
+
});
|
|
229
267
|
const aspectIdentifier = this._options.isReverseSynchronization ? targetRelInstanceId : sourceRelationship.id;
|
|
268
|
+
const jsonProperties = this._forceOldRelationshipProvenanceMethod
|
|
269
|
+
? { targetRelInstanceId }
|
|
270
|
+
: { provenanceRelInstanceId: this._isReverseSynchronization
|
|
271
|
+
? sourceRelationship.id
|
|
272
|
+
: targetRelInstanceId,
|
|
273
|
+
};
|
|
230
274
|
const aspectProps = {
|
|
231
275
|
classFullName: core_backend_1.ExternalSourceAspect.classFullName,
|
|
232
276
|
element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
|
|
233
277
|
scope: { id: this.targetScopeElementId },
|
|
234
278
|
identifier: aspectIdentifier,
|
|
235
279
|
kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
|
|
236
|
-
jsonProperties: JSON.stringify(
|
|
280
|
+
jsonProperties: JSON.stringify(jsonProperties),
|
|
237
281
|
};
|
|
238
|
-
aspectProps.id = this.queryExternalSourceAspectId(aspectProps);
|
|
239
282
|
return aspectProps;
|
|
240
283
|
}
|
|
241
|
-
|
|
284
|
+
/** the changeset in the scoping element's source version found for this transformation
|
|
285
|
+
* @note: the version depends on whether this is a reverse synchronization or not, as
|
|
286
|
+
* it is stored separately for both synchronization directions
|
|
287
|
+
* @note: empty string and -1 for changeset and index if it has never been transformed
|
|
288
|
+
*/
|
|
289
|
+
get _synchronizationVersion() {
|
|
290
|
+
if (!this._cachedSynchronizationVersion) {
|
|
291
|
+
nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps was not set yet");
|
|
292
|
+
const version = this._options.isReverseSynchronization
|
|
293
|
+
? this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion
|
|
294
|
+
: this._targetScopeProvenanceProps.version;
|
|
295
|
+
nodeAssert(version !== undefined, "no version contained in target scope");
|
|
296
|
+
const [id, index] = version === ""
|
|
297
|
+
? ["", -1]
|
|
298
|
+
: version.split(";");
|
|
299
|
+
this._cachedSynchronizationVersion = { index: Number(index), id };
|
|
300
|
+
nodeAssert(!Number.isNaN(this._cachedSynchronizationVersion.index), "bad parse: invalid index in version");
|
|
301
|
+
}
|
|
302
|
+
return this._cachedSynchronizationVersion;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Make sure there are no conflicting other scope-type external source aspects on the *target scope element*,
|
|
306
|
+
* If there are none at all, insert one, then this must be a first synchronization.
|
|
307
|
+
* @returns the last synced version (changesetId) on the target scope's external source aspect,
|
|
308
|
+
* if this was a [BriefcaseDb]($backend)
|
|
309
|
+
*/
|
|
310
|
+
initScopeProvenance() {
|
|
242
311
|
const aspectProps = {
|
|
312
|
+
id: undefined,
|
|
313
|
+
version: undefined,
|
|
243
314
|
classFullName: core_backend_1.ExternalSourceAspect.classFullName,
|
|
244
315
|
element: { id: this.targetScopeElementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
|
|
245
316
|
scope: { id: core_common_1.IModel.rootSubjectId },
|
|
246
|
-
identifier: this.
|
|
317
|
+
identifier: this.provenanceSourceDb.iModelId,
|
|
247
318
|
kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
|
|
319
|
+
jsonProperties: undefined,
|
|
248
320
|
};
|
|
249
|
-
|
|
321
|
+
// FIXME: handle older transformed iModels which do NOT have the version
|
|
322
|
+
// or reverseSyncVersion set correctly
|
|
323
|
+
const externalSource = this.queryScopeExternalSource(aspectProps, { getJsonProperties: true }); // this query includes "identifier"
|
|
324
|
+
aspectProps.id = externalSource.aspectId;
|
|
325
|
+
aspectProps.version = externalSource.version;
|
|
326
|
+
aspectProps.jsonProperties = externalSource.jsonProperties ? JSON.parse(externalSource.jsonProperties) : {};
|
|
250
327
|
if (undefined === aspectProps.id) {
|
|
328
|
+
aspectProps.version = ""; // empty since never before transformed. Will be updated in [[finalizeTransformation]]
|
|
329
|
+
aspectProps.jsonProperties = {
|
|
330
|
+
pendingReverseSyncChangesetIndices: [],
|
|
331
|
+
pendingSyncChangesetIndices: [],
|
|
332
|
+
reverseSyncVersion: "", // empty since never before transformed. Will be updated in first reverse sync
|
|
333
|
+
};
|
|
251
334
|
// this query does not include "identifier" to find possible conflicts
|
|
252
|
-
const sql = `
|
|
335
|
+
const sql = `
|
|
336
|
+
SELECT ECInstanceId
|
|
337
|
+
FROM ${core_backend_1.ExternalSourceAspect.classFullName}
|
|
338
|
+
WHERE Element.Id=:elementId
|
|
339
|
+
AND Scope.Id=:scopeId
|
|
340
|
+
AND Kind=:kind
|
|
341
|
+
LIMIT 1
|
|
342
|
+
`;
|
|
253
343
|
const hasConflictingScope = this.provenanceDb.withPreparedStatement(sql, (statement) => {
|
|
254
344
|
statement.bindId("elementId", aspectProps.element.id);
|
|
255
345
|
statement.bindId("scopeId", aspectProps.scope.id); // this scope.id can never be invalid, we create it above
|
|
@@ -260,41 +350,121 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
260
350
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidId, "Provenance scope conflict");
|
|
261
351
|
}
|
|
262
352
|
if (!this._options.noProvenance) {
|
|
263
|
-
this.provenanceDb.elements.insertAspect(
|
|
353
|
+
this.provenanceDb.elements.insertAspect({
|
|
354
|
+
...aspectProps,
|
|
355
|
+
jsonProperties: JSON.stringify(aspectProps.jsonProperties),
|
|
356
|
+
});
|
|
264
357
|
}
|
|
265
358
|
}
|
|
359
|
+
this._targetScopeProvenanceProps = aspectProps;
|
|
266
360
|
}
|
|
267
|
-
|
|
268
|
-
|
|
361
|
+
/**
|
|
362
|
+
* @returns the id and version of an aspect with the given element, scope, kind, and identifier
|
|
363
|
+
* May also return a reverseSyncVersion from json properties if requested
|
|
364
|
+
*/
|
|
365
|
+
queryScopeExternalSource(aspectProps, { getJsonProperties = false } = {}) {
|
|
366
|
+
const sql = `
|
|
367
|
+
SELECT ECInstanceId, Version
|
|
368
|
+
${getJsonProperties ? ", JsonProperties" : ""}
|
|
369
|
+
FROM ${core_backend_1.ExternalSourceAspect.classFullName}
|
|
370
|
+
WHERE Element.Id=:elementId
|
|
371
|
+
AND Scope.Id=:scopeId
|
|
372
|
+
AND Kind=:kind
|
|
373
|
+
AND Identifier=:identifier
|
|
374
|
+
LIMIT 1
|
|
375
|
+
`;
|
|
376
|
+
const emptyResult = { aspectId: undefined, version: undefined, jsonProperties: undefined };
|
|
269
377
|
return this.provenanceDb.withPreparedStatement(sql, (statement) => {
|
|
270
378
|
statement.bindId("elementId", aspectProps.element.id);
|
|
271
379
|
if (aspectProps.scope === undefined)
|
|
272
|
-
return
|
|
380
|
+
return emptyResult; // return undefined instead of binding an invalid id
|
|
273
381
|
statement.bindId("scopeId", aspectProps.scope.id);
|
|
274
382
|
statement.bindString("kind", aspectProps.kind);
|
|
275
383
|
statement.bindString("identifier", aspectProps.identifier);
|
|
276
|
-
|
|
384
|
+
if (core_bentley_1.DbResult.BE_SQLITE_ROW !== statement.step())
|
|
385
|
+
return emptyResult;
|
|
386
|
+
const aspectId = statement.getValue(0).getId();
|
|
387
|
+
const version = statement.getValue(1).getString();
|
|
388
|
+
const jsonProperties = getJsonProperties ? statement.getValue(2).getString() : undefined;
|
|
389
|
+
return { aspectId, version, jsonProperties };
|
|
277
390
|
});
|
|
278
391
|
}
|
|
279
|
-
/**
|
|
392
|
+
/**
|
|
393
|
+
* Iterate all matching ExternalSourceAspects in the provenance iModel (target unless reverse sync) and call a function for each one.
|
|
394
|
+
* @note provenance is done by federation guids where possible
|
|
395
|
+
* @note this may execute on each element more than once! Only use in cases where that is handled
|
|
396
|
+
*/
|
|
280
397
|
forEachTrackedElement(fn) {
|
|
398
|
+
// FIXME: do we need an alternative for in-iModel transforms?
|
|
399
|
+
if (!this.context.isBetweenIModels)
|
|
400
|
+
return;
|
|
281
401
|
if (!this.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
|
|
282
402
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
|
|
283
403
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
404
|
+
// query for provenanceDb
|
|
405
|
+
const elementIdByFedGuidQuery = `
|
|
406
|
+
SELECT e.ECInstanceId, FederationGuid
|
|
407
|
+
FROM bis.Element e
|
|
408
|
+
WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements
|
|
409
|
+
ORDER BY FederationGuid
|
|
410
|
+
`;
|
|
411
|
+
// iterate through sorted list of fed guids from both dbs to get the intersection
|
|
412
|
+
// NOTE: if we exposed the native attach database support,
|
|
413
|
+
// we could get the intersection of fed guids in one query, not sure if it would be faster
|
|
414
|
+
// OR we could do a raw sqlite query...
|
|
415
|
+
this.sourceDb.withStatement(elementIdByFedGuidQuery, (sourceStmt) => this.targetDb.withStatement(elementIdByFedGuidQuery, (targetStmt) => {
|
|
416
|
+
if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
417
|
+
return;
|
|
418
|
+
let sourceRow = sourceStmt.getRow();
|
|
419
|
+
if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
420
|
+
return;
|
|
421
|
+
let targetRow = targetStmt.getRow();
|
|
422
|
+
// NOTE: these comparisons rely upon the lowercase of the guid,
|
|
423
|
+
// and the fact that '0' < '9' < a' < 'f' in ascii/utf8
|
|
424
|
+
while (true) {
|
|
425
|
+
const currSourceRow = sourceRow, currTargetRow = targetRow;
|
|
426
|
+
if (currSourceRow.federationGuid !== undefined
|
|
427
|
+
&& currTargetRow.federationGuid !== undefined
|
|
428
|
+
&& currSourceRow.federationGuid === currTargetRow.federationGuid) {
|
|
429
|
+
// data flow direction is always sourceDb -> targetDb and it does not depend on where the explicit element provenance is stored
|
|
430
|
+
fn(sourceRow.id, targetRow.id);
|
|
293
431
|
}
|
|
294
|
-
|
|
295
|
-
|
|
432
|
+
if (currTargetRow.federationGuid === undefined
|
|
433
|
+
|| (currSourceRow.federationGuid !== undefined
|
|
434
|
+
&& currSourceRow.federationGuid >= currTargetRow.federationGuid)) {
|
|
435
|
+
if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
436
|
+
return;
|
|
437
|
+
targetRow = targetStmt.getRow();
|
|
438
|
+
}
|
|
439
|
+
if (currSourceRow.federationGuid === undefined
|
|
440
|
+
|| (currTargetRow.federationGuid !== undefined
|
|
441
|
+
&& currSourceRow.federationGuid <= currTargetRow.federationGuid)) {
|
|
442
|
+
if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
443
|
+
return;
|
|
444
|
+
sourceRow = sourceStmt.getRow();
|
|
296
445
|
}
|
|
297
446
|
}
|
|
447
|
+
}));
|
|
448
|
+
// query for provenanceDb
|
|
449
|
+
const provenanceAspectsQuery = `
|
|
450
|
+
SELECT esa.Identifier, Element.Id
|
|
451
|
+
FROM bis.ExternalSourceAspect esa
|
|
452
|
+
WHERE Scope.Id=:scopeId
|
|
453
|
+
AND Kind=:kind
|
|
454
|
+
`;
|
|
455
|
+
// Technically this will a second time call the function (as documented) on
|
|
456
|
+
// victims of the old provenance method that have both fedguids and an inserted aspect.
|
|
457
|
+
// But this is a private function with one known caller where that doesn't matter
|
|
458
|
+
this.provenanceDb.withPreparedStatement(provenanceAspectsQuery, (stmt) => {
|
|
459
|
+
const runFnInDataFlowDirection = (sourceId, targetId) => this._options.isReverseSynchronization ? fn(sourceId, targetId) : fn(targetId, sourceId);
|
|
460
|
+
stmt.bindId("scopeId", this.targetScopeElementId);
|
|
461
|
+
stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
|
|
462
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
463
|
+
// ExternalSourceAspect.Identifier is of type string
|
|
464
|
+
const aspectIdentifier = stmt.getValue(0).getString();
|
|
465
|
+
const elementId = stmt.getValue(1).getId();
|
|
466
|
+
runFnInDataFlowDirection(elementId, aspectIdentifier);
|
|
467
|
+
}
|
|
298
468
|
});
|
|
299
469
|
}
|
|
300
470
|
/** Initialize the source to target Element mapping from ExternalSourceAspects in the target iModel.
|
|
@@ -308,105 +478,329 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
308
478
|
this.context.remapElement(sourceElementId, targetElementId);
|
|
309
479
|
});
|
|
310
480
|
if (args)
|
|
311
|
-
return this.
|
|
481
|
+
return this.remapDeletedSourceEntities();
|
|
312
482
|
}
|
|
313
|
-
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
483
|
+
/**
|
|
484
|
+
* Scan changesets for deleted entities, if in a reverse synchronization, provenance has
|
|
485
|
+
* already been deleted, so we must scan for that as well.
|
|
316
486
|
*/
|
|
317
|
-
async
|
|
487
|
+
async remapDeletedSourceEntities() {
|
|
318
488
|
// we need a connected iModel with changes to remap elements with deletions
|
|
319
|
-
|
|
489
|
+
const notConnectedModel = this.sourceDb.iTwinId === undefined;
|
|
490
|
+
const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
|
|
491
|
+
if (notConnectedModel || noChanges)
|
|
320
492
|
return;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
493
|
+
this._deletedSourceRelationshipData = new Map();
|
|
494
|
+
nodeAssert(this._changeSummaryIds, "change summaries should be initialized before we get here");
|
|
495
|
+
nodeAssert(this._changeSummaryIds.length > 0, "change summaries should have at least one");
|
|
496
|
+
// optimization: if we have provenance, use it to avoid more querying later
|
|
497
|
+
// eventually when itwin.js supports attaching a second iModelDb in JS,
|
|
498
|
+
// this won't have to be a conditional part of the query, and we can always have it by attaching
|
|
499
|
+
const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
|
|
500
|
+
const deletedEntitySql = `
|
|
501
|
+
SELECT
|
|
502
|
+
1 AS IsElemNotRel,
|
|
503
|
+
ic.ChangedInstance.Id AS InstanceId,
|
|
504
|
+
NULL AS InstId2, -- need these columns for relationship ends in the unioned query
|
|
505
|
+
NULL AS InstId3,
|
|
506
|
+
ec.FederationGuid AS FedGuid,
|
|
507
|
+
NULL AS FedGuid2,
|
|
508
|
+
ic.ChangedInstance.ClassId AS ClassId
|
|
509
|
+
${queryCanAccessProvenance ? `
|
|
510
|
+
, coalesce(esa.Identifier, esac.Identifier) AS Identifier1
|
|
511
|
+
, NULL AS Identifier2
|
|
512
|
+
` : ""}
|
|
513
|
+
FROM ecchange.change.InstanceChange ic
|
|
514
|
+
LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') ec
|
|
515
|
+
ON ic.ChangedInstance.Id=ec.ECInstanceId
|
|
516
|
+
${queryCanAccessProvenance ? `
|
|
517
|
+
LEFT JOIN bis.ExternalSourceAspect esa
|
|
518
|
+
ON ec.ECInstanceId=esa.Element.Id
|
|
519
|
+
LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') esac
|
|
520
|
+
ON ec.ECInstanceId=esac.Element.Id
|
|
521
|
+
` : ""}
|
|
522
|
+
WHERE ic.OpCode=:opDelete
|
|
523
|
+
AND ic.Summary.Id=:changeSummaryId
|
|
524
|
+
AND ic.ChangedInstance.ClassId IS (BisCore.Element)
|
|
525
|
+
${queryCanAccessProvenance ? `
|
|
526
|
+
AND (esa.Scope.Id=:targetScopeElement OR esa.Scope.Id IS NULL)
|
|
527
|
+
AND (esa.Kind='Element' OR esa.Kind IS NULL)
|
|
528
|
+
AND (esac.Scope.Id=:targetScopeElement OR esac.Scope.Id IS NULL)
|
|
529
|
+
AND (esac.Kind='Element' OR esac.Kind IS NULL)
|
|
530
|
+
` : ""}
|
|
531
|
+
|
|
532
|
+
UNION ALL
|
|
533
|
+
|
|
534
|
+
SELECT
|
|
535
|
+
0 AS IsElemNotRel,
|
|
536
|
+
ic.ChangedInstance.Id AS InstanceId,
|
|
537
|
+
coalesce(se.ECInstanceId, sec.ECInstanceId) AS InstId2,
|
|
538
|
+
coalesce(te.ECInstanceId, tec.ECInstanceId) AS InstId3,
|
|
539
|
+
coalesce(se.FederationGuid, sec.FederationGuid) AS FedGuid1,
|
|
540
|
+
coalesce(te.FederationGuid, tec.FederationGuid) AS FedGuid2,
|
|
541
|
+
ic.ChangedInstance.ClassId AS ClassId
|
|
542
|
+
${queryCanAccessProvenance ? `
|
|
543
|
+
, coalesce(sesa.Identifier, sesac.Identifier) AS Identifier1
|
|
544
|
+
, coalesce(tesa.Identifier, tesac.Identifier) AS Identifier2
|
|
545
|
+
` : ""}
|
|
546
|
+
FROM ecchange.change.InstanceChange ic
|
|
547
|
+
LEFT JOIN bis.ElementRefersToElements.Changes(:changeSummaryId, 'BeforeDelete') ertec
|
|
548
|
+
ON ic.ChangedInstance.Id=ertec.ECInstanceId
|
|
549
|
+
-- FIXME: test a deletion of both an element and a relationship at the same time
|
|
550
|
+
LEFT JOIN bis.Element se
|
|
551
|
+
ON se.ECInstanceId=ertec.SourceECInstanceId
|
|
552
|
+
LEFT JOIN bis.Element te
|
|
553
|
+
ON te.ECInstanceId=ertec.TargetECInstanceId
|
|
554
|
+
LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') sec
|
|
555
|
+
ON sec.ECInstanceId=ertec.SourceECInstanceId
|
|
556
|
+
LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') tec
|
|
557
|
+
ON tec.ECInstanceId=ertec.TargetECInstanceId
|
|
558
|
+
${queryCanAccessProvenance ? `
|
|
559
|
+
-- NOTE: need to join on both se/te and sec/tec incase the element was deleted
|
|
560
|
+
LEFT JOIN bis.ExternalSourceAspect sesa
|
|
561
|
+
ON se.ECInstanceId=sesa.Element.Id -- don't use *esac*.Identifier because it's a string
|
|
562
|
+
LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') sesac
|
|
563
|
+
ON sec.ECInstanceId=sesac.Element.Id
|
|
564
|
+
LEFT JOIN bis.ExternalSourceAspect tesa
|
|
565
|
+
ON te.ECInstanceId=tesa.Element.Id
|
|
566
|
+
LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') tesac
|
|
567
|
+
ON tec.ECInstanceId=tesac.Element.Id
|
|
568
|
+
` : ""}
|
|
569
|
+
WHERE ic.OpCode=:opDelete
|
|
570
|
+
AND ic.Summary.Id=:changeSummaryId
|
|
571
|
+
AND ic.ChangedInstance.ClassId IS (BisCore.ElementRefersToElements)
|
|
572
|
+
${queryCanAccessProvenance ? `
|
|
573
|
+
AND (sesa.Scope.Id=:targetScopeElement OR sesa.Scope.Id IS NULL)
|
|
574
|
+
AND (sesa.Kind='Relationship' OR sesa.Kind IS NULL)
|
|
575
|
+
AND (sesac.Scope.Id=:targetScopeElement OR sesac.Scope.Id IS NULL)
|
|
576
|
+
AND (sesac.Kind='Relationship' OR sesac.Kind IS NULL)
|
|
577
|
+
AND (tesa.Scope.Id=:targetScopeElement OR tesa.Scope.Id IS NULL)
|
|
578
|
+
AND (tesa.Kind='Relationship' OR tesa.Kind IS NULL)
|
|
579
|
+
AND (tesac.Scope.Id=:targetScopeElement OR tesac.Scope.Id IS NULL)
|
|
580
|
+
AND (tesac.Kind='Relationship' OR tesac.Kind IS NULL)
|
|
581
|
+
` : ""}
|
|
582
|
+
`;
|
|
583
|
+
for (const changeSummaryId of this._changeSummaryIds) {
|
|
584
|
+
// FIXME: test deletion in both forward and reverse sync
|
|
585
|
+
this.sourceDb.withPreparedStatement(deletedEntitySql, (stmt) => {
|
|
586
|
+
stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
|
|
587
|
+
if (queryCanAccessProvenance)
|
|
588
|
+
stmt.bindId("targetScopeElement", this.targetScopeElementId);
|
|
589
|
+
stmt.bindId("changeSummaryId", changeSummaryId);
|
|
590
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
591
|
+
const isElemNotRel = stmt.getValue(0).getBoolean();
|
|
592
|
+
const instId = stmt.getValue(1).getId();
|
|
593
|
+
if (isElemNotRel) {
|
|
594
|
+
const sourceElemFedGuid = stmt.getValue(4).getGuid();
|
|
595
|
+
// "Identifier" is a string, so null value returns '' which doesn't work with ??, and I don't like ||
|
|
596
|
+
let identifierValue;
|
|
597
|
+
// TODO: if I could attach the second db, will probably be much faster to get target id
|
|
598
|
+
// as part of the whole query rather than with _queryElemIdByFedGuid
|
|
599
|
+
const targetId = (queryCanAccessProvenance
|
|
600
|
+
&& (identifierValue = stmt.getValue(7))
|
|
601
|
+
&& !identifierValue.isNull
|
|
602
|
+
&& identifierValue.getString())
|
|
603
|
+
// maybe batching these queries would perform better but we should
|
|
604
|
+
// try to attach the second db and query both together anyway
|
|
605
|
+
|| (sourceElemFedGuid && this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid))
|
|
606
|
+
// FIXME: describe why it's safe to assume nothing has been deleted in provenanceDb
|
|
607
|
+
|| this._queryProvenanceForElement(instId);
|
|
608
|
+
// since we are processing one changeset at a time, we can see local source deletes
|
|
609
|
+
// of entities that were never synced and can be safely ignored
|
|
610
|
+
const deletionNotInTarget = !targetId;
|
|
611
|
+
if (deletionNotInTarget)
|
|
612
|
+
continue;
|
|
613
|
+
this.context.remapElement(instId, targetId);
|
|
365
614
|
}
|
|
366
|
-
|
|
367
|
-
|
|
615
|
+
else { // is deleted relationship
|
|
616
|
+
const classFullName = stmt.getValue(6).getClassNameForClassId();
|
|
617
|
+
const [sourceIdInTarget, targetIdInTarget] = [
|
|
618
|
+
{ guidColumn: 4, identifierColumn: 7, isTarget: false },
|
|
619
|
+
{ guidColumn: 5, identifierColumn: 8, isTarget: true },
|
|
620
|
+
].map(({ guidColumn, identifierColumn }) => {
|
|
621
|
+
const fedGuid = stmt.getValue(guidColumn).getGuid();
|
|
622
|
+
let identifierValue;
|
|
623
|
+
return ((queryCanAccessProvenance
|
|
624
|
+
// FIXME: this is really far from idiomatic, try to undo that
|
|
625
|
+
&& (identifierValue = stmt.getValue(identifierColumn))
|
|
626
|
+
&& !identifierValue.isNull
|
|
627
|
+
&& identifierValue.getString())
|
|
628
|
+
// maybe batching these queries would perform better but we should
|
|
629
|
+
// try to attach the second db and query both together anyway
|
|
630
|
+
|| (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
|
|
631
|
+
});
|
|
632
|
+
// since we are processing one changeset at a time, we can see local source deletes
|
|
633
|
+
// of entities that were never synced and can be safely ignored
|
|
634
|
+
if (sourceIdInTarget && targetIdInTarget) {
|
|
635
|
+
this._deletedSourceRelationshipData.set(instId, {
|
|
636
|
+
classFullName,
|
|
637
|
+
sourceIdInTarget,
|
|
638
|
+
targetIdInTarget,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
// FIXME: describe why it's safe to assume nothing has been deleted in provenanceDb
|
|
643
|
+
const relProvenance = this._queryProvenanceForRelationship(instId, {
|
|
644
|
+
classFullName,
|
|
645
|
+
sourceId: stmt.getValue(2).getId(),
|
|
646
|
+
targetId: stmt.getValue(3).getId(),
|
|
647
|
+
});
|
|
648
|
+
if (relProvenance && relProvenance.relationshipId)
|
|
649
|
+
this._deletedSourceRelationshipData.set(instId, {
|
|
650
|
+
classFullName,
|
|
651
|
+
relId: relProvenance.relationshipId,
|
|
652
|
+
provenanceAspectId: relProvenance.aspectId,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// NEXT: remap sourceId and targetId to target, get provenance there
|
|
658
|
+
// NOTE: it is possible during a forward sync for the target to already have deleted
|
|
659
|
+
// something that the source deleted, in which case we can safely ignore the gone provenance
|
|
660
|
+
});
|
|
368
661
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
662
|
+
}
|
|
663
|
+
_queryProvenanceForElement(entityInProvenanceSourceId) {
|
|
664
|
+
return this.provenanceDb.withPreparedStatement(`
|
|
665
|
+
SELECT esa.Element.Id
|
|
666
|
+
FROM Bis.ExternalSourceAspect esa
|
|
667
|
+
WHERE esa.Kind=?
|
|
668
|
+
AND esa.Scope.Id=?
|
|
669
|
+
AND esa.Identifier=?
|
|
670
|
+
`, (stmt) => {
|
|
671
|
+
stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Element);
|
|
672
|
+
stmt.bindId(2, this.targetScopeElementId);
|
|
673
|
+
stmt.bindString(3, entityInProvenanceSourceId);
|
|
674
|
+
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
675
|
+
return stmt.getValue(0).getId();
|
|
676
|
+
else
|
|
677
|
+
return undefined;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
_queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
|
|
681
|
+
return this.provenanceDb.withPreparedStatement(`
|
|
682
|
+
SELECT
|
|
683
|
+
ECInstanceId,
|
|
684
|
+
JSON_EXTRACT(JsonProperties, '$.targetRelInstanceId'),
|
|
685
|
+
JSON_EXTRACT(JsonProperties, '$.provenanceRelInstanceId')
|
|
686
|
+
FROM Bis.ExternalSourceAspect
|
|
687
|
+
WHERE Kind=?
|
|
688
|
+
AND Scope.Id=?
|
|
689
|
+
AND Identifier=?
|
|
690
|
+
`, (stmt) => {
|
|
691
|
+
stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Relationship);
|
|
692
|
+
stmt.bindId(2, this.targetScopeElementId);
|
|
693
|
+
stmt.bindString(3, entityInProvenanceSourceId);
|
|
694
|
+
if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
695
|
+
return undefined;
|
|
696
|
+
const aspectId = stmt.getValue(0).getId();
|
|
697
|
+
const provenanceRelInstIdVal = stmt.getValue(2);
|
|
698
|
+
const provenanceRelInstanceId = !provenanceRelInstIdVal.isNull
|
|
699
|
+
? provenanceRelInstIdVal.getString()
|
|
700
|
+
: this._queryTargetRelId(sourceRelInfo);
|
|
701
|
+
return {
|
|
702
|
+
aspectId,
|
|
703
|
+
relationshipId: provenanceRelInstanceId,
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
_queryTargetRelId(sourceRelInfo) {
|
|
708
|
+
const targetRelInfo = {
|
|
709
|
+
sourceId: this.context.findTargetElementId(sourceRelInfo.sourceId),
|
|
710
|
+
targetId: this.context.findTargetElementId(sourceRelInfo.targetId),
|
|
711
|
+
};
|
|
712
|
+
if (targetRelInfo.sourceId === undefined || targetRelInfo.targetId === undefined)
|
|
713
|
+
return undefined; // couldn't find an element, rel is invalid or deleted
|
|
714
|
+
return this.targetDb.withPreparedStatement(`
|
|
715
|
+
SELECT ECInstanceId
|
|
716
|
+
FROM bis.ElementRefersToElements
|
|
717
|
+
WHERE SourceECInstanceId=?
|
|
718
|
+
AND TargetECInstanceId=?
|
|
719
|
+
AND ECClassId=?
|
|
720
|
+
`, (stmt) => {
|
|
721
|
+
stmt.bindId(1, targetRelInfo.sourceId);
|
|
722
|
+
stmt.bindId(2, targetRelInfo.targetId);
|
|
723
|
+
stmt.bindId(3, this._targetClassNameToClassId(sourceRelInfo.classFullName));
|
|
724
|
+
if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
725
|
+
return undefined;
|
|
726
|
+
return stmt.getValue(0).getId();
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
_targetClassNameToClassId(classFullName) {
|
|
730
|
+
let classId = this._targetClassNameToClassIdCache.get(classFullName);
|
|
731
|
+
if (classId === undefined) {
|
|
732
|
+
classId = this._getRelClassId(this.targetDb, classFullName);
|
|
733
|
+
this._targetClassNameToClassIdCache.set(classFullName, classId);
|
|
372
734
|
}
|
|
735
|
+
return classId;
|
|
736
|
+
}
|
|
737
|
+
// NOTE: this doesn't handle remapped element classes,
|
|
738
|
+
// but is only used for relationships rn
|
|
739
|
+
_getRelClassId(db, classFullName) {
|
|
740
|
+
return db.withPreparedStatement(`
|
|
741
|
+
SELECT c.ECInstanceId
|
|
742
|
+
FROM ECDbMeta.ECClassDef c
|
|
743
|
+
JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
|
|
744
|
+
WHERE s.Name=? AND c.Name=?
|
|
745
|
+
`, (stmt) => {
|
|
746
|
+
const [schemaName, className] = classFullName.split(".");
|
|
747
|
+
stmt.bindString(1, schemaName);
|
|
748
|
+
stmt.bindString(2, className);
|
|
749
|
+
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
750
|
+
return stmt.getValue(0).getId();
|
|
751
|
+
(0, core_bentley_1.assert)(false, "relationship was not found");
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
_queryElemIdByFedGuid(db, fedGuid) {
|
|
755
|
+
return db.withPreparedStatement("SELECT ECInstanceId FROM Bis.Element WHERE FederationGuid=?", (stmt) => {
|
|
756
|
+
stmt.bindGuid(1, fedGuid);
|
|
757
|
+
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
758
|
+
return stmt.getValue(0).getId();
|
|
759
|
+
else
|
|
760
|
+
return undefined;
|
|
761
|
+
});
|
|
373
762
|
}
|
|
374
763
|
/** Returns `true` if *brute force* delete detections should be run.
|
|
375
764
|
* @note Not relevant for processChanges when change history is known.
|
|
376
765
|
*/
|
|
377
766
|
shouldDetectDeletes() {
|
|
767
|
+
// FIXME: all synchronizations should mark this as false
|
|
378
768
|
if (this._isFirstSynchronization)
|
|
379
769
|
return false; // not necessary the first time since there are no deletes to detect
|
|
380
770
|
if (this._options.isReverseSynchronization)
|
|
381
771
|
return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
|
|
382
772
|
return true;
|
|
383
773
|
}
|
|
384
|
-
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
774
|
+
/**
|
|
775
|
+
* Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
|
|
776
|
+
* in the source iModel.
|
|
777
|
+
* @deprecated in 0.1.x. This method is only called during [[processAll]] when the option
|
|
778
|
+
* [[IModelTransformerOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
|
|
779
|
+
* necessary when using [[processChanges]] since changeset information is sufficient.
|
|
780
|
+
* @note you do not need to call this directly unless processing a subset of an iModel.
|
|
387
781
|
* @throws [[IModelError]] If the required provenance information is not available to detect deletes.
|
|
388
782
|
*/
|
|
389
783
|
async detectElementDeletes() {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
|
|
784
|
+
const sql = `
|
|
785
|
+
SELECT Identifier, Element.Id
|
|
786
|
+
FROM BisCore.ExternalSourceAspect
|
|
787
|
+
WHERE Scope.Id=:scopeId
|
|
788
|
+
AND Kind=:kind
|
|
789
|
+
`;
|
|
790
|
+
nodeAssert(!this._options.isReverseSynchronization, "synchronizations with processChagnes already detect element deletes, don't call detectElementDeletes");
|
|
791
|
+
this.provenanceDb.withPreparedStatement(sql, (stmt) => {
|
|
792
|
+
stmt.bindId("scopeId", this.targetScopeElementId);
|
|
793
|
+
stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
|
|
794
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
795
|
+
// ExternalSourceAspect.Identifier is of type string
|
|
796
|
+
const aspectIdentifier = stmt.getValue(0).getString();
|
|
797
|
+
if (!core_bentley_1.Id64.isId64(aspectIdentifier)) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const targetElemId = stmt.getValue(1).getId();
|
|
801
|
+
const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: `e${aspectIdentifier}` });
|
|
802
|
+
if (wasDeletedInSource)
|
|
803
|
+
this.importer.deleteElement(targetElemId);
|
|
410
804
|
}
|
|
411
805
|
});
|
|
412
806
|
}
|
|
@@ -433,24 +827,53 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
433
827
|
}
|
|
434
828
|
return targetElementProps;
|
|
435
829
|
}
|
|
830
|
+
// FIXME: this is a PoC, see if we minimize memory usage
|
|
831
|
+
_cacheSourceChanges() {
|
|
832
|
+
nodeAssert(this._changeSummaryIds && this._changeSummaryIds.length > 0, "should have changeset data by now");
|
|
833
|
+
this._hasElementChangedCache = new Set();
|
|
834
|
+
const query = `
|
|
835
|
+
SELECT
|
|
836
|
+
ic.ChangedInstance.Id AS InstId
|
|
837
|
+
FROM ecchange.change.InstanceChange ic
|
|
838
|
+
JOIN iModelChange.Changeset imc ON ic.Summary.Id=imc.Summary.Id
|
|
839
|
+
-- FIXME: do relationship entities also need this cache optimization?
|
|
840
|
+
WHERE ic.ChangedInstance.ClassId IS (BisCore.Element)
|
|
841
|
+
AND InVirtualSet(:changeSummaryIds, ic.Summary.Id)
|
|
842
|
+
-- ignore deleted, we take care of those in remapDeletedSourceEntities
|
|
843
|
+
-- include inserted since inserted code-colliding elements should be considered
|
|
844
|
+
-- a change so that the colliding element is exported to the target
|
|
845
|
+
AND ic.OpCode<>:opDelete
|
|
846
|
+
`;
|
|
847
|
+
// there is a single mega-query multi-join+coalescing hack that I used originally to get around
|
|
848
|
+
// only being able to run table.Changes() on one changeset at once, but sqlite only supports up to 64
|
|
849
|
+
// tables in a join. Need to talk to core about .Changes being able to take a set of changesets
|
|
850
|
+
// You can find this version in the `federation-guid-optimization-megaquery` branch
|
|
851
|
+
// I wouldn't use it unless we prove via profiling that it speeds things up significantly
|
|
852
|
+
// And even then let's first try scanning the raw changesets instead of applying them as these queries
|
|
853
|
+
// require
|
|
854
|
+
this.sourceDb.withPreparedStatement(query, (stmt) => {
|
|
855
|
+
stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
|
|
856
|
+
stmt.bindIdSet("changeSummaryIds", this._changeSummaryIds);
|
|
857
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
858
|
+
const instId = stmt.getValue(0).getId();
|
|
859
|
+
this._hasElementChangedCache.add(instId);
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
}
|
|
436
863
|
/** Returns true if a change within sourceElement is detected.
|
|
437
864
|
* @param sourceElement The Element from the source iModel
|
|
438
865
|
* @param targetElementId The Element from the target iModel to compare against.
|
|
439
866
|
* @note A subclass can override this method to provide custom change detection behavior.
|
|
440
867
|
*/
|
|
441
|
-
hasElementChanged(sourceElement,
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return lastModifiedTime !== sourceAspect.version;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
return true;
|
|
868
|
+
hasElementChanged(sourceElement, _targetElementId) {
|
|
869
|
+
if (this._sourceChangeDataState === "no-changes")
|
|
870
|
+
return false;
|
|
871
|
+
if (this._sourceChangeDataState === "unconnected")
|
|
872
|
+
return true;
|
|
873
|
+
nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
|
|
874
|
+
if (this._hasElementChangedCache === undefined)
|
|
875
|
+
this._cacheSourceChanges();
|
|
876
|
+
return this._hasElementChangedCache.has(sourceElement.id);
|
|
454
877
|
}
|
|
455
878
|
static transformCallbackFor(transformer, entity) {
|
|
456
879
|
if (entity instanceof core_backend_1.Element)
|
|
@@ -622,51 +1045,68 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
622
1045
|
targetElementId = this.context.findTargetElementId(sourceElement.id);
|
|
623
1046
|
targetElementProps = this.onTransformElement(sourceElement);
|
|
624
1047
|
}
|
|
1048
|
+
// if an existing remapping was not yet found, check by FederationGuid
|
|
1049
|
+
if (this.context.isBetweenIModels && !core_bentley_1.Id64.isValid(targetElementId) && sourceElement.federationGuid !== undefined) {
|
|
1050
|
+
targetElementId = this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
|
|
1051
|
+
if (core_bentley_1.Id64.isValid(targetElementId))
|
|
1052
|
+
this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found
|
|
1053
|
+
}
|
|
625
1054
|
// if an existing remapping was not yet found, check by Code as long as the CodeScope is valid (invalid means a missing reference so not worth checking)
|
|
626
|
-
if (!core_bentley_1.Id64.
|
|
1055
|
+
if (!core_bentley_1.Id64.isValid(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
|
|
627
1056
|
// respond the same way to undefined code value as the @see Code class, but don't use that class because is trims
|
|
628
1057
|
// whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
|
|
629
1058
|
targetElementProps.code.value = targetElementProps.code.value ?? "";
|
|
630
|
-
|
|
631
|
-
if (undefined !==
|
|
632
|
-
const
|
|
633
|
-
if (
|
|
1059
|
+
const maybeTargetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
|
|
1060
|
+
if (undefined !== maybeTargetElementId) {
|
|
1061
|
+
const maybeTargetElem = this.targetDb.elements.getElement(maybeTargetElementId);
|
|
1062
|
+
if (maybeTargetElem.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
|
|
1063
|
+
targetElementId = maybeTargetElementId;
|
|
634
1064
|
this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
|
|
635
1065
|
}
|
|
636
1066
|
else {
|
|
637
|
-
targetElementId = undefined;
|
|
638
1067
|
targetElementProps.code = core_common_1.Code.createEmpty(); // clear out invalid code
|
|
639
1068
|
}
|
|
640
1069
|
}
|
|
641
1070
|
}
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
if (!this.hasElementChanged(sourceElement, targetElementId)) {
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
1071
|
+
if (core_bentley_1.Id64.isValid(targetElementId) && !this.hasElementChanged(sourceElement, targetElementId))
|
|
1072
|
+
return;
|
|
648
1073
|
this.collectUnmappedReferences(sourceElement);
|
|
649
|
-
//
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1074
|
+
// targetElementId will be valid (indicating update) or undefined (indicating insert)
|
|
1075
|
+
targetElementProps.id
|
|
1076
|
+
= core_bentley_1.Id64.isValid(targetElementId)
|
|
1077
|
+
? targetElementId
|
|
1078
|
+
: undefined;
|
|
653
1079
|
if (!this._options.wasSourceIModelCopiedToTarget) {
|
|
654
1080
|
this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
|
|
655
1081
|
}
|
|
656
1082
|
this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
|
|
657
1083
|
// now that we've mapped this elem we can fix unmapped references to it
|
|
658
1084
|
this.resolvePendingReferences(sourceElement);
|
|
1085
|
+
// the transformer does not currently 'split' or 'join' any elements, therefore, it does not
|
|
1086
|
+
// insert external source aspects because federation guids are sufficient for this.
|
|
1087
|
+
// Other transformer subclasses must insert the appropriate aspect (as provided by a TBD API)
|
|
1088
|
+
// when splitting/joining elements
|
|
1089
|
+
// physical consolidation is an example of a 'joining' transform
|
|
1090
|
+
// FIXME: document this externally!
|
|
1091
|
+
// verify at finalization time that we don't lose provenance on new elements
|
|
1092
|
+
// make public and improve `initElementProvenance` API for usage by consolidators
|
|
659
1093
|
if (!this._options.noProvenance) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1094
|
+
let provenance = this._options.forceExternalSourceAspectProvenance || this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
|
|
1095
|
+
? undefined
|
|
1096
|
+
: sourceElement.federationGuid;
|
|
1097
|
+
if (!provenance) {
|
|
1098
|
+
const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
|
|
1099
|
+
const aspectId = this.queryScopeExternalSource(aspectProps).aspectId;
|
|
1100
|
+
if (aspectId === undefined) {
|
|
1101
|
+
aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
aspectProps.id = aspectId;
|
|
1105
|
+
this.provenanceDb.elements.updateAspect(aspectProps);
|
|
1106
|
+
}
|
|
1107
|
+
provenance = aspectProps;
|
|
667
1108
|
}
|
|
668
|
-
|
|
669
|
-
this.markLastProvenance(aspectProps, { isRelationship: false });
|
|
1109
|
+
this.markLastProvenance(provenance, { isRelationship: false });
|
|
670
1110
|
}
|
|
671
1111
|
}
|
|
672
1112
|
resolvePendingReferences(entity) {
|
|
@@ -704,7 +1144,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
704
1144
|
onDeleteModel(sourceModelId) {
|
|
705
1145
|
// It is possible and apparently occasionally sensical to delete a model without deleting its underlying element.
|
|
706
1146
|
// - If only the model is deleted, [[initFromExternalSourceAspects]] will have already remapped the underlying element since it still exists.
|
|
707
|
-
// - If both were deleted, [[
|
|
1147
|
+
// - If both were deleted, [[remapDeletedSourceEntities]] will find and remap the deleted element making this operation valid
|
|
708
1148
|
const targetModelId = this.context.findTargetElementId(sourceModelId);
|
|
709
1149
|
if (core_bentley_1.Id64.isValidId64(targetModelId)) {
|
|
710
1150
|
this.importer.deleteModel(targetModelId);
|
|
@@ -780,7 +1220,52 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
780
1220
|
* @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
|
|
781
1221
|
*/
|
|
782
1222
|
async processDeferredElements(_numRetries = 3) { }
|
|
1223
|
+
/** called at the end ([[finalizeTransformation]]) of a transformation,
|
|
1224
|
+
* updates the target scope element to say that transformation up through the
|
|
1225
|
+
* source's changeset has been performed.
|
|
1226
|
+
*/
|
|
1227
|
+
_updateSynchronizationVersion() {
|
|
1228
|
+
if (this._sourceChangeDataState !== "has-changes" && !this._isFirstSynchronization)
|
|
1229
|
+
return;
|
|
1230
|
+
nodeAssert(this._targetScopeProvenanceProps);
|
|
1231
|
+
const newVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
|
|
1232
|
+
if (this._options.isReverseSynchronization || this._isFirstSynchronization) {
|
|
1233
|
+
const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
|
|
1234
|
+
core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${newVersion}`);
|
|
1235
|
+
// FIXME: could technically just put a delimiter in the version field to avoid using json properties
|
|
1236
|
+
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = newVersion;
|
|
1237
|
+
}
|
|
1238
|
+
if (!this._options.isReverseSynchronization || this._isFirstSynchronization) {
|
|
1239
|
+
core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${newVersion}`);
|
|
1240
|
+
this._targetScopeProvenanceProps.version = newVersion;
|
|
1241
|
+
}
|
|
1242
|
+
if (this._isSynchronization) {
|
|
1243
|
+
(0, core_bentley_1.assert)(this.targetDb.changeset.index !== undefined && this._startingTargetChangesetIndex !== undefined, "_updateSynchronizationVersion was called without change history");
|
|
1244
|
+
const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
|
|
1245
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
|
|
1246
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
|
|
1247
|
+
const [syncChangesetsToClear, syncChangesetsToUpdate] = this._isReverseSynchronization
|
|
1248
|
+
? [jsonProps.pendingReverseSyncChangesetIndices, jsonProps.pendingSyncChangesetIndices]
|
|
1249
|
+
: [jsonProps.pendingSyncChangesetIndices, jsonProps.pendingReverseSyncChangesetIndices];
|
|
1250
|
+
// NOTE that as documented in [[processChanges]], this assumes that right after
|
|
1251
|
+
// transformation finalization, the work will be saved immediately, otherwise we've
|
|
1252
|
+
// just marked this changeset as a synchronization to ignore, and the user can add other
|
|
1253
|
+
// stuff to it which would break future synchronizations
|
|
1254
|
+
// FIXME: force save for the user to prevent that
|
|
1255
|
+
for (let i = this._startingTargetChangesetIndex + 1; i <= this.targetDb.changeset.index + 1; i++)
|
|
1256
|
+
syncChangesetsToUpdate.push(i);
|
|
1257
|
+
syncChangesetsToClear.length = 0;
|
|
1258
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `new pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
|
|
1259
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `new pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
|
|
1260
|
+
}
|
|
1261
|
+
this.provenanceDb.elements.updateAspect({
|
|
1262
|
+
...this._targetScopeProvenanceProps,
|
|
1263
|
+
jsonProperties: JSON.stringify(this._targetScopeProvenanceProps.jsonProperties),
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
// FIXME: is this necessary when manually using lowlevel transform APIs?
|
|
783
1267
|
finalizeTransformation() {
|
|
1268
|
+
this._updateSynchronizationVersion();
|
|
784
1269
|
if (this._partiallyCommittedEntities.size > 0) {
|
|
785
1270
|
core_bentley_1.Logger.logWarning(loggerCategory, [
|
|
786
1271
|
"The following elements were never fully resolved:",
|
|
@@ -792,6 +1277,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
792
1277
|
partiallyCommittedElem.forceComplete();
|
|
793
1278
|
}
|
|
794
1279
|
}
|
|
1280
|
+
// FIXME: make processAll have a try {} finally {} that cleans this up
|
|
1281
|
+
if (!this._options.noDetachChangeCache) {
|
|
1282
|
+
if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
|
|
1283
|
+
core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
|
|
1284
|
+
}
|
|
795
1285
|
}
|
|
796
1286
|
/** Imports all relationships that subclass from the specified base class.
|
|
797
1287
|
* @param baseRelClassFullName The specified base relationship class.
|
|
@@ -809,40 +1299,52 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
809
1299
|
* This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
|
|
810
1300
|
*/
|
|
811
1301
|
onExportRelationship(sourceRelationship) {
|
|
1302
|
+
const sourceFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.sourceId);
|
|
1303
|
+
const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
|
|
812
1304
|
const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
|
|
813
1305
|
const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
|
|
814
|
-
if (!this._options.noProvenance && core_bentley_1.Id64.
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1306
|
+
if (!this._options.noProvenance && core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
|
|
1307
|
+
let provenance = !this._options.forceExternalSourceAspectProvenance
|
|
1308
|
+
? sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`
|
|
1309
|
+
: undefined;
|
|
1310
|
+
if (!provenance) {
|
|
1311
|
+
const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
|
|
1312
|
+
aspectProps.id = this.queryScopeExternalSource(aspectProps).aspectId;
|
|
1313
|
+
if (undefined === aspectProps.id) {
|
|
1314
|
+
aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
|
|
1315
|
+
}
|
|
1316
|
+
provenance = aspectProps;
|
|
818
1317
|
}
|
|
819
|
-
(
|
|
820
|
-
this.markLastProvenance(aspectProps, { isRelationship: true });
|
|
1318
|
+
this.markLastProvenance(provenance, { isRelationship: true });
|
|
821
1319
|
}
|
|
822
1320
|
}
|
|
823
1321
|
/** Override of [IModelExportHandler.onDeleteRelationship]($transformer) that is called when [IModelExporter]($transformer) detects that a [Relationship]($backend) has been deleted from the source iModel.
|
|
824
1322
|
* This override propagates the delete to the target iModel via [IModelImporter.deleteRelationship]($transformer).
|
|
825
1323
|
*/
|
|
826
1324
|
onDeleteRelationship(sourceRelInstanceId) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
1325
|
+
nodeAssert(this._deletedSourceRelationshipData, "should be defined at initialization by now");
|
|
1326
|
+
const deletedRelData = this._deletedSourceRelationshipData.get(sourceRelInstanceId);
|
|
1327
|
+
if (!deletedRelData) {
|
|
1328
|
+
// this can occur if both the source and target deleted it
|
|
1329
|
+
core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const relArg = deletedRelData.relId ?? {
|
|
1333
|
+
sourceId: deletedRelData.sourceIdInTarget,
|
|
1334
|
+
targetId: deletedRelData.targetIdInTarget,
|
|
1335
|
+
};
|
|
1336
|
+
//
|
|
1337
|
+
// FIXME: make importer.deleteRelationship not need full props
|
|
1338
|
+
const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
|
|
1339
|
+
if (targetRelationship) {
|
|
1340
|
+
this.importer.deleteRelationship(targetRelationship.toJSON());
|
|
1341
|
+
}
|
|
1342
|
+
if (deletedRelData.provenanceAspectId) {
|
|
1343
|
+
this.provenanceDb.elements.deleteAspect(deletedRelData.provenanceAspectId);
|
|
1344
|
+
}
|
|
844
1345
|
}
|
|
845
1346
|
/** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
|
|
1347
|
+
* @deprecated
|
|
846
1348
|
* @see processChanges
|
|
847
1349
|
* @note This method is called from [[processAll]] and is not needed by [[processChanges]], so it only needs to be called directly when processing a subset of an iModel.
|
|
848
1350
|
* @throws [[IModelError]] If the required provenance information is not available to detect deletes.
|
|
@@ -852,13 +1354,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
852
1354
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
|
|
853
1355
|
}
|
|
854
1356
|
const aspectDeleteIds = [];
|
|
855
|
-
const sql = `
|
|
1357
|
+
const sql = `
|
|
1358
|
+
SELECT ECInstanceId, Identifier, JsonProperties
|
|
1359
|
+
FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect
|
|
1360
|
+
WHERE aspect.Scope.Id=:scopeId
|
|
1361
|
+
AND aspect.Kind=:kind
|
|
1362
|
+
`;
|
|
856
1363
|
await this.targetDb.withPreparedStatement(sql, async (statement) => {
|
|
857
1364
|
statement.bindId("scopeId", this.targetScopeElementId);
|
|
858
1365
|
statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
|
|
859
1366
|
while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
|
|
860
1367
|
const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
|
|
861
1368
|
if (undefined === this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
|
|
1369
|
+
// FIXME: make sure matches new provenance-based method
|
|
1370
|
+
// FIXME: use sql JSON_EXTRACT
|
|
862
1371
|
const json = JSON.parse(statement.getValue(2).getString());
|
|
863
1372
|
if (undefined !== json.targetRelInstanceId) {
|
|
864
1373
|
const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
|
|
@@ -880,6 +1389,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
880
1389
|
const targetRelationshipProps = sourceRelationship.toJSON();
|
|
881
1390
|
targetRelationshipProps.sourceId = this.context.findTargetElementId(sourceRelationship.sourceId);
|
|
882
1391
|
targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
|
|
1392
|
+
// TODO: move to cloneRelationship in IModelCloneContext
|
|
883
1393
|
sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
|
|
884
1394
|
if ((core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType) && ("Id" === propertyMetaData.extendedType)) {
|
|
885
1395
|
targetRelationshipProps[propertyName] = this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
|
|
@@ -1041,26 +1551,85 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1041
1551
|
return this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
|
|
1042
1552
|
}
|
|
1043
1553
|
/**
|
|
1044
|
-
* Initialize prerequisites of processing, you must initialize with an [[
|
|
1045
|
-
* are intending process changes, but prefer using [[processChanges]]
|
|
1046
|
-
* Called by all `process*` functions implicitly.
|
|
1554
|
+
* Initialize prerequisites of processing, you must initialize with an [[InitOptions]] if you
|
|
1555
|
+
* are intending to process changes, but prefer using [[processChanges]] explicitly since it calls this.
|
|
1556
|
+
* @note Called by all `process*` functions implicitly.
|
|
1047
1557
|
* Overriders must call `super.initialize()` first
|
|
1048
1558
|
*/
|
|
1049
1559
|
async initialize(args) {
|
|
1050
1560
|
if (this._initialized)
|
|
1051
1561
|
return;
|
|
1052
1562
|
await this.context.initialize();
|
|
1563
|
+
await this._tryInitChangesetData(args);
|
|
1053
1564
|
// eslint-disable-next-line deprecation/deprecation
|
|
1054
1565
|
await this.initFromExternalSourceAspects(args);
|
|
1055
1566
|
this._initialized = true;
|
|
1056
1567
|
}
|
|
1568
|
+
async _tryInitChangesetData(args) {
|
|
1569
|
+
if (!args || this.sourceDb.iTwinId === undefined || this.sourceDb.changeset.index === undefined) {
|
|
1570
|
+
this._sourceChangeDataState = "unconnected";
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
|
|
1574
|
+
if (noChanges) {
|
|
1575
|
+
this._sourceChangeDataState = "no-changes";
|
|
1576
|
+
this._changeSummaryIds = [];
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
// NOTE: that we do NOT download the changesummary for the last transformed version, we want
|
|
1580
|
+
// to ignore those already processed changes
|
|
1581
|
+
const startChangesetIndexOrId = args.startChangeset?.index
|
|
1582
|
+
?? args.startChangeset?.id
|
|
1583
|
+
?? this._synchronizationVersion.index + 1;
|
|
1584
|
+
const endChangesetId = this.sourceDb.changeset.id;
|
|
1585
|
+
const [startChangesetIndex, endChangesetIndex] = await Promise.all(([startChangesetIndexOrId, endChangesetId])
|
|
1586
|
+
.map(async (indexOrId) => typeof indexOrId === "number"
|
|
1587
|
+
? indexOrId
|
|
1588
|
+
: core_backend_1.IModelHost.hubAccess
|
|
1589
|
+
.queryChangeset({
|
|
1590
|
+
iModelId: this.sourceDb.iModelId,
|
|
1591
|
+
// eslint-disable-next-line deprecation/deprecation
|
|
1592
|
+
changeset: { id: indexOrId },
|
|
1593
|
+
accessToken: args.accessToken,
|
|
1594
|
+
})
|
|
1595
|
+
.then((changeset) => changeset.index)));
|
|
1596
|
+
const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
|
|
1597
|
+
// FIXME: add an option to ignore this check
|
|
1598
|
+
if (!this._options.ignoreMissingChangesetsInSynchronizations
|
|
1599
|
+
&& startChangesetIndex !== this._synchronizationVersion.index + 1
|
|
1600
|
+
&& this._synchronizationVersion.index !== -1) {
|
|
1601
|
+
throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},`
|
|
1602
|
+
+ " startChangesetId should be"
|
|
1603
|
+
+ " exactly the first changeset *after* the previous synchronization to not miss data."
|
|
1604
|
+
+ ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}`
|
|
1605
|
+
+ ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'`
|
|
1606
|
+
+ ` which is changeset #${this._synchronizationVersion.index}. The transformer expected`
|
|
1607
|
+
+ ` #${this._synchronizationVersion.index + 1}.`);
|
|
1608
|
+
}
|
|
1609
|
+
nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
|
|
1610
|
+
const changesetsToSkip = this._isReverseSynchronization
|
|
1611
|
+
? this._targetScopeProvenanceProps.jsonProperties.pendingReverseSyncChangesetIndices
|
|
1612
|
+
: this._targetScopeProvenanceProps.jsonProperties.pendingSyncChangesetIndices;
|
|
1613
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
|
|
1614
|
+
this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
|
|
1615
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
|
|
1616
|
+
for (const [first, end] of this._changesetRanges) {
|
|
1617
|
+
this._changeSummaryIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
|
|
1618
|
+
accessToken: args.accessToken,
|
|
1619
|
+
iModelId: this.sourceDb.iModelId,
|
|
1620
|
+
iTwinId: this.sourceDb.iTwinId,
|
|
1621
|
+
range: { first, end },
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
|
|
1625
|
+
this._sourceChangeDataState = "has-changes";
|
|
1626
|
+
}
|
|
1057
1627
|
/** Export everything from the source iModel and import the transformed entities into the target iModel.
|
|
1058
1628
|
* @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
|
|
1059
1629
|
*/
|
|
1060
1630
|
async processAll() {
|
|
1061
|
-
core_bentley_1.Logger.logTrace(loggerCategory, "processAll()");
|
|
1062
1631
|
this.logSettings();
|
|
1063
|
-
this.
|
|
1632
|
+
this.initScopeProvenance();
|
|
1064
1633
|
await this.initialize();
|
|
1065
1634
|
await this.exporter.exportCodeSpecs();
|
|
1066
1635
|
await this.exporter.exportFonts();
|
|
@@ -1080,12 +1649,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1080
1649
|
this.finalizeTransformation();
|
|
1081
1650
|
}
|
|
1082
1651
|
markLastProvenance(sourceAspect, { isRelationship = false }) {
|
|
1083
|
-
this._lastProvenanceEntityInfo
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1652
|
+
this._lastProvenanceEntityInfo
|
|
1653
|
+
= typeof sourceAspect === "string"
|
|
1654
|
+
? sourceAspect
|
|
1655
|
+
: {
|
|
1656
|
+
entityId: sourceAspect.element.id,
|
|
1657
|
+
aspectId: sourceAspect.id,
|
|
1658
|
+
aspectVersion: sourceAspect.version ?? "",
|
|
1659
|
+
aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
|
|
1660
|
+
};
|
|
1089
1661
|
}
|
|
1090
1662
|
/**
|
|
1091
1663
|
* Load the state of the active transformation from an open SQLiteDb
|
|
@@ -1097,17 +1669,35 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1097
1669
|
const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
|
|
1098
1670
|
if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
|
|
1099
1671
|
throw Error("expected row when getting lastProvenanceEntityId from target state table");
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1672
|
+
const entityId = stmt.getValueString(0);
|
|
1673
|
+
const isGuidOrGuidPair = entityId.includes("-");
|
|
1674
|
+
return isGuidOrGuidPair
|
|
1675
|
+
? entityId
|
|
1676
|
+
: {
|
|
1677
|
+
entityId,
|
|
1678
|
+
aspectId: stmt.getValueString(1),
|
|
1679
|
+
aspectVersion: stmt.getValueString(2),
|
|
1680
|
+
aspectKind: stmt.getValueString(3),
|
|
1681
|
+
};
|
|
1106
1682
|
});
|
|
1107
|
-
|
|
1108
|
-
//
|
|
1109
|
-
|
|
1683
|
+
/*
|
|
1684
|
+
// TODO: maybe save transformer state resumption state based on target changset and require calls
|
|
1685
|
+
// to saveChanges
|
|
1686
|
+
if () {
|
|
1687
|
+
const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
|
|
1688
|
+
const isRelProvenance = targetFedGuid !== undefined;
|
|
1689
|
+
const instanceId = isRelProvenance
|
|
1690
|
+
? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
|
|
1691
|
+
: "";
|
|
1692
|
+
//const classId =
|
|
1693
|
+
if (isRelProvenance) {
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
*/
|
|
1697
|
+
const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
|
|
1698
|
+
// ignore provenance check if it's null since we can't bind those ids
|
|
1110
1699
|
!core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
|
|
1700
|
+
!core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
|
|
1111
1701
|
this.provenanceDb.withPreparedStatement(`
|
|
1112
1702
|
SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
|
|
1113
1703
|
WHERE Scope.Id=:scopeId
|
|
@@ -1148,6 +1738,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1148
1738
|
this.context.loadStateFromDb(db);
|
|
1149
1739
|
this.importer.loadStateFromJson(state.importerState);
|
|
1150
1740
|
this.exporter.loadStateFromJson(state.exporterState);
|
|
1741
|
+
this._elementsWithExplicitlyTrackedProvenance = core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
|
|
1151
1742
|
this.loadAdditionalStateJson(state.additionalState);
|
|
1152
1743
|
}
|
|
1153
1744
|
/**
|
|
@@ -1195,6 +1786,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1195
1786
|
const jsonState = {
|
|
1196
1787
|
transformerClass: this.constructor.name,
|
|
1197
1788
|
options: this._options,
|
|
1789
|
+
explicitlyTrackedElements: core_bentley_1.CompressedId64Set.compressSet(this._elementsWithExplicitlyTrackedProvenance),
|
|
1198
1790
|
importerState: this.importer.saveStateToJson(),
|
|
1199
1791
|
exporterState: this.exporter.saveStateToJson(),
|
|
1200
1792
|
additionalState: this.getAdditionalStateJson(),
|
|
@@ -1204,8 +1796,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1204
1796
|
throw Error("Failed to create the js state table in the state database");
|
|
1205
1797
|
if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`
|
|
1206
1798
|
CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
|
|
1207
|
-
--
|
|
1799
|
+
-- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
|
|
1208
1800
|
entityId TEXT,
|
|
1801
|
+
-- the following are only valid if the above entityId is a hex id representation
|
|
1209
1802
|
aspectId TEXT,
|
|
1210
1803
|
aspectVersion TEXT,
|
|
1211
1804
|
aspectKind TEXT
|
|
@@ -1219,10 +1812,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1219
1812
|
throw Error("Failed to insert options into the state database");
|
|
1220
1813
|
});
|
|
1221
1814
|
db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
|
|
1222
|
-
|
|
1223
|
-
stmt.bindString(
|
|
1224
|
-
stmt.bindString(
|
|
1225
|
-
stmt.bindString(
|
|
1815
|
+
const lastProvenanceEntityInfo = this._lastProvenanceEntityInfo;
|
|
1816
|
+
stmt.bindString(1, lastProvenanceEntityInfo?.entityId ?? this._lastProvenanceEntityInfo);
|
|
1817
|
+
stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
|
|
1818
|
+
stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
|
|
1819
|
+
stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
|
|
1226
1820
|
if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
|
|
1227
1821
|
throw Error("Failed to insert options into the state database");
|
|
1228
1822
|
});
|
|
@@ -1250,31 +1844,50 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1250
1844
|
db.closeDb();
|
|
1251
1845
|
}
|
|
1252
1846
|
}
|
|
1253
|
-
async processChanges(
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
this.validateScopeProvenance();
|
|
1257
|
-
const options = typeof accessTokenOrArgs === "string"
|
|
1847
|
+
async processChanges(optionsOrAccessToken, startChangesetId) {
|
|
1848
|
+
this._isSynchronization = true;
|
|
1849
|
+
const args = typeof optionsOrAccessToken === "string"
|
|
1258
1850
|
? {
|
|
1259
|
-
accessToken:
|
|
1260
|
-
startChangeset: startChangesetId
|
|
1261
|
-
|
|
1851
|
+
accessToken: optionsOrAccessToken,
|
|
1852
|
+
startChangeset: startChangesetId
|
|
1853
|
+
? { id: startChangesetId }
|
|
1854
|
+
: this.sourceDb.changeset,
|
|
1262
1855
|
}
|
|
1263
|
-
:
|
|
1264
|
-
|
|
1265
|
-
|
|
1856
|
+
: optionsOrAccessToken;
|
|
1857
|
+
this.logSettings();
|
|
1858
|
+
// FIXME: we used to validateScopeProvenance... does initing it cover that?
|
|
1859
|
+
this.initScopeProvenance();
|
|
1860
|
+
await this.initialize(args);
|
|
1861
|
+
// must wait for initialization of synchronization provenance data
|
|
1862
|
+
const changeArgs = this._changesetRanges
|
|
1863
|
+
? { changesetRanges: this._changesetRanges }
|
|
1864
|
+
: args.startChangeset
|
|
1865
|
+
? { startChangeset: args.startChangeset }
|
|
1866
|
+
: { startChangeset: { index: this._synchronizationVersion.index + 1 } };
|
|
1867
|
+
await this.exporter.exportChanges({ accessToken: args.accessToken, ...changeArgs });
|
|
1266
1868
|
await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
|
|
1267
1869
|
if (this._options.optimizeGeometry)
|
|
1268
1870
|
this.importer.optimizeGeometry(this._options.optimizeGeometry);
|
|
1269
1871
|
this.importer.computeProjectExtents();
|
|
1270
1872
|
this.finalizeTransformation();
|
|
1271
1873
|
}
|
|
1874
|
+
/** Combine an array of source elements into a single target element.
|
|
1875
|
+
* All source and target elements must be created before calling this method.
|
|
1876
|
+
* The "combine" operation is a remap and no properties from the source elements will be exported into the target
|
|
1877
|
+
* and provenance will be explicitly tracked by ExternalSourceAspects
|
|
1878
|
+
*/
|
|
1879
|
+
combineElements(sourceElementIds, targetElementId) {
|
|
1880
|
+
for (const elementId of sourceElementIds) {
|
|
1881
|
+
this.context.remapElement(elementId, targetElementId);
|
|
1882
|
+
this._elementsWithExplicitlyTrackedProvenance.add(elementId);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1272
1885
|
}
|
|
1273
|
-
exports.IModelTransformer = IModelTransformer;
|
|
1274
1886
|
/** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
|
|
1275
1887
|
IModelTransformer.jsStateTable = "TransformerJsState";
|
|
1276
1888
|
/** @internal the name of the table where the target state heuristics is serialized in transformer state dumps */
|
|
1277
1889
|
IModelTransformer.lastProvenanceEntityInfoTable = "LastProvenanceEntityInfo";
|
|
1890
|
+
exports.IModelTransformer = IModelTransformer;
|
|
1278
1891
|
/** IModelTransformer that clones the contents of a template model.
|
|
1279
1892
|
* @beta
|
|
1280
1893
|
*/
|
|
@@ -1299,6 +1912,7 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1299
1912
|
* @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
|
|
1300
1913
|
*/
|
|
1301
1914
|
async placeTemplate3d(sourceTemplateModelId, targetModelId, placement) {
|
|
1915
|
+
await this.initialize();
|
|
1302
1916
|
this.context.remapElement(sourceTemplateModelId, targetModelId);
|
|
1303
1917
|
this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(placement.origin, placement.angles.toMatrix3d());
|
|
1304
1918
|
this._sourceIdToTargetIdMap = new Map();
|
|
@@ -1319,6 +1933,7 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1319
1933
|
* @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
|
|
1320
1934
|
*/
|
|
1321
1935
|
async placeTemplate2d(sourceTemplateModelId, targetModelId, placement) {
|
|
1936
|
+
await this.initialize();
|
|
1322
1937
|
this.context.remapElement(sourceTemplateModelId, targetModelId);
|
|
1323
1938
|
this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(core_geometry_1.Point3d.createFrom(placement.origin), placement.rotation);
|
|
1324
1939
|
this._sourceIdToTargetIdMap = new Map();
|
|
@@ -1355,16 +1970,12 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1355
1970
|
const targetElementProps = super.onTransformElement(sourceElement);
|
|
1356
1971
|
targetElementProps.federationGuid = core_bentley_1.Guid.createValue(); // clone from template should create a new federationGuid
|
|
1357
1972
|
targetElementProps.code = core_common_1.Code.createEmpty(); // clone from template should not maintain codes
|
|
1358
|
-
if (sourceElement instanceof core_backend_1.
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
targetElementProps.placement = placement;
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
else if (sourceElement instanceof core_backend_1.GeometricElement2d) {
|
|
1366
|
-
const placement = core_common_1.Placement2d.fromJSON(targetElementProps.placement);
|
|
1973
|
+
if (sourceElement instanceof core_backend_1.GeometricElement) {
|
|
1974
|
+
const is3d = sourceElement instanceof core_backend_1.GeometricElement3d;
|
|
1975
|
+
const placementClass = is3d ? core_common_1.Placement3d : core_common_1.Placement2d;
|
|
1976
|
+
const placement = (placementClass).fromJSON(targetElementProps.placement);
|
|
1367
1977
|
if (placement.isValid) {
|
|
1978
|
+
nodeAssert(this._transform3d);
|
|
1368
1979
|
placement.multiplyTransform(this._transform3d);
|
|
1369
1980
|
targetElementProps.placement = placement;
|
|
1370
1981
|
}
|
|
@@ -1374,4 +1985,17 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1374
1985
|
}
|
|
1375
1986
|
}
|
|
1376
1987
|
exports.TemplateModelCloner = TemplateModelCloner;
|
|
1988
|
+
function queryElemFedGuid(db, elemId) {
|
|
1989
|
+
return db.withPreparedStatement(`
|
|
1990
|
+
SELECT FederationGuid
|
|
1991
|
+
FROM bis.Element
|
|
1992
|
+
WHERE ECInstanceId=?
|
|
1993
|
+
`, (stmt) => {
|
|
1994
|
+
stmt.bindId(1, elemId);
|
|
1995
|
+
(0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
|
|
1996
|
+
const result = stmt.getValue(0).getGuid();
|
|
1997
|
+
(0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_DONE);
|
|
1998
|
+
return result;
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
1377
2001
|
//# sourceMappingURL=IModelTransformer.js.map
|