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