@itwin/core-backend 5.9.0 → 5.9.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 +8 -1
- package/lib/cjs/BriefcaseManager.d.ts +14 -62
- package/lib/cjs/BriefcaseManager.d.ts.map +1 -1
- package/lib/cjs/BriefcaseManager.js +29 -162
- package/lib/cjs/BriefcaseManager.js.map +1 -1
- package/lib/cjs/IModelJsFs.d.ts +2 -0
- package/lib/cjs/IModelJsFs.d.ts.map +1 -1
- package/lib/cjs/IModelJsFs.js +14 -0
- package/lib/cjs/IModelJsFs.js.map +1 -1
- package/lib/cjs/TxnManager.d.ts +3 -0
- package/lib/cjs/TxnManager.d.ts.map +1 -1
- package/lib/cjs/TxnManager.js +95 -13
- package/lib/cjs/TxnManager.js.map +1 -1
- package/lib/esm/BriefcaseManager.d.ts +14 -62
- package/lib/esm/BriefcaseManager.d.ts.map +1 -1
- package/lib/esm/BriefcaseManager.js +30 -163
- package/lib/esm/BriefcaseManager.js.map +1 -1
- package/lib/esm/IModelJsFs.d.ts +2 -0
- package/lib/esm/IModelJsFs.d.ts.map +1 -1
- package/lib/esm/IModelJsFs.js +14 -0
- package/lib/esm/IModelJsFs.js.map +1 -1
- package/lib/esm/TxnManager.d.ts +3 -0
- package/lib/esm/TxnManager.d.ts.map +1 -1
- package/lib/esm/TxnManager.js +96 -14
- package/lib/esm/TxnManager.js.map +1 -1
- package/lib/esm/test/hubaccess/SemanticRebase.test.js +2242 -68
- package/lib/esm/test/hubaccess/SemanticRebase.test.js.map +1 -1
- package/package.json +13 -13
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* See LICENSE.md in the project root for license terms and full copyright notice.
|
|
4
4
|
*--------------------------------------------------------------------------------------------*/
|
|
5
5
|
import { Id64 } from "@itwin/core-bentley";
|
|
6
|
-
import { Code, IModel, SubCategoryAppearance } from "@itwin/core-common";
|
|
6
|
+
import { Code, IModel, QueryRowFormat, SubCategoryAppearance } from "@itwin/core-common";
|
|
7
7
|
import * as chai from "chai";
|
|
8
8
|
import { HubWrappers, IModelTestUtils, KnownTestLocations } from "..";
|
|
9
9
|
import { BriefcaseManager, ChannelControl, DrawingCategory, IModelJsFs } from "../../core-backend";
|
|
@@ -218,6 +218,210 @@ class TestIModel {
|
|
|
218
218
|
</ECEntityClass>
|
|
219
219
|
</ECSchema>`,
|
|
220
220
|
};
|
|
221
|
+
/** Additional schemas for extended edge-case tests */
|
|
222
|
+
static extendedSchemas = {
|
|
223
|
+
/** v01x00x01 - Adds CUniqueAspect class (ElementUniqueAspect subclass) with AspectProp for aspect rebase tests */
|
|
224
|
+
v01x00x01WithAspect: `<?xml version="1.0" encoding="UTF-8"?>
|
|
225
|
+
<ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
|
|
226
|
+
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
|
|
227
|
+
<ECEntityClass typeName="A">
|
|
228
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
229
|
+
<ECProperty propertyName="PropA" typeName="string"/>
|
|
230
|
+
</ECEntityClass>
|
|
231
|
+
<ECEntityClass typeName="C">
|
|
232
|
+
<BaseClass>A</BaseClass>
|
|
233
|
+
<ECProperty propertyName="PropC" typeName="string"/>
|
|
234
|
+
</ECEntityClass>
|
|
235
|
+
<ECEntityClass typeName="D">
|
|
236
|
+
<BaseClass>A</BaseClass>
|
|
237
|
+
<ECProperty propertyName="PropD" typeName="string"/>
|
|
238
|
+
</ECEntityClass>
|
|
239
|
+
<ECEntityClass typeName="CUniqueAspect" modifier="None">
|
|
240
|
+
<BaseClass>bis:ElementUniqueAspect</BaseClass>
|
|
241
|
+
<ECProperty propertyName="AspectProp" typeName="string"/>
|
|
242
|
+
</ECEntityClass>
|
|
243
|
+
</ECSchema>`,
|
|
244
|
+
/** v01x00x02 - Extends v01WithAspect by adding AspectProp2 to CUniqueAspect (trivial aspect schema evolution) */
|
|
245
|
+
v01x00x02WithAspectProp2: `<?xml version="1.0" encoding="UTF-8"?>
|
|
246
|
+
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
|
|
247
|
+
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
|
|
248
|
+
<ECEntityClass typeName="A">
|
|
249
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
250
|
+
<ECProperty propertyName="PropA" typeName="string"/>
|
|
251
|
+
</ECEntityClass>
|
|
252
|
+
<ECEntityClass typeName="C">
|
|
253
|
+
<BaseClass>A</BaseClass>
|
|
254
|
+
<ECProperty propertyName="PropC" typeName="string"/>
|
|
255
|
+
</ECEntityClass>
|
|
256
|
+
<ECEntityClass typeName="D">
|
|
257
|
+
<BaseClass>A</BaseClass>
|
|
258
|
+
<ECProperty propertyName="PropD" typeName="string"/>
|
|
259
|
+
</ECEntityClass>
|
|
260
|
+
<ECEntityClass typeName="CUniqueAspect" modifier="None">
|
|
261
|
+
<BaseClass>bis:ElementUniqueAspect</BaseClass>
|
|
262
|
+
<ECProperty propertyName="AspectProp" typeName="string"/>
|
|
263
|
+
<ECProperty propertyName="AspectProp2" typeName="string"/>
|
|
264
|
+
</ECEntityClass>
|
|
265
|
+
</ECSchema>`,
|
|
266
|
+
/** v01x00x01 - Adds new entity class E extending A with PropE (tests new class addition) */
|
|
267
|
+
v01x00x01AddClassE: `<?xml version="1.0" encoding="UTF-8"?>
|
|
268
|
+
<ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
|
|
269
|
+
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
|
|
270
|
+
<ECEntityClass typeName="A">
|
|
271
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
272
|
+
<ECProperty propertyName="PropA" typeName="string"/>
|
|
273
|
+
</ECEntityClass>
|
|
274
|
+
<ECEntityClass typeName="C">
|
|
275
|
+
<BaseClass>A</BaseClass>
|
|
276
|
+
<ECProperty propertyName="PropC" typeName="string"/>
|
|
277
|
+
</ECEntityClass>
|
|
278
|
+
<ECEntityClass typeName="D">
|
|
279
|
+
<BaseClass>A</BaseClass>
|
|
280
|
+
<ECProperty propertyName="PropD" typeName="string"/>
|
|
281
|
+
</ECEntityClass>
|
|
282
|
+
<ECEntityClass typeName="E">
|
|
283
|
+
<BaseClass>A</BaseClass>
|
|
284
|
+
<ECProperty propertyName="PropE" typeName="string"/>
|
|
285
|
+
</ECEntityClass>
|
|
286
|
+
</ECSchema>`,
|
|
287
|
+
/** v01x00x02 - Extends v01AddClassE by adding PropE2 to class E */
|
|
288
|
+
v01x00x02AddClassEPropE2: `<?xml version="1.0" encoding="UTF-8"?>
|
|
289
|
+
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
|
|
290
|
+
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
|
|
291
|
+
<ECEntityClass typeName="A">
|
|
292
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
293
|
+
<ECProperty propertyName="PropA" typeName="string"/>
|
|
294
|
+
</ECEntityClass>
|
|
295
|
+
<ECEntityClass typeName="C">
|
|
296
|
+
<BaseClass>A</BaseClass>
|
|
297
|
+
<ECProperty propertyName="PropC" typeName="string"/>
|
|
298
|
+
</ECEntityClass>
|
|
299
|
+
<ECEntityClass typeName="D">
|
|
300
|
+
<BaseClass>A</BaseClass>
|
|
301
|
+
<ECProperty propertyName="PropD" typeName="string"/>
|
|
302
|
+
</ECEntityClass>
|
|
303
|
+
<ECEntityClass typeName="E">
|
|
304
|
+
<BaseClass>A</BaseClass>
|
|
305
|
+
<ECProperty propertyName="PropE" typeName="string"/>
|
|
306
|
+
<ECProperty propertyName="PropE2" typeName="string"/>
|
|
307
|
+
</ECEntityClass>
|
|
308
|
+
</ECSchema>`,
|
|
309
|
+
/** v01x00x01 - Adds multi-type properties (int, double, boolean) to class C for type-variation tests */
|
|
310
|
+
v01x00x01MultiTypeProps: `<?xml version="1.0" encoding="UTF-8"?>
|
|
311
|
+
<ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
|
|
312
|
+
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
|
|
313
|
+
<ECEntityClass typeName="A">
|
|
314
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
315
|
+
<ECProperty propertyName="PropA" typeName="string"/>
|
|
316
|
+
</ECEntityClass>
|
|
317
|
+
<ECEntityClass typeName="C">
|
|
318
|
+
<BaseClass>A</BaseClass>
|
|
319
|
+
<ECProperty propertyName="PropC" typeName="string"/>
|
|
320
|
+
<ECProperty propertyName="PropCInt" typeName="int"/>
|
|
321
|
+
<ECProperty propertyName="PropCDouble" typeName="double"/>
|
|
322
|
+
<ECProperty propertyName="PropCBool" typeName="boolean"/>
|
|
323
|
+
</ECEntityClass>
|
|
324
|
+
<ECEntityClass typeName="D">
|
|
325
|
+
<BaseClass>A</BaseClass>
|
|
326
|
+
<ECProperty propertyName="PropD" typeName="string"/>
|
|
327
|
+
</ECEntityClass>
|
|
328
|
+
</ECSchema>`,
|
|
329
|
+
/** v01x00x02 - Extends v01MultiTypeProps by adding PropD2 to class D */
|
|
330
|
+
v01x00x02MultiTypePropsExtended: `<?xml version="1.0" encoding="UTF-8"?>
|
|
331
|
+
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
|
|
332
|
+
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
|
|
333
|
+
<ECEntityClass typeName="A">
|
|
334
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
335
|
+
<ECProperty propertyName="PropA" typeName="string"/>
|
|
336
|
+
</ECEntityClass>
|
|
337
|
+
<ECEntityClass typeName="C">
|
|
338
|
+
<BaseClass>A</BaseClass>
|
|
339
|
+
<ECProperty propertyName="PropC" typeName="string"/>
|
|
340
|
+
<ECProperty propertyName="PropCInt" typeName="int"/>
|
|
341
|
+
<ECProperty propertyName="PropCDouble" typeName="double"/>
|
|
342
|
+
<ECProperty propertyName="PropCBool" typeName="boolean"/>
|
|
343
|
+
</ECEntityClass>
|
|
344
|
+
<ECEntityClass typeName="D">
|
|
345
|
+
<BaseClass>A</BaseClass>
|
|
346
|
+
<ECProperty propertyName="PropD" typeName="string"/>
|
|
347
|
+
<ECProperty propertyName="PropD2" typeName="string"/>
|
|
348
|
+
</ECEntityClass>
|
|
349
|
+
</ECSchema>`,
|
|
350
|
+
/** v01x00x01 - Adds multi-type properties (int, double, boolean) to class C for type-variation tests */
|
|
351
|
+
v01x00x02MultiTypePropsMovePropDToA: `<?xml version="1.0" encoding="UTF-8"?>
|
|
352
|
+
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
|
|
353
|
+
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
|
|
354
|
+
<ECEntityClass typeName="A">
|
|
355
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
356
|
+
<ECProperty propertyName="PropA" typeName="string"/>
|
|
357
|
+
<ECProperty propertyName="PropD" typeName="string"/>
|
|
358
|
+
</ECEntityClass>
|
|
359
|
+
<ECEntityClass typeName="C">
|
|
360
|
+
<BaseClass>A</BaseClass>
|
|
361
|
+
<ECProperty propertyName="PropC" typeName="string"/>
|
|
362
|
+
<ECProperty propertyName="PropCInt" typeName="int"/>
|
|
363
|
+
<ECProperty propertyName="PropCDouble" typeName="double"/>
|
|
364
|
+
<ECProperty propertyName="PropCBool" typeName="boolean"/>
|
|
365
|
+
</ECEntityClass>
|
|
366
|
+
<ECEntityClass typeName="D">
|
|
367
|
+
<BaseClass>A</BaseClass>
|
|
368
|
+
</ECEntityClass>
|
|
369
|
+
</ECSchema>`,
|
|
370
|
+
/** v01x00x02 - Moves PropD from class D to class A (transforming change for D, analogous to MovePropCToA) */
|
|
371
|
+
v01x00x02MovePropDToA: `<?xml version="1.0" encoding="UTF-8"?>
|
|
372
|
+
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
|
|
373
|
+
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
|
|
374
|
+
<ECEntityClass typeName="A">
|
|
375
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
376
|
+
<ECProperty propertyName="PropA" typeName="string"/>
|
|
377
|
+
<ECProperty propertyName="PropD" typeName="string"/>
|
|
378
|
+
</ECEntityClass>
|
|
379
|
+
<ECEntityClass typeName="C">
|
|
380
|
+
<BaseClass>A</BaseClass>
|
|
381
|
+
<ECProperty propertyName="PropC" typeName="string"/>
|
|
382
|
+
</ECEntityClass>
|
|
383
|
+
<ECEntityClass typeName="D">
|
|
384
|
+
<BaseClass>A</BaseClass>
|
|
385
|
+
</ECEntityClass>
|
|
386
|
+
</ECSchema>`,
|
|
387
|
+
/** v01x00x01 - Adds a binary property (PropCBin) to class C */
|
|
388
|
+
v01x00x01WithBinaryProp: `<?xml version="1.0" encoding="UTF-8"?>
|
|
389
|
+
<ECSchema schemaName="TestDomain" alias="td" version="01.00.01" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
|
|
390
|
+
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
|
|
391
|
+
<ECEntityClass typeName="A">
|
|
392
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
393
|
+
<ECProperty propertyName="PropA" typeName="string"/>
|
|
394
|
+
</ECEntityClass>
|
|
395
|
+
<ECEntityClass typeName="C">
|
|
396
|
+
<BaseClass>A</BaseClass>
|
|
397
|
+
<ECProperty propertyName="PropC" typeName="string"/>
|
|
398
|
+
<ECProperty propertyName="PropCBin" typeName="binary"/>
|
|
399
|
+
</ECEntityClass>
|
|
400
|
+
<ECEntityClass typeName="D">
|
|
401
|
+
<BaseClass>A</BaseClass>
|
|
402
|
+
<ECProperty propertyName="PropD" typeName="string"/>
|
|
403
|
+
</ECEntityClass>
|
|
404
|
+
</ECSchema>`,
|
|
405
|
+
/** v01x00x02 - Extends v01x00x01WithBinaryProp with PropD2 on class D (trivial additive change) */
|
|
406
|
+
v01x00x02WithBinaryPropAndPropD2: `<?xml version="1.0" encoding="UTF-8"?>
|
|
407
|
+
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
|
|
408
|
+
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
|
|
409
|
+
<ECEntityClass typeName="A">
|
|
410
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
411
|
+
<ECProperty propertyName="PropA" typeName="string"/>
|
|
412
|
+
</ECEntityClass>
|
|
413
|
+
<ECEntityClass typeName="C">
|
|
414
|
+
<BaseClass>A</BaseClass>
|
|
415
|
+
<ECProperty propertyName="PropC" typeName="string"/>
|
|
416
|
+
<ECProperty propertyName="PropCBin" typeName="binary"/>
|
|
417
|
+
</ECEntityClass>
|
|
418
|
+
<ECEntityClass typeName="D">
|
|
419
|
+
<BaseClass>A</BaseClass>
|
|
420
|
+
<ECProperty propertyName="PropD" typeName="string"/>
|
|
421
|
+
<ECProperty propertyName="PropD2" typeName="string"/>
|
|
422
|
+
</ECEntityClass>
|
|
423
|
+
</ECSchema>`,
|
|
424
|
+
};
|
|
221
425
|
/**
|
|
222
426
|
* Create and initialize a new test iModel with far and local briefcases.
|
|
223
427
|
* @param testName Unique name for this test (passed to HubMock.startup)
|
|
@@ -285,12 +489,15 @@ class TestIModel {
|
|
|
285
489
|
}
|
|
286
490
|
updateElement(txn, elementId, updates) {
|
|
287
491
|
const briefcase = txn.iModel;
|
|
288
|
-
const element = briefcase.elements.
|
|
492
|
+
const element = briefcase.elements.getElementProps(elementId);
|
|
289
493
|
Object.assign(element, updates);
|
|
290
|
-
txn.updateElement(element
|
|
494
|
+
txn.updateElement(element);
|
|
495
|
+
}
|
|
496
|
+
getElementProps(briefcase, elementId) {
|
|
497
|
+
return briefcase.elements.getElementProps(elementId);
|
|
291
498
|
}
|
|
292
|
-
|
|
293
|
-
return briefcase.
|
|
499
|
+
getModelProps(briefcase, modelId) {
|
|
500
|
+
return briefcase.models.tryGetModelProps(modelId);
|
|
294
501
|
}
|
|
295
502
|
checkIfFolderExists(briefcase, txnId, isSchemaFolder) {
|
|
296
503
|
if (isSchemaFolder)
|
|
@@ -301,6 +508,88 @@ class TestIModel {
|
|
|
301
508
|
const folderPath = BriefcaseManager.getBasePathForSemanticRebaseLocalFiles(briefcase);
|
|
302
509
|
return IModelJsFs.existsSync(folderPath);
|
|
303
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Insert a UniqueAspect onto an element within the given EditTxn.
|
|
513
|
+
* @param txn Active EditTxn
|
|
514
|
+
* @param elementId Owning element's Id
|
|
515
|
+
* @param aspectClassName Full class name, e.g. "TestDomain:CUniqueAspect"
|
|
516
|
+
* @param properties Additional property key/value pairs
|
|
517
|
+
* @returns The new aspect's ECInstanceId
|
|
518
|
+
*/
|
|
519
|
+
insertAspect(txn, elementId, aspectClassName, properties) {
|
|
520
|
+
const aspectProps = {
|
|
521
|
+
classFullName: aspectClassName,
|
|
522
|
+
element: { id: elementId, relClassName: "BisCore.ElementOwnsUniqueAspect" },
|
|
523
|
+
...properties,
|
|
524
|
+
};
|
|
525
|
+
return txn.insertAspect(aspectProps);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Update a UniqueAspect property within the given EditTxn.
|
|
529
|
+
* Reads the existing aspect, merges updates, writes back.
|
|
530
|
+
*/
|
|
531
|
+
updateAspect(txn, elementId, aspectClassName, updates) {
|
|
532
|
+
const briefcase = txn.iModel;
|
|
533
|
+
const aspects = briefcase.elements.getAspects(elementId, aspectClassName);
|
|
534
|
+
chai.expect(aspects.length).to.be.greaterThan(0, "Expected at least one aspect to update");
|
|
535
|
+
const aspect = aspects[0];
|
|
536
|
+
Object.assign(aspect, updates);
|
|
537
|
+
txn.updateAspect(aspect.toJSON());
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Delete a UniqueAspect within the given EditTxn.
|
|
541
|
+
* Reads the existing aspect, then deletes it by instanceId.
|
|
542
|
+
*/
|
|
543
|
+
deleteAspect(txn, elementId, aspectClassName) {
|
|
544
|
+
const briefcase = txn.iModel;
|
|
545
|
+
const aspects = briefcase.elements.getAspects(elementId, aspectClassName);
|
|
546
|
+
chai.expect(aspects.length).to.be.greaterThan(0, "Expected at least one aspect to delete");
|
|
547
|
+
txn.deleteAspect(aspects[0].id);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Read a UniqueAspect from a briefcase and return it as a plain object.
|
|
551
|
+
*/
|
|
552
|
+
getAspect(briefcase, elementId, aspectClassName) {
|
|
553
|
+
const aspects = briefcase.elements.getAspects(elementId, aspectClassName);
|
|
554
|
+
return aspects.length > 0 ? aspects[0] : undefined;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Open and return a third briefcase connected to this iModel (useful for three-briefcase tests).
|
|
558
|
+
* The caller is responsible for closing the returned briefcase.
|
|
559
|
+
*/
|
|
560
|
+
async openExtraBriefcase(accessToken = "extra-user") {
|
|
561
|
+
const extra = await HubWrappers.downloadAndOpenBriefcase({
|
|
562
|
+
iTwinId: HubMock.iTwinId,
|
|
563
|
+
iModelId: this.iModelId,
|
|
564
|
+
accessToken,
|
|
565
|
+
});
|
|
566
|
+
extra.channels.addAllowedChannel(ChannelControl.sharedChannelName);
|
|
567
|
+
return extra;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Execute an ECSql SELECT and collect all rows into a Map keyed by ECInstanceId.
|
|
571
|
+
* The SELECT must list ECInstanceId as the first column. Any additional columns are captured
|
|
572
|
+
* by the caller-supplied names and stored in the returned row objects.
|
|
573
|
+
*
|
|
574
|
+
* Rows are returned using {@link QueryRowFormat.UseJsPropertyNames} so:
|
|
575
|
+
* ECInstanceId → row.id
|
|
576
|
+
* ec_className(ECClassId) → row.className (when aliased as `className`)
|
|
577
|
+
* PropA → row.propA
|
|
578
|
+
* PropC2 → row.propC2
|
|
579
|
+
* etc.
|
|
580
|
+
*
|
|
581
|
+
* @param briefcase The open iModel to query.
|
|
582
|
+
* @param ecsql Complete ECSql SELECT statement whose first projected column is ECInstanceId.
|
|
583
|
+
*/
|
|
584
|
+
static async queryToMap(briefcase, ecsql) {
|
|
585
|
+
const result = new Map();
|
|
586
|
+
const reader = briefcase.createQueryReader(ecsql, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames });
|
|
587
|
+
for await (const row of reader) {
|
|
588
|
+
const r = row.toRow();
|
|
589
|
+
result.set(r.id, r);
|
|
590
|
+
}
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
304
593
|
shutdown() {
|
|
305
594
|
this.far.close();
|
|
306
595
|
this.local.close();
|
|
@@ -360,7 +649,7 @@ describe("Semantic Rebase", function () {
|
|
|
360
649
|
// Local pulls and rebases local changes onto incoming schema change
|
|
361
650
|
await pullChanges(localTxn);
|
|
362
651
|
// Verify: local changes preserved, schema updated
|
|
363
|
-
const element = t.
|
|
652
|
+
const element = t.getElementProps(t.local, elementId);
|
|
364
653
|
chai.expect(element.propA).to.equal("local_update_a", "Local property update should be preserved");
|
|
365
654
|
chai.expect(element.propC).to.equal("value_c", "Original propC should be preserved");
|
|
366
655
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
@@ -399,7 +688,7 @@ describe("Semantic Rebase", function () {
|
|
|
399
688
|
await pullChanges(localTxn);
|
|
400
689
|
chai.expect(t.checkIfFolderExists(t.local, txnProps.id, true)).to.be.true; // after rebase the folder should be there until push is called
|
|
401
690
|
// Verify: incoming data changes applied, local schema preserved
|
|
402
|
-
const element = t.
|
|
691
|
+
const element = t.getElementProps(t.local, elementId);
|
|
403
692
|
chai.expect(element.propA).to.equal("far_update_a", "Incoming property update should be applied");
|
|
404
693
|
chai.expect(element.propC).to.equal("value_c", "Original propC should be preserved");
|
|
405
694
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
@@ -442,10 +731,10 @@ describe("Semantic Rebase", function () {
|
|
|
442
731
|
await pullChanges(localTxn);
|
|
443
732
|
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // its data changes on both sides semantic rebase is not used
|
|
444
733
|
// Verify: both changes applied to their respective elements
|
|
445
|
-
const element1 = t.
|
|
734
|
+
const element1 = t.getElementProps(t.local, elementId1);
|
|
446
735
|
chai.expect(element1.propA).to.equal("value_a1", "Element 1 propA should be unchanged");
|
|
447
736
|
chai.expect(element1.propC).to.equal("far_update_c", "Element 1 incoming update should be applied");
|
|
448
|
-
const element2 = t.
|
|
737
|
+
const element2 = t.getElementProps(t.local, elementId2);
|
|
449
738
|
chai.expect(element2.propA).to.equal("local_update_a", "Element 2 local update should be preserved");
|
|
450
739
|
chai.expect(element2.propC).to.equal("value_c2", "Element 2 propC should be unchanged");
|
|
451
740
|
});
|
|
@@ -500,35 +789,6 @@ describe("Semantic Rebase", function () {
|
|
|
500
789
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
501
790
|
chai.expect(schema.version).to.equal("01.00.02", "Incoming schema (newer) should win, local should not override");
|
|
502
791
|
});
|
|
503
|
-
it("local trivial schema changes onto incoming identical schema changes", async () => {
|
|
504
|
-
t = await TestIModel.initialize("TrivialSchemaIdentical");
|
|
505
|
-
const localTxn = startTestTxn(t.local, "local trivial schema changes onto incoming identical schema changes local");
|
|
506
|
-
const farTxn = startTestTxn(t.far, "local trivial schema changes onto incoming identical schema changes far");
|
|
507
|
-
// Far imports v01.00.01 (adds PropC2)
|
|
508
|
-
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
509
|
-
// Verify that we're holding a shared lock (not exclusive) for semantic rebase
|
|
510
|
-
chai.expect(t.far.locks.holdsSharedLock(IModel.repositoryModelId)).to.be.true;
|
|
511
|
-
chai.expect(t.far.holdsSchemaLock).to.be.false;
|
|
512
|
-
const txnProps = t.far.txns.getLastSavedTxnProps();
|
|
513
|
-
chai.expect(txnProps).to.not.be.undefined;
|
|
514
|
-
chai.expect(txnProps.type).to.equal("Schema");
|
|
515
|
-
chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.true;
|
|
516
|
-
await pushChanges(farTxn, "add PropC2 to class C");
|
|
517
|
-
chai.expect(t.checkIfFolderExists(t.far, txnProps.id, true)).to.be.false; // after push the folder should not be there
|
|
518
|
-
// Local imports the same v01.00.01 (adds PropC2)
|
|
519
|
-
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
520
|
-
const txnPropsLocal = t.local.txns.getLastSavedTxnProps();
|
|
521
|
-
chai.expect(txnPropsLocal).to.not.be.undefined;
|
|
522
|
-
chai.expect(txnPropsLocal.type).to.equal("Schema");
|
|
523
|
-
chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.true;
|
|
524
|
-
// Local pulls and rebases
|
|
525
|
-
await pullChanges(localTxn);
|
|
526
|
-
chai.expect(t.checkIfFolderExists(t.local, txnPropsLocal.id, true)).to.be.false; // after rebase the folder should not be there as both are identical
|
|
527
|
-
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // there should not be a rebase folder because the rebase folder is deleted if it contains nothing after rebase
|
|
528
|
-
// Verify: schema preserved (both sides identical)
|
|
529
|
-
const schema = t.local.getSchemaProps("TestDomain");
|
|
530
|
-
chai.expect(schema.version).to.equal("01.00.01", "Schema should be v01.00.01");
|
|
531
|
-
});
|
|
532
792
|
it("local trivial schema changes onto incoming identical schema changes with data changes on both sides", async () => {
|
|
533
793
|
t = await TestIModel.initialize("TrivialSchemaIdenticalWithData");
|
|
534
794
|
const localTxn = startTestTxn(t.local, "local trivial schema changes onto incoming identical schema changes with data local");
|
|
@@ -571,11 +831,11 @@ describe("Semantic Rebase", function () {
|
|
|
571
831
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
572
832
|
chai.expect(schema.version).to.equal("01.00.01", "Schema should be v01.00.01");
|
|
573
833
|
// Verify: both elements exist with their original properties
|
|
574
|
-
const farElement = t.
|
|
834
|
+
const farElement = t.getElementProps(t.local, farElementId);
|
|
575
835
|
chai.expect(farElement.propA).to.equal("far_value_a", "Far element propA should be preserved");
|
|
576
836
|
chai.expect(farElement.propC).to.equal("far_value_c", "Far element propC should be preserved");
|
|
577
837
|
chai.expect(farElement.propC2).to.equal("far_value_c2", "Far element propC2 should be preserved");
|
|
578
|
-
const localElement = t.
|
|
838
|
+
const localElement = t.getElementProps(t.local, localElementId);
|
|
579
839
|
chai.expect(localElement.propA).to.equal("local_value_a", "Local element propA should be preserved");
|
|
580
840
|
chai.expect(localElement.propC).to.equal("local_value_c", "Local element propC should be preserved");
|
|
581
841
|
chai.expect(localElement.propC2).to.equal("local_value_c2", "Local element propC2 should be preserved");
|
|
@@ -691,10 +951,10 @@ describe("Semantic Rebase", function () {
|
|
|
691
951
|
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // after push the folder should not be there
|
|
692
952
|
// Verify: both elements have PropC intact, schema transformed locally
|
|
693
953
|
t.local.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
694
|
-
const farElement = t.
|
|
954
|
+
const farElement = t.getElementProps(t.local, farElementId);
|
|
695
955
|
chai.expect(farElement.propA).to.equal("far_value_a", "Far element propA should be preserved");
|
|
696
956
|
chai.expect(farElement.propC).to.equal("far_value_c", "Far element propC should be preserved after transform");
|
|
697
|
-
const localElement = t.
|
|
957
|
+
const localElement = t.getElementProps(t.local, localElementId);
|
|
698
958
|
chai.expect(localElement.propA).to.equal("local_value_a", "Local element propA should be preserved");
|
|
699
959
|
chai.expect(localElement.propC).to.equal("local_value_c", "Local element propC should be preserved after transform");
|
|
700
960
|
});
|
|
@@ -728,10 +988,10 @@ describe("Semantic Rebase", function () {
|
|
|
728
988
|
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // after push the folder should not be there
|
|
729
989
|
t.local.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
730
990
|
// Verify: both elements have PropC intact after incoming transform
|
|
731
|
-
const farElement = t.
|
|
991
|
+
const farElement = t.getElementProps(t.local, farElementId);
|
|
732
992
|
chai.expect(farElement.propA).to.equal("far_value_a", "Far element propA should be preserved");
|
|
733
993
|
chai.expect(farElement.propC).to.equal("far_value_c", "Far element propC should be preserved after incoming transform");
|
|
734
|
-
const localElement = t.
|
|
994
|
+
const localElement = t.getElementProps(t.local, localElementId);
|
|
735
995
|
chai.expect(localElement.propA).to.equal("local_value_a", "Local element propA should be preserved");
|
|
736
996
|
chai.expect(localElement.propC).to.equal("local_value_c", "Local element propC should be preserved after incoming transform");
|
|
737
997
|
});
|
|
@@ -781,16 +1041,16 @@ describe("Semantic Rebase", function () {
|
|
|
781
1041
|
t.far.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
782
1042
|
t.local.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
783
1043
|
// Verify: all elements have both PropC and PropD intact
|
|
784
|
-
const farElemC = t.
|
|
1044
|
+
const farElemC = t.getElementProps(t.local, farElementC);
|
|
785
1045
|
chai.expect(farElemC.propA).to.equal("far_value_a_c", "Far element C propA should be preserved");
|
|
786
1046
|
chai.expect(farElemC.propC).to.equal("far_value_c", "Far element C propC should be preserved after both transforms");
|
|
787
|
-
const farElemD = t.
|
|
1047
|
+
const farElemD = t.getElementProps(t.local, farElementD);
|
|
788
1048
|
chai.expect(farElemD.propA).to.equal("far_value_a_d", "Far element D propA should be preserved");
|
|
789
1049
|
chai.expect(farElemD.propD).to.equal("far_value_d", "Far element D propD should be preserved after both transforms");
|
|
790
|
-
const localElemC = t.
|
|
1050
|
+
const localElemC = t.getElementProps(t.local, localElementC);
|
|
791
1051
|
chai.expect(localElemC.propA).to.equal("local_value_a_c", "Local element C propA should be preserved");
|
|
792
1052
|
chai.expect(localElemC.propC).to.equal("local_value_c", "Local element C propC should be preserved after both transforms");
|
|
793
|
-
const localElemD = t.
|
|
1053
|
+
const localElemD = t.getElementProps(t.local, localElementD);
|
|
794
1054
|
chai.expect(localElemD.propA).to.equal("local_value_a_d", "Local element D propA should be preserved");
|
|
795
1055
|
chai.expect(localElemD.propD).to.equal("local_value_d", "Local element D propD should be preserved after both transforms");
|
|
796
1056
|
});
|
|
@@ -822,7 +1082,7 @@ describe("Semantic Rebase", function () {
|
|
|
822
1082
|
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
823
1083
|
t.updateElement(localTxn, elementId, { propC: "local_modified_c" });
|
|
824
1084
|
localTxn.saveChanges("local update propC");
|
|
825
|
-
let element = t.
|
|
1085
|
+
let element = t.getElementProps(t.local, elementId);
|
|
826
1086
|
chai.expect(element.propA).to.equal("initial_value_a", "PropA should be unchanged");
|
|
827
1087
|
chai.expect(element.propC).to.equal("local_modified_c", "PropC should have the local modified value before incoming transform");
|
|
828
1088
|
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // no schema change yet on local so no rebase folder
|
|
@@ -832,7 +1092,7 @@ describe("Semantic Rebase", function () {
|
|
|
832
1092
|
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false;
|
|
833
1093
|
t.local.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
834
1094
|
// Verify: PropC has the modified local value after the transform
|
|
835
|
-
element = t.
|
|
1095
|
+
element = t.getElementProps(t.local, elementId);
|
|
836
1096
|
chai.expect(element.propA).to.equal("initial_value_a", "PropA should be unchanged");
|
|
837
1097
|
chai.expect(element.propC).to.equal("local_modified_c", "PropC should have the local modified value after incoming transform");
|
|
838
1098
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
@@ -868,10 +1128,10 @@ describe("Semantic Rebase", function () {
|
|
|
868
1128
|
await pushChanges(localTxn, "far move PropC to A");
|
|
869
1129
|
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // after push the folder should not be there
|
|
870
1130
|
t.local.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
871
|
-
const elementFar = t.
|
|
1131
|
+
const elementFar = t.getElementProps(t.local, elementIdFar);
|
|
872
1132
|
chai.expect(elementFar.propA).to.equal("far_value_a", "PropA should be unchanged");
|
|
873
1133
|
chai.expect(elementFar.propC).to.equal("far_value_c", "PropC should be unchanged");
|
|
874
|
-
const elementLocal = t.
|
|
1134
|
+
const elementLocal = t.getElementProps(t.local, elementIdLocal);
|
|
875
1135
|
chai.expect(elementLocal.propA).to.equal("local_value_a", "PropA should be unchanged");
|
|
876
1136
|
chai.expect(elementLocal.propC).to.equal("local_value_c", "PropC should be unchanged");
|
|
877
1137
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
@@ -925,10 +1185,10 @@ describe("Semantic Rebase", function () {
|
|
|
925
1185
|
await pullChanges(localTxn);
|
|
926
1186
|
t.local.clearCaches();
|
|
927
1187
|
// Verify: both local data changes preserved after incoming transform
|
|
928
|
-
const element1 = t.
|
|
1188
|
+
const element1 = t.getElementProps(t.local, elementId1);
|
|
929
1189
|
chai.expect(element1.propA).to.equal("first_update_a", "First element propA update should be preserved");
|
|
930
1190
|
chai.expect(element1.propC).to.equal("initial_c", "First element propC should be preserved after transform");
|
|
931
|
-
const element2 = t.
|
|
1191
|
+
const element2 = t.getElementProps(t.local, elementId2);
|
|
932
1192
|
chai.expect(element2.propA).to.equal("second_element_a", "Second element propA should be preserved");
|
|
933
1193
|
chai.expect(element2.propC).to.equal("second_element_c", "Second element propC should be preserved after transform");
|
|
934
1194
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
@@ -967,10 +1227,10 @@ describe("Semantic Rebase", function () {
|
|
|
967
1227
|
await pullChanges(localTxn);
|
|
968
1228
|
t.local.clearCaches();
|
|
969
1229
|
// Verify: both incoming data changes applied, local schema transformation preserved
|
|
970
|
-
const element1 = t.
|
|
1230
|
+
const element1 = t.getElementProps(t.local, elementId1);
|
|
971
1231
|
chai.expect(element1.propA).to.equal("far_first_update_a", "First element incoming update should be applied");
|
|
972
1232
|
chai.expect(element1.propC).to.equal("initial_c", "First element propC should be preserved after transform");
|
|
973
|
-
const element2 = t.
|
|
1233
|
+
const element2 = t.getElementProps(t.local, elementId2);
|
|
974
1234
|
chai.expect(element2.propA).to.equal("far_second_element_a", "Second element should exist with correct propA");
|
|
975
1235
|
chai.expect(element2.propC).to.equal("far_second_element_c", "Second element propC should be preserved after transform");
|
|
976
1236
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
@@ -991,6 +1251,7 @@ describe("Semantic Rebase", function () {
|
|
|
991
1251
|
// Verify: element was not saved, schema was not imported
|
|
992
1252
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
993
1253
|
chai.expect(schema.version).to.equal("01.00.00", "Schema should remain at v01.00.00");
|
|
1254
|
+
chai.expect(t.local.isOpen).to.be.true;
|
|
994
1255
|
});
|
|
995
1256
|
});
|
|
996
1257
|
/**
|
|
@@ -1041,10 +1302,10 @@ describe("Semantic Rebase with indirect changes", function () {
|
|
|
1041
1302
|
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // there should not be a rebase folder because no schema change on either side
|
|
1042
1303
|
chai.expect(Id64.isValidId64(elementIdFar) && Id64.isValidId64(elementIdLocal)).to.be.true;
|
|
1043
1304
|
t.local.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
1044
|
-
const elementFar = t.
|
|
1305
|
+
const elementFar = t.getElementProps(t.local, elementIdFar);
|
|
1045
1306
|
chai.expect(elementFar.propA).to.equal("far_value_a", "PropA should be unchanged");
|
|
1046
1307
|
chai.expect(elementFar.propC).to.equal("far_value_c", "PropC should be unchanged");
|
|
1047
|
-
const elementLocal = t.
|
|
1308
|
+
const elementLocal = t.getElementProps(t.local, elementIdLocal);
|
|
1048
1309
|
chai.expect(elementLocal.propA).to.equal("local_value_a", "PropA should be unchanged");
|
|
1049
1310
|
chai.expect(elementLocal.propC).to.equal("local_value_c", "PropC should be unchanged");
|
|
1050
1311
|
});
|
|
@@ -1083,10 +1344,10 @@ describe("Semantic Rebase with indirect changes", function () {
|
|
|
1083
1344
|
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false;
|
|
1084
1345
|
chai.expect(Id64.isValidId64(elementIdFar) && Id64.isValidId64(elementIdLocal)).to.be.true;
|
|
1085
1346
|
t.local.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
1086
|
-
const elementFar = t.
|
|
1347
|
+
const elementFar = t.getElementProps(t.local, elementIdFar);
|
|
1087
1348
|
chai.expect(elementFar.propA).to.equal("far_value_a", "PropA should be unchanged");
|
|
1088
1349
|
chai.expect(elementFar.propC).to.equal("far_value_c", "PropC should be unchanged");
|
|
1089
|
-
const elementLocal = t.
|
|
1350
|
+
const elementLocal = t.getElementProps(t.local, elementIdLocal);
|
|
1090
1351
|
chai.expect(elementLocal.propA).to.equal("local_value_a", "PropA should be unchanged");
|
|
1091
1352
|
chai.expect(elementLocal.propC).to.equal("local_value_c", "PropC should be unchanged");
|
|
1092
1353
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
@@ -1132,10 +1393,10 @@ describe("Semantic Rebase with indirect changes", function () {
|
|
|
1132
1393
|
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // after push the folder should not be there
|
|
1133
1394
|
chai.expect(Id64.isValidId64(elementIdFar) && Id64.isValidId64(elementIdLocal)).to.be.true;
|
|
1134
1395
|
t.local.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
1135
|
-
const elementFar = t.
|
|
1396
|
+
const elementFar = t.getElementProps(t.local, elementIdFar);
|
|
1136
1397
|
chai.expect(elementFar.propA).to.equal("far_value_a", "PropA should be unchanged");
|
|
1137
1398
|
chai.expect(elementFar.propC).to.equal("far_value_c", "PropC should be unchanged");
|
|
1138
|
-
const elementLocal = t.
|
|
1399
|
+
const elementLocal = t.getElementProps(t.local, elementIdLocal);
|
|
1139
1400
|
chai.expect(elementLocal.propA).to.equal("local_value_a", "PropA should be unchanged");
|
|
1140
1401
|
chai.expect(elementLocal.propC).to.equal("local_value_c", "PropC should be unchanged");
|
|
1141
1402
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
@@ -1176,10 +1437,10 @@ describe("Semantic Rebase with indirect changes", function () {
|
|
|
1176
1437
|
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false;
|
|
1177
1438
|
chai.expect(Id64.isValidId64(elementIdFar) && Id64.isValidId64(elementIdLocal)).to.be.true;
|
|
1178
1439
|
t.local.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
1179
|
-
const elementFar = t.
|
|
1440
|
+
const elementFar = t.getElementProps(t.local, elementIdFar);
|
|
1180
1441
|
chai.expect(elementFar.propA).to.equal("far_value_a", "PropA should be unchanged");
|
|
1181
1442
|
chai.expect(elementFar.propC).to.equal("far_value_c", "PropC should be unchanged");
|
|
1182
|
-
const elementLocal = t.
|
|
1443
|
+
const elementLocal = t.getElementProps(t.local, elementIdLocal);
|
|
1183
1444
|
chai.expect(elementLocal.propA).to.equal("local_value_a", "PropA should be unchanged");
|
|
1184
1445
|
chai.expect(elementLocal.propC).to.equal("local_value_c", "PropC should be unchanged");
|
|
1185
1446
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
@@ -1225,10 +1486,10 @@ describe("Semantic Rebase with indirect changes", function () {
|
|
|
1225
1486
|
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // schema change is no op and data changes are generated on the fly and removed once rebased so rebase folder should not be there
|
|
1226
1487
|
chai.expect(Id64.isValidId64(elementIdFar) && Id64.isValidId64(elementIdLocal)).to.be.true;
|
|
1227
1488
|
t.local.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
1228
|
-
const elementFar = t.
|
|
1489
|
+
const elementFar = t.getElementProps(t.local, elementIdFar);
|
|
1229
1490
|
chai.expect(elementFar.propA).to.equal("far_value_a", "PropA should be unchanged");
|
|
1230
1491
|
chai.expect(elementFar.propC).to.equal("far_value_c", "PropC should be unchanged");
|
|
1231
|
-
const elementLocal = t.
|
|
1492
|
+
const elementLocal = t.getElementProps(t.local, elementIdLocal);
|
|
1232
1493
|
chai.expect(elementLocal.propA).to.equal("local_value_a", "PropA should be unchanged");
|
|
1233
1494
|
chai.expect(elementLocal.propC).to.equal("local_value_c", "PropC should be unchanged");
|
|
1234
1495
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
@@ -1268,14 +1529,1927 @@ describe("Semantic Rebase with indirect changes", function () {
|
|
|
1268
1529
|
chai.expect(t.checkIfFolderExists(t.local, localTxnProps.id, true)).to.be.false; // after push the schema folder should not be there
|
|
1269
1530
|
chai.expect(Id64.isValidId64(elementIdFar) && Id64.isValidId64(elementIdLocal)).to.be.true;
|
|
1270
1531
|
t.local.clearCaches(); // Clear caches to ensure we read transformed properties from iModel
|
|
1271
|
-
const elementFar = t.
|
|
1532
|
+
const elementFar = t.getElementProps(t.local, elementIdFar);
|
|
1272
1533
|
chai.expect(elementFar.propA).to.equal("far_value_a", "PropA should be unchanged");
|
|
1273
1534
|
chai.expect(elementFar.propC).to.equal("far_value_c", "PropC should be unchanged");
|
|
1274
|
-
const elementLocal = t.
|
|
1535
|
+
const elementLocal = t.getElementProps(t.local, elementIdLocal);
|
|
1275
1536
|
chai.expect(elementLocal.propA).to.equal("local_value_a", "PropA should be unchanged");
|
|
1276
1537
|
chai.expect(elementLocal.propC).to.equal("local_value_c", "PropC should be unchanged");
|
|
1277
1538
|
const schema = t.local.getSchemaProps("TestDomain");
|
|
1278
1539
|
chai.expect(schema.version).to.equal("01.00.02", "Schema should be transformed to v01.00.02");
|
|
1279
1540
|
});
|
|
1280
1541
|
});
|
|
1542
|
+
/**
|
|
1543
|
+
* Test suite for data conflicts, conflict handlers, lifecycle events, and mixed schema+conflict scenarios during semantic rebase.
|
|
1544
|
+
*/
|
|
1545
|
+
describe("Semantic Rebase - Data Correctness Under Conflict", function () {
|
|
1546
|
+
this.timeout(60000);
|
|
1547
|
+
let t;
|
|
1548
|
+
before(async () => {
|
|
1549
|
+
await TestUtils.shutdownBackend();
|
|
1550
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
1551
|
+
});
|
|
1552
|
+
afterEach(() => {
|
|
1553
|
+
if (t) {
|
|
1554
|
+
t.shutdown();
|
|
1555
|
+
t = undefined;
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
after(async () => {
|
|
1559
|
+
await TestUtils.shutdownBackend();
|
|
1560
|
+
await TestUtils.startBackend();
|
|
1561
|
+
});
|
|
1562
|
+
// ─── Section F: Conflicts with Schema Changes ────────────────────────────────
|
|
1563
|
+
// Every test below verifies ELEMENT DATA CORRECTNESS after semantic rebase.
|
|
1564
|
+
it("F1: local data patch on element survives transforming schema rebase: propC value preserved after column migration", async () => {
|
|
1565
|
+
t = await TestIModel.initialize("F1ConflictDuringTransformingSchemaRebase");
|
|
1566
|
+
let localTxn = startTestTxn(t.local, "F1 local");
|
|
1567
|
+
let farTxn = startTestTxn(t.far, "F1 far");
|
|
1568
|
+
// Create shared element with propC populated
|
|
1569
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1570
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "initial_a", propC: "initial_c" });
|
|
1571
|
+
farTxn.saveChanges("create shared element");
|
|
1572
|
+
await pushChanges(farTxn, "create shared element");
|
|
1573
|
+
farTxn = startTestTxn(t.far, "F1 far 2");
|
|
1574
|
+
await pullChanges(localTxn);
|
|
1575
|
+
localTxn = startTestTxn(t.local, "F1 local 2");
|
|
1576
|
+
// Local locks elementId and makes its data change BEFORE far pushes.
|
|
1577
|
+
// If local tried to lock after far's push (at a newer changeset index),
|
|
1578
|
+
// doesBriefcaseRequirePullBeforeLock would throw PullIsRequired.
|
|
1579
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
1580
|
+
t.updateElement(localTxn, elementId, { propC: "local_updated_c" });
|
|
1581
|
+
localTxn.saveChanges("local update propC");
|
|
1582
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x02MovePropCToA]);
|
|
1583
|
+
// Far pushes a trivial schema only — no data change on elementId, no exclusive lock on it.
|
|
1584
|
+
// Far's schema import acquires only a shared lock on repositoryModelId and never touches
|
|
1585
|
+
// elementId's lock record, so local's already-held exclusive lock is not contended.
|
|
1586
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
1587
|
+
await pushChanges(farTxn, "far push trivial schema v01");
|
|
1588
|
+
// Local pulls:
|
|
1589
|
+
// Incoming: trivial schema (v01) applied.
|
|
1590
|
+
// Rebase:
|
|
1591
|
+
// 1. Local's transforming schema txn reinstated → v02 applied (PropC column migrated from C to A)
|
|
1592
|
+
// 2. Local's data patch reinstated → propC set to "local_updated_c"
|
|
1593
|
+
// Bug scenario: if applyInstancePatch fails to map propC to the migrated A.PropC column,
|
|
1594
|
+
// the value "local_updated_c" would be silently dropped and "initial_c" would remain.
|
|
1595
|
+
await pullChanges(localTxn);
|
|
1596
|
+
t.local.clearCaches();
|
|
1597
|
+
const element = t.getElementProps(t.local, elementId);
|
|
1598
|
+
chai.expect(element.propC).to.equal("local_updated_c", "Local propC value should survive after transforming schema rebase");
|
|
1599
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
1600
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v01.00.02 after rebase");
|
|
1601
|
+
});
|
|
1602
|
+
it("F3: local element deletion + incoming transforming schema change: delete is reinstated, element stays gone and checking GeometricGuid of Model [BUG]", async () => {
|
|
1603
|
+
t = await TestIModel.initialize("F2DeleteIncomingTransform");
|
|
1604
|
+
let localTxn = startTestTxn(t.local, "F2 local");
|
|
1605
|
+
let farTxn = startTestTxn(t.far, "F2 far");
|
|
1606
|
+
// Create shared element (shared model lock — no exclusive lock record on elementId)
|
|
1607
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1608
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "initial_a", propC: "initial_c" });
|
|
1609
|
+
farTxn.saveChanges("create shared element");
|
|
1610
|
+
await pushChanges(farTxn, "create shared element");
|
|
1611
|
+
farTxn = startTestTxn(t.far, "F2 far 2");
|
|
1612
|
+
await pullChanges(localTxn);
|
|
1613
|
+
localTxn = startTestTxn(t.local, "F2 local 2");
|
|
1614
|
+
// Far imports transforming schema (moves PropC from C to A); no exclusive lock on elementId
|
|
1615
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02MovePropCToA]);
|
|
1616
|
+
await pushChanges(farTxn, "far import transforming schema");
|
|
1617
|
+
// Local deletes the element — safe because far never exclusively locked elementId,
|
|
1618
|
+
// so lastExclusiveReleaseChangesetIndex for elementId is still undefined.
|
|
1619
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
1620
|
+
localTxn.deleteElement(elementId);
|
|
1621
|
+
localTxn.saveChanges("local delete element");
|
|
1622
|
+
const drawingModel = t.getModelProps(t.local, t.drawingModelId);
|
|
1623
|
+
const geometricGuidBefore = drawingModel.geometryGuid;
|
|
1624
|
+
// Local pulls - incoming transforming schema applied, then local deletion reinstated
|
|
1625
|
+
await pullChanges(localTxn);
|
|
1626
|
+
t.local.clearCaches();
|
|
1627
|
+
chai.expect(() => t.getElementProps(t.local, elementId)).to.throw(`element not found`);
|
|
1628
|
+
const drawingModelAfter = t.getModelProps(t.local, t.drawingModelId);
|
|
1629
|
+
const geometricGuidAfter = drawingModelAfter.geometryGuid;
|
|
1630
|
+
chai.expect(geometricGuidAfter).to.not.equal(geometricGuidBefore, "GeometricGuid of the model should not remain the same after rebase"); // BUG should exactly be same
|
|
1631
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
1632
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be updated to v01.00.02");
|
|
1633
|
+
});
|
|
1634
|
+
it("F4: incoming element deletion + local transforming schema change: schema upgrade survives, element absent", async () => {
|
|
1635
|
+
t = await TestIModel.initialize("F4IncomingDeleteLocalTransform");
|
|
1636
|
+
let localTxn = startTestTxn(t.local, "F4 local");
|
|
1637
|
+
let farTxn = startTestTxn(t.far, "F4 far");
|
|
1638
|
+
// Create shared element
|
|
1639
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1640
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "initial_a", propC: "initial_c" });
|
|
1641
|
+
farTxn.saveChanges("create shared element");
|
|
1642
|
+
await pushChanges(farTxn, "create shared element");
|
|
1643
|
+
farTxn = startTestTxn(t.far, "F4 far 2");
|
|
1644
|
+
await pullChanges(localTxn);
|
|
1645
|
+
localTxn = startTestTxn(t.local, "F4 local 2");
|
|
1646
|
+
// Far exclusively locks and deletes the element, then pushes
|
|
1647
|
+
await t.far.locks.acquireLocks({ exclusive: elementId });
|
|
1648
|
+
farTxn.deleteElement(elementId);
|
|
1649
|
+
farTxn.saveChanges("far delete element");
|
|
1650
|
+
await pushChanges(farTxn, "far delete element");
|
|
1651
|
+
// Local imports transforming schema only.
|
|
1652
|
+
// NOTE: do NOT lock elementId here. After far's exclusive lock + push, the element's
|
|
1653
|
+
// lastExclusiveReleaseChangesetIndex is set at a newer changeset index than local's head.
|
|
1654
|
+
// acquireLocks on elementId from local would throw PullIsRequired via
|
|
1655
|
+
// doesBriefcaseRequirePullBeforeLock. The schema import alone is sufficient to test that
|
|
1656
|
+
// semantic rebase handles the case where an incoming changeset deleted an element that
|
|
1657
|
+
// the local schema txn has no data patches for.
|
|
1658
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x02MovePropCToA]);
|
|
1659
|
+
// Local pulls - far's delete applied as incoming changeset; local schema txn reinstated.
|
|
1660
|
+
// No data patch on elementId → no NotFound conflict. Schema upgrade succeeds cleanly.
|
|
1661
|
+
await pullChanges(localTxn);
|
|
1662
|
+
t.local.clearCaches();
|
|
1663
|
+
// Element is gone (deleted by incoming changeset)
|
|
1664
|
+
chai.expect(() => t.getElementProps(t.local, elementId)).to.throw(`element not found`);
|
|
1665
|
+
// Schema was upgraded successfully despite the element deletion
|
|
1666
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
1667
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema upgrade should survive when incoming changeset deleted an element");
|
|
1668
|
+
});
|
|
1669
|
+
it("F5: insert → update → delete of same element across three sequential local data txns; incoming schema → element absent and no stale ECInstanceId", async () => {
|
|
1670
|
+
// This test probes the ordering of sequential data patches.
|
|
1671
|
+
// If patches are applied out of order the update will hit NotFound (element not yet inserted)
|
|
1672
|
+
// or the delete will resurrect a value that the update produced.
|
|
1673
|
+
t = await TestIModel.initialize("F5InsertUpdateDeleteChain");
|
|
1674
|
+
const localTxn = startTestTxn(t.local, "F5 local");
|
|
1675
|
+
const farTxn = startTestTxn(t.far, "F5 far");
|
|
1676
|
+
// Far pushes a data change to create an incoming changeset
|
|
1677
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1678
|
+
t.insertElement(farTxn, "TestDomain:C", { propA: "far_a", propC: "far_c" });
|
|
1679
|
+
farTxn.saveChanges("far insert element");
|
|
1680
|
+
await pushChanges(farTxn, "far create element");
|
|
1681
|
+
// Local schema txn — semantic rebase trigger
|
|
1682
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
1683
|
+
// Local data txn1: insert element E1 (shared model lock; never locked by far)
|
|
1684
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1685
|
+
const e1Id = t.insertElement(localTxn, "TestDomain:C", { propA: "e1_initial", propC: "e1_c" });
|
|
1686
|
+
localTxn.saveChanges("local insert E1 (txn1)");
|
|
1687
|
+
// Local data txn2: update E1 (local inserted it → no lock violation)
|
|
1688
|
+
t.updateElement(localTxn, e1Id, { propA: "e1_updated", propC2: "e1_c2" });
|
|
1689
|
+
localTxn.saveChanges("local update E1 (txn2)");
|
|
1690
|
+
// Local data txn3: delete E1
|
|
1691
|
+
localTxn.deleteElement(e1Id);
|
|
1692
|
+
localTxn.saveChanges("local delete E1 (txn3)");
|
|
1693
|
+
await pullChanges(localTxn);
|
|
1694
|
+
chai.expect(() => t.getElementProps(t.local, e1Id)).to.throw(`element not found`);
|
|
1695
|
+
});
|
|
1696
|
+
it("F6: local inserts elements of three different classes across separate data txns; incoming trivial schema; ECInstanceIds and classNames preserved", async () => {
|
|
1697
|
+
// This test probes class-resolution in constructPatchInstance (resolves classFullName from
|
|
1698
|
+
// ECClassId / $meta.classFullName / $meta.fallbackClassId) for multiple BIS subclasses.
|
|
1699
|
+
// If class resolution is wrong, insertInstance will fail or create elements under the wrong class.
|
|
1700
|
+
t = await TestIModel.initialize("F6ThreeClassInserts");
|
|
1701
|
+
const localTxn = startTestTxn(t.local, "F6 local");
|
|
1702
|
+
const farTxn = startTestTxn(t.far, "F6 far");
|
|
1703
|
+
// Far pushes an incoming data change
|
|
1704
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1705
|
+
const farElemId = t.insertElement(farTxn, "TestDomain:A", { propA: "far_a" });
|
|
1706
|
+
farTxn.saveChanges("far insert element");
|
|
1707
|
+
await pushChanges(farTxn, "far create element");
|
|
1708
|
+
// Local schema txn — semantic rebase trigger (trivial: adds PropC2)
|
|
1709
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
1710
|
+
// Local data txns: insert one element per class (A, C, D) in three separate txns.
|
|
1711
|
+
// All use shared model lock — none of these elements were ever locked by far.
|
|
1712
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1713
|
+
const aId = t.insertElement(localTxn, "TestDomain:A", { propA: "local_a_only" });
|
|
1714
|
+
localTxn.saveChanges("local insert A (txn1)");
|
|
1715
|
+
const cId = t.insertElement(localTxn, "TestDomain:C", { propA: "local_c_a", propC: "local_c_val" });
|
|
1716
|
+
localTxn.saveChanges("local insert C (txn2)");
|
|
1717
|
+
const dId = t.insertElement(localTxn, "TestDomain:D", { propA: "local_d_a", propD: "local_d_val" });
|
|
1718
|
+
localTxn.saveChanges("local insert D (txn3)");
|
|
1719
|
+
// Capture ECInstanceIds before pull — they must be identical after rebase (forceUseId)
|
|
1720
|
+
const prePullIds = { aId, cId, dId };
|
|
1721
|
+
await pullChanges(localTxn);
|
|
1722
|
+
t.local.clearCaches();
|
|
1723
|
+
// Verify each element: ECInstanceId preserved, classFullName correct, props intact
|
|
1724
|
+
const aElem = t.getElementProps(t.local, prePullIds.aId);
|
|
1725
|
+
chai.expect(aElem.id).to.equal(prePullIds.aId, "Class A ECInstanceId must be preserved (forceUseId)");
|
|
1726
|
+
chai.expect(aElem.classFullName).to.equal("TestDomain:A", "Class A classFullName must be correct");
|
|
1727
|
+
chai.expect(aElem.propA).to.equal("local_a_only");
|
|
1728
|
+
const cElem = t.getElementProps(t.local, prePullIds.cId);
|
|
1729
|
+
chai.expect(cElem.id).to.equal(prePullIds.cId, "Class C ECInstanceId must be preserved (forceUseId)");
|
|
1730
|
+
chai.expect(cElem.classFullName).to.equal("TestDomain:C", "Class C classFullName must be correct");
|
|
1731
|
+
chai.expect(cElem.propA).to.equal("local_c_a");
|
|
1732
|
+
chai.expect(cElem.propC).to.equal("local_c_val");
|
|
1733
|
+
const dElem = t.getElementProps(t.local, prePullIds.dId);
|
|
1734
|
+
chai.expect(dElem.id).to.equal(prePullIds.dId, "Class D ECInstanceId must be preserved (forceUseId)");
|
|
1735
|
+
chai.expect(dElem.classFullName).to.equal("TestDomain:D", "Class D classFullName must be correct");
|
|
1736
|
+
chai.expect(dElem.propA).to.equal("local_d_a");
|
|
1737
|
+
chai.expect(dElem.propD).to.equal("local_d_val");
|
|
1738
|
+
// Also confirm all three IDs appear in an ECSqlReader scan (polymorphic query on base class A)
|
|
1739
|
+
const allRows = await TestIModel.queryToMap(t.local, `SELECT ECInstanceId, ec_className(ECClassId) AS className FROM TestDomain.A`);
|
|
1740
|
+
chai.expect(allRows.has(prePullIds.aId), "A element must appear in ECSqlReader scan").to.be.true;
|
|
1741
|
+
chai.expect(allRows.has(prePullIds.cId), "C element must appear in ECSqlReader scan").to.be.true;
|
|
1742
|
+
chai.expect(allRows.has(prePullIds.dId), "D element must appear in ECSqlReader scan").to.be.true;
|
|
1743
|
+
// Confirm className is resolved correctly in the ECSql results
|
|
1744
|
+
chai.expect(allRows.get(prePullIds.cId)?.className).to.include("C", "ECSqlReader className for C element must include 'C'");
|
|
1745
|
+
chai.expect(allRows.get(prePullIds.dId)?.className).to.include("D", "ECSqlReader className for D element must include 'D'");
|
|
1746
|
+
// Far's element must also be present and unmodified
|
|
1747
|
+
const farElem = t.getElementProps(t.local, farElemId);
|
|
1748
|
+
chai.expect(farElem.propA).to.equal("far_a");
|
|
1749
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
1750
|
+
chai.expect(schema.version).to.equal("01.00.01");
|
|
1751
|
+
});
|
|
1752
|
+
});
|
|
1753
|
+
/**
|
|
1754
|
+
* Multi-step schema upgrade chains.
|
|
1755
|
+
* Tests scenarios where one or both sides import schemas in multiple sequential steps before the rebase.
|
|
1756
|
+
*/
|
|
1757
|
+
describe("Semantic Rebase - Multi-Step Schema Upgrade Chains", function () {
|
|
1758
|
+
this.timeout(60000);
|
|
1759
|
+
let t;
|
|
1760
|
+
before(async () => {
|
|
1761
|
+
await TestUtils.shutdownBackend();
|
|
1762
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
1763
|
+
});
|
|
1764
|
+
afterEach(() => {
|
|
1765
|
+
if (t) {
|
|
1766
|
+
t.shutdown();
|
|
1767
|
+
t = undefined;
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
after(async () => {
|
|
1771
|
+
await TestUtils.shutdownBackend();
|
|
1772
|
+
await TestUtils.startBackend();
|
|
1773
|
+
});
|
|
1774
|
+
it("G1: local imports schema in two chained steps (v01→v02) before pulling; incoming has v01 only", async () => {
|
|
1775
|
+
// Local: v01 then v02. Incoming (far): only v01.
|
|
1776
|
+
// Expected: local v02 wins because it is strictly newer.
|
|
1777
|
+
t = await TestIModel.initialize("G1LocalChainedSchemaUpgrade");
|
|
1778
|
+
const farTxn = startTestTxn(t.far, "G1 far");
|
|
1779
|
+
const localTxn = startTestTxn(t.local, "G1 local");
|
|
1780
|
+
// Far imports v01 and pushes
|
|
1781
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
1782
|
+
const farTxnProps = t.far.txns.getLastSavedTxnProps();
|
|
1783
|
+
await pushChanges(farTxn, "far import v01");
|
|
1784
|
+
chai.expect(t.checkIfFolderExists(t.far, farTxnProps.id, true)).to.be.false; // cleaned up on push
|
|
1785
|
+
// Local: import v01, then immediately upgrade to v02 (chain before any pull)
|
|
1786
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
1787
|
+
const localTxnPropsV01 = t.local.txns.getLastSavedTxnProps();
|
|
1788
|
+
chai.expect(localTxnPropsV01).to.not.be.undefined;
|
|
1789
|
+
chai.expect(t.checkIfFolderExists(t.local, localTxnPropsV01.id, true)).to.be.true;
|
|
1790
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
|
|
1791
|
+
const localTxnPropsV02 = t.local.txns.getLastSavedTxnProps();
|
|
1792
|
+
chai.expect(localTxnPropsV02).to.not.be.undefined;
|
|
1793
|
+
chai.expect(t.checkIfFolderExists(t.local, localTxnPropsV02.id, true)).to.be.true;
|
|
1794
|
+
// Local pulls: incoming has v01, local already at v02 → local v02 txn wins, v01 local txn is a no-op
|
|
1795
|
+
await pullChanges(localTxn);
|
|
1796
|
+
// v01 local txn became no-op (incoming v01 arrived and covers it)
|
|
1797
|
+
chai.expect(t.checkIfFolderExists(t.local, localTxnPropsV01.id, true)).to.be.false;
|
|
1798
|
+
// v02 is still pending push
|
|
1799
|
+
chai.expect(t.checkIfFolderExists(t.local, localTxnPropsV02.id, true)).to.be.true;
|
|
1800
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
1801
|
+
chai.expect(schema.version).to.equal("01.00.02", "Local v02 should win after chain import rebase");
|
|
1802
|
+
// PropD2 (from v02) should be visible
|
|
1803
|
+
const classD = await t.local.schemaContext.getSchemaItem("TestDomain", "D", EntityClass);
|
|
1804
|
+
chai.expect(await classD.getProperty("PropD2")).to.exist;
|
|
1805
|
+
});
|
|
1806
|
+
it("G2: incoming has two sequential schema changesets (v01 then v02), local has only data changes", async () => {
|
|
1807
|
+
// Far: schema v01 (cs1) then schema v02 (cs2).
|
|
1808
|
+
// Local: data change only → rebased on top of both schema changesets.
|
|
1809
|
+
t = await TestIModel.initialize("G2IncomingTwoSchemaChangesets");
|
|
1810
|
+
let farTxn = startTestTxn(t.far, "G2 far");
|
|
1811
|
+
let localTxn = startTestTxn(t.local, "G2 local");
|
|
1812
|
+
// Create shared element on far, push
|
|
1813
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1814
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "initial_a", propC: "initial_c" });
|
|
1815
|
+
farTxn.saveChanges("create element");
|
|
1816
|
+
await pushChanges(farTxn, "create element");
|
|
1817
|
+
farTxn = startTestTxn(t.far, "G2 far 2");
|
|
1818
|
+
// Local pulls to get element
|
|
1819
|
+
await pullChanges(localTxn);
|
|
1820
|
+
localTxn = startTestTxn(t.local, "G2 local 2");
|
|
1821
|
+
// Far: schema changeset 1 (v01)
|
|
1822
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
1823
|
+
await pushChanges(farTxn, "far v01");
|
|
1824
|
+
farTxn = startTestTxn(t.far, "G2 far 3");
|
|
1825
|
+
// Far: schema changeset 2 (v02)
|
|
1826
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
|
|
1827
|
+
await pushChanges(farTxn, "far v02");
|
|
1828
|
+
// Local: data change only (update element propA)
|
|
1829
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
1830
|
+
t.updateElement(localTxn, elementId, { propA: "local_updated_a" });
|
|
1831
|
+
localTxn.saveChanges("local update propA");
|
|
1832
|
+
// Local pulls: both far schema changesets are incoming data, local data rebase on top
|
|
1833
|
+
await pullChanges(localTxn);
|
|
1834
|
+
const element = t.getElementProps(t.local, elementId);
|
|
1835
|
+
chai.expect(element.propA).to.equal("local_updated_a", "Local data change should be preserved after two incoming schema changesets");
|
|
1836
|
+
chai.expect(element.propC).to.equal("initial_c", "propC should be unchanged");
|
|
1837
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
1838
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02 after two incoming schema changesets");
|
|
1839
|
+
// Both PropC2 and PropD2 should exist
|
|
1840
|
+
const classC = await t.local.schemaContext.getSchemaItem("TestDomain", "C", EntityClass);
|
|
1841
|
+
chai.expect(await classC.getProperty("PropC2")).to.exist;
|
|
1842
|
+
const classD = await t.local.schemaContext.getSchemaItem("TestDomain", "D", EntityClass);
|
|
1843
|
+
chai.expect(await classD.getProperty("PropD2")).to.exist;
|
|
1844
|
+
});
|
|
1845
|
+
it("G3: local imports schema in two steps with data between them; incoming has data only", async () => {
|
|
1846
|
+
// Local: data txn → schema v01 txn → data txn → schema v02 txn.
|
|
1847
|
+
// Incoming (far): data changes to different elements.
|
|
1848
|
+
// Expected: all four local txns preserved in order, far data also visible.
|
|
1849
|
+
t = await TestIModel.initialize("G3LocalSchemaDataInterleaved");
|
|
1850
|
+
let farTxn = startTestTxn(t.far, "G3 far");
|
|
1851
|
+
let localTxn = startTestTxn(t.local, "G3 local");
|
|
1852
|
+
// Create two shared elements via far and push
|
|
1853
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1854
|
+
const sharedId1 = t.insertElement(farTxn, "TestDomain:C", { propA: "shared_a1", propC: "shared_c1" });
|
|
1855
|
+
const sharedId2 = t.insertElement(farTxn, "TestDomain:C", { propA: "shared_a2", propC: "shared_c2" });
|
|
1856
|
+
farTxn.saveChanges("far create shared elements");
|
|
1857
|
+
await pushChanges(farTxn, "far create shared elements");
|
|
1858
|
+
farTxn = startTestTxn(t.far, "G3 far 2");
|
|
1859
|
+
// Both pull
|
|
1860
|
+
await pullChanges(localTxn);
|
|
1861
|
+
localTxn = startTestTxn(t.local, "G3 local 2");
|
|
1862
|
+
// Far: update sharedId1 and push (data only)
|
|
1863
|
+
await t.far.locks.acquireLocks({ exclusive: sharedId1 });
|
|
1864
|
+
t.updateElement(farTxn, sharedId1, { propA: "far_updated_a1" });
|
|
1865
|
+
farTxn.saveChanges("far update sharedId1");
|
|
1866
|
+
await pushChanges(farTxn, "far update sharedId1");
|
|
1867
|
+
// Local: txn1 - update sharedId2 (data)
|
|
1868
|
+
await t.local.locks.acquireLocks({ exclusive: sharedId2 });
|
|
1869
|
+
t.updateElement(localTxn, sharedId2, { propA: "local_updated_a2" });
|
|
1870
|
+
localTxn.saveChanges("local txn1 update sharedId2");
|
|
1871
|
+
// Local: txn2 - schema v01 (adds PropC2)
|
|
1872
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
1873
|
+
// Local: txn3 - insert new element using new PropC2
|
|
1874
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1875
|
+
const localNewId = t.insertElement(localTxn, "TestDomain:C", { propA: "new_local_a", propC: "new_local_c", propC2: "new_local_c2" });
|
|
1876
|
+
localTxn.saveChanges("local txn3 insert element with PropC2");
|
|
1877
|
+
// Local: txn4 - schema v02 (adds PropD2)
|
|
1878
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
|
|
1879
|
+
// Local pulls and rebases all four local txns onto incoming data change
|
|
1880
|
+
await pullChanges(localTxn);
|
|
1881
|
+
t.local.clearCaches();
|
|
1882
|
+
// sharedId1 should have far's update
|
|
1883
|
+
const elem1 = t.getElementProps(t.local, sharedId1);
|
|
1884
|
+
chai.expect(elem1.propA).to.equal("far_updated_a1", "Far update to sharedId1 should be applied");
|
|
1885
|
+
// sharedId2 should have local's update
|
|
1886
|
+
const elem2 = t.getElementProps(t.local, sharedId2);
|
|
1887
|
+
chai.expect(elem2.propA).to.equal("local_updated_a2", "Local update to sharedId2 should be preserved");
|
|
1888
|
+
// New locally-inserted element with PropC2 should exist
|
|
1889
|
+
const newElem = t.getElementProps(t.local, localNewId);
|
|
1890
|
+
chai.expect(newElem.propA).to.equal("new_local_a", "Locally-inserted element should be preserved");
|
|
1891
|
+
chai.expect(newElem.propC2).to.equal("new_local_c2", "PropC2 from local element should be preserved");
|
|
1892
|
+
// Schema should be at v02 (both local schema imports preserved)
|
|
1893
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
1894
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02 after two local schema imports");
|
|
1895
|
+
});
|
|
1896
|
+
it("G4: three successive schema increments from three different pushes, local rebases all", async () => {
|
|
1897
|
+
// far pushes v01, then v02, then v03 as separate changesets.
|
|
1898
|
+
// local has a single data change it needs to rebase on top of all three.
|
|
1899
|
+
t = await TestIModel.initialize("G4ThreeSuccessiveSchemaIncrements");
|
|
1900
|
+
let farTxn = startTestTxn(t.far, "G4 far");
|
|
1901
|
+
let localTxn = startTestTxn(t.local, "G4 local");
|
|
1902
|
+
// Create shared element on far
|
|
1903
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1904
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "init_a", propC: "init_c" });
|
|
1905
|
+
farTxn.saveChanges("far create element");
|
|
1906
|
+
await pushChanges(farTxn, "far create element");
|
|
1907
|
+
farTxn = startTestTxn(t.far, "G4 far 2");
|
|
1908
|
+
// Both pull to sync
|
|
1909
|
+
await pullChanges(localTxn);
|
|
1910
|
+
localTxn = startTestTxn(t.local, "G4 local 2");
|
|
1911
|
+
// Far pushes v01
|
|
1912
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
1913
|
+
await pushChanges(farTxn, "far v01");
|
|
1914
|
+
farTxn = startTestTxn(t.far, "G4 far 3");
|
|
1915
|
+
// Far pushes v02
|
|
1916
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
|
|
1917
|
+
await pushChanges(farTxn, "far v02");
|
|
1918
|
+
farTxn = startTestTxn(t.far, "G4 far 4");
|
|
1919
|
+
// Far pushes v03 (moves PropC and PropD to A)
|
|
1920
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x03MovePropCAndD]);
|
|
1921
|
+
await pushChanges(farTxn, "far v03");
|
|
1922
|
+
// Local: data change only
|
|
1923
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
1924
|
+
t.updateElement(localTxn, elementId, { propA: "local_updated_a", propC: "local_updated_c" });
|
|
1925
|
+
localTxn.saveChanges("local update element");
|
|
1926
|
+
// Local pulls and rebases through all three schema changesets
|
|
1927
|
+
await pullChanges(localTxn);
|
|
1928
|
+
t.local.clearCaches();
|
|
1929
|
+
// Local data change should survive all three schema transforms
|
|
1930
|
+
const element = t.getElementProps(t.local, elementId);
|
|
1931
|
+
chai.expect(element.propA).to.equal("local_updated_a", "Local propA should be preserved after three schema increments");
|
|
1932
|
+
chai.expect(element.propC).to.equal("local_updated_c", "Local propC should be preserved after three schema increments");
|
|
1933
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
1934
|
+
chai.expect(schema.version).to.equal("01.00.03", "Schema should be at v03 after three incoming schema increments");
|
|
1935
|
+
});
|
|
1936
|
+
});
|
|
1937
|
+
/**
|
|
1938
|
+
* ElementAspect changes during semantic rebase.
|
|
1939
|
+
* Tests that aspect insert/update/delete operations are correctly captured and reinstated.
|
|
1940
|
+
*/
|
|
1941
|
+
describe("Semantic Rebase - ElementAspect Changes", function () {
|
|
1942
|
+
this.timeout(60000);
|
|
1943
|
+
let t;
|
|
1944
|
+
before(async () => {
|
|
1945
|
+
await TestUtils.shutdownBackend();
|
|
1946
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
1947
|
+
});
|
|
1948
|
+
afterEach(() => {
|
|
1949
|
+
if (t) {
|
|
1950
|
+
t.shutdown();
|
|
1951
|
+
t = undefined;
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
after(async () => {
|
|
1955
|
+
await TestUtils.shutdownBackend();
|
|
1956
|
+
await TestUtils.startBackend();
|
|
1957
|
+
});
|
|
1958
|
+
it("H1: local inserts UniqueAspect; incoming trivial schema change → aspect preserved after rebase", async () => {
|
|
1959
|
+
t = await TestIModel.initialize("H1AspectInsertIncomingTrivial");
|
|
1960
|
+
let farTxn = startTestTxn(t.far, "H1 far");
|
|
1961
|
+
let localTxn = startTestTxn(t.local, "H1 local");
|
|
1962
|
+
// Set up base schema with aspect class on both sides
|
|
1963
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x01WithAspect]);
|
|
1964
|
+
await pushChanges(farTxn, "import aspect schema");
|
|
1965
|
+
farTxn = startTestTxn(t.far, "H1 far 2");
|
|
1966
|
+
await pullChanges(localTxn);
|
|
1967
|
+
localTxn = startTestTxn(t.local, "H1 local 2");
|
|
1968
|
+
// Create an element on far, push
|
|
1969
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
1970
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "elem_a", propC: "elem_c" });
|
|
1971
|
+
farTxn.saveChanges("far create element");
|
|
1972
|
+
await pushChanges(farTxn, "far create element");
|
|
1973
|
+
farTxn = startTestTxn(t.far, "H1 far 3");
|
|
1974
|
+
// Local pulls element
|
|
1975
|
+
await pullChanges(localTxn);
|
|
1976
|
+
localTxn = startTestTxn(t.local, "H1 local 3");
|
|
1977
|
+
// Far imports trivial schema (adds AspectProp2 to CUniqueAspect)
|
|
1978
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x02WithAspectProp2]);
|
|
1979
|
+
await pushChanges(farTxn, "far add AspectProp2");
|
|
1980
|
+
// Local inserts a UniqueAspect onto the element
|
|
1981
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
1982
|
+
t.insertAspect(localTxn, elementId, "TestDomain:CUniqueAspect", { aspectProp: "local_aspect_value" });
|
|
1983
|
+
localTxn.saveChanges("local insert aspect");
|
|
1984
|
+
// Local pulls: data rebase must preserve the aspect insertion
|
|
1985
|
+
await pullChanges(localTxn);
|
|
1986
|
+
t.local.clearCaches();
|
|
1987
|
+
// Aspect should still exist with correct value
|
|
1988
|
+
const aspect = t.getAspect(t.local, elementId, "TestDomain:CUniqueAspect");
|
|
1989
|
+
chai.expect(aspect, "Aspect should exist after rebase").to.not.be.undefined;
|
|
1990
|
+
chai.expect(aspect.aspectProp).to.equal("local_aspect_value", "AspectProp should be preserved after rebase");
|
|
1991
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
1992
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02 with AspectProp2 added");
|
|
1993
|
+
});
|
|
1994
|
+
it("H2: local inserts UniqueAspect; incoming transforming schema change → aspect preserved after transform", async () => {
|
|
1995
|
+
t = await TestIModel.initialize("H2AspectInsertIncomingTransform");
|
|
1996
|
+
let farTxn = startTestTxn(t.far, "H2 far");
|
|
1997
|
+
let localTxn = startTestTxn(t.local, "H2 local");
|
|
1998
|
+
// Set up base schema with aspect class on both sides
|
|
1999
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x01WithAspect]);
|
|
2000
|
+
await pushChanges(farTxn, "import aspect schema");
|
|
2001
|
+
farTxn = startTestTxn(t.far, "H2 far 2");
|
|
2002
|
+
await pullChanges(localTxn);
|
|
2003
|
+
localTxn = startTestTxn(t.local, "H2 local 2");
|
|
2004
|
+
// Create element on far, push
|
|
2005
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2006
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "elem_a", propC: "elem_c" });
|
|
2007
|
+
farTxn.saveChanges("far create element");
|
|
2008
|
+
await pushChanges(farTxn, "far create element");
|
|
2009
|
+
farTxn = startTestTxn(t.far, "H2 far 3");
|
|
2010
|
+
// Local pulls element
|
|
2011
|
+
await pullChanges(localTxn);
|
|
2012
|
+
localTxn = startTestTxn(t.local, "H2 local 3");
|
|
2013
|
+
// Far imports transforming schema: moves PropC from C to A (v02 also brings AspectProp2)
|
|
2014
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x02WithAspectProp2]);
|
|
2015
|
+
await pushChanges(farTxn, "far import transforming schema with aspect change");
|
|
2016
|
+
// Local inserts a UniqueAspect
|
|
2017
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
2018
|
+
t.insertAspect(localTxn, elementId, "TestDomain:CUniqueAspect", { aspectProp: "aspect_before_transform" });
|
|
2019
|
+
localTxn.saveChanges("local insert aspect");
|
|
2020
|
+
// Local pulls: aspect is captured then reinstated after schema transform
|
|
2021
|
+
await pullChanges(localTxn);
|
|
2022
|
+
t.local.clearCaches();
|
|
2023
|
+
// Aspect should still exist
|
|
2024
|
+
const aspect = t.getAspect(t.local, elementId, "TestDomain:CUniqueAspect");
|
|
2025
|
+
chai.expect(aspect, "Aspect should exist after transforming schema rebase").to.not.be.undefined;
|
|
2026
|
+
chai.expect(aspect.aspectProp, "AspectProp value should be preserved").to.equal("aspect_before_transform");
|
|
2027
|
+
// Element itself should also be intact
|
|
2028
|
+
const element = t.getElementProps(t.local, elementId);
|
|
2029
|
+
chai.expect(element.propA, "Element propA should be preserved").to.equal("elem_a");
|
|
2030
|
+
});
|
|
2031
|
+
it("H3: local updates UniqueAspect property; incoming trivial schema change → aspect update preserved", async () => {
|
|
2032
|
+
t = await TestIModel.initialize("H3AspectUpdateIncomingTrivial");
|
|
2033
|
+
let farTxn = startTestTxn(t.far, "H3 far");
|
|
2034
|
+
let localTxn = startTestTxn(t.local, "H3 local");
|
|
2035
|
+
// Both get the aspect schema first
|
|
2036
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x01WithAspect]);
|
|
2037
|
+
await pushChanges(farTxn, "import aspect schema");
|
|
2038
|
+
farTxn = startTestTxn(t.far, "H3 far 2");
|
|
2039
|
+
await pullChanges(localTxn);
|
|
2040
|
+
localTxn = startTestTxn(t.local, "H3 local 2");
|
|
2041
|
+
// Far creates element + inserts aspect, pushes
|
|
2042
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2043
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "elem_a", propC: "elem_c" });
|
|
2044
|
+
t.insertAspect(farTxn, elementId, "TestDomain:CUniqueAspect", { aspectProp: "initial_aspect" });
|
|
2045
|
+
farTxn.saveChanges("far create element + aspect");
|
|
2046
|
+
await pushChanges(farTxn, "far create element + aspect");
|
|
2047
|
+
farTxn = startTestTxn(t.far, "H3 far 3");
|
|
2048
|
+
// Local pulls to get the element and aspect
|
|
2049
|
+
await pullChanges(localTxn);
|
|
2050
|
+
localTxn = startTestTxn(t.local, "H3 local 3");
|
|
2051
|
+
// Far imports trivial schema (adds AspectProp2 to CUniqueAspect), pushes
|
|
2052
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x02WithAspectProp2]);
|
|
2053
|
+
await pushChanges(farTxn, "far add AspectProp2");
|
|
2054
|
+
// Local updates the aspect's AspectProp
|
|
2055
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
2056
|
+
t.updateAspect(localTxn, elementId, "TestDomain:CUniqueAspect", { aspectProp: "updated_aspect_value" });
|
|
2057
|
+
localTxn.saveChanges("local update aspect");
|
|
2058
|
+
// Local pulls: aspect update should survive the schema rebase
|
|
2059
|
+
await pullChanges(localTxn);
|
|
2060
|
+
t.local.clearCaches();
|
|
2061
|
+
const aspect = t.getAspect(t.local, elementId, "TestDomain:CUniqueAspect");
|
|
2062
|
+
chai.expect(aspect).to.not.be.undefined;
|
|
2063
|
+
chai.expect(aspect.aspectProp).to.equal("updated_aspect_value", "Aspect update should be preserved after trivial schema rebase");
|
|
2064
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2065
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02");
|
|
2066
|
+
});
|
|
2067
|
+
it("H4: local deletes UniqueAspect; incoming trivial schema change → aspect stays deleted after rebase", async () => {
|
|
2068
|
+
t = await TestIModel.initialize("H4AspectDeleteIncomingTrivial");
|
|
2069
|
+
let farTxn = startTestTxn(t.far, "H4 far");
|
|
2070
|
+
let localTxn = startTestTxn(t.local, "H4 local");
|
|
2071
|
+
// Both get the aspect schema
|
|
2072
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x01WithAspect]);
|
|
2073
|
+
await pushChanges(farTxn, "import aspect schema");
|
|
2074
|
+
farTxn = startTestTxn(t.far, "H4 far 2");
|
|
2075
|
+
await pullChanges(localTxn);
|
|
2076
|
+
localTxn = startTestTxn(t.local, "H4 local 2");
|
|
2077
|
+
// Far creates element + aspect, pushes
|
|
2078
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2079
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "elem_a", propC: "elem_c" });
|
|
2080
|
+
t.insertAspect(farTxn, elementId, "TestDomain:CUniqueAspect", { aspectProp: "initial_aspect" });
|
|
2081
|
+
farTxn.saveChanges("far create element + aspect");
|
|
2082
|
+
await pushChanges(farTxn, "far create element + aspect");
|
|
2083
|
+
farTxn = startTestTxn(t.far, "H4 far 3");
|
|
2084
|
+
// Local pulls to get the element and aspect
|
|
2085
|
+
await pullChanges(localTxn);
|
|
2086
|
+
localTxn = startTestTxn(t.local, "H4 local 3");
|
|
2087
|
+
// Far imports trivial schema, pushes
|
|
2088
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x02WithAspectProp2]);
|
|
2089
|
+
await pushChanges(farTxn, "far add AspectProp2");
|
|
2090
|
+
// Local deletes the aspect
|
|
2091
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
2092
|
+
t.deleteAspect(localTxn, elementId, "TestDomain:CUniqueAspect");
|
|
2093
|
+
localTxn.saveChanges("local delete aspect");
|
|
2094
|
+
// Local pulls: aspect deletion should be preserved after schema rebase
|
|
2095
|
+
await pullChanges(localTxn);
|
|
2096
|
+
t.local.clearCaches();
|
|
2097
|
+
// The element should still exist but have no aspect
|
|
2098
|
+
const element = t.getElementProps(t.local, elementId);
|
|
2099
|
+
chai.expect(element, "Element should still exist after rebase").to.not.be.undefined;
|
|
2100
|
+
const aspects = t.local.elements.getAspects(elementId, "TestDomain:CUniqueAspect");
|
|
2101
|
+
chai.expect(aspects.length).to.equal(0, "Aspect should be gone after deletion is reinstated");
|
|
2102
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2103
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02");
|
|
2104
|
+
});
|
|
2105
|
+
it("H5: local inserts aspect; incoming adds same aspect class schema change AND data → both preserved", async () => {
|
|
2106
|
+
t = await TestIModel.initialize("H5AspectInsertIncomingSchemaAndData");
|
|
2107
|
+
let farTxn = startTestTxn(t.far, "H5 far");
|
|
2108
|
+
let localTxn = startTestTxn(t.local, "H5 local");
|
|
2109
|
+
// Both get the base aspect schema
|
|
2110
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x01WithAspect]);
|
|
2111
|
+
await pushChanges(farTxn, "import aspect schema");
|
|
2112
|
+
farTxn = startTestTxn(t.far, "H5 far 2");
|
|
2113
|
+
await pullChanges(localTxn);
|
|
2114
|
+
localTxn = startTestTxn(t.local, "H5 local 2");
|
|
2115
|
+
// Far creates two elements and inserts aspects on them
|
|
2116
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2117
|
+
const farElementId = t.insertElement(farTxn, "TestDomain:C", { propA: "far_a", propC: "far_c" });
|
|
2118
|
+
t.insertAspect(farTxn, farElementId, "TestDomain:CUniqueAspect", { aspectProp: "far_aspect" });
|
|
2119
|
+
farTxn.saveChanges("far create element + aspect");
|
|
2120
|
+
// Far also upgrades the schema to v02 (adds AspectProp2)
|
|
2121
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x02WithAspectProp2]);
|
|
2122
|
+
await pushChanges(farTxn, "far schema upgrade + element with aspect");
|
|
2123
|
+
// Local creates its own element and inserts an aspect with both AspectProp values
|
|
2124
|
+
// (local is still at v01 with only AspectProp; we insert only that)
|
|
2125
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2126
|
+
const localElementId = t.insertElement(localTxn, "TestDomain:C", { propA: "local_a", propC: "local_c" });
|
|
2127
|
+
t.insertAspect(localTxn, localElementId, "TestDomain:CUniqueAspect", { aspectProp: "local_aspect" });
|
|
2128
|
+
localTxn.saveChanges("local create element + aspect");
|
|
2129
|
+
// Local pulls: incoming has schema v02 + far's element+aspect; local data rebase on top
|
|
2130
|
+
await pullChanges(localTxn);
|
|
2131
|
+
t.local.clearCaches();
|
|
2132
|
+
// Far's element and aspect should be present
|
|
2133
|
+
const farAspect = t.getAspect(t.local, farElementId, "TestDomain:CUniqueAspect");
|
|
2134
|
+
chai.expect(farAspect).to.not.be.undefined;
|
|
2135
|
+
chai.expect(farAspect.aspectProp).to.equal("far_aspect");
|
|
2136
|
+
// Local's element and aspect should also be preserved
|
|
2137
|
+
const localAspect = t.getAspect(t.local, localElementId, "TestDomain:CUniqueAspect");
|
|
2138
|
+
chai.expect(localAspect).to.not.be.undefined;
|
|
2139
|
+
chai.expect(localAspect.aspectProp).to.equal("local_aspect", "Local aspect should survive rebase onto incoming schema+data");
|
|
2140
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2141
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02");
|
|
2142
|
+
});
|
|
2143
|
+
});
|
|
2144
|
+
/**
|
|
2145
|
+
* Property type variations during semantic rebase.
|
|
2146
|
+
* Ensures int, double, and boolean property values are preserved correctly through rebase.
|
|
2147
|
+
*/
|
|
2148
|
+
describe("Semantic Rebase - Property Type Variations", function () {
|
|
2149
|
+
this.timeout(60000);
|
|
2150
|
+
let t;
|
|
2151
|
+
before(async () => {
|
|
2152
|
+
await TestUtils.shutdownBackend();
|
|
2153
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
2154
|
+
});
|
|
2155
|
+
afterEach(() => {
|
|
2156
|
+
if (t) {
|
|
2157
|
+
t.shutdown();
|
|
2158
|
+
t = undefined;
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
after(async () => {
|
|
2162
|
+
await TestUtils.shutdownBackend();
|
|
2163
|
+
await TestUtils.startBackend();
|
|
2164
|
+
});
|
|
2165
|
+
it("I1: int, double, and boolean properties preserved through trivial schema rebase", async () => {
|
|
2166
|
+
// Schema v01 adds int/double/bool props to class C.
|
|
2167
|
+
// Local sets those values on an element, far imports v02 (adds PropD2 on D).
|
|
2168
|
+
// After rebase: multi-type values should survive unchanged.
|
|
2169
|
+
t = await TestIModel.initialize("I1MultiTypePropsRebase");
|
|
2170
|
+
let farTxn = startTestTxn(t.far, "I1 far");
|
|
2171
|
+
let localTxn = startTestTxn(t.local, "I1 local");
|
|
2172
|
+
// Both get the multi-type schema (v01)
|
|
2173
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x01MultiTypeProps]);
|
|
2174
|
+
await pushChanges(farTxn, "import multi-type schema v01");
|
|
2175
|
+
farTxn = startTestTxn(t.far, "I1 far 2");
|
|
2176
|
+
await pullChanges(localTxn);
|
|
2177
|
+
localTxn = startTestTxn(t.local, "I1 local 2");
|
|
2178
|
+
// Create element with all multi-type properties on local, push
|
|
2179
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2180
|
+
const elementId = t.insertElement(localTxn, "TestDomain:C", {
|
|
2181
|
+
propA: "string_val",
|
|
2182
|
+
propC: "another_string",
|
|
2183
|
+
propCInt: 42,
|
|
2184
|
+
propCDouble: 3.14159,
|
|
2185
|
+
propCBool: true,
|
|
2186
|
+
});
|
|
2187
|
+
localTxn.saveChanges("local create multi-type element");
|
|
2188
|
+
await pushChanges(localTxn, "create multi-type element");
|
|
2189
|
+
localTxn = startTestTxn(t.local, "I1 local 3");
|
|
2190
|
+
// Far imports v02 (adds PropD2 — trivial, unrelated change), pushes
|
|
2191
|
+
await pullChanges(farTxn);
|
|
2192
|
+
farTxn = startTestTxn(t.far, "I1 far 3");
|
|
2193
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x02MultiTypePropsExtended]);
|
|
2194
|
+
await pushChanges(farTxn, "far import v02 with PropD2");
|
|
2195
|
+
// Local updates the int and bool properties
|
|
2196
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
2197
|
+
t.updateElement(localTxn, elementId, { propCInt: 100, propCBool: false });
|
|
2198
|
+
localTxn.saveChanges("local update int and bool");
|
|
2199
|
+
// Local pulls: data rebase should preserve all type values
|
|
2200
|
+
await pullChanges(localTxn);
|
|
2201
|
+
t.local.clearCaches();
|
|
2202
|
+
const element = t.getElementProps(t.local, elementId);
|
|
2203
|
+
chai.expect(element.propCInt).to.equal(100, "Int property update should be preserved");
|
|
2204
|
+
chai.expect(element.propCDouble).to.equal(3.14159, "Double property should remain from original insert");
|
|
2205
|
+
chai.expect(element.propCBool).to.equal(false, "Boolean property update should be preserved");
|
|
2206
|
+
chai.expect(element.propC).to.equal("another_string", "String property should be preserved");
|
|
2207
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2208
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02");
|
|
2209
|
+
});
|
|
2210
|
+
it("I2: int, double, and boolean properties preserved through transforming schema rebase", async () => {
|
|
2211
|
+
// Local creates element with multi-type props, far imports transforming schema (moves PropD to A).
|
|
2212
|
+
// After rebase: all multi-type values should survive even though the schema layout changed for D.
|
|
2213
|
+
t = await TestIModel.initialize("I2MultiTypePropsTransformRebase");
|
|
2214
|
+
let farTxn = startTestTxn(t.far, "I2 far");
|
|
2215
|
+
let localTxn = startTestTxn(t.local, "I2 local");
|
|
2216
|
+
// Both get multi-type schema (v01)
|
|
2217
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x01MultiTypeProps]);
|
|
2218
|
+
await pushChanges(farTxn, "import multi-type schema v01");
|
|
2219
|
+
farTxn = startTestTxn(t.far, "I2 far 2");
|
|
2220
|
+
await pullChanges(localTxn);
|
|
2221
|
+
localTxn = startTestTxn(t.local, "I2 local 2");
|
|
2222
|
+
// Far creates a shared element, push
|
|
2223
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2224
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", {
|
|
2225
|
+
propA: "shared_a",
|
|
2226
|
+
propC: "shared_c",
|
|
2227
|
+
propCInt: 7,
|
|
2228
|
+
propCDouble: 2.718,
|
|
2229
|
+
propCBool: true,
|
|
2230
|
+
});
|
|
2231
|
+
farTxn.saveChanges("far create multi-type element");
|
|
2232
|
+
await pushChanges(farTxn, "far create element");
|
|
2233
|
+
farTxn = startTestTxn(t.far, "I2 far 3");
|
|
2234
|
+
// Both pull to sync
|
|
2235
|
+
await pullChanges(localTxn);
|
|
2236
|
+
localTxn = startTestTxn(t.local, "I2 local 3");
|
|
2237
|
+
// Far imports transforming schema (v02 moves PropD to A)
|
|
2238
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x02MultiTypePropsMovePropDToA]);
|
|
2239
|
+
await pushChanges(farTxn, "far move PropD to A");
|
|
2240
|
+
// Local updates the multi-type properties on the element
|
|
2241
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
2242
|
+
t.updateElement(localTxn, elementId, { propCInt: 99, propCDouble: 1.414, propCBool: false });
|
|
2243
|
+
localTxn.saveChanges("local update multi-type props");
|
|
2244
|
+
// Local pulls: data rebase with schema transform
|
|
2245
|
+
await pullChanges(localTxn);
|
|
2246
|
+
t.local.clearCaches();
|
|
2247
|
+
const element = t.getElementProps(t.local, elementId);
|
|
2248
|
+
chai.expect(element.propCInt).to.equal(99, "Int property should be preserved after transforming schema rebase");
|
|
2249
|
+
chai.expect(element.propCDouble).to.closeTo(1.414, 0.0001, "Double property should be preserved");
|
|
2250
|
+
chai.expect(element.propCBool).to.equal(false, "Boolean property should be preserved");
|
|
2251
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2252
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02");
|
|
2253
|
+
});
|
|
2254
|
+
it("I3: element with null/undefined property values rebased correctly through schema transform", async () => {
|
|
2255
|
+
// Element created with some properties left unset (null/undefined).
|
|
2256
|
+
// Rebase should not fail and unset properties should remain absent.
|
|
2257
|
+
t = await TestIModel.initialize("I3NullPropertyRebase");
|
|
2258
|
+
let farTxn = startTestTxn(t.far, "I3 far");
|
|
2259
|
+
let localTxn = startTestTxn(t.local, "I3 local");
|
|
2260
|
+
// Create element on local with only propA set (propC left unset)
|
|
2261
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2262
|
+
const elementId = t.insertElement(localTxn, "TestDomain:C", { propA: "only_a" });
|
|
2263
|
+
// propC is intentionally not set (will be undefined/null)
|
|
2264
|
+
localTxn.saveChanges("create element with partial props");
|
|
2265
|
+
await pushChanges(localTxn, "create partially populated element");
|
|
2266
|
+
localTxn = startTestTxn(t.local, "I3 local 2");
|
|
2267
|
+
// Far imports transforming schema (moves PropC from C to A), pushes
|
|
2268
|
+
await pullChanges(farTxn);
|
|
2269
|
+
farTxn = startTestTxn(t.far, "I3 far 2");
|
|
2270
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02MovePropCToA]);
|
|
2271
|
+
await pushChanges(farTxn, "far move PropC to A");
|
|
2272
|
+
// Local makes a data change to update propA
|
|
2273
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
2274
|
+
t.updateElement(localTxn, elementId, { propA: "updated_a" });
|
|
2275
|
+
localTxn.saveChanges("local update propA only");
|
|
2276
|
+
// Local pulls: data rebase should handle null/absent propC gracefully
|
|
2277
|
+
await pullChanges(localTxn);
|
|
2278
|
+
t.local.clearCaches();
|
|
2279
|
+
const element = t.getElementProps(t.local, elementId);
|
|
2280
|
+
chai.expect(element.propA).to.equal("updated_a", "PropA update should be preserved");
|
|
2281
|
+
// propC was never set so it should still be absent or null after the transform
|
|
2282
|
+
chai.expect(element.propC === undefined || element.propC === null || element.propC === "").to.be.true,
|
|
2283
|
+
"PropC should remain absent/null after rebase when it was never set";
|
|
2284
|
+
});
|
|
2285
|
+
it("I4: binary (UInt8Array) property values preserved through insert and update during trivial schema rebase", async () => {
|
|
2286
|
+
// Schema v01 adds a binary property (PropCBin) to class C.
|
|
2287
|
+
// Local inserts an element with a Uint8Array value, then updates it to a different Uint8Array.
|
|
2288
|
+
// Far imports v02 (adds PropD2 — trivial, unrelated change) to trigger semantic rebase.
|
|
2289
|
+
// After rebase: the updated binary value should survive as a Uint8Array with the correct bytes.
|
|
2290
|
+
t = await TestIModel.initialize("I4BinaryPropRebase");
|
|
2291
|
+
let farTxn = startTestTxn(t.far, "I4 far");
|
|
2292
|
+
let localTxn = startTestTxn(t.local, "I4 local");
|
|
2293
|
+
// Both sides get the binary-prop schema (v01)
|
|
2294
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x01WithBinaryProp]);
|
|
2295
|
+
await pushChanges(farTxn, "import binary-prop schema v01");
|
|
2296
|
+
farTxn = startTestTxn(t.far, "I4 far 2");
|
|
2297
|
+
await pullChanges(localTxn);
|
|
2298
|
+
localTxn = startTestTxn(t.local, "I4 local 2");
|
|
2299
|
+
// Local inserts an element with an initial binary value, pushes so both sides are in sync
|
|
2300
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2301
|
+
const initialBin = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]);
|
|
2302
|
+
const elementId = t.insertElement(localTxn, "TestDomain:C", {
|
|
2303
|
+
propA: "binary_test",
|
|
2304
|
+
propC: "some_string",
|
|
2305
|
+
propCBin: initialBin,
|
|
2306
|
+
});
|
|
2307
|
+
localTxn.saveChanges("local insert element with binary prop");
|
|
2308
|
+
await pushChanges(localTxn, "create element with binary prop");
|
|
2309
|
+
localTxn = startTestTxn(t.local, "I4 local 3");
|
|
2310
|
+
// Far imports v02 (adds PropD2 — trivial change), pushes to trigger rebase on local's next pull
|
|
2311
|
+
await pullChanges(farTxn);
|
|
2312
|
+
farTxn = startTestTxn(t.far, "I4 far 3");
|
|
2313
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x02WithBinaryPropAndPropD2]);
|
|
2314
|
+
await pushChanges(farTxn, "far import v02 with PropD2");
|
|
2315
|
+
// Local updates the binary property to a new Uint8Array value (NOT pushed yet)
|
|
2316
|
+
const updatedBin = new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]);
|
|
2317
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
2318
|
+
t.updateElement(localTxn, elementId, { propCBin: updatedBin });
|
|
2319
|
+
localTxn.saveChanges("local update binary prop");
|
|
2320
|
+
// Local pulls: semantic rebase should reinstate the binary property update
|
|
2321
|
+
await pullChanges(localTxn);
|
|
2322
|
+
t.local.clearCaches();
|
|
2323
|
+
const element = t.getElementProps(t.local, elementId);
|
|
2324
|
+
chai.expect(element.propCBin).to.be.instanceOf(Uint8Array, "Binary property should be returned as a Uint8Array");
|
|
2325
|
+
chai.expect(Array.from(element.propCBin)).to.deep.equal(Array.from(updatedBin), "Updated binary value should be preserved byte-for-byte after semantic rebase");
|
|
2326
|
+
chai.expect(element.propC).to.equal("some_string", "String property should be unchanged");
|
|
2327
|
+
chai.expect(element.propA).to.equal("binary_test", "PropA should be unchanged");
|
|
2328
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2329
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02 after rebase");
|
|
2330
|
+
});
|
|
2331
|
+
});
|
|
2332
|
+
/**
|
|
2333
|
+
* Both sides delete the same element.
|
|
2334
|
+
* Edge case where both local and far delete the same element independently.
|
|
2335
|
+
*/
|
|
2336
|
+
describe("Semantic Rebase - Both Sides Delete Same Element", function () {
|
|
2337
|
+
this.timeout(60000);
|
|
2338
|
+
let t;
|
|
2339
|
+
before(async () => {
|
|
2340
|
+
await TestUtils.shutdownBackend();
|
|
2341
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
2342
|
+
});
|
|
2343
|
+
afterEach(() => {
|
|
2344
|
+
if (t) {
|
|
2345
|
+
t.shutdown();
|
|
2346
|
+
t = undefined;
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
2349
|
+
after(async () => {
|
|
2350
|
+
await TestUtils.shutdownBackend();
|
|
2351
|
+
await TestUtils.startBackend();
|
|
2352
|
+
});
|
|
2353
|
+
it("J2: far deletes element, local makes data change to a DIFFERENT element + schema import → element gone, other changes preserved", async () => {
|
|
2354
|
+
t = await TestIModel.initialize("J2FarDeleteLocalSchemaAndData");
|
|
2355
|
+
let farTxn = startTestTxn(t.far, "J2 far");
|
|
2356
|
+
let localTxn = startTestTxn(t.local, "J2 local");
|
|
2357
|
+
// Create two shared elements
|
|
2358
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2359
|
+
const deletedElementId = t.insertElement(farTxn, "TestDomain:C", { propA: "del_a", propC: "del_c" });
|
|
2360
|
+
const keepElementId = t.insertElement(farTxn, "TestDomain:D", { propA: "keep_a", propD: "keep_d" });
|
|
2361
|
+
farTxn.saveChanges("create two elements");
|
|
2362
|
+
await pushChanges(farTxn, "create two elements");
|
|
2363
|
+
farTxn = startTestTxn(t.far, "J2 far 2");
|
|
2364
|
+
await pullChanges(localTxn);
|
|
2365
|
+
localTxn = startTestTxn(t.local, "J2 local 2");
|
|
2366
|
+
// Far imports schema + deletes one element
|
|
2367
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2368
|
+
await t.far.locks.acquireLocks({ exclusive: deletedElementId });
|
|
2369
|
+
farTxn.deleteElement(deletedElementId);
|
|
2370
|
+
farTxn.saveChanges("far delete element");
|
|
2371
|
+
await pushChanges(farTxn, "far schema + delete element");
|
|
2372
|
+
// Local: imports a different schema upgrade + updates the other element
|
|
2373
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2374
|
+
await t.local.locks.acquireLocks({ exclusive: keepElementId });
|
|
2375
|
+
t.updateElement(localTxn, keepElementId, { propA: "local_updated_keep_a" });
|
|
2376
|
+
localTxn.saveChanges("local update keep element");
|
|
2377
|
+
// Local pulls
|
|
2378
|
+
await pullChanges(localTxn);
|
|
2379
|
+
t.local.clearCaches();
|
|
2380
|
+
// Deleted element should be gone
|
|
2381
|
+
chai.expect(() => t.getElementProps(t.local, deletedElementId)).to.throw(`element not found`, "Deleted element should not be found after rebase");
|
|
2382
|
+
// Kept element should have local's update
|
|
2383
|
+
const keepElement = t.getElementProps(t.local, keepElementId);
|
|
2384
|
+
chai.expect(keepElement.propA).to.equal("local_updated_keep_a", "Keep element's local update should be preserved");
|
|
2385
|
+
// Schema should be at v01
|
|
2386
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2387
|
+
chai.expect(schema.version).to.equal("01.00.01", "Schema should be at v01");
|
|
2388
|
+
});
|
|
2389
|
+
});
|
|
2390
|
+
/**
|
|
2391
|
+
* Three-briefcase scenarios.
|
|
2392
|
+
* Tests interactions when three separate briefcases are involved in schema+data operations.
|
|
2393
|
+
*/
|
|
2394
|
+
describe("Semantic Rebase - Three Briefcase Scenarios", function () {
|
|
2395
|
+
this.timeout(60000);
|
|
2396
|
+
let t;
|
|
2397
|
+
before(async () => {
|
|
2398
|
+
await TestUtils.shutdownBackend();
|
|
2399
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
2400
|
+
});
|
|
2401
|
+
afterEach(() => {
|
|
2402
|
+
if (t) {
|
|
2403
|
+
t.shutdown();
|
|
2404
|
+
t = undefined;
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2407
|
+
after(async () => {
|
|
2408
|
+
await TestUtils.shutdownBackend();
|
|
2409
|
+
await TestUtils.startBackend();
|
|
2410
|
+
});
|
|
2411
|
+
it("K1: schema from extra briefcase, data from far, local rebases both → all changes preserved", async () => {
|
|
2412
|
+
// extra imports schema, pushes.
|
|
2413
|
+
// far creates data element, pushes.
|
|
2414
|
+
// local has local data change.
|
|
2415
|
+
// local pulls: must rebase on top of schema+data from two different sources.
|
|
2416
|
+
t = await TestIModel.initialize("K1ThreeBriefcaseSchemaAndData");
|
|
2417
|
+
const farTxn = startTestTxn(t.far, "K1 far");
|
|
2418
|
+
const localTxn = startTestTxn(t.local, "K1 local");
|
|
2419
|
+
// Extra briefcase imports schema and pushes
|
|
2420
|
+
const extra = await t.openExtraBriefcase("extra-user-k1");
|
|
2421
|
+
try {
|
|
2422
|
+
extra.channels.addAllowedChannel(ChannelControl.sharedChannelName);
|
|
2423
|
+
const extraTxn = startTestTxn(extra, "K1 extra");
|
|
2424
|
+
await importSchemaStrings(extraTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2425
|
+
await pushChanges(extraTxn, "extra import schema v01");
|
|
2426
|
+
}
|
|
2427
|
+
finally {
|
|
2428
|
+
// Keep extra open for the test duration; it's closed here after push
|
|
2429
|
+
extra.close();
|
|
2430
|
+
}
|
|
2431
|
+
// Far pulls schema, creates element with PropC2, pushes
|
|
2432
|
+
await pullChanges(farTxn);
|
|
2433
|
+
const farTxn2 = startTestTxn(t.far, "K1 far 2");
|
|
2434
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2435
|
+
const farElementId = t.insertElement(farTxn2, "TestDomain:C", { propA: "far_a", propC: "far_c", propC2: "far_c2" });
|
|
2436
|
+
farTxn2.saveChanges("far create element");
|
|
2437
|
+
await pushChanges(farTxn2, "far create element");
|
|
2438
|
+
// Local makes a data change (creates its own element, still at old schema)
|
|
2439
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2440
|
+
const localElementId = t.insertElement(localTxn, "TestDomain:C", { propA: "local_a", propC: "local_c" });
|
|
2441
|
+
localTxn.saveChanges("local create element");
|
|
2442
|
+
// Local pulls: must get schema from extra + element from far, then rebase local element
|
|
2443
|
+
await pullChanges(localTxn);
|
|
2444
|
+
t.local.clearCaches();
|
|
2445
|
+
// Far's element should be visible
|
|
2446
|
+
const farElement = t.getElementProps(t.local, farElementId);
|
|
2447
|
+
chai.expect(farElement.propA).to.equal("far_a", "Far element should be visible after rebase");
|
|
2448
|
+
chai.expect(farElement.propC2).to.equal("far_c2", "Far element PropC2 should be preserved");
|
|
2449
|
+
// Local's element should also be preserved
|
|
2450
|
+
const localElement = t.getElementProps(t.local, localElementId);
|
|
2451
|
+
chai.expect(localElement.propA).to.equal("local_a", "Local element should be preserved after rebase");
|
|
2452
|
+
chai.expect(localElement.propC).to.equal("local_c", "Local element propC should be preserved");
|
|
2453
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2454
|
+
chai.expect(schema.version).to.equal("01.00.01", "Schema should be at v01 from extra briefcase");
|
|
2455
|
+
});
|
|
2456
|
+
it("K2: two sequential schema changes from different briefcases, local rebases correctly through both", async () => {
|
|
2457
|
+
// extra-a pushes schema v01.
|
|
2458
|
+
// extra-b pulls v01, pushes schema v02.
|
|
2459
|
+
// local makes data change and must pull both schema changesets.
|
|
2460
|
+
t = await TestIModel.initialize("K2TwoSchemaSourcesSequential");
|
|
2461
|
+
const localTxn = startTestTxn(t.local, "K2 local");
|
|
2462
|
+
// Create shared element via far and push
|
|
2463
|
+
const farTxn = startTestTxn(t.far, "K2 far");
|
|
2464
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2465
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "shared_a", propC: "shared_c" });
|
|
2466
|
+
farTxn.saveChanges("far create shared element");
|
|
2467
|
+
await pushChanges(farTxn, "create shared element");
|
|
2468
|
+
// Local pulls element
|
|
2469
|
+
await pullChanges(localTxn);
|
|
2470
|
+
const localTxn2 = startTestTxn(t.local, "K2 local 2");
|
|
2471
|
+
// extra-a pushes schema v01
|
|
2472
|
+
const extraA = await t.openExtraBriefcase("extra-a-k2");
|
|
2473
|
+
try {
|
|
2474
|
+
const extraATxn = startTestTxn(extraA, "K2 extra-a");
|
|
2475
|
+
await importSchemaStrings(extraATxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2476
|
+
await pushChanges(extraATxn, "extra-a import schema v01");
|
|
2477
|
+
}
|
|
2478
|
+
finally {
|
|
2479
|
+
extraA.close();
|
|
2480
|
+
}
|
|
2481
|
+
// extra-b pulls v01, pushes schema v02 (adds PropD2)
|
|
2482
|
+
const extraB = await t.openExtraBriefcase("extra-b-k2");
|
|
2483
|
+
try {
|
|
2484
|
+
await extraB.pullChanges(); // picks up v01
|
|
2485
|
+
const extraBTxn = startTestTxn(extraB, "K2 extra-b");
|
|
2486
|
+
await importSchemaStrings(extraBTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
|
|
2487
|
+
await pushChanges(extraBTxn, "extra-b import schema v02");
|
|
2488
|
+
}
|
|
2489
|
+
finally {
|
|
2490
|
+
extraB.close();
|
|
2491
|
+
}
|
|
2492
|
+
// Local makes data change: update shared element's propA
|
|
2493
|
+
await t.local.locks.acquireLocks({ exclusive: elementId });
|
|
2494
|
+
t.updateElement(localTxn2, elementId, { propA: "local_updated_a" });
|
|
2495
|
+
localTxn2.saveChanges("local update propA");
|
|
2496
|
+
// Local pulls: must process v01 + v02 schema changesets and then rebase local data
|
|
2497
|
+
await pullChanges(localTxn2);
|
|
2498
|
+
t.local.clearCaches();
|
|
2499
|
+
const element = t.getElementProps(t.local, elementId);
|
|
2500
|
+
chai.expect(element.propA).to.equal("local_updated_a", "Local data change should be preserved after two schema changesets from different sources");
|
|
2501
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2502
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02 after two schema changesets");
|
|
2503
|
+
});
|
|
2504
|
+
});
|
|
2505
|
+
/**
|
|
2506
|
+
* Multiple pulls without push between them.
|
|
2507
|
+
* Tests that semantic rebase state is handled correctly when the local briefcase
|
|
2508
|
+
* pulls multiple times before pushing, accumulating rebase operations.
|
|
2509
|
+
*/
|
|
2510
|
+
describe("Semantic Rebase - Multiple Pulls Without Push", function () {
|
|
2511
|
+
this.timeout(60000);
|
|
2512
|
+
let t;
|
|
2513
|
+
before(async () => {
|
|
2514
|
+
await TestUtils.shutdownBackend();
|
|
2515
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
2516
|
+
});
|
|
2517
|
+
afterEach(() => {
|
|
2518
|
+
if (t) {
|
|
2519
|
+
t.shutdown();
|
|
2520
|
+
t = undefined;
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
2523
|
+
after(async () => {
|
|
2524
|
+
await TestUtils.shutdownBackend();
|
|
2525
|
+
await TestUtils.startBackend();
|
|
2526
|
+
});
|
|
2527
|
+
it("L2: local makes two separate schema imports across two pull cycles without pushing", async () => {
|
|
2528
|
+
// Pull 1: local has schema v01, far has data → rebase, local wins with v01
|
|
2529
|
+
// Pull 2 (no push): local imports v02, far pushes more data → rebase, local wins with v02
|
|
2530
|
+
t = await TestIModel.initialize("L2TwoSchemaImportsTwoPulls");
|
|
2531
|
+
let farTxn = startTestTxn(t.far, "L2 far");
|
|
2532
|
+
let localTxn = startTestTxn(t.local, "L2 local");
|
|
2533
|
+
// Create shared element
|
|
2534
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2535
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "initial_a", propC: "initial_c" });
|
|
2536
|
+
farTxn.saveChanges("create element");
|
|
2537
|
+
await pushChanges(farTxn, "create element");
|
|
2538
|
+
farTxn = startTestTxn(t.far, "L2 far 2");
|
|
2539
|
+
await pullChanges(localTxn);
|
|
2540
|
+
localTxn = startTestTxn(t.local, "L2 local 2");
|
|
2541
|
+
// Far pushes data change for pull cycle 1
|
|
2542
|
+
await t.far.locks.acquireLocks({ exclusive: elementId });
|
|
2543
|
+
t.updateElement(farTxn, elementId, { propC: "far_c_update_1" });
|
|
2544
|
+
farTxn.saveChanges("far data round 1");
|
|
2545
|
+
await pushChanges(farTxn, "far data round 1");
|
|
2546
|
+
farTxn = startTestTxn(t.far, "L2 far 3");
|
|
2547
|
+
// Local: schema v01 import
|
|
2548
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2549
|
+
// Pull cycle 1
|
|
2550
|
+
await pullChanges(localTxn);
|
|
2551
|
+
chai.expect(t.local.getSchemaProps("TestDomain").version).to.equal("01.00.01", "Schema should be v01 after first pull");
|
|
2552
|
+
// Far pushes data change for pull cycle 2
|
|
2553
|
+
localTxn = startTestTxn(t.local, "L2 local 3");
|
|
2554
|
+
await t.far.locks.acquireLocks({ exclusive: elementId });
|
|
2555
|
+
t.updateElement(farTxn, elementId, { propC: "far_c_update_2" });
|
|
2556
|
+
farTxn.saveChanges("far data round 2");
|
|
2557
|
+
await pushChanges(farTxn, "far data round 2");
|
|
2558
|
+
// Local: schema v02 import (still not pushed v01 yet)
|
|
2559
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
|
|
2560
|
+
// Pull cycle 2 without push
|
|
2561
|
+
await pullChanges(localTxn);
|
|
2562
|
+
t.local.clearCaches();
|
|
2563
|
+
// Schema should be at v02 (local upgrade wins)
|
|
2564
|
+
chai.expect(t.local.getSchemaProps("TestDomain").version).to.equal("01.00.02", "Schema should be v02 after second pull without push");
|
|
2565
|
+
// Both far data changes should be present
|
|
2566
|
+
const element = t.getElementProps(t.local, elementId);
|
|
2567
|
+
chai.expect(element.propC).to.equal("far_c_update_2", "Far data round 2 should be applied");
|
|
2568
|
+
});
|
|
2569
|
+
it("L3: pull when there are no incoming changes (already up to date) → no rebase folders created", async () => {
|
|
2570
|
+
t = await TestIModel.initialize("L3PullNoIncomingChanges");
|
|
2571
|
+
const localTxn = startTestTxn(t.local, "L3 local");
|
|
2572
|
+
// Local imports schema (creates rebase folder)
|
|
2573
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2574
|
+
const localTxnProps = t.local.txns.getLastSavedTxnProps();
|
|
2575
|
+
chai.expect(localTxnProps).to.not.be.undefined;
|
|
2576
|
+
chai.expect(t.checkIfFolderExists(t.local, localTxnProps.id, true)).to.be.true;
|
|
2577
|
+
// Pull when far has nothing new — no rebase should happen
|
|
2578
|
+
// (local is ahead of far's schema; there are no incoming changesets)
|
|
2579
|
+
const rebaseBasePathBeforePull = BriefcaseManager.getBasePathForSemanticRebaseLocalFiles(t.local);
|
|
2580
|
+
await pullChanges(localTxn);
|
|
2581
|
+
// Schema folder should still exist (local schema is pending push)
|
|
2582
|
+
const rebaseBasePathAfterPull = BriefcaseManager.getBasePathForSemanticRebaseLocalFiles(t.local);
|
|
2583
|
+
chai.expect(IModelJsFs.existsSync(rebaseBasePathAfterPull)).to.be.true;
|
|
2584
|
+
chai.expect(rebaseBasePathBeforePull).to.equal(rebaseBasePathAfterPull, "Rebase path should be unchanged");
|
|
2585
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2586
|
+
chai.expect(schema.version).to.equal("01.00.01", "Local schema should still be v01 after no-op pull");
|
|
2587
|
+
});
|
|
2588
|
+
});
|
|
2589
|
+
/**
|
|
2590
|
+
* New class addition to schema.
|
|
2591
|
+
* Tests that newly added entity classes and their instances survive semantic rebase.
|
|
2592
|
+
*/
|
|
2593
|
+
describe("Semantic Rebase - New Class Addition to Schema", function () {
|
|
2594
|
+
this.timeout(60000);
|
|
2595
|
+
let t;
|
|
2596
|
+
before(async () => {
|
|
2597
|
+
await TestUtils.shutdownBackend();
|
|
2598
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
2599
|
+
});
|
|
2600
|
+
afterEach(() => {
|
|
2601
|
+
if (t) {
|
|
2602
|
+
t.shutdown();
|
|
2603
|
+
t = undefined;
|
|
2604
|
+
}
|
|
2605
|
+
});
|
|
2606
|
+
after(async () => {
|
|
2607
|
+
await TestUtils.shutdownBackend();
|
|
2608
|
+
await TestUtils.startBackend();
|
|
2609
|
+
});
|
|
2610
|
+
it("N1: far adds new class E to schema; local creates instances of existing class A + pulls → both visible", async () => {
|
|
2611
|
+
t = await TestIModel.initialize("N1FarAddsNewClassLocalCreatesData");
|
|
2612
|
+
const farTxn = startTestTxn(t.far, "N1 far");
|
|
2613
|
+
const localTxn = startTestTxn(t.local, "N1 local");
|
|
2614
|
+
// Far imports schema with new class E, creates an element of class E, pushes
|
|
2615
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x01AddClassE]);
|
|
2616
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2617
|
+
const farElementE = t.insertElement(farTxn, "TestDomain:E", { propA: "far_e_a", propE: "far_e" });
|
|
2618
|
+
farTxn.saveChanges("far create element E");
|
|
2619
|
+
await pushChanges(farTxn, "far schema+element E");
|
|
2620
|
+
// Local creates an instance of class C (old class) and pulls
|
|
2621
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2622
|
+
const localElementC = t.insertElement(localTxn, "TestDomain:C", { propA: "local_c_a", propC: "local_c" });
|
|
2623
|
+
localTxn.saveChanges("local create element C");
|
|
2624
|
+
await pullChanges(localTxn);
|
|
2625
|
+
t.local.clearCaches();
|
|
2626
|
+
// Far's element E should be visible on local
|
|
2627
|
+
const elemE = t.getElementProps(t.local, farElementE);
|
|
2628
|
+
chai.expect(elemE.propA).to.equal("far_e_a", "Far element E should be visible after rebase");
|
|
2629
|
+
chai.expect(elemE.propE).to.equal("far_e", "PropE of element E should be correct");
|
|
2630
|
+
// Local's element C should be preserved
|
|
2631
|
+
const elemC = t.getElementProps(t.local, localElementC);
|
|
2632
|
+
chai.expect(elemC.propA).to.equal("local_c_a", "Local element C should be preserved after rebase");
|
|
2633
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2634
|
+
chai.expect(schema.version).to.equal("01.00.01", "Schema should be at v01 with class E added");
|
|
2635
|
+
});
|
|
2636
|
+
it("N2: both sides add same new class E → local version wins when higher, all instances preserved", async () => {
|
|
2637
|
+
t = await TestIModel.initialize("N2BothSidesAddNewClass");
|
|
2638
|
+
const farTxn = startTestTxn(t.far, "N2 far");
|
|
2639
|
+
const localTxn = startTestTxn(t.local, "N2 local");
|
|
2640
|
+
// Far imports v01 (adds class E with PropE), pushes
|
|
2641
|
+
await importSchemaStrings(farTxn, [TestIModel.extendedSchemas.v01x00x01AddClassE]);
|
|
2642
|
+
await pushChanges(farTxn, "far import v01 with class E");
|
|
2643
|
+
// Local imports v02 (adds class E with PropE + PropE2 — higher version), does NOT push yet
|
|
2644
|
+
await importSchemaStrings(localTxn, [TestIModel.extendedSchemas.v01x00x02AddClassEPropE2]);
|
|
2645
|
+
// Local pulls: local v02 > far v01 → local wins
|
|
2646
|
+
await pullChanges(localTxn);
|
|
2647
|
+
t.local.clearCaches();
|
|
2648
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2649
|
+
chai.expect(schema.version).to.equal("01.00.02", "Local v02 should win over far's v01");
|
|
2650
|
+
// Both PropE and PropE2 should be present on class E
|
|
2651
|
+
const classE = await t.local.schemaContext.getSchemaItem("TestDomain", "E", EntityClass);
|
|
2652
|
+
chai.expect(classE).to.not.be.undefined;
|
|
2653
|
+
chai.expect(await classE.getProperty("PropE")).to.exist;
|
|
2654
|
+
chai.expect(await classE.getProperty("PropE2")).to.exist;
|
|
2655
|
+
});
|
|
2656
|
+
it("N3: local adds new class E with instances; incoming has transforming schema change on existing class → both preserved", async () => {
|
|
2657
|
+
t = await TestIModel.initialize("N3LocalNewClassIncomingTransform");
|
|
2658
|
+
let farTxn = startTestTxn(t.far, "N3 far");
|
|
2659
|
+
let localTxn = startTestTxn(t.local, "N3 local");
|
|
2660
|
+
// Create a C element on far, push, then both pull
|
|
2661
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2662
|
+
const cElementId = t.insertElement(farTxn, "TestDomain:C", { propA: "c_initial_a", propC: "c_initial_c" });
|
|
2663
|
+
farTxn.saveChanges("create C element");
|
|
2664
|
+
await pushChanges(farTxn, "create C element");
|
|
2665
|
+
farTxn = startTestTxn(t.far, "N3 far 2");
|
|
2666
|
+
await pullChanges(localTxn);
|
|
2667
|
+
localTxn = startTestTxn(t.local, "N3 local 2");
|
|
2668
|
+
// Far imports transforming schema: moves PropC from C to A, AND adds class E (v01.00.02 + E)
|
|
2669
|
+
// We use a combined schema for this
|
|
2670
|
+
const v01x00x02WithBothChanges = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2671
|
+
<ECSchema schemaName="TestDomain" alias="td" version="01.00.02" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
|
|
2672
|
+
<ECSchemaReference name="BisCore" version="01.00.23" alias="bis"/>
|
|
2673
|
+
<ECEntityClass typeName="A">
|
|
2674
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
2675
|
+
<ECProperty propertyName="PropA" typeName="string"/>
|
|
2676
|
+
<ECProperty propertyName="PropC" typeName="string"/>
|
|
2677
|
+
</ECEntityClass>
|
|
2678
|
+
<ECEntityClass typeName="C">
|
|
2679
|
+
<BaseClass>A</BaseClass>
|
|
2680
|
+
</ECEntityClass>
|
|
2681
|
+
<ECEntityClass typeName="D">
|
|
2682
|
+
<BaseClass>A</BaseClass>
|
|
2683
|
+
<ECProperty propertyName="PropD" typeName="string"/>
|
|
2684
|
+
</ECEntityClass>
|
|
2685
|
+
<ECEntityClass typeName="E">
|
|
2686
|
+
<BaseClass>A</BaseClass>
|
|
2687
|
+
<ECProperty propertyName="PropE" typeName="string"/>
|
|
2688
|
+
</ECEntityClass>
|
|
2689
|
+
</ECSchema>`;
|
|
2690
|
+
await importSchemaStrings(farTxn, [v01x00x02WithBothChanges]);
|
|
2691
|
+
await pushChanges(farTxn, "far import transform+new class");
|
|
2692
|
+
// Local imports v01 (just adds class E) and creates two elements: one C, one E
|
|
2693
|
+
await importSchemaStrings(localTxn, [TestIModel.extendedSchemas.v01x00x01AddClassE]);
|
|
2694
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2695
|
+
const localElemE = t.insertElement(localTxn, "TestDomain:E", { propA: "local_e_a", propE: "local_e" });
|
|
2696
|
+
localTxn.saveChanges("local create E element");
|
|
2697
|
+
// Local also updates the C element that far's transform will affect
|
|
2698
|
+
await t.local.locks.acquireLocks({ exclusive: cElementId });
|
|
2699
|
+
t.updateElement(localTxn, cElementId, { propC: "local_updated_c" });
|
|
2700
|
+
localTxn.saveChanges("local update C element PropC");
|
|
2701
|
+
// Local pulls: far's transform schema (v02) > local's v01 schema → incoming wins for schema
|
|
2702
|
+
// But local's data changes (E element + C element update) should survive
|
|
2703
|
+
await pullChanges(localTxn);
|
|
2704
|
+
t.local.clearCaches();
|
|
2705
|
+
// Far's schema (v02) should win since it has higher version
|
|
2706
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2707
|
+
chai.expect(schema.version).to.equal("01.00.02", "Far's schema v02 should win over local's v01");
|
|
2708
|
+
// Local E element should exist
|
|
2709
|
+
const elemE = t.getElementProps(t.local, localElemE);
|
|
2710
|
+
chai.expect(elemE.propA).to.equal("local_e_a", "Local element E should be preserved");
|
|
2711
|
+
chai.expect(elemE.propE).to.equal("local_e", "PropE should be preserved");
|
|
2712
|
+
// C element should have local's PropC update (PropC is now on class A in v02)
|
|
2713
|
+
const elemC = t.getElementProps(t.local, cElementId);
|
|
2714
|
+
chai.expect(elemC.propC).to.equal("local_updated_c", "Local PropC update should be preserved after schema transform");
|
|
2715
|
+
});
|
|
2716
|
+
});
|
|
2717
|
+
/**
|
|
2718
|
+
* Guard conditions and error paths for semantic rebase.
|
|
2719
|
+
* Tests boundary conditions like importing schema while rebasing, concurrent pull attempts, etc.
|
|
2720
|
+
*/
|
|
2721
|
+
describe("Semantic Rebase - Guard Conditions and Error Paths", function () {
|
|
2722
|
+
this.timeout(60000);
|
|
2723
|
+
let t;
|
|
2724
|
+
before(async () => {
|
|
2725
|
+
await TestUtils.shutdownBackend();
|
|
2726
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
2727
|
+
});
|
|
2728
|
+
afterEach(() => {
|
|
2729
|
+
if (t) {
|
|
2730
|
+
t.shutdown();
|
|
2731
|
+
t = undefined;
|
|
2732
|
+
}
|
|
2733
|
+
});
|
|
2734
|
+
after(async () => {
|
|
2735
|
+
await TestUtils.shutdownBackend();
|
|
2736
|
+
await TestUtils.startBackend();
|
|
2737
|
+
});
|
|
2738
|
+
it("P1: importing schema during active rebase (via onRebaseTxnBegin hook) throws 'Cannot import schemas while rebasing'", async () => {
|
|
2739
|
+
t = await TestIModel.initialize("P1ImportSchemaWhileRebasing");
|
|
2740
|
+
const farTxn = startTestTxn(t.far, "P1 far");
|
|
2741
|
+
const localTxn = startTestTxn(t.local, "P1 local");
|
|
2742
|
+
// Far imports schema and pushes (triggers semantic rebase path on local)
|
|
2743
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2744
|
+
await pushChanges(farTxn, "far import schema v01");
|
|
2745
|
+
// Local imports schema to ensure it takes the semantic rebase code path
|
|
2746
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2747
|
+
// Hook into onRebaseTxnBegin to attempt a schema import during rebase
|
|
2748
|
+
let importErrorDuringRebase;
|
|
2749
|
+
t.local.txns.rebaser.onRebaseTxnBegin.addOnce(async () => {
|
|
2750
|
+
try {
|
|
2751
|
+
// This import should fail: importing schemas while rebasing is not allowed
|
|
2752
|
+
await t.local.importSchemaStrings([TestIModel.schemas.v01x00x02AddPropD2]);
|
|
2753
|
+
}
|
|
2754
|
+
catch (e) {
|
|
2755
|
+
importErrorDuringRebase = e;
|
|
2756
|
+
}
|
|
2757
|
+
});
|
|
2758
|
+
// Pull triggers the rebase which fires the hook
|
|
2759
|
+
try {
|
|
2760
|
+
await pullChanges(localTxn);
|
|
2761
|
+
}
|
|
2762
|
+
catch {
|
|
2763
|
+
// rebase might throw due to the bad import inside; that's acceptable
|
|
2764
|
+
}
|
|
2765
|
+
chai.expect(importErrorDuringRebase).to.not.be.undefined, "Schema import during rebase should throw";
|
|
2766
|
+
chai.expect(importErrorDuringRebase?.message ?? "").to.include("rebasing", "Error message should mention rebasing");
|
|
2767
|
+
});
|
|
2768
|
+
it("P3: after a failed rebase, briefcase is not stuck and remains usable", async () => {
|
|
2769
|
+
// If a rebase fails (e.g., incompatible schema), the briefcase should remain recoverable.
|
|
2770
|
+
// This test verifies the briefcase stays open and queryable after the failed pull.
|
|
2771
|
+
t = await TestIModel.initialize("P3RecoveryAfterFailedRebase");
|
|
2772
|
+
let farTxn = startTestTxn(t.far, "P3 far");
|
|
2773
|
+
let localTxn = startTestTxn(t.local, "P3 local");
|
|
2774
|
+
// Create shared element, push
|
|
2775
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2776
|
+
t.insertElement(farTxn, "TestDomain:C", { propA: "initial_a", propC: "initial_c" });
|
|
2777
|
+
farTxn.saveChanges("create element");
|
|
2778
|
+
await pushChanges(farTxn, "create element");
|
|
2779
|
+
farTxn = startTestTxn(t.far, "P3 far 2");
|
|
2780
|
+
await pullChanges(localTxn);
|
|
2781
|
+
localTxn = startTestTxn(t.local, "P3 local 2");
|
|
2782
|
+
// Far imports schema with PropC2 as int (will cause conflict with local's string PropC2)
|
|
2783
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2784
|
+
await pushChanges(farTxn, "far import v01 PropC2 string");
|
|
2785
|
+
// Local imports incompatible schema (PropC2 as int, higher version → will conflict with far's string)
|
|
2786
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x02AddPropC2Incompatible]);
|
|
2787
|
+
// First pull attempt: should fail with "ECSchema Upgrade failed"
|
|
2788
|
+
await chai.expect(pullChanges(localTxn)).to.be.rejectedWith("ECSchema Upgrade failed");
|
|
2789
|
+
// Briefcase should still be openable and queryable after failure
|
|
2790
|
+
// (BriefcaseManager should have rolled back the failed rebase)
|
|
2791
|
+
const schemaAfterFailure = t.local.getSchemaProps("TestDomain");
|
|
2792
|
+
// Schema version is unclear after abort, but the DB should still be functional
|
|
2793
|
+
chai.expect(schemaAfterFailure, "Schema query should succeed after failed rebase").to.not.be.undefined;
|
|
2794
|
+
// The briefcase is still open and the DB is usable
|
|
2795
|
+
chai.expect(t.local.isOpen, "Briefcase should still be open after failed rebase").to.be.true;
|
|
2796
|
+
});
|
|
2797
|
+
it("P4: a second local schema import succeeds even when semantic rebase state folders already exist", async () => {
|
|
2798
|
+
// Tests that semantic rebase state folders do not block further local schema imports.
|
|
2799
|
+
// After a schema import + pull that creates rebase folders, a second schema import on local
|
|
2800
|
+
// must still go through cleanly (the semantic rebase folders should NOT block a new import).
|
|
2801
|
+
t = await TestIModel.initialize("P4SecondImportAfterRebaseState");
|
|
2802
|
+
let farTxn = startTestTxn(t.far, "P4 far");
|
|
2803
|
+
let localTxn = startTestTxn(t.local, "P4 local");
|
|
2804
|
+
// Far pushes data change
|
|
2805
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2806
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "initial_a", propC: "initial_c" });
|
|
2807
|
+
farTxn.saveChanges("create element");
|
|
2808
|
+
await pushChanges(farTxn, "create element");
|
|
2809
|
+
farTxn = startTestTxn(t.far, "P4 far 2");
|
|
2810
|
+
await pullChanges(localTxn);
|
|
2811
|
+
localTxn = startTestTxn(t.local, "P4 local 2");
|
|
2812
|
+
// Far pushes another data update
|
|
2813
|
+
await t.far.locks.acquireLocks({ exclusive: elementId });
|
|
2814
|
+
t.updateElement(farTxn, elementId, { propA: "far_updated_a" });
|
|
2815
|
+
farTxn.saveChanges("far update");
|
|
2816
|
+
await pushChanges(farTxn, "far update element");
|
|
2817
|
+
// Local imports schema v01
|
|
2818
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2819
|
+
const firstSchemaTxnProps = t.local.txns.getLastSavedTxnProps();
|
|
2820
|
+
chai.expect(firstSchemaTxnProps).to.not.be.undefined;
|
|
2821
|
+
chai.expect(t.checkIfFolderExists(t.local, firstSchemaTxnProps.id, true)).to.be.true;
|
|
2822
|
+
// Local pulls (rebase: local schema onto far data)
|
|
2823
|
+
await pullChanges(localTxn);
|
|
2824
|
+
localTxn = startTestTxn(t.local, "P4 local after pull");
|
|
2825
|
+
// Schema folder should persist (local schema v01 is still pending push)
|
|
2826
|
+
chai.expect(t.checkIfFolderExists(t.local, firstSchemaTxnProps.id, true)).to.be.true;
|
|
2827
|
+
// Now local wants to import schema v02 (further upgrade) — this should succeed
|
|
2828
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
|
|
2829
|
+
const secondSchemaTxnProps = t.local.txns.getLastSavedTxnProps();
|
|
2830
|
+
chai.expect(secondSchemaTxnProps).to.not.be.undefined;
|
|
2831
|
+
chai.expect(t.checkIfFolderExists(t.local, secondSchemaTxnProps.id, true)).to.be.true;
|
|
2832
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2833
|
+
chai.expect(schema.version).to.equal("01.00.02", "Local should be at v02 after second schema import");
|
|
2834
|
+
});
|
|
2835
|
+
});
|
|
2836
|
+
/**
|
|
2837
|
+
* Complex insert-update-delete sequences.
|
|
2838
|
+
* Tests scenarios where local txns contain a mix of insert, update, and delete operations
|
|
2839
|
+
* that need to be correctly captured and reinstated during semantic rebase.
|
|
2840
|
+
*/
|
|
2841
|
+
describe("Semantic Rebase - Complex Insert-Update-Delete Sequences", function () {
|
|
2842
|
+
this.timeout(60000);
|
|
2843
|
+
let t;
|
|
2844
|
+
before(async () => {
|
|
2845
|
+
await TestUtils.shutdownBackend();
|
|
2846
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
2847
|
+
});
|
|
2848
|
+
afterEach(() => {
|
|
2849
|
+
if (t) {
|
|
2850
|
+
t.shutdown();
|
|
2851
|
+
t = undefined;
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
after(async () => {
|
|
2855
|
+
await TestUtils.shutdownBackend();
|
|
2856
|
+
await TestUtils.startBackend();
|
|
2857
|
+
});
|
|
2858
|
+
it("O1: local insert → update → delete of same element in three txns; incoming transforming schema → element stays deleted", async () => {
|
|
2859
|
+
// Sequence: local inserts elem, updates it, then deletes it — all before pulling.
|
|
2860
|
+
// Incoming: transforming schema change.
|
|
2861
|
+
// Expected: element remains deleted (all three local txns reinstated correctly).
|
|
2862
|
+
t = await TestIModel.initialize("O1InsertUpdateDeleteSequence");
|
|
2863
|
+
const farTxn = startTestTxn(t.far, "O1 far");
|
|
2864
|
+
const localTxn = startTestTxn(t.local, "O1 local");
|
|
2865
|
+
// Far imports transforming schema and pushes
|
|
2866
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02MovePropCToA]);
|
|
2867
|
+
await pushChanges(farTxn, "far import transforming schema");
|
|
2868
|
+
// Local: txn1 - insert element
|
|
2869
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2870
|
+
const tempElementId = t.insertElement(localTxn, "TestDomain:C", { propA: "temp_a", propC: "temp_c" });
|
|
2871
|
+
localTxn.saveChanges("local txn1 insert temp element");
|
|
2872
|
+
// Local: txn2 - update the element
|
|
2873
|
+
t.updateElement(localTxn, tempElementId, { propA: "temp_updated_a" });
|
|
2874
|
+
localTxn.saveChanges("local txn2 update temp element");
|
|
2875
|
+
// Local: txn3 - delete the element
|
|
2876
|
+
localTxn.deleteElement(tempElementId);
|
|
2877
|
+
localTxn.saveChanges("local txn3 delete temp element");
|
|
2878
|
+
// Local pulls: three local txns (insert+update+delete) rebased on top of schema transform
|
|
2879
|
+
await pullChanges(localTxn);
|
|
2880
|
+
t.local.clearCaches();
|
|
2881
|
+
// Element should be gone (last operation was delete)
|
|
2882
|
+
chai.expect(() => t.getElementProps(t.local, tempElementId)).to.throw(`element not found`, "Element should be deleted after rebase");
|
|
2883
|
+
// Schema should be at v02
|
|
2884
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2885
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02 after rebase");
|
|
2886
|
+
});
|
|
2887
|
+
it("O2: local insert + delete of one element, plus insert + update of another → incoming schema → both correct", async () => {
|
|
2888
|
+
t = await TestIModel.initialize("O2InsertDeleteInsertUpdate");
|
|
2889
|
+
const farTxn = startTestTxn(t.far, "O2 far");
|
|
2890
|
+
const localTxn = startTestTxn(t.local, "O2 local");
|
|
2891
|
+
// Far imports trivial schema, pushes
|
|
2892
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2893
|
+
await pushChanges(farTxn, "far trivial schema import");
|
|
2894
|
+
// Local: txn1 - insert temporary element (will be deleted) + persistent element
|
|
2895
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2896
|
+
const tempId = t.insertElement(localTxn, "TestDomain:C", { propA: "temp_a", propC: "temp_c" });
|
|
2897
|
+
const persistId = t.insertElement(localTxn, "TestDomain:D", { propA: "persist_a", propD: "persist_d" });
|
|
2898
|
+
localTxn.saveChanges("local txn1 insert two elements");
|
|
2899
|
+
// Local: txn2 - update persistent element
|
|
2900
|
+
t.updateElement(localTxn, persistId, { propA: "persist_updated_a" });
|
|
2901
|
+
localTxn.saveChanges("local txn2 update persist element");
|
|
2902
|
+
// Local: txn3 - delete temporary element
|
|
2903
|
+
localTxn.deleteElement(tempId);
|
|
2904
|
+
localTxn.saveChanges("local txn3 delete temp element");
|
|
2905
|
+
// Local pulls: all three txns rebased on top of far's trivial schema
|
|
2906
|
+
await pullChanges(localTxn);
|
|
2907
|
+
t.local.clearCaches();
|
|
2908
|
+
// Temp element should be gone
|
|
2909
|
+
chai.expect(() => t.getElementProps(t.local, tempId)).to.throw(`element not found`, "Element should be deleted after rebase");
|
|
2910
|
+
// Persist element should have the update
|
|
2911
|
+
const persistElem = t.getElementProps(t.local, persistId);
|
|
2912
|
+
chai.expect(persistElem.propA).to.equal("persist_updated_a", "Persist element propA should be updated");
|
|
2913
|
+
chai.expect(persistElem.propD).to.equal("persist_d", "Persist element propD should be preserved");
|
|
2914
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2915
|
+
chai.expect(schema.version).to.equal("01.00.01", "Schema should be at v01");
|
|
2916
|
+
});
|
|
2917
|
+
it("O3: schema txn sandwiched between data txns; incoming data change → all local operations preserved in correct order", async () => {
|
|
2918
|
+
// Pattern: local txn1 (data) → txn2 (schema) → txn3 (data using new schema property)
|
|
2919
|
+
// Incoming: far has a data change on a different element.
|
|
2920
|
+
// Expected: txn1 data, schema, and txn3 data all reinstated correctly.
|
|
2921
|
+
t = await TestIModel.initialize("O3SchemaSandwichedBetweenData");
|
|
2922
|
+
let farTxn = startTestTxn(t.far, "O3 far");
|
|
2923
|
+
let localTxn = startTestTxn(t.local, "O3 local");
|
|
2924
|
+
// Create shared elements on far and push; both pull
|
|
2925
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2926
|
+
const sharedId = t.insertElement(farTxn, "TestDomain:C", { propA: "shared_a", propC: "shared_c" });
|
|
2927
|
+
farTxn.saveChanges("create shared element");
|
|
2928
|
+
await pushChanges(farTxn, "create shared element");
|
|
2929
|
+
farTxn = startTestTxn(t.far, "O3 far 2");
|
|
2930
|
+
await pullChanges(localTxn);
|
|
2931
|
+
localTxn = startTestTxn(t.local, "O3 local 2");
|
|
2932
|
+
// Far updates shared element and pushes (data-only incoming)
|
|
2933
|
+
await t.far.locks.acquireLocks({ exclusive: sharedId });
|
|
2934
|
+
t.updateElement(farTxn, sharedId, { propC: "far_updated_c" });
|
|
2935
|
+
farTxn.saveChanges("far update shared element");
|
|
2936
|
+
await pushChanges(farTxn, "far data update");
|
|
2937
|
+
// Local txn1: insert new element
|
|
2938
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2939
|
+
const newId1 = t.insertElement(localTxn, "TestDomain:C", { propA: "new_a", propC: "new_c" });
|
|
2940
|
+
localTxn.saveChanges("local txn3 insert element with PropC2");
|
|
2941
|
+
// Local txn2: import schema (adds PropC2)
|
|
2942
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
2943
|
+
// Local txn3: insert new element using the new PropC2 property
|
|
2944
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2945
|
+
const newId2 = t.insertElement(localTxn, "TestDomain:C", { propA: "new_a", propC: "new_c", propC2: "new_c2_value" });
|
|
2946
|
+
localTxn.saveChanges("local txn3 insert element with PropC2");
|
|
2947
|
+
// Local pulls: txn1+schema+txn3 rebased on top of far's data update
|
|
2948
|
+
await pullChanges(localTxn);
|
|
2949
|
+
t.local.clearCaches();
|
|
2950
|
+
// Shared element should have far's propC update
|
|
2951
|
+
const sharedElem = t.getElementProps(t.local, sharedId);
|
|
2952
|
+
chai.expect(sharedElem.propA).to.equal("shared_a", "Initial propA update should be preserved");
|
|
2953
|
+
chai.expect(sharedElem.propC).to.equal("far_updated_c", "Far propC update should be applied");
|
|
2954
|
+
// Two New element with PropC2 should exist
|
|
2955
|
+
const newElem1 = t.getElementProps(t.local, newId1);
|
|
2956
|
+
chai.expect(newElem1.propA).to.equal("new_a", "New element propA should be preserved");
|
|
2957
|
+
chai.expect(newElem1.propC).to.equal("new_c", "New element propC should be preserved through schema rebase");
|
|
2958
|
+
const newElem2 = t.getElementProps(t.local, newId2);
|
|
2959
|
+
chai.expect(newElem2.propA).to.equal("new_a", "New element propA should be preserved");
|
|
2960
|
+
chai.expect(newElem2.propC2).to.equal("new_c2_value", "New element PropC2 should be preserved through schema rebase");
|
|
2961
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2962
|
+
chai.expect(schema.version).to.equal("01.00.01", "Schema should be at v01");
|
|
2963
|
+
});
|
|
2964
|
+
it("O4: five local data transactions each touching a different element; incoming transforming schema → all five preserved", async () => {
|
|
2965
|
+
// Stress-tests the capturePatchInstances + reinstatement path with 5 separate data txns.
|
|
2966
|
+
t = await TestIModel.initialize("O4FiveDataTxnsTransformingSchema");
|
|
2967
|
+
let farTxn = startTestTxn(t.far, "O4 far");
|
|
2968
|
+
let localTxn = startTestTxn(t.local, "O4 local");
|
|
2969
|
+
// Create five elements on far and push; both pull
|
|
2970
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
2971
|
+
const ids = [];
|
|
2972
|
+
for (let i = 0; i < 5; i++) {
|
|
2973
|
+
ids.push(t.insertElement(farTxn, "TestDomain:C", { propA: `initial_a_${i}`, propC: `initial_c_${i}` }));
|
|
2974
|
+
}
|
|
2975
|
+
farTxn.saveChanges("create five elements");
|
|
2976
|
+
await pushChanges(farTxn, "create five elements");
|
|
2977
|
+
farTxn = startTestTxn(t.far, "O4 far 2");
|
|
2978
|
+
await pullChanges(localTxn);
|
|
2979
|
+
localTxn = startTestTxn(t.local, "O4 local 2");
|
|
2980
|
+
// Far imports transforming schema (moves PropC to A) and pushes
|
|
2981
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02MovePropCToA]);
|
|
2982
|
+
await pushChanges(farTxn, "far transforming schema");
|
|
2983
|
+
// Local makes five separate data txns, one per element
|
|
2984
|
+
for (let i = 0; i < 5; i++) {
|
|
2985
|
+
await t.local.locks.acquireLocks({ exclusive: ids[i] });
|
|
2986
|
+
t.updateElement(localTxn, ids[i], { propA: `local_updated_a_${i}`, propC: `local_updated_c_${i}` });
|
|
2987
|
+
localTxn.saveChanges(`local txn${i + 1} update element ${i}`);
|
|
2988
|
+
}
|
|
2989
|
+
// Local pulls: all five data txns rebased on top of transforming schema
|
|
2990
|
+
await pullChanges(localTxn);
|
|
2991
|
+
t.local.clearCaches();
|
|
2992
|
+
// All five elements should have their local updates preserved
|
|
2993
|
+
for (let i = 0; i < 5; i++) {
|
|
2994
|
+
const elem = t.getElementProps(t.local, ids[i]);
|
|
2995
|
+
chai.expect(elem.propA).to.equal(`local_updated_a_${i}`, `Element ${i} propA should be preserved`);
|
|
2996
|
+
chai.expect(elem.propC).to.equal(`local_updated_c_${i}`, `Element ${i} propC should be preserved after transform`);
|
|
2997
|
+
}
|
|
2998
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
2999
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02 after transforming rebase");
|
|
3000
|
+
});
|
|
3001
|
+
});
|
|
3002
|
+
/**
|
|
3003
|
+
* Cleanup and folder lifecycle edge cases.
|
|
3004
|
+
* Tests that rebase folder state is correctly managed in unusual lifecycle scenarios.
|
|
3005
|
+
*/
|
|
3006
|
+
describe("Semantic Rebase - Cleanup and Folder Lifecycle", function () {
|
|
3007
|
+
this.timeout(60000);
|
|
3008
|
+
let t;
|
|
3009
|
+
before(async () => {
|
|
3010
|
+
await TestUtils.shutdownBackend();
|
|
3011
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
3012
|
+
});
|
|
3013
|
+
afterEach(() => {
|
|
3014
|
+
if (t) {
|
|
3015
|
+
t.shutdown();
|
|
3016
|
+
t = undefined;
|
|
3017
|
+
}
|
|
3018
|
+
});
|
|
3019
|
+
after(async () => {
|
|
3020
|
+
await TestUtils.shutdownBackend();
|
|
3021
|
+
await TestUtils.startBackend();
|
|
3022
|
+
});
|
|
3023
|
+
it("M1: schema folder cleaned up on push; subsequent pull creates no leftover folders", async () => {
|
|
3024
|
+
t = await TestIModel.initialize("M1SchemaFolderCleanupOnPush");
|
|
3025
|
+
let farTxn = startTestTxn(t.far, "M1 far");
|
|
3026
|
+
let localTxn = startTestTxn(t.local, "M1 local");
|
|
3027
|
+
// Far pushes data
|
|
3028
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
3029
|
+
const elementId = t.insertElement(farTxn, "TestDomain:C", { propA: "initial_a", propC: "initial_c" });
|
|
3030
|
+
farTxn.saveChanges("far create element");
|
|
3031
|
+
await pushChanges(farTxn, "far create element");
|
|
3032
|
+
farTxn = startTestTxn(t.far, "M1 far 2");
|
|
3033
|
+
await pullChanges(localTxn);
|
|
3034
|
+
localTxn = startTestTxn(t.local, "M1 local 2");
|
|
3035
|
+
// Far pushes another update
|
|
3036
|
+
await t.far.locks.acquireLocks({ exclusive: elementId });
|
|
3037
|
+
t.updateElement(farTxn, elementId, { propA: "far_updated_a" });
|
|
3038
|
+
farTxn.saveChanges("far update element");
|
|
3039
|
+
await pushChanges(farTxn, "far update element");
|
|
3040
|
+
// Local imports schema (creates schema folder)
|
|
3041
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
3042
|
+
const schemaTxnProps = t.local.txns.getLastSavedTxnProps();
|
|
3043
|
+
chai.expect(schemaTxnProps).to.not.be.undefined;
|
|
3044
|
+
chai.expect(t.checkIfFolderExists(t.local, schemaTxnProps.id, true)).to.be.true;
|
|
3045
|
+
// Local pulls (rebase: local schema onto far's data update)
|
|
3046
|
+
await pullChanges(localTxn);
|
|
3047
|
+
// Schema folder should still exist (local schema wins and is pending push)
|
|
3048
|
+
chai.expect(t.checkIfFolderExists(t.local, schemaTxnProps.id, true)).to.be.true;
|
|
3049
|
+
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.true;
|
|
3050
|
+
// Local pushes: all rebase folders should be cleaned up
|
|
3051
|
+
localTxn = startTestTxn(t.local, "M1 local after pull");
|
|
3052
|
+
await pushChanges(localTxn, "local push schema");
|
|
3053
|
+
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false;
|
|
3054
|
+
// Schema folder for the pushed txn is now gone
|
|
3055
|
+
chai.expect(t.checkIfFolderExists(t.local, schemaTxnProps.id, true)).to.be.false;
|
|
3056
|
+
// Pull again (nothing new): no new folders should appear
|
|
3057
|
+
localTxn = startTestTxn(t.local, "M1 local after push");
|
|
3058
|
+
await pullChanges(localTxn);
|
|
3059
|
+
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false;
|
|
3060
|
+
});
|
|
3061
|
+
it("M3: multiple successive push/pull cycles preserve rebase folder invariants throughout", async () => {
|
|
3062
|
+
// Cycle 1: local schema → pull (no-op schema) → push → verify clean
|
|
3063
|
+
// Cycle 2: far schema → local data → pull (local data rebased onto far schema) → push → verify clean
|
|
3064
|
+
t = await TestIModel.initialize("M3SuccessivePushPullCycles");
|
|
3065
|
+
let farTxn = startTestTxn(t.far, "M3 far");
|
|
3066
|
+
let localTxn = startTestTxn(t.local, "M3 local");
|
|
3067
|
+
// --- Cycle 1: Local schema, nothing on far ---
|
|
3068
|
+
// Local imports v01, pulls (nothing incoming), pushes
|
|
3069
|
+
await importSchemaStrings(localTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
3070
|
+
const cycle1TxnProps = t.local.txns.getLastSavedTxnProps();
|
|
3071
|
+
chai.expect(t.checkIfFolderExists(t.local, cycle1TxnProps.id, true)).to.be.true;
|
|
3072
|
+
await pullChanges(localTxn); // nothing incoming → no rebase
|
|
3073
|
+
chai.expect(t.checkIfFolderExists(t.local, cycle1TxnProps.id, true)).to.be.true; // still pending push
|
|
3074
|
+
localTxn = startTestTxn(t.local, "M3 local after cycle1 pull");
|
|
3075
|
+
await pushChanges(localTxn, "local push schema v01");
|
|
3076
|
+
chai.expect(t.checkIfFolderExists(t.local, cycle1TxnProps.id, true)).to.be.false; // cleaned on push
|
|
3077
|
+
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false;
|
|
3078
|
+
// --- Cycle 2: Far pushes schema v02, local creates data element ---
|
|
3079
|
+
await pullChanges(farTxn); // far pulls v01
|
|
3080
|
+
farTxn = startTestTxn(t.far, "M3 far 2");
|
|
3081
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
|
|
3082
|
+
await pushChanges(farTxn, "far push schema v02");
|
|
3083
|
+
localTxn = startTestTxn(t.local, "M3 local cycle2");
|
|
3084
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
3085
|
+
const cycle2ElemId = t.insertElement(localTxn, "TestDomain:C", { propA: "cycle2_a", propC: "cycle2_c" });
|
|
3086
|
+
localTxn.saveChanges("local create cycle2 element");
|
|
3087
|
+
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // data-only local, schema incoming
|
|
3088
|
+
// Actually because far has a schema change incoming, semantic rebase WILL be used here
|
|
3089
|
+
// The data folder will be created on the fly and removed after rebase
|
|
3090
|
+
await pullChanges(localTxn);
|
|
3091
|
+
chai.expect(t.checkifRebaseFolderExists(t.local)).to.be.false; // data folder removed after rebase
|
|
3092
|
+
t.local.clearCaches();
|
|
3093
|
+
const cycle2Elem = t.getElementProps(t.local, cycle2ElemId);
|
|
3094
|
+
chai.expect(cycle2Elem.propA).to.equal("cycle2_a", "Cycle 2 element should be preserved");
|
|
3095
|
+
const schema = t.local.getSchemaProps("TestDomain");
|
|
3096
|
+
chai.expect(schema.version).to.equal("01.00.02", "Schema should be at v02 after cycle 2");
|
|
3097
|
+
});
|
|
3098
|
+
});
|
|
3099
|
+
describe("Semantic Rebase - Multi-Pull Verification", function () {
|
|
3100
|
+
this.timeout(90000);
|
|
3101
|
+
let t;
|
|
3102
|
+
before(async () => {
|
|
3103
|
+
await TestUtils.shutdownBackend();
|
|
3104
|
+
await TestUtils.startBackend({ useSemanticRebase: true });
|
|
3105
|
+
});
|
|
3106
|
+
afterEach(() => {
|
|
3107
|
+
if (t) {
|
|
3108
|
+
t.shutdown();
|
|
3109
|
+
t = undefined;
|
|
3110
|
+
}
|
|
3111
|
+
});
|
|
3112
|
+
after(async () => {
|
|
3113
|
+
await TestUtils.shutdownBackend();
|
|
3114
|
+
await TestUtils.startBackend();
|
|
3115
|
+
});
|
|
3116
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
3117
|
+
// R2: Three consecutive pulls, each triggering rebase through escalating schema
|
|
3118
|
+
// changes (trivial → trivial → transforming).
|
|
3119
|
+
// Full ECSql snapshot taken before and after every pull.
|
|
3120
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
3121
|
+
it("R2: three consecutive pulls through escalating schema changes; full ECSql snapshot after each", async () => {
|
|
3122
|
+
t = await TestIModel.initialize("R2ThreePullsEscalatingSchema");
|
|
3123
|
+
let farTxn = startTestTxn(t.far, "R2 far");
|
|
3124
|
+
let localTxn = startTestTxn(t.local, "R2 local");
|
|
3125
|
+
// ── Phase 0: shared setup ─────────────────────────────────────────────────
|
|
3126
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
3127
|
+
const c1Id = t.insertElement(farTxn, "TestDomain:C", { propA: "c1_a_init", propC: "c1_c_init" });
|
|
3128
|
+
const c2Id = t.insertElement(farTxn, "TestDomain:C", { propA: "c2_a_init", propC: "c2_c_init" });
|
|
3129
|
+
const d1Id = t.insertElement(farTxn, "TestDomain:D", { propA: "d1_a_init", propD: "d1_d_init" });
|
|
3130
|
+
farTxn.saveChanges("create three shared elements");
|
|
3131
|
+
await pushChanges(farTxn, "create shared elements");
|
|
3132
|
+
farTxn = startTestTxn(t.far, "R2 far 2");
|
|
3133
|
+
await pullChanges(localTxn);
|
|
3134
|
+
localTxn = startTestTxn(t.local, "R2 local 2");
|
|
3135
|
+
// ── Round 1: far: schema v01 (PropC2) + update c1; local: insert r1Elem ──
|
|
3136
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
3137
|
+
await t.far.locks.acquireLocks({ exclusive: c1Id });
|
|
3138
|
+
t.updateElement(farTxn, c1Id, { propA: "c1_a_r1" });
|
|
3139
|
+
farTxn.saveChanges("far r1 update c1");
|
|
3140
|
+
await pushChanges(farTxn, "R2 far round1");
|
|
3141
|
+
farTxn = startTestTxn(t.far, "R2 far 3");
|
|
3142
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
3143
|
+
const r1ElemId = t.insertElement(localTxn, "TestDomain:C", { propA: "r1_a", propC: "r1_c" });
|
|
3144
|
+
localTxn.saveChanges("local r1 insert r1Elem");
|
|
3145
|
+
// Pull #1
|
|
3146
|
+
await pullChanges(localTxn);
|
|
3147
|
+
t.local.clearCaches({ instanceCachesOnly: true });
|
|
3148
|
+
// c1: far's propA update applied; propC unchanged
|
|
3149
|
+
const c1After1 = t.getElementProps(t.local, c1Id);
|
|
3150
|
+
chai.expect(c1After1.propA).to.equal("c1_a_r1", "c1 propA should be updated by far after pull #1");
|
|
3151
|
+
chai.expect(c1After1.propC).to.equal("c1_c_init", "c1 propC should be unchanged after pull #1");
|
|
3152
|
+
// c2: unchanged
|
|
3153
|
+
const c2After1 = t.getElementProps(t.local, c2Id);
|
|
3154
|
+
chai.expect(c2After1.propA).to.equal("c2_a_init", "c2 propA should be unchanged after pull #1");
|
|
3155
|
+
chai.expect(c2After1.propC).to.equal("c2_c_init", "c2 propC should be unchanged after pull #1");
|
|
3156
|
+
// r1Elem: insert preserved with same ECInstanceId
|
|
3157
|
+
const r1After1 = t.getElementProps(t.local, r1ElemId);
|
|
3158
|
+
chai.expect(r1After1.propA).to.equal("r1_a", "r1Elem propA should be preserved after pull #1 rebase");
|
|
3159
|
+
chai.expect(r1After1.propC).to.equal("r1_c", "r1Elem propC should be preserved after pull #1 rebase");
|
|
3160
|
+
chai.expect(r1After1.id).to.equal(r1ElemId, "r1ElemId ECInstanceId must be stable after pull #1");
|
|
3161
|
+
// d1: unchanged
|
|
3162
|
+
const d1After1 = t.getElementProps(t.local, d1Id);
|
|
3163
|
+
chai.expect(d1After1.propA).to.equal("d1_a_init", "d1 propA should be unchanged after pull #1");
|
|
3164
|
+
chai.expect(d1After1.propD).to.equal("d1_d_init", "d1 propD should be unchanged after pull #1");
|
|
3165
|
+
chai.expect(t.local.getSchemaProps("TestDomain").version).to.equal("01.00.01", "Schema v01 after pull #1");
|
|
3166
|
+
// ── Round 2: far: schema v02 (PropD2) + update d1; local: update r1Elem ─
|
|
3167
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
|
|
3168
|
+
await t.far.locks.acquireLocks({ exclusive: d1Id });
|
|
3169
|
+
t.updateElement(farTxn, d1Id, { propA: "d1_a_r2" });
|
|
3170
|
+
farTxn.saveChanges("far r2 update d1");
|
|
3171
|
+
await pushChanges(farTxn, "R2 far round2");
|
|
3172
|
+
farTxn = startTestTxn(t.far, "R2 far 4");
|
|
3173
|
+
localTxn = startTestTxn(t.local, "R2 local r2 update");
|
|
3174
|
+
await t.local.locks.acquireLocks({ exclusive: r1ElemId });
|
|
3175
|
+
t.updateElement(localTxn, r1ElemId, { propA: "r1_a_updated" });
|
|
3176
|
+
localTxn.saveChanges("local r2 update r1Elem");
|
|
3177
|
+
// Pull #2
|
|
3178
|
+
await pullChanges(localTxn);
|
|
3179
|
+
t.local.clearCaches({ instanceCachesOnly: true });
|
|
3180
|
+
// c1: propA from pull #1; unchanged this round
|
|
3181
|
+
const c1After2 = t.getElementProps(t.local, c1Id);
|
|
3182
|
+
chai.expect(c1After2.propA).to.equal("c1_a_r1", "c1 propA should remain from pull #1 after pull #2");
|
|
3183
|
+
chai.expect(c1After2.propC).to.equal("c1_c_init", "c1 propC should be unchanged after pull #2");
|
|
3184
|
+
// c2: unchanged
|
|
3185
|
+
const c2After2 = t.getElementProps(t.local, c2Id);
|
|
3186
|
+
chai.expect(c2After2.propA).to.equal("c2_a_init", "c2 propA should be unchanged after pull #2");
|
|
3187
|
+
// r1Elem: propA update must survive rebase
|
|
3188
|
+
const r1After2 = t.getElementProps(t.local, r1ElemId);
|
|
3189
|
+
chai.expect(r1After2.propA).to.equal("r1_a_updated", "r1Elem propA update should survive pull #2 rebase");
|
|
3190
|
+
chai.expect(r1After2.propC).to.equal("r1_c", "r1Elem propC should be unchanged after pull #2 rebase");
|
|
3191
|
+
chai.expect(r1After2.id).to.equal(r1ElemId, "r1ElemId ECInstanceId must be stable after pull #2");
|
|
3192
|
+
// d1: far's propA update applied
|
|
3193
|
+
const d1After2 = t.getElementProps(t.local, d1Id);
|
|
3194
|
+
chai.expect(d1After2.propA).to.equal("d1_a_r2", "d1 propA should be updated by far after pull #2");
|
|
3195
|
+
chai.expect(d1After2.propD).to.equal("d1_d_init", "d1 propD should be unchanged after pull #2");
|
|
3196
|
+
chai.expect(d1After2.id).to.equal(d1Id, "d1Id ECInstanceId must be stable after pull #2");
|
|
3197
|
+
chai.expect(t.local.getSchemaProps("TestDomain").version).to.equal("01.00.02", "Schema v02 after pull #2");
|
|
3198
|
+
// ── Round 3: far: schema v03 (transforming: moves PropC to A) + update c2;
|
|
3199
|
+
// local: insert r3Elem ──────────────────────────────────────────
|
|
3200
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x03MovePropCAndD]);
|
|
3201
|
+
await t.far.locks.acquireLocks({ exclusive: c2Id });
|
|
3202
|
+
t.updateElement(farTxn, c2Id, { propA: "c2_a_r3" });
|
|
3203
|
+
farTxn.saveChanges("far r3 update c2");
|
|
3204
|
+
await pushChanges(farTxn, "R2 far round3");
|
|
3205
|
+
localTxn = startTestTxn(t.local, "R2 local r3 insert");
|
|
3206
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
3207
|
+
const r3ElemId = t.insertElement(localTxn, "TestDomain:D", { propA: "r3_a" });
|
|
3208
|
+
localTxn.saveChanges("local r3 insert r3Elem");
|
|
3209
|
+
// Pull #3 — transforming schema rebase
|
|
3210
|
+
await pullChanges(localTxn);
|
|
3211
|
+
t.local.clearCaches({ instanceCachesOnly: true });
|
|
3212
|
+
// c1: propA from pull #1; PropC moved to A so query it as propC still accessible via A
|
|
3213
|
+
const c1After3 = t.getElementProps(t.local, c1Id);
|
|
3214
|
+
chai.expect(c1After3.propA).to.equal("c1_a_r1", "c1 propA should remain from pull #1 after pull #3");
|
|
3215
|
+
// c2: far's propA update from round 3
|
|
3216
|
+
const c2After3 = t.getElementProps(t.local, c2Id);
|
|
3217
|
+
chai.expect(c2After3.propA).to.equal("c2_a_r3", "c2 propA should be updated by far after pull #3");
|
|
3218
|
+
// r1Elem: propA update from round 2 must survive transforming rebase
|
|
3219
|
+
const r1After3 = t.getElementProps(t.local, r1ElemId);
|
|
3220
|
+
chai.expect(r1After3.propA).to.equal("r1_a_updated", "r1Elem propA update should survive transforming pull #3 rebase");
|
|
3221
|
+
chai.expect(r1After3.id).to.equal(r1ElemId, "r1ElemId ECInstanceId must be stable after pull #3");
|
|
3222
|
+
// d1: propA from round 2
|
|
3223
|
+
const d1After3 = t.getElementProps(t.local, d1Id);
|
|
3224
|
+
chai.expect(d1After3.propA).to.equal("d1_a_r2", "d1 propA should remain from pull #2 after pull #3");
|
|
3225
|
+
chai.expect(d1After3.id).to.equal(d1Id, "d1Id ECInstanceId must be stable after pull #3");
|
|
3226
|
+
// r3Elem: local insert preserved
|
|
3227
|
+
const r3After3 = t.getElementProps(t.local, r3ElemId);
|
|
3228
|
+
chai.expect(r3After3.propA).to.equal("r3_a", "r3Elem propA should be preserved after pull #3 rebase");
|
|
3229
|
+
chai.expect(r3After3.id).to.equal(r3ElemId, "r3ElemId ECInstanceId must be stable after pull #3");
|
|
3230
|
+
chai.expect(t.local.getSchemaProps("TestDomain").version).to.equal("01.00.03", "Schema v03 (MovePropCAndD) after pull #3");
|
|
3231
|
+
});
|
|
3232
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
3233
|
+
// R3: Multi-pull with insert, update, and delete in different rounds.
|
|
3234
|
+
// Two pulls each rebase. Element lifecycle verified at every stage with
|
|
3235
|
+
// ECSqlReader (ECInstanceId, className, domain props).
|
|
3236
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
3237
|
+
it("R3: two pulls with insert/update/delete across rounds; element lifecycle verified", async () => {
|
|
3238
|
+
t = await TestIModel.initialize("R3MultiPullInsertUpdateDelete");
|
|
3239
|
+
let farTxn = startTestTxn(t.far, "R3 far");
|
|
3240
|
+
let localTxn = startTestTxn(t.local, "R3 local");
|
|
3241
|
+
// ── Phase 0: shared elements ──────────────────────────────────────────────
|
|
3242
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
3243
|
+
const sharedC = t.insertElement(farTxn, "TestDomain:C", { propA: "sc_a_init", propC: "sc_c_init" });
|
|
3244
|
+
const sharedD = t.insertElement(farTxn, "TestDomain:D", { propA: "sd_a_init", propD: "sd_d_init" });
|
|
3245
|
+
farTxn.saveChanges("create shared elements");
|
|
3246
|
+
await pushChanges(farTxn, "shared elements");
|
|
3247
|
+
farTxn = startTestTxn(t.far, "R3 far 2");
|
|
3248
|
+
await pullChanges(localTxn);
|
|
3249
|
+
localTxn = startTestTxn(t.local, "R3 local 2");
|
|
3250
|
+
// ── Round 1: far: schema v01 + update sharedC.propA ──────────────────────
|
|
3251
|
+
// local: insert ephemeral C + insert persistent D + delete ephemeral
|
|
3252
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
3253
|
+
await t.far.locks.acquireLocks({ exclusive: sharedC });
|
|
3254
|
+
t.updateElement(farTxn, sharedC, { propA: "sc_a_r1" });
|
|
3255
|
+
farTxn.saveChanges("far r1 update sharedC");
|
|
3256
|
+
await pushChanges(farTxn, "R3 far round1");
|
|
3257
|
+
farTxn = startTestTxn(t.far, "R3 far 3");
|
|
3258
|
+
// Local txn #1: insert ephemeral element (will be deleted before pull #1)
|
|
3259
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
3260
|
+
const ephemeralId = t.insertElement(localTxn, "TestDomain:C", { propA: "eph_a", propC: "eph_c" });
|
|
3261
|
+
localTxn.saveChanges("local r1 insert ephemeral");
|
|
3262
|
+
// Local txn #2: insert persistent D element
|
|
3263
|
+
const persistDId = t.insertElement(localTxn, "TestDomain:D", { propA: "pd_a_init", propD: "pd_d_init" });
|
|
3264
|
+
localTxn.saveChanges("local r1 insert persistD");
|
|
3265
|
+
// Local txn #3: delete the ephemeral element
|
|
3266
|
+
localTxn.deleteElement(ephemeralId);
|
|
3267
|
+
localTxn.saveChanges("local r1 delete ephemeral");
|
|
3268
|
+
// Pull #1: rebase insert-persistD + delete-ephemeral txns onto incoming schema v01 + sharedC update
|
|
3269
|
+
await pullChanges(localTxn);
|
|
3270
|
+
chai.expect(() => t.getElementProps(t.local, ephemeralId)).to.throw(`element not found`, "Ephemeral element should be deleted after pull #1");
|
|
3271
|
+
const elementProps = t.getElementProps(t.local, persistDId);
|
|
3272
|
+
chai.expect(elementProps.propA).to.equal("pd_a_init", "Persistent D propA should be preserved after pull #1");
|
|
3273
|
+
chai.expect(elementProps.propD).to.equal("pd_d_init", "Persistent D propD should be preserved after pull #1");
|
|
3274
|
+
const sharedCAfter1 = t.getElementProps(t.local, sharedC);
|
|
3275
|
+
chai.expect(sharedCAfter1.propA).to.equal("sc_a_r1", "sharedC propA should be updated by far after pull #1");
|
|
3276
|
+
chai.expect(sharedCAfter1.propC).to.equal("sc_c_init", "sharedC propC should be unchanged after pull #1");
|
|
3277
|
+
chai.expect(sharedCAfter1.propC2).to.be.undefined;
|
|
3278
|
+
const sharedDAfter1 = t.getElementProps(t.local, sharedD);
|
|
3279
|
+
chai.expect(sharedDAfter1.propA).to.equal("sd_a_init", "sharedD propA should be unchanged after pull #1");
|
|
3280
|
+
chai.expect(sharedDAfter1.propD).to.equal("sd_d_init", "sharedD propD should be unchanged after pull #1");
|
|
3281
|
+
});
|
|
3282
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
3283
|
+
// R4: Three consecutive pulls where local never pushes.
|
|
3284
|
+
// Each round: local makes multiple data txns, far pushes a schema change.
|
|
3285
|
+
// Complete ECSql verification of all elements after all three pulls.
|
|
3286
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
3287
|
+
it("R4: three pulls without push; local accumulates txns; all elements verified after each", async () => {
|
|
3288
|
+
t = await TestIModel.initialize("R4ThreePullsNoPush");
|
|
3289
|
+
let farTxn = startTestTxn(t.far, "R4 far");
|
|
3290
|
+
let localTxn = startTestTxn(t.local, "R4 local");
|
|
3291
|
+
// ── Phase 0: create 2 C elements and 1 D element on far, both pull ────────
|
|
3292
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
3293
|
+
const baseC1 = t.insertElement(farTxn, "TestDomain:C", { propA: "bc1_a", propC: "bc1_c" });
|
|
3294
|
+
const baseC2 = t.insertElement(farTxn, "TestDomain:C", { propA: "bc2_a", propC: "bc2_c" });
|
|
3295
|
+
const baseD1 = t.insertElement(farTxn, "TestDomain:D", { propA: "bd1_a", propD: "bd1_d" });
|
|
3296
|
+
farTxn.saveChanges("create base elements");
|
|
3297
|
+
await pushChanges(farTxn, "base elements");
|
|
3298
|
+
farTxn = startTestTxn(t.far, "R4 far 2");
|
|
3299
|
+
await pullChanges(localTxn);
|
|
3300
|
+
localTxn = startTestTxn(t.local, "R4 local 2");
|
|
3301
|
+
// ── Round 1: far: trivial schema v01 (PropC2 additive)
|
|
3302
|
+
// local: TWO data txns – update baseC1 + insert localC1 ─────────
|
|
3303
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
3304
|
+
await pushChanges(farTxn, "R4 far schema v01");
|
|
3305
|
+
farTxn = startTestTxn(t.far, "R4 far 3");
|
|
3306
|
+
// Local data txn A: update baseC1
|
|
3307
|
+
await t.local.locks.acquireLocks({ exclusive: baseC1 });
|
|
3308
|
+
t.updateElement(localTxn, baseC1, { propA: "bc1_a_loc_r1" });
|
|
3309
|
+
localTxn.saveChanges("local r1 update baseC1");
|
|
3310
|
+
// Local data txn B: insert localC1
|
|
3311
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
3312
|
+
const localC1 = t.insertElement(localTxn, "TestDomain:C", { propA: "lc1_a_r1", propC: "lc1_c_r1" });
|
|
3313
|
+
localTxn.saveChanges("local r1 insert localC1");
|
|
3314
|
+
// Pull #1
|
|
3315
|
+
await pullChanges(localTxn);
|
|
3316
|
+
t.local.clearCaches({ instanceCachesOnly: true });
|
|
3317
|
+
// baseC1: local propA update preserved
|
|
3318
|
+
const bc1After1 = t.getElementProps(t.local, baseC1);
|
|
3319
|
+
chai.expect(bc1After1.propA).to.equal("bc1_a_loc_r1", "baseC1 propA update should survive pull #1 rebase");
|
|
3320
|
+
chai.expect(bc1After1.propC).to.equal("bc1_c", "baseC1 propC should be unchanged after pull #1");
|
|
3321
|
+
// baseC2: unchanged
|
|
3322
|
+
const bc2After1 = t.getElementProps(t.local, baseC2);
|
|
3323
|
+
chai.expect(bc2After1.propA).to.equal("bc2_a", "baseC2 propA should be unchanged after pull #1");
|
|
3324
|
+
chai.expect(bc2After1.propC).to.equal("bc2_c", "baseC2 propC should be unchanged after pull #1");
|
|
3325
|
+
// localC1: insert preserved with same ECInstanceId
|
|
3326
|
+
const lc1After1 = t.getElementProps(t.local, localC1);
|
|
3327
|
+
chai.expect(lc1After1.propA).to.equal("lc1_a_r1", "localC1 propA should be preserved after pull #1 rebase");
|
|
3328
|
+
chai.expect(lc1After1.propC).to.equal("lc1_c_r1", "localC1 propC should be preserved after pull #1 rebase");
|
|
3329
|
+
chai.expect(lc1After1.id).to.equal(localC1, "localC1 ECInstanceId must be stable after pull #1");
|
|
3330
|
+
// baseD1: unchanged
|
|
3331
|
+
const bd1After1 = t.getElementProps(t.local, baseD1);
|
|
3332
|
+
chai.expect(bd1After1.propA).to.equal("bd1_a", "baseD1 propA should be unchanged after pull #1");
|
|
3333
|
+
chai.expect(bd1After1.propD).to.equal("bd1_d", "baseD1 propD should be unchanged after pull #1");
|
|
3334
|
+
chai.expect(t.local.getSchemaProps("TestDomain").version).to.equal("01.00.01", "Schema v01 after pull #1");
|
|
3335
|
+
// ── Round 2: far: trivial schema v02 (PropD2 additive) + update baseD1
|
|
3336
|
+
// local: update localC1.propA + insert localD1 ──────────────────
|
|
3337
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02AddPropD2]);
|
|
3338
|
+
await t.far.locks.acquireLocks({ exclusive: baseD1 });
|
|
3339
|
+
t.updateElement(farTxn, baseD1, { propA: "bd1_a_r2" });
|
|
3340
|
+
farTxn.saveChanges("far r2 update baseD1");
|
|
3341
|
+
await pushChanges(farTxn, "R4 far schema v02 + update baseD1");
|
|
3342
|
+
farTxn = startTestTxn(t.far, "R4 far 4");
|
|
3343
|
+
localTxn = startTestTxn(t.local, "R4 local r2");
|
|
3344
|
+
await t.local.locks.acquireLocks({ exclusive: localC1 });
|
|
3345
|
+
t.updateElement(localTxn, localC1, { propA: "lc1_a_r2_upd" });
|
|
3346
|
+
localTxn.saveChanges("local r2 update localC1");
|
|
3347
|
+
await t.local.locks.acquireLocks({ shared: t.drawingModelId });
|
|
3348
|
+
const localD1 = t.insertElement(localTxn, "TestDomain:D", { propA: "ld1_a_r2", propD: "ld1_d_r2" });
|
|
3349
|
+
localTxn.saveChanges("local r2 insert localD1");
|
|
3350
|
+
// Pull #2
|
|
3351
|
+
await pullChanges(localTxn);
|
|
3352
|
+
t.local.clearCaches({ instanceCachesOnly: true });
|
|
3353
|
+
// baseC1: propA from round 1 unchanged
|
|
3354
|
+
const bc1After2 = t.getElementProps(t.local, baseC1);
|
|
3355
|
+
chai.expect(bc1After2.propA).to.equal("bc1_a_loc_r1", "baseC1 propA should remain from round 1 after pull #2");
|
|
3356
|
+
chai.expect(bc1After2.propC).to.equal("bc1_c", "baseC1 propC should be unchanged after pull #2");
|
|
3357
|
+
// localC1: propA update must survive
|
|
3358
|
+
const lc1After2 = t.getElementProps(t.local, localC1);
|
|
3359
|
+
chai.expect(lc1After2.propA).to.equal("lc1_a_r2_upd", "localC1 propA update should survive pull #2 rebase");
|
|
3360
|
+
chai.expect(lc1After2.propC).to.equal("lc1_c_r1", "localC1 propC should be unchanged after pull #2 rebase");
|
|
3361
|
+
chai.expect(lc1After2.id).to.equal(localC1, "localC1 ECInstanceId must be stable after pull #2");
|
|
3362
|
+
// baseD1: far's propA update applied
|
|
3363
|
+
const bd1After2 = t.getElementProps(t.local, baseD1);
|
|
3364
|
+
chai.expect(bd1After2.propA).to.equal("bd1_a_r2", "baseD1 propA should be updated by far after pull #2");
|
|
3365
|
+
// localD1: insert preserved
|
|
3366
|
+
const ld1After2 = t.getElementProps(t.local, localD1);
|
|
3367
|
+
chai.expect(ld1After2.propA).to.equal("ld1_a_r2", "localD1 propA should be preserved after pull #2 rebase");
|
|
3368
|
+
chai.expect(ld1After2.propD).to.equal("ld1_d_r2", "localD1 propD should be preserved after pull #2 rebase");
|
|
3369
|
+
chai.expect(ld1After2.id).to.equal(localD1, "localD1 ECInstanceId must be stable after pull #2");
|
|
3370
|
+
chai.expect(t.local.getSchemaProps("TestDomain").version).to.equal("01.00.02", "Schema v02 after pull #2");
|
|
3371
|
+
// ── Round 3: far: transforming schema (moves PropC to A) + update baseC2.propA
|
|
3372
|
+
// local: delete localC1 only.
|
|
3373
|
+
//
|
|
3374
|
+
// NOTE: local does NOT attempt to lock baseC2 here. After far exclusively locked
|
|
3375
|
+
// baseC2 and pushed (releasing the lock at a newer changeset index),
|
|
3376
|
+
// `LocalHub.doesBriefcaseRequirePullBeforeLock` would throw PullIsRequired for any
|
|
3377
|
+
// lock request on baseC2 from local (still behind on changesets). The test therefore
|
|
3378
|
+
// only exercises local deleting one of its own elements (localC1), which local itself
|
|
3379
|
+
// inserted and locked — those locks have lastExclusiveReleaseChangesetIndex = undefined,
|
|
3380
|
+
// so no pull is required before re-acquiring them.
|
|
3381
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x03MovePropCAndD]);
|
|
3382
|
+
await t.far.locks.acquireLocks({ exclusive: baseC2 });
|
|
3383
|
+
t.updateElement(farTxn, baseC2, { propA: "bc2_a_r3" });
|
|
3384
|
+
farTxn.saveChanges("far r3 update baseC2");
|
|
3385
|
+
await pushChanges(farTxn, "R4 far round3");
|
|
3386
|
+
localTxn = startTestTxn(t.local, "R4 local r3 delete");
|
|
3387
|
+
await t.local.locks.acquireLocks({ exclusive: localC1 });
|
|
3388
|
+
localTxn.deleteElement(localC1);
|
|
3389
|
+
localTxn.saveChanges("local r3 delete localC1");
|
|
3390
|
+
// Pull #3 — transforming schema rebase
|
|
3391
|
+
await pullChanges(localTxn);
|
|
3392
|
+
// baseC1: propA from round 1 unchanged
|
|
3393
|
+
const bc1After3 = t.getElementProps(t.local, baseC1);
|
|
3394
|
+
chai.expect(bc1After3.propA).to.equal("bc1_a_loc_r1", "baseC1 propA should remain from round 1 after pull #3");
|
|
3395
|
+
chai.expect(bc1After3.propC).to.equal("bc1_c", "baseC1 propC should be unchanged after pull #3");
|
|
3396
|
+
// baseC2: propA updated by far in round 3; PropC moved to A but still accessible via propC query
|
|
3397
|
+
const bc2After3 = t.getElementProps(t.local, baseC2);
|
|
3398
|
+
chai.expect(bc2After3.propA).to.equal("bc2_a_r3", "baseC2 propA should be changed after pull #3");
|
|
3399
|
+
chai.expect(bc2After3.propC).to.equal("bc2_c", "baseC2 propC should be unchanged after pull #3");
|
|
3400
|
+
// localC1: must be deleted
|
|
3401
|
+
chai.expect(() => t.getElementProps(t.local, localC1)).to.throw(`element not found`, "localC1 should be deleted after pull #3");
|
|
3402
|
+
// baseD1: far's propA update applied
|
|
3403
|
+
const bd1After3 = t.getElementProps(t.local, baseD1);
|
|
3404
|
+
chai.expect(bd1After3.propA).to.equal("bd1_a_r2", "baseD1 propA should be updated by far after pull #3");
|
|
3405
|
+
// localD1: insert preserved
|
|
3406
|
+
const ld1After3 = t.getElementProps(t.local, localD1);
|
|
3407
|
+
chai.expect(ld1After3.propA).to.equal("ld1_a_r2", "localD1 propA should be preserved after pull #3 rebase");
|
|
3408
|
+
chai.expect(ld1After3.propD).to.equal("ld1_d_r2", "localD1 propD should be preserved after pull #3 rebase");
|
|
3409
|
+
chai.expect(ld1After2.id).to.equal(localD1, "localD1 ECInstanceId must be stable after pull #3");
|
|
3410
|
+
chai.expect(t.local.getSchemaProps("TestDomain").version).to.equal("01.00.03", "Schema v02 after pull #3");
|
|
3411
|
+
});
|
|
3412
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
3413
|
+
// R5: Two consecutive pulls where local never pushes.
|
|
3414
|
+
// Each round: local makes multiple data txns, far pushes a schema change.
|
|
3415
|
+
// Complete ECSql verification of all elements after all three pulls.
|
|
3416
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
3417
|
+
it("R5: two consecutive pulls each trigger rebase with local changes; ECInstanceId/className/props verified after each", async () => {
|
|
3418
|
+
t = await TestIModel.initialize("R5TwoPullsEachRebase");
|
|
3419
|
+
let farTxn = startTestTxn(t.far, "R5 far");
|
|
3420
|
+
let localTxn = startTestTxn(t.local, "R5 local");
|
|
3421
|
+
// ── Phase 0: create shared elements on far, both pull to sync ────────────
|
|
3422
|
+
await t.far.locks.acquireLocks({ shared: t.drawingModelId });
|
|
3423
|
+
const cElemId = t.insertElement(farTxn, "TestDomain:C", { propA: "c_a_init", propC: "c_c_init" });
|
|
3424
|
+
farTxn.saveChanges("create shared elements");
|
|
3425
|
+
await pushChanges(farTxn, "create shared elements");
|
|
3426
|
+
farTxn = startTestTxn(t.far, "R5 far 2");
|
|
3427
|
+
await pullChanges(localTxn);
|
|
3428
|
+
localTxn = startTestTxn(t.local, "R5 local 2");
|
|
3429
|
+
await t.local.locks.acquireLocks({ exclusive: cElemId });
|
|
3430
|
+
t.updateElement(localTxn, cElemId, { propC: "c_c_local" });
|
|
3431
|
+
localTxn.saveChanges("local update to cElemId.propC");
|
|
3432
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x01AddPropC2]);
|
|
3433
|
+
await pushChanges(farTxn, "far schema v01");
|
|
3434
|
+
farTxn = startTestTxn(t.far, "R1 far 3");
|
|
3435
|
+
await pullChanges(localTxn);
|
|
3436
|
+
t.local.clearCaches({ instanceCachesOnly: true });
|
|
3437
|
+
const elementAfterSecondPull = t.getElementProps(t.local, cElemId);
|
|
3438
|
+
chai.expect(elementAfterSecondPull).to.not.be.undefined;
|
|
3439
|
+
chai.expect(elementAfterSecondPull.propC).to.equal("c_c_local", "Local update to cElemId.propC should survive first pull's rebase");
|
|
3440
|
+
chai.expect(elementAfterSecondPull.propA).to.equal("c_a_init", "Far's update to cElemId.propA should be applied after first pull");
|
|
3441
|
+
chai.expect(elementAfterSecondPull.propC2).to.be.undefined;
|
|
3442
|
+
await importSchemaStrings(farTxn, [TestIModel.schemas.v01x00x02MovePropCToA]);
|
|
3443
|
+
await pushChanges(farTxn, "far schema v02");
|
|
3444
|
+
farTxn = startTestTxn(t.far, "R1 far 4");
|
|
3445
|
+
await pullChanges(localTxn);
|
|
3446
|
+
t.local.clearCaches({ instanceCachesOnly: true });
|
|
3447
|
+
const elementAfterThirdPull = t.getElementProps(t.local, cElemId);
|
|
3448
|
+
chai.expect(elementAfterThirdPull).to.not.be.undefined;
|
|
3449
|
+
chai.expect(elementAfterThirdPull.propC).to.equal("c_c_local", "Local update to cElemId.propC should survive second pull's rebase");
|
|
3450
|
+
chai.expect(elementAfterThirdPull.propA).to.equal("c_a_init", "Far's update to cElemId.propA should be applied after second pull");
|
|
3451
|
+
chai.expect(elementAfterThirdPull.propC2).to.be.undefined;
|
|
3452
|
+
chai.expect(t.local.getSchemaProps("TestDomain").version).to.equal("01.00.02", "Schema must be v02 after pull #3");
|
|
3453
|
+
});
|
|
3454
|
+
});
|
|
1281
3455
|
//# sourceMappingURL=SemanticRebase.test.js.map
|