@itwin/imodel-transformer 0.3.2-dev.0 → 0.3.18-fedguidopt.6
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 +1 -9
- 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/BranchProvenanceInitializer.d.ts +10 -3
- package/lib/cjs/BranchProvenanceInitializer.d.ts.map +1 -1
- package/lib/cjs/BranchProvenanceInitializer.js +82 -10
- package/lib/cjs/BranchProvenanceInitializer.js.map +1 -1
- package/lib/cjs/IModelCloneContext.d.ts.map +1 -1
- package/lib/cjs/IModelCloneContext.js +0 -2
- package/lib/cjs/IModelCloneContext.js.map +1 -1
- package/lib/cjs/IModelExporter.d.ts +31 -8
- package/lib/cjs/IModelExporter.d.ts.map +1 -1
- package/lib/cjs/IModelExporter.js +81 -35
- package/lib/cjs/IModelExporter.js.map +1 -1
- package/lib/cjs/IModelTransformer.d.ts +162 -28
- package/lib/cjs/IModelTransformer.d.ts.map +1 -1
- package/lib/cjs/IModelTransformer.js +917 -261
- 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];
|
|
@@ -112,18 +119,46 @@ 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;
|
|
117
128
|
/** Set of entity keys which were not exported and don't need to be tracked for pending reference resolution.
|
|
118
129
|
* @note Currently only tracks elements which were not exported.
|
|
119
130
|
*/
|
|
120
131
|
this._skippedEntities = new Set();
|
|
132
|
+
// FIXME: add test using this
|
|
133
|
+
/**
|
|
134
|
+
* Previously the transformer would insert provenance always pointing to the "target" relationship.
|
|
135
|
+
* It should (and now by default does) instead insert provenance pointing to the provenanceSource
|
|
136
|
+
* SEE: https://github.com/iTwin/imodel-transformer/issues/54
|
|
137
|
+
* This exists only to facilitate testing that the transformer can handle the older, flawed method
|
|
138
|
+
*/
|
|
139
|
+
this._forceOldRelationshipProvenanceMethod = false;
|
|
140
|
+
/** NOTE: the json properties must be converted to string before insertion */
|
|
141
|
+
this._targetScopeProvenanceProps = undefined;
|
|
142
|
+
/**
|
|
143
|
+
* Index of the changeset that the transformer was at when the transformation begins (was constructed).
|
|
144
|
+
* Used to determine at the end which changesets were part of a synchronization.
|
|
145
|
+
*/
|
|
146
|
+
this._startingChangesetIndices = undefined;
|
|
147
|
+
this._cachedSynchronizationVersion = undefined;
|
|
148
|
+
this._targetClassNameToClassIdCache = new Map();
|
|
149
|
+
// if undefined, it can be initialized by calling [[this._cacheSourceChanges]]
|
|
150
|
+
this._hasElementChangedCache = undefined;
|
|
151
|
+
this._deletedSourceRelationshipData = undefined;
|
|
121
152
|
this._yieldManager = new core_bentley_1.YieldManager();
|
|
122
153
|
/** The directory where schemas will be exported, a random temporary directory */
|
|
123
154
|
this._schemaExportDir = path.join(core_backend_1.KnownLocations.tmpdir, core_bentley_1.Guid.createValue());
|
|
124
155
|
this._longNamedSchemasMap = new Map();
|
|
125
156
|
/** state to prevent reinitialization, @see [[initialize]] */
|
|
126
157
|
this._initialized = false;
|
|
158
|
+
/** length === 0 when _changeDataState = "no-change", length > 0 means "has-changes", otherwise undefined */
|
|
159
|
+
this._changeSummaryIds = undefined;
|
|
160
|
+
this._sourceChangeDataState = "uninited";
|
|
161
|
+
/** previous provenance, either a federation guid, a `${sourceFedGuid}/${targetFedGuid}` pair, or required aspect props */
|
|
127
162
|
this._lastProvenanceEntityInfo = nullLastProvenanceEntityInfo;
|
|
128
163
|
// initialize IModelTransformOptions
|
|
129
164
|
this._options = {
|
|
@@ -171,6 +206,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
171
206
|
this.targetDb = this.importer.targetDb;
|
|
172
207
|
// create the IModelCloneContext, it must be initialized later
|
|
173
208
|
this.context = new IModelCloneContext_1.IModelCloneContext(this.sourceDb, this.targetDb);
|
|
209
|
+
if (this.sourceDb.isBriefcase && this.targetDb.isBriefcase) {
|
|
210
|
+
nodeAssert(this.sourceDb.changeset.index !== undefined && this.targetDb.changeset.index !== undefined, "database has no changeset index");
|
|
211
|
+
this._startingChangesetIndices = {
|
|
212
|
+
target: this.targetDb.changeset.index,
|
|
213
|
+
source: this.sourceDb.changeset.index,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
174
216
|
}
|
|
175
217
|
/** Dispose any native resources associated with this IModelTransformer. */
|
|
176
218
|
dispose() {
|
|
@@ -219,9 +261,32 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
219
261
|
};
|
|
220
262
|
return aspectProps;
|
|
221
263
|
}
|
|
264
|
+
static initRelationshipProvenanceOptions(sourceRelInstanceId, targetRelInstanceId, args) {
|
|
265
|
+
const provenanceDb = args.isReverseSynchronization ? args.sourceDb : args.targetDb;
|
|
266
|
+
const aspectIdentifier = args.isReverseSynchronization ? targetRelInstanceId : sourceRelInstanceId;
|
|
267
|
+
const provenanceRelInstanceId = args.isReverseSynchronization ? sourceRelInstanceId : targetRelInstanceId;
|
|
268
|
+
const elementId = provenanceDb.withPreparedStatement("SELECT SourceECInstanceId FROM bis.ElementRefersToElements WHERE ECInstanceId=?", (stmt) => {
|
|
269
|
+
stmt.bindId(1, provenanceRelInstanceId);
|
|
270
|
+
nodeAssert(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
|
|
271
|
+
return stmt.getValue(0).getId();
|
|
272
|
+
});
|
|
273
|
+
const jsonProperties = args.forceOldRelationshipProvenanceMethod
|
|
274
|
+
? { targetRelInstanceId }
|
|
275
|
+
: { provenanceRelInstanceId };
|
|
276
|
+
const aspectProps = {
|
|
277
|
+
classFullName: core_backend_1.ExternalSourceAspect.classFullName,
|
|
278
|
+
element: { id: elementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
|
|
279
|
+
scope: { id: args.targetScopeElementId },
|
|
280
|
+
identifier: aspectIdentifier,
|
|
281
|
+
kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
|
|
282
|
+
jsonProperties: JSON.stringify(jsonProperties),
|
|
283
|
+
};
|
|
284
|
+
return aspectProps;
|
|
285
|
+
}
|
|
222
286
|
/** Create an ExternalSourceAspectProps in a standard way for an Element in an iModel --> iModel transformation. */
|
|
223
287
|
initElementProvenance(sourceElementId, targetElementId) {
|
|
224
288
|
return IModelTransformer.initElementProvenanceOptions(sourceElementId, targetElementId, {
|
|
289
|
+
// FIXME: deprecate isReverseSync option and instead detect from targetScopeElement provenance
|
|
225
290
|
isReverseSynchronization: !!this._options.isReverseSynchronization,
|
|
226
291
|
targetScopeElementId: this.targetScopeElementId,
|
|
227
292
|
sourceDb: this.sourceDb,
|
|
@@ -233,32 +298,73 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
233
298
|
* The ECInstanceId of the relationship in the target iModel will be stored in the JsonProperties of the ExternalSourceAspect.
|
|
234
299
|
*/
|
|
235
300
|
initRelationshipProvenance(sourceRelationship, targetRelInstanceId) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
identifier: aspectIdentifier,
|
|
244
|
-
kind: core_backend_1.ExternalSourceAspect.Kind.Relationship,
|
|
245
|
-
jsonProperties: JSON.stringify({ targetRelInstanceId }),
|
|
246
|
-
};
|
|
247
|
-
aspectProps.id = this.queryExternalSourceAspectId(aspectProps);
|
|
248
|
-
return aspectProps;
|
|
301
|
+
return IModelTransformer.initRelationshipProvenanceOptions(sourceRelationship.id, targetRelInstanceId, {
|
|
302
|
+
sourceDb: this.sourceDb,
|
|
303
|
+
targetDb: this.targetDb,
|
|
304
|
+
isReverseSynchronization: !!this._options.isReverseSynchronization,
|
|
305
|
+
targetScopeElementId: this.targetScopeElementId,
|
|
306
|
+
forceOldRelationshipProvenanceMethod: this._forceOldRelationshipProvenanceMethod,
|
|
307
|
+
});
|
|
249
308
|
}
|
|
250
|
-
|
|
309
|
+
/** the changeset in the scoping element's source version found for this transformation
|
|
310
|
+
* @note: the version depends on whether this is a reverse synchronization or not, as
|
|
311
|
+
* it is stored separately for both synchronization directions
|
|
312
|
+
* @note: empty string and -1 for changeset and index if it has never been transformed
|
|
313
|
+
*/
|
|
314
|
+
get _synchronizationVersion() {
|
|
315
|
+
if (!this._cachedSynchronizationVersion) {
|
|
316
|
+
nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps was not set yet");
|
|
317
|
+
const version = this._options.isReverseSynchronization
|
|
318
|
+
? this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion
|
|
319
|
+
: this._targetScopeProvenanceProps.version;
|
|
320
|
+
nodeAssert(version !== undefined, "no version contained in target scope");
|
|
321
|
+
const [id, index] = version === ""
|
|
322
|
+
? ["", -1]
|
|
323
|
+
: version.split(";");
|
|
324
|
+
this._cachedSynchronizationVersion = { index: Number(index), id };
|
|
325
|
+
nodeAssert(!Number.isNaN(this._cachedSynchronizationVersion.index), "bad parse: invalid index in version");
|
|
326
|
+
}
|
|
327
|
+
return this._cachedSynchronizationVersion;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Make sure there are no conflicting other scope-type external source aspects on the *target scope element*,
|
|
331
|
+
* If there are none at all, insert one, then this must be a first synchronization.
|
|
332
|
+
* @returns the last synced version (changesetId) on the target scope's external source aspect,
|
|
333
|
+
* if this was a [BriefcaseDb]($backend)
|
|
334
|
+
*/
|
|
335
|
+
initScopeProvenance() {
|
|
251
336
|
const aspectProps = {
|
|
337
|
+
id: undefined,
|
|
338
|
+
version: undefined,
|
|
252
339
|
classFullName: core_backend_1.ExternalSourceAspect.classFullName,
|
|
253
340
|
element: { id: this.targetScopeElementId, relClassName: core_backend_1.ElementOwnsExternalSourceAspects.classFullName },
|
|
254
341
|
scope: { id: core_common_1.IModel.rootSubjectId },
|
|
255
|
-
identifier: this.
|
|
342
|
+
identifier: this.provenanceSourceDb.iModelId,
|
|
256
343
|
kind: core_backend_1.ExternalSourceAspect.Kind.Scope,
|
|
344
|
+
jsonProperties: undefined,
|
|
257
345
|
};
|
|
258
|
-
|
|
346
|
+
// FIXME: handle older transformed iModels which do NOT have the version
|
|
347
|
+
// or reverseSyncVersion set correctly
|
|
348
|
+
const externalSource = this.queryScopeExternalSource(aspectProps, { getJsonProperties: true }); // this query includes "identifier"
|
|
349
|
+
aspectProps.id = externalSource.aspectId;
|
|
350
|
+
aspectProps.version = externalSource.version;
|
|
351
|
+
aspectProps.jsonProperties = externalSource.jsonProperties ? JSON.parse(externalSource.jsonProperties) : {};
|
|
259
352
|
if (undefined === aspectProps.id) {
|
|
353
|
+
aspectProps.version = ""; // empty since never before transformed. Will be updated in [[finalizeTransformation]]
|
|
354
|
+
aspectProps.jsonProperties = {
|
|
355
|
+
pendingReverseSyncChangesetIndices: [],
|
|
356
|
+
pendingSyncChangesetIndices: [],
|
|
357
|
+
reverseSyncVersion: "", // empty since never before transformed. Will be updated in first reverse sync
|
|
358
|
+
};
|
|
260
359
|
// this query does not include "identifier" to find possible conflicts
|
|
261
|
-
const sql = `
|
|
360
|
+
const sql = `
|
|
361
|
+
SELECT ECInstanceId
|
|
362
|
+
FROM ${core_backend_1.ExternalSourceAspect.classFullName}
|
|
363
|
+
WHERE Element.Id=:elementId
|
|
364
|
+
AND Scope.Id=:scopeId
|
|
365
|
+
AND Kind=:kind
|
|
366
|
+
LIMIT 1
|
|
367
|
+
`;
|
|
262
368
|
const hasConflictingScope = this.provenanceDb.withPreparedStatement(sql, (statement) => {
|
|
263
369
|
statement.bindId("elementId", aspectProps.element.id);
|
|
264
370
|
statement.bindId("scopeId", aspectProps.scope.id); // this scope.id can never be invalid, we create it above
|
|
@@ -269,46 +375,128 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
269
375
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidId, "Provenance scope conflict");
|
|
270
376
|
}
|
|
271
377
|
if (!this._options.noProvenance) {
|
|
272
|
-
this.provenanceDb.elements.insertAspect(
|
|
378
|
+
this.provenanceDb.elements.insertAspect({
|
|
379
|
+
...aspectProps,
|
|
380
|
+
jsonProperties: JSON.stringify(aspectProps.jsonProperties),
|
|
381
|
+
});
|
|
273
382
|
}
|
|
274
383
|
}
|
|
384
|
+
this._targetScopeProvenanceProps = aspectProps;
|
|
275
385
|
}
|
|
276
|
-
|
|
277
|
-
|
|
386
|
+
/**
|
|
387
|
+
* @returns the id and version of an aspect with the given element, scope, kind, and identifier
|
|
388
|
+
* May also return a reverseSyncVersion from json properties if requested
|
|
389
|
+
*/
|
|
390
|
+
queryScopeExternalSource(aspectProps, { getJsonProperties = false } = {}) {
|
|
391
|
+
const sql = `
|
|
392
|
+
SELECT ECInstanceId, Version
|
|
393
|
+
${getJsonProperties ? ", JsonProperties" : ""}
|
|
394
|
+
FROM ${core_backend_1.ExternalSourceAspect.classFullName}
|
|
395
|
+
WHERE Element.Id=:elementId
|
|
396
|
+
AND Scope.Id=:scopeId
|
|
397
|
+
AND Kind=:kind
|
|
398
|
+
AND Identifier=:identifier
|
|
399
|
+
LIMIT 1
|
|
400
|
+
`;
|
|
401
|
+
const emptyResult = { aspectId: undefined, version: undefined, jsonProperties: undefined };
|
|
278
402
|
return this.provenanceDb.withPreparedStatement(sql, (statement) => {
|
|
279
403
|
statement.bindId("elementId", aspectProps.element.id);
|
|
280
404
|
if (aspectProps.scope === undefined)
|
|
281
|
-
return
|
|
405
|
+
return emptyResult; // return undefined instead of binding an invalid id
|
|
282
406
|
statement.bindId("scopeId", aspectProps.scope.id);
|
|
283
407
|
statement.bindString("kind", aspectProps.kind);
|
|
284
408
|
statement.bindString("identifier", aspectProps.identifier);
|
|
285
|
-
|
|
409
|
+
if (core_bentley_1.DbResult.BE_SQLITE_ROW !== statement.step())
|
|
410
|
+
return emptyResult;
|
|
411
|
+
const aspectId = statement.getValue(0).getId();
|
|
412
|
+
const version = statement.getValue(1).getString();
|
|
413
|
+
const jsonProperties = getJsonProperties ? statement.getValue(2).getString() : undefined;
|
|
414
|
+
return { aspectId, version, jsonProperties };
|
|
286
415
|
});
|
|
287
416
|
}
|
|
288
|
-
/**
|
|
417
|
+
/**
|
|
418
|
+
* Iterate all matching federation guids and ExternalSourceAspects in the provenance iModel (target unless reverse sync)
|
|
419
|
+
* and call a function for each one.
|
|
420
|
+
* @note provenance is done by federation guids where possible
|
|
421
|
+
* @note this may execute on each element more than once! Only use in cases where that is handled
|
|
422
|
+
*/
|
|
289
423
|
static forEachTrackedElement(args) {
|
|
424
|
+
if (args.provenanceDb === args.provenanceSourceDb)
|
|
425
|
+
return;
|
|
290
426
|
if (!args.provenanceDb.containsClass(core_backend_1.ExternalSourceAspect.classFullName)) {
|
|
291
427
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, "The BisCore schema version of the target database is too old");
|
|
292
428
|
}
|
|
293
|
-
const
|
|
294
|
-
args.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
429
|
+
const sourceDb = args.isReverseSynchronization ? args.provenanceDb : args.provenanceSourceDb;
|
|
430
|
+
const targetDb = args.isReverseSynchronization ? args.provenanceSourceDb : args.provenanceDb;
|
|
431
|
+
// query for provenanceDb
|
|
432
|
+
const elementIdByFedGuidQuery = `
|
|
433
|
+
SELECT e.ECInstanceId, FederationGuid
|
|
434
|
+
FROM bis.Element e
|
|
435
|
+
WHERE e.ECInstanceId NOT IN (0x1, 0xe, 0x10) -- special static elements
|
|
436
|
+
ORDER BY FederationGuid
|
|
437
|
+
`;
|
|
438
|
+
// iterate through sorted list of fed guids from both dbs to get the intersection
|
|
439
|
+
// NOTE: if we exposed the native attach database support,
|
|
440
|
+
// we could get the intersection of fed guids in one query, not sure if it would be faster
|
|
441
|
+
// OR we could do a raw sqlite query...
|
|
442
|
+
sourceDb.withStatement(elementIdByFedGuidQuery, (sourceStmt) => targetDb.withStatement(elementIdByFedGuidQuery, (targetStmt) => {
|
|
443
|
+
if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
444
|
+
return;
|
|
445
|
+
let sourceRow = sourceStmt.getRow();
|
|
446
|
+
if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
447
|
+
return;
|
|
448
|
+
let targetRow = targetStmt.getRow();
|
|
449
|
+
// NOTE: these comparisons rely upon the lowercase of the guid,
|
|
450
|
+
// and the fact that '0' < '9' < a' < 'f' in ascii/utf8
|
|
451
|
+
while (true) {
|
|
452
|
+
const currSourceRow = sourceRow, currTargetRow = targetRow;
|
|
453
|
+
if (currSourceRow.federationGuid !== undefined
|
|
454
|
+
&& currTargetRow.federationGuid !== undefined
|
|
455
|
+
&& currSourceRow.federationGuid === currTargetRow.federationGuid) {
|
|
456
|
+
// data flow direction is always sourceDb -> targetDb and it does not depend on where the explicit element provenance is stored
|
|
457
|
+
args.fn(sourceRow.id, targetRow.id);
|
|
302
458
|
}
|
|
303
|
-
|
|
304
|
-
|
|
459
|
+
if (currTargetRow.federationGuid === undefined
|
|
460
|
+
|| (currSourceRow.federationGuid !== undefined
|
|
461
|
+
&& currSourceRow.federationGuid >= currTargetRow.federationGuid)) {
|
|
462
|
+
if (targetStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
463
|
+
return;
|
|
464
|
+
targetRow = targetStmt.getRow();
|
|
465
|
+
}
|
|
466
|
+
if (currSourceRow.federationGuid === undefined
|
|
467
|
+
|| (currTargetRow.federationGuid !== undefined
|
|
468
|
+
&& currSourceRow.federationGuid <= currTargetRow.federationGuid)) {
|
|
469
|
+
if (sourceStmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
470
|
+
return;
|
|
471
|
+
sourceRow = sourceStmt.getRow();
|
|
305
472
|
}
|
|
306
473
|
}
|
|
474
|
+
}));
|
|
475
|
+
// query for provenanceDb
|
|
476
|
+
const provenanceAspectsQuery = `
|
|
477
|
+
SELECT esa.Identifier, Element.Id
|
|
478
|
+
FROM bis.ExternalSourceAspect esa
|
|
479
|
+
WHERE Scope.Id=:scopeId
|
|
480
|
+
AND Kind=:kind
|
|
481
|
+
`;
|
|
482
|
+
// Technically this will a second time call the function (as documented) on
|
|
483
|
+
// victims of the old provenance method that have both fedguids and an inserted aspect.
|
|
484
|
+
// But this is a private function with one known caller where that doesn't matter
|
|
485
|
+
args.provenanceDb.withPreparedStatement(provenanceAspectsQuery, (stmt) => {
|
|
486
|
+
const runFnInDataFlowDirection = (sourceId, targetId) => args.isReverseSynchronization ? args.fn(sourceId, targetId) : args.fn(targetId, sourceId);
|
|
487
|
+
stmt.bindId("scopeId", args.targetScopeElementId);
|
|
488
|
+
stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
|
|
489
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
490
|
+
// ExternalSourceAspect.Identifier is of type string
|
|
491
|
+
const aspectIdentifier = stmt.getValue(0).getString();
|
|
492
|
+
const elementId = stmt.getValue(1).getId();
|
|
493
|
+
runFnInDataFlowDirection(elementId, aspectIdentifier);
|
|
494
|
+
}
|
|
307
495
|
});
|
|
308
496
|
}
|
|
309
497
|
forEachTrackedElement(fn) {
|
|
310
498
|
return IModelTransformer.forEachTrackedElement({
|
|
311
|
-
provenanceSourceDb: this.
|
|
499
|
+
provenanceSourceDb: this.provenanceSourceDb,
|
|
312
500
|
provenanceDb: this.provenanceDb,
|
|
313
501
|
targetScopeElementId: this.targetScopeElementId,
|
|
314
502
|
isReverseSynchronization: !!this._options.isReverseSynchronization,
|
|
@@ -326,105 +514,349 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
326
514
|
this.context.remapElement(sourceElementId, targetElementId);
|
|
327
515
|
});
|
|
328
516
|
if (args)
|
|
329
|
-
return this.
|
|
517
|
+
return this.remapDeletedSourceEntities();
|
|
330
518
|
}
|
|
331
|
-
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
519
|
+
/**
|
|
520
|
+
* Scan changesets for deleted entities, if in a reverse synchronization, provenance has
|
|
521
|
+
* already been deleted, so we must scan for that as well.
|
|
334
522
|
*/
|
|
335
|
-
async
|
|
523
|
+
async remapDeletedSourceEntities() {
|
|
336
524
|
// we need a connected iModel with changes to remap elements with deletions
|
|
337
|
-
|
|
525
|
+
const notConnectedModel = this.sourceDb.iTwinId === undefined;
|
|
526
|
+
const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
|
|
527
|
+
if (notConnectedModel || noChanges)
|
|
338
528
|
return;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
529
|
+
this._deletedSourceRelationshipData = new Map();
|
|
530
|
+
nodeAssert(this._changeSummaryIds, "change summaries should be initialized before we get here");
|
|
531
|
+
nodeAssert(this._changeSummaryIds.length > 0, "change summaries should have at least one");
|
|
532
|
+
const alreadyImportedElementInserts = new Set();
|
|
533
|
+
const alreadyImportedModelInserts = new Set();
|
|
534
|
+
this.exporter.sourceDbChanges?.element.insertIds.forEach((insertedSourceElementId) => {
|
|
535
|
+
const targetElementId = this.context.findTargetElementId(insertedSourceElementId);
|
|
536
|
+
if (core_bentley_1.Id64.isValid(targetElementId))
|
|
537
|
+
alreadyImportedElementInserts.add(targetElementId);
|
|
538
|
+
});
|
|
539
|
+
this.exporter.sourceDbChanges?.model.insertIds.forEach((insertedSourceModelId) => {
|
|
540
|
+
const targetModelId = this.context.findTargetElementId(insertedSourceModelId);
|
|
541
|
+
if (core_bentley_1.Id64.isValid(targetModelId))
|
|
542
|
+
alreadyImportedModelInserts.add(targetModelId);
|
|
543
|
+
});
|
|
544
|
+
// optimization: if we have provenance, use it to avoid more querying later
|
|
545
|
+
// eventually when itwin.js supports attaching a second iModelDb in JS,
|
|
546
|
+
// this won't have to be a conditional part of the query, and we can always have it by attaching
|
|
547
|
+
const queryCanAccessProvenance = this.sourceDb === this.provenanceDb;
|
|
548
|
+
const deletedEntitySql = `
|
|
549
|
+
SELECT
|
|
550
|
+
1 AS IsElemNotRel,
|
|
551
|
+
ic.ChangedInstance.Id AS InstanceId,
|
|
552
|
+
NULL AS InstId2, -- need these columns for relationship ends in the unioned query
|
|
553
|
+
NULL AS InstId3,
|
|
554
|
+
ec.FederationGuid AS FedGuid,
|
|
555
|
+
NULL AS FedGuid2,
|
|
556
|
+
ic.ChangedInstance.ClassId AS ClassId
|
|
557
|
+
${queryCanAccessProvenance ? `
|
|
558
|
+
, coalesce(esa.Identifier, esac.Identifier) AS Identifier1
|
|
559
|
+
, NULL AS Identifier2
|
|
560
|
+
` : ""}
|
|
561
|
+
FROM ecchange.change.InstanceChange ic
|
|
562
|
+
LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') ec
|
|
563
|
+
ON ic.ChangedInstance.Id=ec.ECInstanceId
|
|
564
|
+
${queryCanAccessProvenance ? `
|
|
565
|
+
LEFT JOIN bis.ExternalSourceAspect esa
|
|
566
|
+
ON ec.ECInstanceId=esa.Element.Id
|
|
567
|
+
LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') esac
|
|
568
|
+
ON ec.ECInstanceId=esac.Element.Id
|
|
569
|
+
` : ""}
|
|
570
|
+
WHERE ic.OpCode=:opDelete
|
|
571
|
+
AND ic.Summary.Id=:changeSummaryId
|
|
572
|
+
AND ic.ChangedInstance.ClassId IS (BisCore.Element)
|
|
573
|
+
${queryCanAccessProvenance ? `
|
|
574
|
+
AND (esa.Scope.Id=:targetScopeElement OR esa.Scope.Id IS NULL)
|
|
575
|
+
AND (esa.Kind='Element' OR esa.Kind IS NULL)
|
|
576
|
+
AND (esac.Scope.Id=:targetScopeElement OR esac.Scope.Id IS NULL)
|
|
577
|
+
AND (esac.Kind='Element' OR esac.Kind IS NULL)
|
|
578
|
+
` : ""}
|
|
579
|
+
|
|
580
|
+
UNION ALL
|
|
581
|
+
|
|
582
|
+
SELECT
|
|
583
|
+
0 AS IsElemNotRel,
|
|
584
|
+
ic.ChangedInstance.Id AS InstanceId,
|
|
585
|
+
coalesce(se.ECInstanceId, sec.ECInstanceId) AS InstId2,
|
|
586
|
+
coalesce(te.ECInstanceId, tec.ECInstanceId) AS InstId3,
|
|
587
|
+
coalesce(se.FederationGuid, sec.FederationGuid) AS FedGuid1,
|
|
588
|
+
coalesce(te.FederationGuid, tec.FederationGuid) AS FedGuid2,
|
|
589
|
+
ic.ChangedInstance.ClassId AS ClassId
|
|
590
|
+
${queryCanAccessProvenance ? `
|
|
591
|
+
, coalesce(sesa.Identifier, sesac.Identifier) AS Identifier1
|
|
592
|
+
, coalesce(tesa.Identifier, tesac.Identifier) AS Identifier2
|
|
593
|
+
` : ""}
|
|
594
|
+
FROM ecchange.change.InstanceChange ic
|
|
595
|
+
LEFT JOIN bis.ElementRefersToElements.Changes(:changeSummaryId, 'BeforeDelete') ertec
|
|
596
|
+
ON ic.ChangedInstance.Id=ertec.ECInstanceId
|
|
597
|
+
-- FIXME: test a deletion of both an element and a relationship at the same time
|
|
598
|
+
LEFT JOIN bis.Element se
|
|
599
|
+
ON se.ECInstanceId=ertec.SourceECInstanceId
|
|
600
|
+
LEFT JOIN bis.Element te
|
|
601
|
+
ON te.ECInstanceId=ertec.TargetECInstanceId
|
|
602
|
+
LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') sec
|
|
603
|
+
ON sec.ECInstanceId=ertec.SourceECInstanceId
|
|
604
|
+
LEFT JOIN bis.Element.Changes(:changeSummaryId, 'BeforeDelete') tec
|
|
605
|
+
ON tec.ECInstanceId=ertec.TargetECInstanceId
|
|
606
|
+
${queryCanAccessProvenance ? `
|
|
607
|
+
-- NOTE: need to join on both se/te and sec/tec incase the element was deleted
|
|
608
|
+
LEFT JOIN bis.ExternalSourceAspect sesa
|
|
609
|
+
ON se.ECInstanceId=sesa.Element.Id -- don't use *esac*.Identifier because it's a string
|
|
610
|
+
LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') sesac
|
|
611
|
+
ON sec.ECInstanceId=sesac.Element.Id
|
|
612
|
+
LEFT JOIN bis.ExternalSourceAspect tesa
|
|
613
|
+
ON te.ECInstanceId=tesa.Element.Id
|
|
614
|
+
LEFT JOIN bis.ExternalSourceAspect.Changes(:changeSummaryId, 'BeforeDelete') tesac
|
|
615
|
+
ON tec.ECInstanceId=tesac.Element.Id
|
|
616
|
+
` : ""}
|
|
617
|
+
WHERE ic.OpCode=:opDelete
|
|
618
|
+
AND ic.Summary.Id=:changeSummaryId
|
|
619
|
+
AND ic.ChangedInstance.ClassId IS (BisCore.ElementRefersToElements)
|
|
620
|
+
${queryCanAccessProvenance ? `
|
|
621
|
+
AND (sesa.Scope.Id=:targetScopeElement OR sesa.Scope.Id IS NULL)
|
|
622
|
+
AND (sesa.Kind='Relationship' OR sesa.Kind IS NULL)
|
|
623
|
+
AND (sesac.Scope.Id=:targetScopeElement OR sesac.Scope.Id IS NULL)
|
|
624
|
+
AND (sesac.Kind='Relationship' OR sesac.Kind IS NULL)
|
|
625
|
+
AND (tesa.Scope.Id=:targetScopeElement OR tesa.Scope.Id IS NULL)
|
|
626
|
+
AND (tesa.Kind='Relationship' OR tesa.Kind IS NULL)
|
|
627
|
+
AND (tesac.Scope.Id=:targetScopeElement OR tesac.Scope.Id IS NULL)
|
|
628
|
+
AND (tesac.Kind='Relationship' OR tesac.Kind IS NULL)
|
|
629
|
+
` : ""}
|
|
630
|
+
`;
|
|
631
|
+
for (const changeSummaryId of this._changeSummaryIds) {
|
|
632
|
+
// FIXME: test deletion in both forward and reverse sync
|
|
633
|
+
this.sourceDb.withPreparedStatement(deletedEntitySql, (stmt) => {
|
|
634
|
+
stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
|
|
635
|
+
if (queryCanAccessProvenance)
|
|
636
|
+
stmt.bindId("targetScopeElement", this.targetScopeElementId);
|
|
637
|
+
stmt.bindId("changeSummaryId", changeSummaryId);
|
|
638
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
639
|
+
const isElemNotRel = stmt.getValue(0).getBoolean();
|
|
640
|
+
const instId = stmt.getValue(1).getId();
|
|
641
|
+
if (isElemNotRel) {
|
|
642
|
+
const sourceElemFedGuid = stmt.getValue(4).getGuid();
|
|
643
|
+
// "Identifier" is a string, so null value returns '' which doesn't work with ??, and I don't like ||
|
|
644
|
+
let identifierValue;
|
|
645
|
+
// TODO: if I could attach the second db, will probably be much faster to get target id
|
|
646
|
+
// as part of the whole query rather than with _queryElemIdByFedGuid
|
|
647
|
+
const targetId = (queryCanAccessProvenance
|
|
648
|
+
&& (identifierValue = stmt.getValue(7))
|
|
649
|
+
&& !identifierValue.isNull
|
|
650
|
+
&& identifierValue.getString())
|
|
651
|
+
// maybe batching these queries would perform better but we should
|
|
652
|
+
// try to attach the second db and query both together anyway
|
|
653
|
+
|| (sourceElemFedGuid && this._queryElemIdByFedGuid(this.targetDb, sourceElemFedGuid))
|
|
654
|
+
// FIXME: describe why it's safe to assume nothing has been deleted in provenanceDb
|
|
655
|
+
|| this._queryProvenanceForElement(instId);
|
|
656
|
+
// since we are processing one changeset at a time, we can see local source deletes
|
|
657
|
+
// of entities that were never synced and can be safely ignored
|
|
658
|
+
const deletionNotInTarget = !targetId;
|
|
659
|
+
if (deletionNotInTarget)
|
|
660
|
+
continue;
|
|
661
|
+
this.context.remapElement(instId, targetId);
|
|
662
|
+
// If an entity insert and an entity delete both point to the same entity in target iModel, that means that entity was recreated.
|
|
663
|
+
// In such case an entity update will be triggered and we no longer need to delete the entity.
|
|
664
|
+
if (alreadyImportedElementInserts.has(targetId)) {
|
|
665
|
+
this.exporter.sourceDbChanges?.element.deleteIds.delete(instId);
|
|
666
|
+
}
|
|
667
|
+
if (alreadyImportedModelInserts.has(targetId)) {
|
|
668
|
+
this.exporter.sourceDbChanges?.model.deleteIds.delete(instId);
|
|
669
|
+
}
|
|
383
670
|
}
|
|
384
|
-
|
|
385
|
-
|
|
671
|
+
else { // is deleted relationship
|
|
672
|
+
const classFullName = stmt.getValue(6).getClassNameForClassId();
|
|
673
|
+
const [sourceIdInTarget, targetIdInTarget] = [
|
|
674
|
+
{ guidColumn: 4, identifierColumn: 7, isTarget: false },
|
|
675
|
+
{ guidColumn: 5, identifierColumn: 8, isTarget: true },
|
|
676
|
+
].map(({ guidColumn, identifierColumn }) => {
|
|
677
|
+
const fedGuid = stmt.getValue(guidColumn).getGuid();
|
|
678
|
+
let identifierValue;
|
|
679
|
+
return ((queryCanAccessProvenance
|
|
680
|
+
// FIXME: this is really far from idiomatic, try to undo that
|
|
681
|
+
&& (identifierValue = stmt.getValue(identifierColumn))
|
|
682
|
+
&& !identifierValue.isNull
|
|
683
|
+
&& identifierValue.getString())
|
|
684
|
+
// maybe batching these queries would perform better but we should
|
|
685
|
+
// try to attach the second db and query both together anyway
|
|
686
|
+
|| (fedGuid && this._queryElemIdByFedGuid(this.targetDb, fedGuid)));
|
|
687
|
+
});
|
|
688
|
+
// since we are processing one changeset at a time, we can see local source deletes
|
|
689
|
+
// of entities that were never synced and can be safely ignored
|
|
690
|
+
if (sourceIdInTarget && targetIdInTarget) {
|
|
691
|
+
this._deletedSourceRelationshipData.set(instId, {
|
|
692
|
+
classFullName,
|
|
693
|
+
sourceIdInTarget,
|
|
694
|
+
targetIdInTarget,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
// FIXME: describe why it's safe to assume nothing has been deleted in provenanceDb
|
|
699
|
+
const relProvenance = this._queryProvenanceForRelationship(instId, {
|
|
700
|
+
classFullName,
|
|
701
|
+
sourceId: stmt.getValue(2).getId(),
|
|
702
|
+
targetId: stmt.getValue(3).getId(),
|
|
703
|
+
});
|
|
704
|
+
if (relProvenance && relProvenance.relationshipId)
|
|
705
|
+
this._deletedSourceRelationshipData.set(instId, {
|
|
706
|
+
classFullName,
|
|
707
|
+
relId: relProvenance.relationshipId,
|
|
708
|
+
provenanceAspectId: relProvenance.aspectId,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// NEXT: remap sourceId and targetId to target, get provenance there
|
|
714
|
+
// NOTE: it is possible during a forward sync for the target to already have deleted
|
|
715
|
+
// something that the source deleted, in which case we can safely ignore the gone provenance
|
|
716
|
+
});
|
|
386
717
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
718
|
+
}
|
|
719
|
+
_queryProvenanceForElement(entityInProvenanceSourceId) {
|
|
720
|
+
return this.provenanceDb.withPreparedStatement(`
|
|
721
|
+
SELECT esa.Element.Id
|
|
722
|
+
FROM Bis.ExternalSourceAspect esa
|
|
723
|
+
WHERE esa.Kind=?
|
|
724
|
+
AND esa.Scope.Id=?
|
|
725
|
+
AND esa.Identifier=?
|
|
726
|
+
`, (stmt) => {
|
|
727
|
+
stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Element);
|
|
728
|
+
stmt.bindId(2, this.targetScopeElementId);
|
|
729
|
+
stmt.bindString(3, entityInProvenanceSourceId);
|
|
730
|
+
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
731
|
+
return stmt.getValue(0).getId();
|
|
732
|
+
else
|
|
733
|
+
return undefined;
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
_queryProvenanceForRelationship(entityInProvenanceSourceId, sourceRelInfo) {
|
|
737
|
+
return this.provenanceDb.withPreparedStatement(`
|
|
738
|
+
SELECT
|
|
739
|
+
ECInstanceId,
|
|
740
|
+
JSON_EXTRACT(JsonProperties, '$.targetRelInstanceId'),
|
|
741
|
+
JSON_EXTRACT(JsonProperties, '$.provenanceRelInstanceId')
|
|
742
|
+
FROM Bis.ExternalSourceAspect
|
|
743
|
+
WHERE Kind=?
|
|
744
|
+
AND Scope.Id=?
|
|
745
|
+
AND Identifier=?
|
|
746
|
+
`, (stmt) => {
|
|
747
|
+
stmt.bindString(1, core_backend_1.ExternalSourceAspect.Kind.Relationship);
|
|
748
|
+
stmt.bindId(2, this.targetScopeElementId);
|
|
749
|
+
stmt.bindString(3, entityInProvenanceSourceId);
|
|
750
|
+
if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
751
|
+
return undefined;
|
|
752
|
+
const aspectId = stmt.getValue(0).getId();
|
|
753
|
+
const provenanceRelInstIdVal = stmt.getValue(2);
|
|
754
|
+
const provenanceRelInstanceId = !provenanceRelInstIdVal.isNull
|
|
755
|
+
? provenanceRelInstIdVal.getString()
|
|
756
|
+
: this._queryTargetRelId(sourceRelInfo);
|
|
757
|
+
return {
|
|
758
|
+
aspectId,
|
|
759
|
+
relationshipId: provenanceRelInstanceId,
|
|
760
|
+
};
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
_queryTargetRelId(sourceRelInfo) {
|
|
764
|
+
const targetRelInfo = {
|
|
765
|
+
sourceId: this.context.findTargetElementId(sourceRelInfo.sourceId),
|
|
766
|
+
targetId: this.context.findTargetElementId(sourceRelInfo.targetId),
|
|
767
|
+
};
|
|
768
|
+
if (targetRelInfo.sourceId === undefined || targetRelInfo.targetId === undefined)
|
|
769
|
+
return undefined; // couldn't find an element, rel is invalid or deleted
|
|
770
|
+
return this.targetDb.withPreparedStatement(`
|
|
771
|
+
SELECT ECInstanceId
|
|
772
|
+
FROM bis.ElementRefersToElements
|
|
773
|
+
WHERE SourceECInstanceId=?
|
|
774
|
+
AND TargetECInstanceId=?
|
|
775
|
+
AND ECClassId=?
|
|
776
|
+
`, (stmt) => {
|
|
777
|
+
stmt.bindId(1, targetRelInfo.sourceId);
|
|
778
|
+
stmt.bindId(2, targetRelInfo.targetId);
|
|
779
|
+
stmt.bindId(3, this._targetClassNameToClassId(sourceRelInfo.classFullName));
|
|
780
|
+
if (stmt.step() !== core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
781
|
+
return undefined;
|
|
782
|
+
return stmt.getValue(0).getId();
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
_targetClassNameToClassId(classFullName) {
|
|
786
|
+
let classId = this._targetClassNameToClassIdCache.get(classFullName);
|
|
787
|
+
if (classId === undefined) {
|
|
788
|
+
classId = this._getRelClassId(this.targetDb, classFullName);
|
|
789
|
+
this._targetClassNameToClassIdCache.set(classFullName, classId);
|
|
390
790
|
}
|
|
791
|
+
return classId;
|
|
792
|
+
}
|
|
793
|
+
// NOTE: this doesn't handle remapped element classes,
|
|
794
|
+
// but is only used for relationships rn
|
|
795
|
+
_getRelClassId(db, classFullName) {
|
|
796
|
+
return db.withPreparedStatement(`
|
|
797
|
+
SELECT c.ECInstanceId
|
|
798
|
+
FROM ECDbMeta.ECClassDef c
|
|
799
|
+
JOIN ECDbMeta.ECSchemaDef s ON c.Schema.Id=s.ECInstanceId
|
|
800
|
+
WHERE s.Name=? AND c.Name=?
|
|
801
|
+
`, (stmt) => {
|
|
802
|
+
const [schemaName, className] = classFullName.split(".");
|
|
803
|
+
stmt.bindString(1, schemaName);
|
|
804
|
+
stmt.bindString(2, className);
|
|
805
|
+
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
806
|
+
return stmt.getValue(0).getId();
|
|
807
|
+
(0, core_bentley_1.assert)(false, "relationship was not found");
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
_queryElemIdByFedGuid(db, fedGuid) {
|
|
811
|
+
return db.withPreparedStatement("SELECT ECInstanceId FROM Bis.Element WHERE FederationGuid=?", (stmt) => {
|
|
812
|
+
stmt.bindGuid(1, fedGuid);
|
|
813
|
+
if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
|
|
814
|
+
return stmt.getValue(0).getId();
|
|
815
|
+
else
|
|
816
|
+
return undefined;
|
|
817
|
+
});
|
|
391
818
|
}
|
|
392
819
|
/** Returns `true` if *brute force* delete detections should be run.
|
|
393
820
|
* @note Not relevant for processChanges when change history is known.
|
|
394
821
|
*/
|
|
395
822
|
shouldDetectDeletes() {
|
|
823
|
+
// FIXME: all synchronizations should mark this as false
|
|
396
824
|
if (this._isFirstSynchronization)
|
|
397
825
|
return false; // not necessary the first time since there are no deletes to detect
|
|
398
826
|
if (this._options.isReverseSynchronization)
|
|
399
827
|
return false; // not possible for a reverse synchronization since provenance will be deleted when element is deleted
|
|
400
828
|
return true;
|
|
401
829
|
}
|
|
402
|
-
/**
|
|
403
|
-
*
|
|
404
|
-
*
|
|
830
|
+
/**
|
|
831
|
+
* Detect Element deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against Elements
|
|
832
|
+
* in the source iModel.
|
|
833
|
+
* @deprecated in 0.1.x. This method is only called during [[processAll]] when the option
|
|
834
|
+
* [[IModelTransformerOptions.forceExternalSourceAspectProvenance]] is enabled. It is not
|
|
835
|
+
* necessary when using [[processChanges]] since changeset information is sufficient.
|
|
836
|
+
* @note you do not need to call this directly unless processing a subset of an iModel.
|
|
405
837
|
* @throws [[IModelError]] If the required provenance information is not available to detect deletes.
|
|
406
838
|
*/
|
|
407
839
|
async detectElementDeletes() {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (
|
|
427
|
-
|
|
840
|
+
const sql = `
|
|
841
|
+
SELECT Identifier, Element.Id
|
|
842
|
+
FROM BisCore.ExternalSourceAspect
|
|
843
|
+
WHERE Scope.Id=:scopeId
|
|
844
|
+
AND Kind=:kind
|
|
845
|
+
`;
|
|
846
|
+
nodeAssert(!this._options.isReverseSynchronization, "synchronizations with processChagnes already detect element deletes, don't call detectElementDeletes");
|
|
847
|
+
this.provenanceDb.withPreparedStatement(sql, (stmt) => {
|
|
848
|
+
stmt.bindId("scopeId", this.targetScopeElementId);
|
|
849
|
+
stmt.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Element);
|
|
850
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
851
|
+
// ExternalSourceAspect.Identifier is of type string
|
|
852
|
+
const aspectIdentifier = stmt.getValue(0).getString();
|
|
853
|
+
if (!core_bentley_1.Id64.isId64(aspectIdentifier)) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const targetElemId = stmt.getValue(1).getId();
|
|
857
|
+
const wasDeletedInSource = !EntityUnifier_1.EntityUnifier.exists(this.sourceDb, { entityReference: `e${aspectIdentifier}` });
|
|
858
|
+
if (wasDeletedInSource)
|
|
859
|
+
this.importer.deleteElement(targetElemId);
|
|
428
860
|
}
|
|
429
861
|
});
|
|
430
862
|
}
|
|
@@ -451,24 +883,53 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
451
883
|
}
|
|
452
884
|
return targetElementProps;
|
|
453
885
|
}
|
|
886
|
+
// FIXME: this is a PoC, see if we minimize memory usage
|
|
887
|
+
_cacheSourceChanges() {
|
|
888
|
+
nodeAssert(this._changeSummaryIds && this._changeSummaryIds.length > 0, "should have changeset data by now");
|
|
889
|
+
this._hasElementChangedCache = new Set();
|
|
890
|
+
const query = `
|
|
891
|
+
SELECT
|
|
892
|
+
ic.ChangedInstance.Id AS InstId
|
|
893
|
+
FROM ecchange.change.InstanceChange ic
|
|
894
|
+
JOIN iModelChange.Changeset imc ON ic.Summary.Id=imc.Summary.Id
|
|
895
|
+
-- FIXME: do relationship entities also need this cache optimization?
|
|
896
|
+
WHERE ic.ChangedInstance.ClassId IS (BisCore.Element)
|
|
897
|
+
AND InVirtualSet(:changeSummaryIds, ic.Summary.Id)
|
|
898
|
+
-- ignore deleted, we take care of those in remapDeletedSourceEntities
|
|
899
|
+
-- include inserted since inserted code-colliding elements should be considered
|
|
900
|
+
-- a change so that the colliding element is exported to the target
|
|
901
|
+
AND ic.OpCode<>:opDelete
|
|
902
|
+
`;
|
|
903
|
+
// there is a single mega-query multi-join+coalescing hack that I used originally to get around
|
|
904
|
+
// only being able to run table.Changes() on one changeset at once, but sqlite only supports up to 64
|
|
905
|
+
// tables in a join. Need to talk to core about .Changes being able to take a set of changesets
|
|
906
|
+
// You can find this version in the `federation-guid-optimization-megaquery` branch
|
|
907
|
+
// I wouldn't use it unless we prove via profiling that it speeds things up significantly
|
|
908
|
+
// And even then let's first try scanning the raw changesets instead of applying them as these queries
|
|
909
|
+
// require
|
|
910
|
+
this.sourceDb.withPreparedStatement(query, (stmt) => {
|
|
911
|
+
stmt.bindInteger("opDelete", core_common_1.ChangeOpCode.Delete);
|
|
912
|
+
stmt.bindIdSet("changeSummaryIds", this._changeSummaryIds);
|
|
913
|
+
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
|
|
914
|
+
const instId = stmt.getValue(0).getId();
|
|
915
|
+
this._hasElementChangedCache.add(instId);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
454
919
|
/** Returns true if a change within sourceElement is detected.
|
|
455
920
|
* @param sourceElement The Element from the source iModel
|
|
456
921
|
* @param targetElementId The Element from the target iModel to compare against.
|
|
457
922
|
* @note A subclass can override this method to provide custom change detection behavior.
|
|
458
923
|
*/
|
|
459
|
-
hasElementChanged(sourceElement,
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return lastModifiedTime !== sourceAspect.version;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
return true;
|
|
924
|
+
hasElementChanged(sourceElement, _targetElementId) {
|
|
925
|
+
if (this._sourceChangeDataState === "no-changes")
|
|
926
|
+
return false;
|
|
927
|
+
if (this._sourceChangeDataState === "unconnected")
|
|
928
|
+
return true;
|
|
929
|
+
nodeAssert(this._sourceChangeDataState === "has-changes", "change data should be initialized by now");
|
|
930
|
+
if (this._hasElementChangedCache === undefined)
|
|
931
|
+
this._cacheSourceChanges();
|
|
932
|
+
return this._hasElementChangedCache.has(sourceElement.id);
|
|
472
933
|
}
|
|
473
934
|
static transformCallbackFor(transformer, entity) {
|
|
474
935
|
if (entity instanceof core_backend_1.Element)
|
|
@@ -658,51 +1119,68 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
658
1119
|
targetElementId = this.context.findTargetElementId(sourceElement.id);
|
|
659
1120
|
targetElementProps = this.onTransformElement(sourceElement);
|
|
660
1121
|
}
|
|
1122
|
+
// if an existing remapping was not yet found, check by FederationGuid
|
|
1123
|
+
if (this.context.isBetweenIModels && !core_bentley_1.Id64.isValid(targetElementId) && sourceElement.federationGuid !== undefined) {
|
|
1124
|
+
targetElementId = this._queryElemIdByFedGuid(this.targetDb, sourceElement.federationGuid) ?? core_bentley_1.Id64.invalid;
|
|
1125
|
+
if (core_bentley_1.Id64.isValid(targetElementId))
|
|
1126
|
+
this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found
|
|
1127
|
+
}
|
|
661
1128
|
// 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)
|
|
662
|
-
if (!core_bentley_1.Id64.
|
|
1129
|
+
if (!core_bentley_1.Id64.isValid(targetElementId) && core_bentley_1.Id64.isValidId64(targetElementProps.code.scope)) {
|
|
663
1130
|
// respond the same way to undefined code value as the @see Code class, but don't use that class because is trims
|
|
664
1131
|
// whitespace from the value, and there are iModels out there with untrimmed whitespace that we ought not to trim
|
|
665
1132
|
targetElementProps.code.value = targetElementProps.code.value ?? "";
|
|
666
|
-
|
|
667
|
-
if (undefined !==
|
|
668
|
-
const
|
|
669
|
-
if (
|
|
1133
|
+
const maybeTargetElementId = this.targetDb.elements.queryElementIdByCode(targetElementProps.code);
|
|
1134
|
+
if (undefined !== maybeTargetElementId) {
|
|
1135
|
+
const maybeTargetElem = this.targetDb.elements.getElement(maybeTargetElementId);
|
|
1136
|
+
if (maybeTargetElem.classFullName === targetElementProps.classFullName) { // ensure code remapping doesn't change the target class
|
|
1137
|
+
targetElementId = maybeTargetElementId;
|
|
670
1138
|
this.context.remapElement(sourceElement.id, targetElementId); // record that the targetElement was found by Code
|
|
671
1139
|
}
|
|
672
1140
|
else {
|
|
673
|
-
targetElementId = undefined;
|
|
674
1141
|
targetElementProps.code = core_common_1.Code.createEmpty(); // clear out invalid code
|
|
675
1142
|
}
|
|
676
1143
|
}
|
|
677
1144
|
}
|
|
678
|
-
if (
|
|
679
|
-
|
|
680
|
-
if (!this.hasElementChanged(sourceElement, targetElementId)) {
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
1145
|
+
if (core_bentley_1.Id64.isValid(targetElementId) && !this.hasElementChanged(sourceElement, targetElementId))
|
|
1146
|
+
return;
|
|
684
1147
|
this.collectUnmappedReferences(sourceElement);
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1148
|
+
// targetElementId will be valid (indicating update) or undefined (indicating insert)
|
|
1149
|
+
targetElementProps.id
|
|
1150
|
+
= core_bentley_1.Id64.isValid(targetElementId)
|
|
1151
|
+
? targetElementId
|
|
1152
|
+
: undefined;
|
|
689
1153
|
if (!this._options.wasSourceIModelCopiedToTarget) {
|
|
690
1154
|
this.importer.importElement(targetElementProps); // don't need to import if iModel was copied
|
|
691
1155
|
}
|
|
692
1156
|
this.context.remapElement(sourceElement.id, targetElementProps.id); // targetElementProps.id assigned by importElement
|
|
693
1157
|
// now that we've mapped this elem we can fix unmapped references to it
|
|
694
1158
|
this.resolvePendingReferences(sourceElement);
|
|
1159
|
+
// the transformer does not currently 'split' or 'join' any elements, therefore, it does not
|
|
1160
|
+
// insert external source aspects because federation guids are sufficient for this.
|
|
1161
|
+
// Other transformer subclasses must insert the appropriate aspect (as provided by a TBD API)
|
|
1162
|
+
// when splitting/joining elements
|
|
1163
|
+
// physical consolidation is an example of a 'joining' transform
|
|
1164
|
+
// FIXME: document this externally!
|
|
1165
|
+
// verify at finalization time that we don't lose provenance on new elements
|
|
1166
|
+
// make public and improve `initElementProvenance` API for usage by consolidators
|
|
695
1167
|
if (!this._options.noProvenance) {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1168
|
+
let provenance = this._options.forceExternalSourceAspectProvenance || this._elementsWithExplicitlyTrackedProvenance.has(sourceElement.id)
|
|
1169
|
+
? undefined
|
|
1170
|
+
: sourceElement.federationGuid;
|
|
1171
|
+
if (!provenance) {
|
|
1172
|
+
const aspectProps = this.initElementProvenance(sourceElement.id, targetElementProps.id);
|
|
1173
|
+
const aspectId = this.queryScopeExternalSource(aspectProps).aspectId;
|
|
1174
|
+
if (aspectId === undefined) {
|
|
1175
|
+
aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
aspectProps.id = aspectId;
|
|
1179
|
+
this.provenanceDb.elements.updateAspect(aspectProps);
|
|
1180
|
+
}
|
|
1181
|
+
provenance = aspectProps;
|
|
703
1182
|
}
|
|
704
|
-
|
|
705
|
-
this.markLastProvenance(aspectProps, { isRelationship: false });
|
|
1183
|
+
this.markLastProvenance(provenance, { isRelationship: false });
|
|
706
1184
|
}
|
|
707
1185
|
}
|
|
708
1186
|
resolvePendingReferences(entity) {
|
|
@@ -740,50 +1218,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
740
1218
|
onDeleteModel(sourceModelId) {
|
|
741
1219
|
// It is possible and apparently occasionally sensical to delete a model without deleting its underlying element.
|
|
742
1220
|
// - If only the model is deleted, [[initFromExternalSourceAspects]] will have already remapped the underlying element since it still exists.
|
|
743
|
-
// - If both were deleted, [[
|
|
1221
|
+
// - If both were deleted, [[remapDeletedSourceEntities]] will find and remap the deleted element making this operation valid
|
|
744
1222
|
const targetModelId = this.context.findTargetElementId(sourceModelId);
|
|
745
|
-
if (
|
|
746
|
-
return;
|
|
747
|
-
if (this.exporter.sourceDbChanges?.element.deleteIds.has(sourceModelId)) {
|
|
748
|
-
const isDefinitionPartition = this.targetDb.withPreparedStatement(`
|
|
749
|
-
SELECT 1
|
|
750
|
-
FROM bis.DefinitionPartition
|
|
751
|
-
WHERE ECInstanceId=?
|
|
752
|
-
`, (stmt) => {
|
|
753
|
-
stmt.bindId(1, targetModelId);
|
|
754
|
-
const val = stmt.step();
|
|
755
|
-
switch (val) {
|
|
756
|
-
case core_bentley_1.DbResult.BE_SQLITE_ROW: return true;
|
|
757
|
-
case core_bentley_1.DbResult.BE_SQLITE_DONE: return false;
|
|
758
|
-
default: (0, core_bentley_1.assert)(false, `unexpected db result: '${stmt}'`);
|
|
759
|
-
}
|
|
760
|
-
});
|
|
761
|
-
if (isDefinitionPartition) {
|
|
762
|
-
// Skipping model deletion because model's partition will also be deleted.
|
|
763
|
-
// It expects that model will be present and will fail if it's missing.
|
|
764
|
-
// Model will be deleted when its partition will be deleted.
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
try {
|
|
1223
|
+
if (core_bentley_1.Id64.isValidId64(targetModelId)) {
|
|
769
1224
|
this.importer.deleteModel(targetModelId);
|
|
770
1225
|
}
|
|
771
|
-
catch (error) {
|
|
772
|
-
const isDeletionProhibitedErr = error instanceof core_common_1.IModelError && (error.errorNumber === core_bentley_1.IModelStatus.DeletionProhibited || error.errorNumber === core_bentley_1.IModelStatus.ForeignKeyConstraint);
|
|
773
|
-
if (!isDeletionProhibitedErr)
|
|
774
|
-
throw error;
|
|
775
|
-
// Transformer tries to delete models before it deletes elements. Definition models cannot be deleted unless all of their modeled elements are deleted first.
|
|
776
|
-
// In case a definition model needs to be deleted we need to skip it for now and register its modeled partition for deletion.
|
|
777
|
-
// The `OnDeleteElement` calls `DeleteElementTree` Which deletes the model together with its partition after deleting all of the modeled elements.
|
|
778
|
-
this.scheduleModeledPartitionDeletion(sourceModelId);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
/** Schedule modeled partition deletion */
|
|
782
|
-
scheduleModeledPartitionDeletion(sourceModelId) {
|
|
783
|
-
const deletedElements = this.exporter.sourceDbChanges?.element.deleteIds;
|
|
784
|
-
if (!deletedElements.has(sourceModelId)) {
|
|
785
|
-
deletedElements.add(sourceModelId);
|
|
786
|
-
}
|
|
787
1226
|
}
|
|
788
1227
|
/** Cause the model container, contents, and sub-models to be exported from the source iModel and imported into the target iModel.
|
|
789
1228
|
* @param sourceModeledElementId Import this [Model]($backend) from the source IModelDb.
|
|
@@ -855,7 +1294,68 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
855
1294
|
* @deprecated in 3.x. This method is no longer necessary since the transformer no longer needs to defer elements
|
|
856
1295
|
*/
|
|
857
1296
|
async processDeferredElements(_numRetries = 3) { }
|
|
1297
|
+
/** called at the end ([[finalizeTransformation]]) of a transformation,
|
|
1298
|
+
* updates the target scope element to say that transformation up through the
|
|
1299
|
+
* source's changeset has been performed. Also stores all changesets that occurred
|
|
1300
|
+
* during the transformation as "pending synchronization changeset indices"
|
|
1301
|
+
*
|
|
1302
|
+
* You generally should not call this function yourself and use [[processChanges]] instead.
|
|
1303
|
+
* It is public for unsupported use cases of custom synchronization transforms.
|
|
1304
|
+
* @note if you are not running processChanges in this transformation, this will fail
|
|
1305
|
+
* without setting the `force` option to `true`
|
|
1306
|
+
*/
|
|
1307
|
+
updateSynchronizationVersion({ force = false } = {}) {
|
|
1308
|
+
if (!force && (this._sourceChangeDataState !== "has-changes" && !this._isFirstSynchronization))
|
|
1309
|
+
return;
|
|
1310
|
+
nodeAssert(this._targetScopeProvenanceProps);
|
|
1311
|
+
const sourceVersion = `${this.sourceDb.changeset.id};${this.sourceDb.changeset.index}`;
|
|
1312
|
+
const targetVersion = `${this.targetDb.changeset.id};${this.targetDb.changeset.index}`;
|
|
1313
|
+
if (this._isFirstSynchronization) {
|
|
1314
|
+
this._targetScopeProvenanceProps.version = sourceVersion;
|
|
1315
|
+
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = targetVersion;
|
|
1316
|
+
}
|
|
1317
|
+
else if (this._options.isReverseSynchronization) {
|
|
1318
|
+
const oldVersion = this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion;
|
|
1319
|
+
core_bentley_1.Logger.logInfo(loggerCategory, `updating reverse version from ${oldVersion} to ${sourceVersion}`);
|
|
1320
|
+
this._targetScopeProvenanceProps.jsonProperties.reverseSyncVersion = sourceVersion;
|
|
1321
|
+
}
|
|
1322
|
+
else if (!this._options.isReverseSynchronization) {
|
|
1323
|
+
core_bentley_1.Logger.logInfo(loggerCategory, `updating sync version from ${this._targetScopeProvenanceProps.version} to ${sourceVersion}`);
|
|
1324
|
+
this._targetScopeProvenanceProps.version = sourceVersion;
|
|
1325
|
+
}
|
|
1326
|
+
if (this._isSynchronization) {
|
|
1327
|
+
(0, core_bentley_1.assert)(this.targetDb.changeset.index !== undefined && this._startingChangesetIndices !== undefined, "updateSynchronizationVersion was called without change history");
|
|
1328
|
+
const jsonProps = this._targetScopeProvenanceProps.jsonProperties;
|
|
1329
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
|
|
1330
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `previous pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
|
|
1331
|
+
const [syncChangesetsToClear, syncChangesetsToUpdate] = this._isReverseSynchronization
|
|
1332
|
+
? [jsonProps.pendingReverseSyncChangesetIndices, jsonProps.pendingSyncChangesetIndices]
|
|
1333
|
+
: [jsonProps.pendingSyncChangesetIndices, jsonProps.pendingReverseSyncChangesetIndices];
|
|
1334
|
+
// NOTE that as documented in [[processChanges]], this assumes that right after
|
|
1335
|
+
// transformation finalization, the work will be saved immediately, otherwise we've
|
|
1336
|
+
// just marked this changeset as a synchronization to ignore, and the user can add other
|
|
1337
|
+
// stuff to it which would break future synchronizations
|
|
1338
|
+
// FIXME: force save for the user to prevent that
|
|
1339
|
+
for (let i = this._startingChangesetIndices.target + 1; i <= this.targetDb.changeset.index + 1; i++)
|
|
1340
|
+
syncChangesetsToUpdate.push(i);
|
|
1341
|
+
syncChangesetsToClear.length = 0;
|
|
1342
|
+
// if reverse sync then we may have received provenance changes which should be marked as sync changes
|
|
1343
|
+
if (this._isReverseSynchronization) {
|
|
1344
|
+
nodeAssert(this.sourceDb.changeset.index, "changeset didn't exist");
|
|
1345
|
+
for (let i = this._startingChangesetIndices.source + 1; i <= this.sourceDb.changeset.index + 1; i++)
|
|
1346
|
+
jsonProps.pendingReverseSyncChangesetIndices.push(i);
|
|
1347
|
+
}
|
|
1348
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `new pendingReverseSyncChanges: ${jsonProps.pendingReverseSyncChangesetIndices}`);
|
|
1349
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `new pendingSyncChanges: ${jsonProps.pendingSyncChangesetIndices}`);
|
|
1350
|
+
}
|
|
1351
|
+
this.provenanceDb.elements.updateAspect({
|
|
1352
|
+
...this._targetScopeProvenanceProps,
|
|
1353
|
+
jsonProperties: JSON.stringify(this._targetScopeProvenanceProps.jsonProperties),
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
// FIXME: is this necessary when manually using lowlevel transform APIs?
|
|
858
1357
|
finalizeTransformation() {
|
|
1358
|
+
this.updateSynchronizationVersion();
|
|
859
1359
|
if (this._partiallyCommittedEntities.size > 0) {
|
|
860
1360
|
core_bentley_1.Logger.logWarning(loggerCategory, [
|
|
861
1361
|
"The following elements were never fully resolved:",
|
|
@@ -867,6 +1367,11 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
867
1367
|
partiallyCommittedElem.forceComplete();
|
|
868
1368
|
}
|
|
869
1369
|
}
|
|
1370
|
+
// FIXME: make processAll have a try {} finally {} that cleans this up
|
|
1371
|
+
if (!this._options.noDetachChangeCache) {
|
|
1372
|
+
if (core_backend_1.ChangeSummaryManager.isChangeCacheAttached(this.sourceDb))
|
|
1373
|
+
core_backend_1.ChangeSummaryManager.detachChangeCache(this.sourceDb);
|
|
1374
|
+
}
|
|
870
1375
|
}
|
|
871
1376
|
/** Imports all relationships that subclass from the specified base class.
|
|
872
1377
|
* @param baseRelClassFullName The specified base relationship class.
|
|
@@ -884,40 +1389,52 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
884
1389
|
* This override calls [[onTransformRelationship]] and then [IModelImporter.importRelationship]($transformer) to update the target iModel.
|
|
885
1390
|
*/
|
|
886
1391
|
onExportRelationship(sourceRelationship) {
|
|
1392
|
+
const sourceFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.sourceId);
|
|
1393
|
+
const targetFedGuid = queryElemFedGuid(this.sourceDb, sourceRelationship.targetId);
|
|
887
1394
|
const targetRelationshipProps = this.onTransformRelationship(sourceRelationship);
|
|
888
1395
|
const targetRelationshipInstanceId = this.importer.importRelationship(targetRelationshipProps);
|
|
889
|
-
if (!this._options.noProvenance && core_bentley_1.Id64.
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1396
|
+
if (!this._options.noProvenance && core_bentley_1.Id64.isValid(targetRelationshipInstanceId)) {
|
|
1397
|
+
let provenance = !this._options.forceExternalSourceAspectProvenance
|
|
1398
|
+
? sourceFedGuid && targetFedGuid && `${sourceFedGuid}/${targetFedGuid}`
|
|
1399
|
+
: undefined;
|
|
1400
|
+
if (!provenance) {
|
|
1401
|
+
const aspectProps = this.initRelationshipProvenance(sourceRelationship, targetRelationshipInstanceId);
|
|
1402
|
+
aspectProps.id = this.queryScopeExternalSource(aspectProps).aspectId;
|
|
1403
|
+
if (undefined === aspectProps.id) {
|
|
1404
|
+
aspectProps.id = this.provenanceDb.elements.insertAspect(aspectProps);
|
|
1405
|
+
}
|
|
1406
|
+
provenance = aspectProps;
|
|
893
1407
|
}
|
|
894
|
-
(
|
|
895
|
-
this.markLastProvenance(aspectProps, { isRelationship: true });
|
|
1408
|
+
this.markLastProvenance(provenance, { isRelationship: true });
|
|
896
1409
|
}
|
|
897
1410
|
}
|
|
898
1411
|
/** Override of [IModelExportHandler.onDeleteRelationship]($transformer) that is called when [IModelExporter]($transformer) detects that a [Relationship]($backend) has been deleted from the source iModel.
|
|
899
1412
|
* This override propagates the delete to the target iModel via [IModelImporter.deleteRelationship]($transformer).
|
|
900
1413
|
*/
|
|
901
1414
|
onDeleteRelationship(sourceRelInstanceId) {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
}
|
|
1415
|
+
nodeAssert(this._deletedSourceRelationshipData, "should be defined at initialization by now");
|
|
1416
|
+
const deletedRelData = this._deletedSourceRelationshipData.get(sourceRelInstanceId);
|
|
1417
|
+
if (!deletedRelData) {
|
|
1418
|
+
// this can occur if both the source and target deleted it
|
|
1419
|
+
core_bentley_1.Logger.logWarning(loggerCategory, "tried to delete a relationship that wasn't in change data");
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
const relArg = deletedRelData.relId ?? {
|
|
1423
|
+
sourceId: deletedRelData.sourceIdInTarget,
|
|
1424
|
+
targetId: deletedRelData.targetIdInTarget,
|
|
1425
|
+
};
|
|
1426
|
+
//
|
|
1427
|
+
// FIXME: make importer.deleteRelationship not need full props
|
|
1428
|
+
const targetRelationship = this.targetDb.relationships.tryGetInstance(deletedRelData.classFullName, relArg);
|
|
1429
|
+
if (targetRelationship) {
|
|
1430
|
+
this.importer.deleteRelationship(targetRelationship.toJSON());
|
|
1431
|
+
}
|
|
1432
|
+
if (deletedRelData.provenanceAspectId) {
|
|
1433
|
+
this.provenanceDb.elements.deleteAspect(deletedRelData.provenanceAspectId);
|
|
1434
|
+
}
|
|
919
1435
|
}
|
|
920
1436
|
/** Detect Relationship deletes using ExternalSourceAspects in the target iModel and a *brute force* comparison against relationships in the source iModel.
|
|
1437
|
+
* @deprecated
|
|
921
1438
|
* @see processChanges
|
|
922
1439
|
* @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.
|
|
923
1440
|
* @throws [[IModelError]] If the required provenance information is not available to detect deletes.
|
|
@@ -927,13 +1444,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
927
1444
|
throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadRequest, "Cannot detect deletes when isReverseSynchronization=true");
|
|
928
1445
|
}
|
|
929
1446
|
const aspectDeleteIds = [];
|
|
930
|
-
const sql = `
|
|
1447
|
+
const sql = `
|
|
1448
|
+
SELECT ECInstanceId, Identifier, JsonProperties
|
|
1449
|
+
FROM ${core_backend_1.ExternalSourceAspect.classFullName} aspect
|
|
1450
|
+
WHERE aspect.Scope.Id=:scopeId
|
|
1451
|
+
AND aspect.Kind=:kind
|
|
1452
|
+
`;
|
|
931
1453
|
await this.targetDb.withPreparedStatement(sql, async (statement) => {
|
|
932
1454
|
statement.bindId("scopeId", this.targetScopeElementId);
|
|
933
1455
|
statement.bindString("kind", core_backend_1.ExternalSourceAspect.Kind.Relationship);
|
|
934
1456
|
while (core_bentley_1.DbResult.BE_SQLITE_ROW === statement.step()) {
|
|
935
1457
|
const sourceRelInstanceId = core_bentley_1.Id64.fromJSON(statement.getValue(1).getString());
|
|
936
1458
|
if (undefined === this.sourceDb.relationships.tryGetInstanceProps(core_backend_1.ElementRefersToElements.classFullName, sourceRelInstanceId)) {
|
|
1459
|
+
// FIXME: make sure matches new provenance-based method
|
|
1460
|
+
// FIXME: use sql JSON_EXTRACT
|
|
937
1461
|
const json = JSON.parse(statement.getValue(2).getString());
|
|
938
1462
|
if (undefined !== json.targetRelInstanceId) {
|
|
939
1463
|
const targetRelationship = this.targetDb.relationships.getInstance(core_backend_1.ElementRefersToElements.classFullName, json.targetRelInstanceId);
|
|
@@ -955,6 +1479,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
955
1479
|
const targetRelationshipProps = sourceRelationship.toJSON();
|
|
956
1480
|
targetRelationshipProps.sourceId = this.context.findTargetElementId(sourceRelationship.sourceId);
|
|
957
1481
|
targetRelationshipProps.targetId = this.context.findTargetElementId(sourceRelationship.targetId);
|
|
1482
|
+
// TODO: move to cloneRelationship in IModelCloneContext
|
|
958
1483
|
sourceRelationship.forEachProperty((propertyName, propertyMetaData) => {
|
|
959
1484
|
if ((core_common_1.PrimitiveTypeCode.Long === propertyMetaData.primitiveType) && ("Id" === propertyMetaData.extendedType)) {
|
|
960
1485
|
targetRelationshipProps[propertyName] = this.context.findTargetElementId(sourceRelationship.asAny[propertyName]);
|
|
@@ -1116,26 +1641,86 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1116
1641
|
return this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
|
|
1117
1642
|
}
|
|
1118
1643
|
/**
|
|
1119
|
-
* Initialize prerequisites of processing, you must initialize with an [[
|
|
1120
|
-
* are intending process changes, but prefer using [[processChanges]]
|
|
1121
|
-
* Called by all `process*` functions implicitly.
|
|
1644
|
+
* Initialize prerequisites of processing, you must initialize with an [[InitOptions]] if you
|
|
1645
|
+
* are intending to process changes, but prefer using [[processChanges]] explicitly since it calls this.
|
|
1646
|
+
* @note Called by all `process*` functions implicitly.
|
|
1122
1647
|
* Overriders must call `super.initialize()` first
|
|
1123
1648
|
*/
|
|
1124
1649
|
async initialize(args) {
|
|
1125
1650
|
if (this._initialized)
|
|
1126
1651
|
return;
|
|
1127
1652
|
await this.context.initialize();
|
|
1653
|
+
await this._tryInitChangesetData(args);
|
|
1654
|
+
await this.exporter.initialize(this.getExportInitOpts(args ?? {}));
|
|
1655
|
+
// Exporter must be initialized prior to `initFromExternalSourceAspects` in order to handle entity recreations.
|
|
1128
1656
|
// eslint-disable-next-line deprecation/deprecation
|
|
1129
1657
|
await this.initFromExternalSourceAspects(args);
|
|
1130
1658
|
this._initialized = true;
|
|
1131
1659
|
}
|
|
1660
|
+
async _tryInitChangesetData(args) {
|
|
1661
|
+
if (!args || this.sourceDb.iTwinId === undefined || this.sourceDb.changeset.index === undefined) {
|
|
1662
|
+
this._sourceChangeDataState = "unconnected";
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const noChanges = this._synchronizationVersion.index === this.sourceDb.changeset.index;
|
|
1666
|
+
if (noChanges) {
|
|
1667
|
+
this._sourceChangeDataState = "no-changes";
|
|
1668
|
+
this._changeSummaryIds = [];
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
// NOTE: that we do NOT download the changesummary for the last transformed version, we want
|
|
1672
|
+
// to ignore those already processed changes
|
|
1673
|
+
const startChangesetIndexOrId = args.startChangeset?.index
|
|
1674
|
+
?? args.startChangeset?.id
|
|
1675
|
+
?? this._synchronizationVersion.index + 1;
|
|
1676
|
+
const endChangesetId = this.sourceDb.changeset.id;
|
|
1677
|
+
const [startChangesetIndex, endChangesetIndex] = await Promise.all(([startChangesetIndexOrId, endChangesetId])
|
|
1678
|
+
.map(async (indexOrId) => typeof indexOrId === "number"
|
|
1679
|
+
? indexOrId
|
|
1680
|
+
: core_backend_1.IModelHost.hubAccess
|
|
1681
|
+
.queryChangeset({
|
|
1682
|
+
iModelId: this.sourceDb.iModelId,
|
|
1683
|
+
// eslint-disable-next-line deprecation/deprecation
|
|
1684
|
+
changeset: { id: indexOrId },
|
|
1685
|
+
accessToken: args.accessToken,
|
|
1686
|
+
})
|
|
1687
|
+
.then((changeset) => changeset.index)));
|
|
1688
|
+
const missingChangesets = startChangesetIndex > this._synchronizationVersion.index + 1;
|
|
1689
|
+
if (!this._options.ignoreMissingChangesetsInSynchronizations
|
|
1690
|
+
&& startChangesetIndex !== this._synchronizationVersion.index + 1
|
|
1691
|
+
&& this._synchronizationVersion.index !== -1) {
|
|
1692
|
+
throw Error(`synchronization is ${missingChangesets ? "missing changesets" : ""},`
|
|
1693
|
+
+ " startChangesetId should be"
|
|
1694
|
+
+ " exactly the first changeset *after* the previous synchronization to not miss data."
|
|
1695
|
+
+ ` You specified '${startChangesetIndexOrId}' which is changeset #${startChangesetIndex}`
|
|
1696
|
+
+ ` but the previous synchronization for this targetScopeElement was '${this._synchronizationVersion.id}'`
|
|
1697
|
+
+ ` which is changeset #${this._synchronizationVersion.index}. The transformer expected`
|
|
1698
|
+
+ ` #${this._synchronizationVersion.index + 1}.`);
|
|
1699
|
+
}
|
|
1700
|
+
nodeAssert(this._targetScopeProvenanceProps, "_targetScopeProvenanceProps should be set by now");
|
|
1701
|
+
const changesetsToSkip = this._isReverseSynchronization
|
|
1702
|
+
? this._targetScopeProvenanceProps.jsonProperties.pendingReverseSyncChangesetIndices
|
|
1703
|
+
: this._targetScopeProvenanceProps.jsonProperties.pendingSyncChangesetIndices;
|
|
1704
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `changesets to skip: ${changesetsToSkip}`);
|
|
1705
|
+
this._changesetRanges = (0, Algo_1.rangesFromRangeAndSkipped)(startChangesetIndex, endChangesetIndex, changesetsToSkip);
|
|
1706
|
+
core_bentley_1.Logger.logTrace(loggerCategory, `ranges: ${this._changesetRanges}`);
|
|
1707
|
+
for (const [first, end] of this._changesetRanges) {
|
|
1708
|
+
this._changeSummaryIds = await core_backend_1.ChangeSummaryManager.createChangeSummaries({
|
|
1709
|
+
accessToken: args.accessToken,
|
|
1710
|
+
iModelId: this.sourceDb.iModelId,
|
|
1711
|
+
iTwinId: this.sourceDb.iTwinId,
|
|
1712
|
+
range: { first, end },
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
core_backend_1.ChangeSummaryManager.attachChangeCache(this.sourceDb);
|
|
1716
|
+
this._sourceChangeDataState = "has-changes";
|
|
1717
|
+
}
|
|
1132
1718
|
/** Export everything from the source iModel and import the transformed entities into the target iModel.
|
|
1133
1719
|
* @note [[processSchemas]] is not called automatically since the target iModel may want a different collection of schemas.
|
|
1134
1720
|
*/
|
|
1135
1721
|
async processAll() {
|
|
1136
|
-
core_bentley_1.Logger.logTrace(loggerCategory, "processAll()");
|
|
1137
1722
|
this.logSettings();
|
|
1138
|
-
this.
|
|
1723
|
+
this.initScopeProvenance();
|
|
1139
1724
|
await this.initialize();
|
|
1140
1725
|
await this.exporter.exportCodeSpecs();
|
|
1141
1726
|
await this.exporter.exportFonts();
|
|
@@ -1155,12 +1740,15 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1155
1740
|
this.finalizeTransformation();
|
|
1156
1741
|
}
|
|
1157
1742
|
markLastProvenance(sourceAspect, { isRelationship = false }) {
|
|
1158
|
-
this._lastProvenanceEntityInfo
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1743
|
+
this._lastProvenanceEntityInfo
|
|
1744
|
+
= typeof sourceAspect === "string"
|
|
1745
|
+
? sourceAspect
|
|
1746
|
+
: {
|
|
1747
|
+
entityId: sourceAspect.element.id,
|
|
1748
|
+
aspectId: sourceAspect.id,
|
|
1749
|
+
aspectVersion: sourceAspect.version ?? "",
|
|
1750
|
+
aspectKind: isRelationship ? core_backend_1.ExternalSourceAspect.Kind.Relationship : core_backend_1.ExternalSourceAspect.Kind.Element,
|
|
1751
|
+
};
|
|
1164
1752
|
}
|
|
1165
1753
|
/**
|
|
1166
1754
|
* Load the state of the active transformation from an open SQLiteDb
|
|
@@ -1172,17 +1760,35 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1172
1760
|
const lastProvenanceEntityInfo = db.withSqliteStatement(`SELECT entityId, aspectId, aspectVersion, aspectKind FROM ${IModelTransformer.lastProvenanceEntityInfoTable}`, (stmt) => {
|
|
1173
1761
|
if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
|
|
1174
1762
|
throw Error("expected row when getting lastProvenanceEntityId from target state table");
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1763
|
+
const entityId = stmt.getValueString(0);
|
|
1764
|
+
const isGuidOrGuidPair = entityId.includes("-");
|
|
1765
|
+
return isGuidOrGuidPair
|
|
1766
|
+
? entityId
|
|
1767
|
+
: {
|
|
1768
|
+
entityId,
|
|
1769
|
+
aspectId: stmt.getValueString(1),
|
|
1770
|
+
aspectVersion: stmt.getValueString(2),
|
|
1771
|
+
aspectKind: stmt.getValueString(3),
|
|
1772
|
+
};
|
|
1181
1773
|
});
|
|
1182
|
-
|
|
1183
|
-
//
|
|
1184
|
-
|
|
1774
|
+
/*
|
|
1775
|
+
// TODO: maybe save transformer state resumption state based on target changset and require calls
|
|
1776
|
+
// to saveChanges
|
|
1777
|
+
if () {
|
|
1778
|
+
const [sourceFedGuid, targetFedGuid, relClassFullName] = lastProvenanceEntityInfo.split("/");
|
|
1779
|
+
const isRelProvenance = targetFedGuid !== undefined;
|
|
1780
|
+
const instanceId = isRelProvenance
|
|
1781
|
+
? this.targetDb.elements.getElement({federationGuid: sourceFedGuid})
|
|
1782
|
+
: "";
|
|
1783
|
+
//const classId =
|
|
1784
|
+
if (isRelProvenance) {
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
*/
|
|
1788
|
+
const targetHasCorrectLastProvenance = typeof lastProvenanceEntityInfo === "string" ||
|
|
1789
|
+
// ignore provenance check if it's null since we can't bind those ids
|
|
1185
1790
|
!core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.entityId) ||
|
|
1791
|
+
!core_bentley_1.Id64.isValidId64(lastProvenanceEntityInfo.aspectId) ||
|
|
1186
1792
|
this.provenanceDb.withPreparedStatement(`
|
|
1187
1793
|
SELECT Version FROM ${core_backend_1.ExternalSourceAspect.classFullName}
|
|
1188
1794
|
WHERE Scope.Id=:scopeId
|
|
@@ -1223,9 +1829,13 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1223
1829
|
this.context.loadStateFromDb(db);
|
|
1224
1830
|
this.importer.loadStateFromJson(state.importerState);
|
|
1225
1831
|
this.exporter.loadStateFromJson(state.exporterState);
|
|
1832
|
+
this._elementsWithExplicitlyTrackedProvenance = core_bentley_1.CompressedId64Set.decompressSet(state.explicitlyTrackedElements);
|
|
1226
1833
|
this.loadAdditionalStateJson(state.additionalState);
|
|
1227
1834
|
}
|
|
1228
1835
|
/**
|
|
1836
|
+
* @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
|
|
1837
|
+
* from the original changeset
|
|
1838
|
+
*
|
|
1229
1839
|
* Return a new transformer instance with the same remappings state as saved from a previous [[IModelTransformer.saveStateToFile]] call.
|
|
1230
1840
|
* This allows you to "resume" an iModel transformation, you will have to call [[IModelTransformer.processChanges]]/[[IModelTransformer.processAll]]
|
|
1231
1841
|
* again but the remapping state will cause already mapped elements to be skipped.
|
|
@@ -1270,6 +1880,7 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1270
1880
|
const jsonState = {
|
|
1271
1881
|
transformerClass: this.constructor.name,
|
|
1272
1882
|
options: this._options,
|
|
1883
|
+
explicitlyTrackedElements: core_bentley_1.CompressedId64Set.compressSet(this._elementsWithExplicitlyTrackedProvenance),
|
|
1273
1884
|
importerState: this.importer.saveStateToJson(),
|
|
1274
1885
|
exporterState: this.exporter.saveStateToJson(),
|
|
1275
1886
|
additionalState: this.getAdditionalStateJson(),
|
|
@@ -1279,8 +1890,9 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1279
1890
|
throw Error("Failed to create the js state table in the state database");
|
|
1280
1891
|
if (core_bentley_1.DbResult.BE_SQLITE_DONE !== db.executeSQL(`
|
|
1281
1892
|
CREATE TABLE ${IModelTransformer.lastProvenanceEntityInfoTable} (
|
|
1282
|
-
--
|
|
1893
|
+
-- either the invalid id for null provenance state, federation guid (or pair for rels) of the entity, or a hex element id
|
|
1283
1894
|
entityId TEXT,
|
|
1895
|
+
-- the following are only valid if the above entityId is a hex id representation
|
|
1284
1896
|
aspectId TEXT,
|
|
1285
1897
|
aspectVersion TEXT,
|
|
1286
1898
|
aspectKind TEXT
|
|
@@ -1294,16 +1906,20 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1294
1906
|
throw Error("Failed to insert options into the state database");
|
|
1295
1907
|
});
|
|
1296
1908
|
db.withSqliteStatement(`INSERT INTO ${IModelTransformer.lastProvenanceEntityInfoTable} (entityId, aspectId, aspectVersion, aspectKind) VALUES (?,?,?,?)`, (stmt) => {
|
|
1297
|
-
|
|
1298
|
-
stmt.bindString(
|
|
1299
|
-
stmt.bindString(
|
|
1300
|
-
stmt.bindString(
|
|
1909
|
+
const lastProvenanceEntityInfo = this._lastProvenanceEntityInfo;
|
|
1910
|
+
stmt.bindString(1, lastProvenanceEntityInfo?.entityId ?? this._lastProvenanceEntityInfo);
|
|
1911
|
+
stmt.bindString(2, lastProvenanceEntityInfo?.aspectId ?? "");
|
|
1912
|
+
stmt.bindString(3, lastProvenanceEntityInfo?.aspectVersion ?? "");
|
|
1913
|
+
stmt.bindString(4, lastProvenanceEntityInfo?.aspectKind ?? "");
|
|
1301
1914
|
if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
|
|
1302
1915
|
throw Error("Failed to insert options into the state database");
|
|
1303
1916
|
});
|
|
1304
1917
|
db.saveChanges();
|
|
1305
1918
|
}
|
|
1306
1919
|
/**
|
|
1920
|
+
* @deprecated in 0.1.x, this is buggy, and it is now equivalently efficient to simply restart the transformation
|
|
1921
|
+
* from the original changeset
|
|
1922
|
+
*
|
|
1307
1923
|
* Save the state of the active transformation to a file path, if a file at the path already exists, it will be overwritten
|
|
1308
1924
|
* This state can be used by [[IModelTransformer.resumeTransformation]] to resume a transformation from this point.
|
|
1309
1925
|
* The serialization format is a custom sqlite database.
|
|
@@ -1325,25 +1941,54 @@ class IModelTransformer extends IModelExporter_1.IModelExportHandler {
|
|
|
1325
1941
|
db.closeDb();
|
|
1326
1942
|
}
|
|
1327
1943
|
}
|
|
1328
|
-
async processChanges(
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
this.
|
|
1332
|
-
const
|
|
1944
|
+
async processChanges(optionsOrAccessToken, startChangesetId) {
|
|
1945
|
+
this._isSynchronization = true;
|
|
1946
|
+
// FIXME: we used to validateScopeProvenance... does initing it cover that?
|
|
1947
|
+
this.initScopeProvenance();
|
|
1948
|
+
const args = typeof optionsOrAccessToken === "string"
|
|
1333
1949
|
? {
|
|
1334
|
-
accessToken:
|
|
1335
|
-
startChangeset: startChangesetId
|
|
1336
|
-
|
|
1950
|
+
accessToken: optionsOrAccessToken,
|
|
1951
|
+
startChangeset: startChangesetId
|
|
1952
|
+
? { id: startChangesetId }
|
|
1953
|
+
: { index: this._synchronizationVersion.index + 1 },
|
|
1337
1954
|
}
|
|
1338
|
-
:
|
|
1339
|
-
|
|
1340
|
-
await this.
|
|
1955
|
+
: optionsOrAccessToken;
|
|
1956
|
+
this.logSettings();
|
|
1957
|
+
await this.initialize(args);
|
|
1958
|
+
// must wait for initialization of synchronization provenance data
|
|
1959
|
+
await this.exporter.exportChanges(this.getExportInitOpts(args));
|
|
1341
1960
|
await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation
|
|
1342
1961
|
if (this._options.optimizeGeometry)
|
|
1343
1962
|
this.importer.optimizeGeometry(this._options.optimizeGeometry);
|
|
1344
1963
|
this.importer.computeProjectExtents();
|
|
1345
1964
|
this.finalizeTransformation();
|
|
1346
1965
|
}
|
|
1966
|
+
/** Changeset data must be initialized in order to build correct changeOptions.
|
|
1967
|
+
* Call [[IModelTransformer.initialize]] for initialization of synchronization provenance data
|
|
1968
|
+
*/
|
|
1969
|
+
getExportInitOpts(opts) {
|
|
1970
|
+
if (!this._isSynchronization)
|
|
1971
|
+
return {};
|
|
1972
|
+
return {
|
|
1973
|
+
accessToken: opts.accessToken,
|
|
1974
|
+
...this._changesetRanges
|
|
1975
|
+
? { changesetRanges: this._changesetRanges }
|
|
1976
|
+
: opts.startChangeset
|
|
1977
|
+
? { startChangeset: opts.startChangeset }
|
|
1978
|
+
: { startChangeset: { index: this._synchronizationVersion.index + 1 } },
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
/** Combine an array of source elements into a single target element.
|
|
1982
|
+
* All source and target elements must be created before calling this method.
|
|
1983
|
+
* The "combine" operation is a remap and no properties from the source elements will be exported into the target
|
|
1984
|
+
* and provenance will be explicitly tracked by ExternalSourceAspects
|
|
1985
|
+
*/
|
|
1986
|
+
combineElements(sourceElementIds, targetElementId) {
|
|
1987
|
+
for (const elementId of sourceElementIds) {
|
|
1988
|
+
this.context.remapElement(elementId, targetElementId);
|
|
1989
|
+
this._elementsWithExplicitlyTrackedProvenance.add(elementId);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1347
1992
|
}
|
|
1348
1993
|
exports.IModelTransformer = IModelTransformer;
|
|
1349
1994
|
/** @internal the name of the table where javascript state of the transformer is serialized in transformer state dumps */
|
|
@@ -1374,6 +2019,7 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1374
2019
|
* @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
|
|
1375
2020
|
*/
|
|
1376
2021
|
async placeTemplate3d(sourceTemplateModelId, targetModelId, placement) {
|
|
2022
|
+
await this.initialize();
|
|
1377
2023
|
this.context.remapElement(sourceTemplateModelId, targetModelId);
|
|
1378
2024
|
this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(placement.origin, placement.angles.toMatrix3d());
|
|
1379
2025
|
this._sourceIdToTargetIdMap = new Map();
|
|
@@ -1394,6 +2040,7 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1394
2040
|
* @returns The mapping of sourceElementIds from the template model to the instantiated targetElementIds in the targetDb in case further processing is required.
|
|
1395
2041
|
*/
|
|
1396
2042
|
async placeTemplate2d(sourceTemplateModelId, targetModelId, placement) {
|
|
2043
|
+
await this.initialize();
|
|
1397
2044
|
this.context.remapElement(sourceTemplateModelId, targetModelId);
|
|
1398
2045
|
this._transform3d = core_geometry_1.Transform.createOriginAndMatrix(core_geometry_1.Point3d.createFrom(placement.origin), placement.rotation);
|
|
1399
2046
|
this._sourceIdToTargetIdMap = new Map();
|
|
@@ -1430,16 +2077,12 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1430
2077
|
const targetElementProps = super.onTransformElement(sourceElement);
|
|
1431
2078
|
targetElementProps.federationGuid = core_bentley_1.Guid.createValue(); // clone from template should create a new federationGuid
|
|
1432
2079
|
targetElementProps.code = core_common_1.Code.createEmpty(); // clone from template should not maintain codes
|
|
1433
|
-
if (sourceElement instanceof core_backend_1.
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
targetElementProps.placement = placement;
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
else if (sourceElement instanceof core_backend_1.GeometricElement2d) {
|
|
1441
|
-
const placement = core_common_1.Placement2d.fromJSON(targetElementProps.placement);
|
|
2080
|
+
if (sourceElement instanceof core_backend_1.GeometricElement) {
|
|
2081
|
+
const is3d = sourceElement instanceof core_backend_1.GeometricElement3d;
|
|
2082
|
+
const placementClass = is3d ? core_common_1.Placement3d : core_common_1.Placement2d;
|
|
2083
|
+
const placement = (placementClass).fromJSON(targetElementProps.placement);
|
|
1442
2084
|
if (placement.isValid) {
|
|
2085
|
+
nodeAssert(this._transform3d);
|
|
1443
2086
|
placement.multiplyTransform(this._transform3d);
|
|
1444
2087
|
targetElementProps.placement = placement;
|
|
1445
2088
|
}
|
|
@@ -1449,4 +2092,17 @@ class TemplateModelCloner extends IModelTransformer {
|
|
|
1449
2092
|
}
|
|
1450
2093
|
}
|
|
1451
2094
|
exports.TemplateModelCloner = TemplateModelCloner;
|
|
2095
|
+
function queryElemFedGuid(db, elemId) {
|
|
2096
|
+
return db.withPreparedStatement(`
|
|
2097
|
+
SELECT FederationGuid
|
|
2098
|
+
FROM bis.Element
|
|
2099
|
+
WHERE ECInstanceId=?
|
|
2100
|
+
`, (stmt) => {
|
|
2101
|
+
stmt.bindId(1, elemId);
|
|
2102
|
+
(0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW);
|
|
2103
|
+
const result = stmt.getValue(0).getGuid();
|
|
2104
|
+
(0, core_bentley_1.assert)(stmt.step() === core_bentley_1.DbResult.BE_SQLITE_DONE);
|
|
2105
|
+
return result;
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
1452
2108
|
//# sourceMappingURL=IModelTransformer.js.map
|