@itwin/core-backend 5.12.0-dev.6 → 5.12.0-dev.8

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.
@@ -3279,6 +3279,61 @@ describe("ChangesetReader — openFile + openGroup", () => {
3279
3279
  }
3280
3280
  }
3281
3281
  });
3282
+ it("openGroup: insert in one changeset then delete in another yields no instances for the element (net no-op)", async () => {
3283
+ const adminToken = "super manager token";
3284
+ const targetDir = path.join(KnownTestLocations.outputDir, rwIModelId, "changesets");
3285
+ // Record how many changesets already exist so we can identify the new ones by index
3286
+ let changesets = await HubMock.downloadChangesets({ iModelId: rwIModelId, targetDir });
3287
+ const baseCount = changesets.length;
3288
+ // Wait so that LastMod on bis_Model gets a distinct timestamp before the insert txn
3289
+ await new Promise((resolve) => setTimeout(resolve, 300));
3290
+ // --- Push baseCount+1: insert a brand-new element ---
3291
+ await rwIModel.locks.acquireLocks({ shared: drawingModelId });
3292
+ const noOpElementId = txn.insertElement({
3293
+ classFullName: "TestDomain:SimpleElement",
3294
+ model: drawingModelId,
3295
+ category: drawingCategoryId,
3296
+ code: Code.createEmpty(),
3297
+ BinProp: new Uint8Array([0xde, 0xad, 0xbe, 0xef]),
3298
+ });
3299
+ txn.saveChanges("insert no-op element");
3300
+ await rwIModel.pushChanges({ description: "insert no-op element", accessToken: adminToken });
3301
+ changesets = await HubMock.downloadChangesets({ iModelId: rwIModelId, targetDir });
3302
+ expect(changesets.length).to.equal(baseCount + 1);
3303
+ const insertCs = changesets[baseCount];
3304
+ // Wait so that LastMod on bis_Model gets a distinct timestamp before the delete txn
3305
+ await new Promise((resolve) => setTimeout(resolve, 300));
3306
+ // --- Push baseCount+2: delete the same element ---
3307
+ await rwIModel.locks.acquireLocks({ exclusive: noOpElementId });
3308
+ txn.deleteElement(noOpElementId);
3309
+ txn.saveChanges("delete no-op element");
3310
+ await rwIModel.pushChanges({ description: "delete no-op element", accessToken: adminToken });
3311
+ changesets = await HubMock.downloadChangesets({ iModelId: rwIModelId, targetDir });
3312
+ expect(changesets.length).to.equal(baseCount + 2);
3313
+ const deleteCs = changesets[baseCount + 1];
3314
+ // === openGroup: insert + delete streamed together — element never existed net, so no instances ===
3315
+ {
3316
+ const env_18 = { stack: [], error: void 0, hasError: false };
3317
+ try {
3318
+ const reader = __addDisposableResource(env_18, ChangesetReader.openGroup({ db: rwIModel, changesetFiles: [insertCs.pathname, deleteCs.pathname] }), false);
3319
+ const pcu = __addDisposableResource(env_18, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
3320
+ while (reader.step())
3321
+ pcu.appendFrom(reader);
3322
+ const instances = Array.from(pcu.instances);
3323
+ // Insert followed immediately by delete cancels out: the unifier must produce zero instances
3324
+ // for this element because the net effect on the database is nothing changed.
3325
+ const elemInstances = instances.filter((i) => i.ECInstanceId === noOpElementId);
3326
+ expect(elemInstances).to.have.length(0, `expected no-op for element ${noOpElementId} but got ${elemInstances.length} instance(s)`);
3327
+ }
3328
+ catch (e_18) {
3329
+ env_18.error = e_18;
3330
+ env_18.hasError = true;
3331
+ }
3332
+ finally {
3333
+ __disposeResources(env_18);
3334
+ }
3335
+ }
3336
+ });
3282
3337
  });
3283
3338
  describe("ChangesetReader — openLocalChanges + openInmemoryChanges", () => {
3284
3339
  let rwIModel;
@@ -3341,10 +3396,10 @@ describe("ChangesetReader — openLocalChanges + openInmemoryChanges", () => {
3341
3396
  });
3342
3397
  txn.saveChanges("insert element");
3343
3398
  {
3344
- const env_18 = { stack: [], error: void 0, hasError: false };
3399
+ const env_19 = { stack: [], error: void 0, hasError: false };
3345
3400
  try {
3346
- const reader = __addDisposableResource(env_18, ChangesetReader.openLocalChanges({ db: rwIModel, rowOptions: { abbreviateBlobs: false } }), false);
3347
- const pcu = __addDisposableResource(env_18, new PartialChangeUnifier(ChangeUnifierCache.createSqliteBackedCache()), false);
3401
+ const reader = __addDisposableResource(env_19, ChangesetReader.openLocalChanges({ db: rwIModel, rowOptions: { abbreviateBlobs: false } }), false);
3402
+ const pcu = __addDisposableResource(env_19, new PartialChangeUnifier(ChangeUnifierCache.createSqliteBackedCache()), false);
3348
3403
  while (reader.step())
3349
3404
  pcu.appendFrom(reader);
3350
3405
  const instances = Array.from(pcu.instances);
@@ -3380,12 +3435,12 @@ describe("ChangesetReader — openLocalChanges + openInmemoryChanges", () => {
3380
3435
  'JsonProperties', 'LastMod', 'Model.Id', 'Origin', 'Parent', 'Pt3dProp', 'Rotation',
3381
3436
  'TypeDefinition', 'UserLabel'].sort());
3382
3437
  }
3383
- catch (e_18) {
3384
- env_18.error = e_18;
3385
- env_18.hasError = true;
3438
+ catch (e_19) {
3439
+ env_19.error = e_19;
3440
+ env_19.hasError = true;
3386
3441
  }
3387
3442
  finally {
3388
- __disposeResources(env_18);
3443
+ __disposeResources(env_19);
3389
3444
  }
3390
3445
  }
3391
3446
  // Wait so that LastMod on bis_Model gets a distinct timestamp before the update txn
@@ -3400,10 +3455,10 @@ describe("ChangesetReader — openLocalChanges + openInmemoryChanges", () => {
3400
3455
  ],
3401
3456
  });
3402
3457
  {
3403
- const env_19 = { stack: [], error: void 0, hasError: false };
3458
+ const env_20 = { stack: [], error: void 0, hasError: false };
3404
3459
  try {
3405
- const reader = __addDisposableResource(env_19, ChangesetReader.openInMemoryChanges({ db: rwIModel, rowOptions: { abbreviateBlobs: false } }), false);
3406
- const pcu = __addDisposableResource(env_19, new PartialChangeUnifier(ChangeUnifierCache.createSqliteBackedCache()), false);
3460
+ const reader = __addDisposableResource(env_20, ChangesetReader.openInMemoryChanges({ db: rwIModel, rowOptions: { abbreviateBlobs: false } }), false);
3461
+ const pcu = __addDisposableResource(env_20, new PartialChangeUnifier(ChangeUnifierCache.createSqliteBackedCache()), false);
3407
3462
  while (reader.step())
3408
3463
  pcu.appendFrom(reader);
3409
3464
  const instances = Array.from(pcu.instances);
@@ -3441,20 +3496,20 @@ describe("ChangesetReader — openLocalChanges + openInmemoryChanges", () => {
3441
3496
  assert.equal(elemOld.$meta.isIndirectChange, false);
3442
3497
  assert.deepEqual(elemOld.$meta.changeFetchedPropNames.sort(), ['BBoxHigh', 'BBoxLow', 'BinProp', 'ECInstanceId', 'GuidArrProp', 'LastMod', 'Origin', 'Pt3dProp', 'Rotation'].sort());
3443
3498
  }
3444
- catch (e_19) {
3445
- env_19.error = e_19;
3446
- env_19.hasError = true;
3499
+ catch (e_20) {
3500
+ env_20.error = e_20;
3501
+ env_20.hasError = true;
3447
3502
  }
3448
3503
  finally {
3449
- __disposeResources(env_19);
3504
+ __disposeResources(env_20);
3450
3505
  }
3451
3506
  }
3452
3507
  // === openLocalChanges: insert + update as a single stream so becomes insert ===
3453
3508
  {
3454
- const env_20 = { stack: [], error: void 0, hasError: false };
3509
+ const env_21 = { stack: [], error: void 0, hasError: false };
3455
3510
  try {
3456
- const reader = __addDisposableResource(env_20, ChangesetReader.openLocalChanges({ db: rwIModel, includeInMemoryChanges: true, rowOptions: { abbreviateBlobs: false } }), false);
3457
- const pcu = __addDisposableResource(env_20, new PartialChangeUnifier(ChangeUnifierCache.createSqliteBackedCache()), false);
3511
+ const reader = __addDisposableResource(env_21, ChangesetReader.openLocalChanges({ db: rwIModel, includeInMemoryChanges: true, rowOptions: { abbreviateBlobs: false } }), false);
3512
+ const pcu = __addDisposableResource(env_21, new PartialChangeUnifier(ChangeUnifierCache.createSqliteBackedCache()), false);
3458
3513
  while (reader.step())
3459
3514
  pcu.appendFrom(reader);
3460
3515
  const instances = Array.from(pcu.instances);
@@ -3494,12 +3549,12 @@ describe("ChangesetReader — openLocalChanges + openInmemoryChanges", () => {
3494
3549
  'Rotation', 'TypeDefinition', 'UserLabel'].sort());
3495
3550
  assert.equal(elemNew.$meta.isIndirectChange, false);
3496
3551
  }
3497
- catch (e_20) {
3498
- env_20.error = e_20;
3499
- env_20.hasError = true;
3552
+ catch (e_21) {
3553
+ env_21.error = e_21;
3554
+ env_21.hasError = true;
3500
3555
  }
3501
3556
  finally {
3502
- __disposeResources(env_20);
3557
+ __disposeResources(env_21);
3503
3558
  }
3504
3559
  }
3505
3560
  });
@@ -3548,7 +3603,7 @@ describe("ChangesetReader: behaviour in case imodel is not in sync with changese
3548
3603
  HubMock.shutdown();
3549
3604
  });
3550
3605
  it("openFile() reads the middle changeset of an insert → update → delete lifecycle", async () => {
3551
- const env_21 = { stack: [], error: void 0, hasError: false };
3606
+ const env_22 = { stack: [], error: void 0, hasError: false };
3552
3607
  try {
3553
3608
  const adminToken = "super manager token";
3554
3609
  // Push 2 (insert): insert element with category1
@@ -3596,8 +3651,8 @@ describe("ChangesetReader: behaviour in case imodel is not in sync with changese
3596
3651
  const changesets = await HubMock.downloadChangesets({ iModelId: rwIModelId, targetDir });
3597
3652
  expect(changesets.length).to.equal(4);
3598
3653
  const middleChangeset = changesets[Math.floor(changesets.length / 2)]; // index 2 = update
3599
- const reader = __addDisposableResource(env_21, ChangesetReader.openFile({ db: rwIModel, fileName: middleChangeset.pathname, propFilter: PropertyFilter.InstanceKey, rowOptions: { classIdsToClassNames: true } }), false);
3600
- const pcu = __addDisposableResource(env_21, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
3654
+ const reader = __addDisposableResource(env_22, ChangesetReader.openFile({ db: rwIModel, fileName: middleChangeset.pathname, propFilter: PropertyFilter.InstanceKey, rowOptions: { classIdsToClassNames: true } }), false);
3655
+ const pcu = __addDisposableResource(env_22, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
3601
3656
  while (reader.step())
3602
3657
  pcu.appendFrom(reader);
3603
3658
  const instances = Array.from(pcu.instances);
@@ -3616,16 +3671,16 @@ describe("ChangesetReader: behaviour in case imodel is not in sync with changese
3616
3671
  expect(bisElementNew.map((i => i.ECClassId)).sort()).to.deep.equal(["BisCore.GeometricElement2d", "BisCore.Element"].sort());
3617
3672
  expect(bisElementOld.map((i => i.ECClassId)).sort()).to.deep.equal(["BisCore.GeometricElement2d", "BisCore.Element"].sort());
3618
3673
  }
3619
- catch (e_21) {
3620
- env_21.error = e_21;
3621
- env_21.hasError = true;
3674
+ catch (e_22) {
3675
+ env_22.error = e_22;
3676
+ env_22.hasError = true;
3622
3677
  }
3623
3678
  finally {
3624
- __disposeResources(env_21);
3679
+ __disposeResources(env_22);
3625
3680
  }
3626
3681
  });
3627
3682
  it("openTxn() reads the middle txn of an insert → update → update lifecycle", async () => {
3628
- const env_22 = { stack: [], error: void 0, hasError: false };
3683
+ const env_23 = { stack: [], error: void 0, hasError: false };
3629
3684
  try {
3630
3685
  // Push 2 (insert): insert element with category1
3631
3686
  const geomArray = [
@@ -3667,8 +3722,8 @@ describe("ChangesetReader: behaviour in case imodel is not in sync with changese
3667
3722
  s: { x: 100.0, y: 200.0 },
3668
3723
  });
3669
3724
  txn.saveChanges("update element");
3670
- const reader = __addDisposableResource(env_22, ChangesetReader.openTxn({ db: rwIModel, txnId }), false);
3671
- const pcu = __addDisposableResource(env_22, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
3725
+ const reader = __addDisposableResource(env_23, ChangesetReader.openTxn({ db: rwIModel, txnId }), false);
3726
+ const pcu = __addDisposableResource(env_23, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
3672
3727
  while (reader.step())
3673
3728
  pcu.appendFrom(reader);
3674
3729
  const instances = Array.from(pcu.instances);
@@ -3690,12 +3745,12 @@ describe("ChangesetReader: behaviour in case imodel is not in sync with changese
3690
3745
  expect(elementOld.$meta.changeFetchedPropNames).to.include("s.X");
3691
3746
  expect(elementOld.$meta.changeFetchedPropNames).to.not.include("s.Y");
3692
3747
  }
3693
- catch (e_22) {
3694
- env_22.error = e_22;
3695
- env_22.hasError = true;
3748
+ catch (e_23) {
3749
+ env_23.error = e_23;
3750
+ env_23.hasError = true;
3696
3751
  }
3697
3752
  finally {
3698
- __disposeResources(env_22);
3753
+ __disposeResources(env_23);
3699
3754
  }
3700
3755
  });
3701
3756
  });
@@ -3778,7 +3833,7 @@ describe("ChangesetReader: overflow table graceful recovery when ExclusiveRootCl
3778
3833
  HubMock.shutdown();
3779
3834
  });
3780
3835
  it("openFile() recovers gracefully when ExclusiveRootClassId is NULL for overflow table", () => {
3781
- const env_23 = { stack: [], error: void 0, hasError: false };
3836
+ const env_24 = { stack: [], error: void 0, hasError: false };
3782
3837
  try {
3783
3838
  /**
3784
3839
  * Simulate the broken state: clear ExclusiveRootClassId so the reader cannot
@@ -3788,7 +3843,7 @@ describe("ChangesetReader: overflow table graceful recovery when ExclusiveRootCl
3788
3843
  */
3789
3844
  expect(rwIModel[_nativeDb].executeSql("UPDATE ec_Table SET ExclusiveRootClassId=NULL WHERE Name='bis_GeometricElement2d_Overflow'")).to.equal(DbResult.BE_SQLITE_OK);
3790
3845
  let assertedOnOverflowTable = false;
3791
- const reader = __addDisposableResource(env_23, ChangesetReader.openFile({ db: rwIModel, fileName: updateChangesetPathname }), false);
3846
+ const reader = __addDisposableResource(env_24, ChangesetReader.openFile({ db: rwIModel, fileName: updateChangesetPathname }), false);
3792
3847
  while (reader.step()) {
3793
3848
  if (reader.tableName !== "bis_GeometricElement2d_Overflow")
3794
3849
  continue;
@@ -3811,21 +3866,21 @@ describe("ChangesetReader: overflow table graceful recovery when ExclusiveRootCl
3811
3866
  }
3812
3867
  assert.isTrue(assertedOnOverflowTable, "Expected at least one row from the overflow table");
3813
3868
  }
3814
- catch (e_23) {
3815
- env_23.error = e_23;
3816
- env_23.hasError = true;
3869
+ catch (e_24) {
3870
+ env_24.error = e_24;
3871
+ env_24.hasError = true;
3817
3872
  }
3818
3873
  finally {
3819
- __disposeResources(env_23);
3874
+ __disposeResources(env_24);
3820
3875
  }
3821
3876
  });
3822
3877
  });
3823
3878
  describe("ChangesetReader: instance reused with a different class (class change in Updated row)", () => {
3824
3879
  it("openFile() correctly identifies ECClassId change from T1 to T2 in a buggy changeset", async () => {
3825
- const env_24 = { stack: [], error: void 0, hasError: false };
3880
+ const env_25 = { stack: [], error: void 0, hasError: false };
3826
3881
  try {
3827
3882
  /**
3828
- * Same scenario as ChangesetReader.test.ts: "Instance update to a different class (bug)".
3883
+ * Same scenario as SQliteChangesetReaderAndChangesetECAdaptor.test.ts: "Instance update to a different class (bug)".
3829
3884
  * Verifies ChangesetReader behaviour when an instance ID is reused with a different class.
3830
3885
  *
3831
3886
  * Steps:
@@ -3927,7 +3982,7 @@ describe("ChangesetReader: instance reused with a different class (class change
3927
3982
  // so it correctly sees T2 for the new (inserted) side and T1 for the old (deleted) side.
3928
3983
  let bisElementAsserted = false;
3929
3984
  let bisGeomElement2dAsserted = false;
3930
- const reader = __addDisposableResource(env_24, ChangesetReader.openFile({ db: b1, fileName: changesets[1].pathname }), false);
3985
+ const reader = __addDisposableResource(env_25, ChangesetReader.openFile({ db: b1, fileName: changesets[1].pathname }), false);
3931
3986
  while (reader.step()) {
3932
3987
  if (reader.tableName === "bis_Element" && reader.op === "Updated") {
3933
3988
  bisElementAsserted = true;
@@ -3957,12 +4012,12 @@ describe("ChangesetReader: instance reused with a different class (class change
3957
4012
  b1.close();
3958
4013
  HubMock.shutdown();
3959
4014
  }
3960
- catch (e_24) {
3961
- env_24.error = e_24;
3962
- env_24.hasError = true;
4015
+ catch (e_25) {
4016
+ env_25.error = e_25;
4017
+ env_25.hasError = true;
3963
4018
  }
3964
4019
  finally {
3965
- __disposeResources(env_24);
4020
+ __disposeResources(env_25);
3966
4021
  }
3967
4022
  });
3968
4023
  });
@@ -4256,7 +4311,7 @@ describe("ChangesetReader: invalid inputs", () => {
4256
4311
  expect(() => ChangesetReader.openFile({ db: iModel, fileName: KnownTestLocations.outputDir })).to.throw();
4257
4312
  });
4258
4313
  it("openFile: path points to a plain text file (not .changeset) throws", () => {
4259
- const env_25 = { stack: [], error: void 0, hasError: false };
4314
+ const env_26 = { stack: [], error: void 0, hasError: false };
4260
4315
  try {
4261
4316
  const txtFile = path.join(KnownTestLocations.outputDir, "not_a_changeset.txt");
4262
4317
  if (fs.existsSync(txtFile))
@@ -4264,19 +4319,19 @@ describe("ChangesetReader: invalid inputs", () => {
4264
4319
  // Write a small non-changeset file and expect the reader to reject it.
4265
4320
  fs.writeFileSync(txtFile, "this is not a changeset");
4266
4321
  // The native code asserts on an invalid changeset file. Suppress this.
4267
- const _disableAssertions = __addDisposableResource(env_25, new DisableNativeAssertions(), false);
4268
- const reader = __addDisposableResource(env_25, ChangesetReader.openFile({ db: iModel, fileName: txtFile }), false);
4322
+ const _disableAssertions = __addDisposableResource(env_26, new DisableNativeAssertions(), false);
4323
+ const reader = __addDisposableResource(env_26, ChangesetReader.openFile({ db: iModel, fileName: txtFile }), false);
4269
4324
  assert.equal(reader.step(), false, "Expected step() to return false for an invalid changeset file");
4270
4325
  assert.equal(reader.step(), false, "Expected step() to return false for an invalid changeset file");
4271
4326
  assert.equal(reader.step(), false, "Expected step() to return false for an invalid changeset file");
4272
4327
  assert.equal(reader.step(), false, "Expected step() to return false for an invalid changeset file");
4273
4328
  }
4274
- catch (e_25) {
4275
- env_25.error = e_25;
4276
- env_25.hasError = true;
4329
+ catch (e_26) {
4330
+ env_26.error = e_26;
4331
+ env_26.hasError = true;
4277
4332
  }
4278
4333
  finally {
4279
- __disposeResources(env_25);
4334
+ __disposeResources(env_26);
4280
4335
  }
4281
4336
  });
4282
4337
  // --- openGroup ---
@@ -4296,17 +4351,17 @@ describe("ChangesetReader: invalid inputs", () => {
4296
4351
  // Passing the same changeset file twice is unusual but not necessarily an error at open time.
4297
4352
  // The reader should open and step without throwing; the caller is responsible for any semantic issues.
4298
4353
  expect(() => {
4299
- const env_26 = { stack: [], error: void 0, hasError: false };
4354
+ const env_27 = { stack: [], error: void 0, hasError: false };
4300
4355
  try {
4301
- const reader = __addDisposableResource(env_26, ChangesetReader.openGroup({ db: iModel, changesetFiles: [validChangesetPath, validChangesetPath] }), false);
4356
+ const reader = __addDisposableResource(env_27, ChangesetReader.openGroup({ db: iModel, changesetFiles: [validChangesetPath, validChangesetPath] }), false);
4302
4357
  while (reader.step()) { /* drain */ }
4303
4358
  }
4304
- catch (e_26) {
4305
- env_26.error = e_26;
4306
- env_26.hasError = true;
4359
+ catch (e_27) {
4360
+ env_27.error = e_27;
4361
+ env_27.hasError = true;
4307
4362
  }
4308
4363
  finally {
4309
- __disposeResources(env_26);
4364
+ __disposeResources(env_27);
4310
4365
  }
4311
4366
  }).to.not.throw();
4312
4367
  });
@@ -4319,24 +4374,10 @@ describe("ChangesetReader: invalid inputs", () => {
4319
4374
  });
4320
4375
  // --- property accessors before step() ---
4321
4376
  it("accessing tableName before step() throws", () => {
4322
- const env_27 = { stack: [], error: void 0, hasError: false };
4323
- try {
4324
- const reader = __addDisposableResource(env_27, ChangesetReader.openFile({ db: iModel, fileName: validChangesetPath }), false);
4325
- expect(() => reader.tableName).to.throw();
4326
- }
4327
- catch (e_27) {
4328
- env_27.error = e_27;
4329
- env_27.hasError = true;
4330
- }
4331
- finally {
4332
- __disposeResources(env_27);
4333
- }
4334
- });
4335
- it("accessing isECTable before step() throws", () => {
4336
4377
  const env_28 = { stack: [], error: void 0, hasError: false };
4337
4378
  try {
4338
4379
  const reader = __addDisposableResource(env_28, ChangesetReader.openFile({ db: iModel, fileName: validChangesetPath }), false);
4339
- expect(() => reader.isECTable).to.throw();
4380
+ expect(() => reader.tableName).to.throw();
4340
4381
  }
4341
4382
  catch (e_28) {
4342
4383
  env_28.error = e_28;
@@ -4346,11 +4387,11 @@ describe("ChangesetReader: invalid inputs", () => {
4346
4387
  __disposeResources(env_28);
4347
4388
  }
4348
4389
  });
4349
- it("accessing isIndirectChange before step() throws", () => {
4390
+ it("accessing isECTable before step() throws", () => {
4350
4391
  const env_29 = { stack: [], error: void 0, hasError: false };
4351
4392
  try {
4352
4393
  const reader = __addDisposableResource(env_29, ChangesetReader.openFile({ db: iModel, fileName: validChangesetPath }), false);
4353
- expect(() => reader.isIndirectChange).to.throw();
4394
+ expect(() => reader.isECTable).to.throw();
4354
4395
  }
4355
4396
  catch (e_29) {
4356
4397
  env_29.error = e_29;
@@ -4360,6 +4401,20 @@ describe("ChangesetReader: invalid inputs", () => {
4360
4401
  __disposeResources(env_29);
4361
4402
  }
4362
4403
  });
4404
+ it("accessing isIndirectChange before step() throws", () => {
4405
+ const env_30 = { stack: [], error: void 0, hasError: false };
4406
+ try {
4407
+ const reader = __addDisposableResource(env_30, ChangesetReader.openFile({ db: iModel, fileName: validChangesetPath }), false);
4408
+ expect(() => reader.isIndirectChange).to.throw();
4409
+ }
4410
+ catch (e_30) {
4411
+ env_30.error = e_30;
4412
+ env_30.hasError = true;
4413
+ }
4414
+ finally {
4415
+ __disposeResources(env_30);
4416
+ }
4417
+ });
4363
4418
  it("openGroup: should throw if spillThresholdInBytes is negative", () => {
4364
4419
  expect(() => {
4365
4420
  ChangesetReader.openGroup({ db: iModel, changesetFiles: [validChangesetPath, validChangesetPath], spillThresholdInBytes: -1 });
@@ -4471,19 +4526,19 @@ describe("ChangesetReader — spillThresholdInBytes (spill-to-disk)", () => {
4471
4526
  }
4472
4527
  /** Reads all instances from a reader using an in-memory unifier cache. */
4473
4528
  function drainReader(reader) {
4474
- const env_30 = { stack: [], error: void 0, hasError: false };
4529
+ const env_31 = { stack: [], error: void 0, hasError: false };
4475
4530
  try {
4476
- const pcu = __addDisposableResource(env_30, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
4531
+ const pcu = __addDisposableResource(env_31, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
4477
4532
  while (reader.step())
4478
4533
  pcu.appendFrom(reader);
4479
4534
  return Array.from(pcu.instances);
4480
4535
  }
4481
- catch (e_30) {
4482
- env_30.error = e_30;
4483
- env_30.hasError = true;
4536
+ catch (e_31) {
4537
+ env_31.error = e_31;
4538
+ env_31.hasError = true;
4484
4539
  }
4485
4540
  finally {
4486
- __disposeResources(env_30);
4541
+ __disposeResources(env_31);
4487
4542
  }
4488
4543
  }
4489
4544
  /** Sorts instances by a stable key for deterministic deep-equality comparisons. */
@@ -4711,26 +4766,26 @@ describe("ChangesetReader: strict mode (column-count mismatch)", () => {
4711
4766
  await importSchemaStrings(txn, [STRICT_MODE_SCHEMA_V2]);
4712
4767
  // Strict mode ON: binary has one column less than the live table.
4713
4768
  {
4714
- const env_31 = { stack: [], error: void 0, hasError: false };
4769
+ const env_32 = { stack: [], error: void 0, hasError: false };
4715
4770
  try {
4716
- const reader = __addDisposableResource(env_31, ChangesetReader.openTxn({ db, txnId }), false);
4771
+ const reader = __addDisposableResource(env_32, ChangesetReader.openTxn({ db, txnId }), false);
4717
4772
  reader.enableStrictMode();
4718
4773
  expect(() => { while (reader.step()) { /* drain */ } }).to.throw();
4719
4774
  }
4720
- catch (e_31) {
4721
- env_31.error = e_31;
4722
- env_31.hasError = true;
4775
+ catch (e_32) {
4776
+ env_32.error = e_32;
4777
+ env_32.hasError = true;
4723
4778
  }
4724
4779
  finally {
4725
- __disposeResources(env_31);
4780
+ __disposeResources(env_32);
4726
4781
  }
4727
4782
  }
4728
4783
  {
4729
- const env_32 = { stack: [], error: void 0, hasError: false };
4784
+ const env_33 = { stack: [], error: void 0, hasError: false };
4730
4785
  try {
4731
- const reader = __addDisposableResource(env_32, ChangesetReader.openTxn({ db, txnId }), false);
4786
+ const reader = __addDisposableResource(env_33, ChangesetReader.openTxn({ db, txnId }), false);
4732
4787
  reader.disableStrictMode(); // no-op when already off — verifies the API does not throw
4733
- const pcu = __addDisposableResource(env_32, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
4788
+ const pcu = __addDisposableResource(env_33, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
4734
4789
  while (reader.step())
4735
4790
  pcu.appendFrom(reader);
4736
4791
  const instances = Array.from(pcu.instances);
@@ -4816,12 +4871,12 @@ describe("ChangesetReader: strict mode (column-count mismatch)", () => {
4816
4871
  ].sort());
4817
4872
  assert.equal(elemNew.$meta.isIndirectChange, false);
4818
4873
  }
4819
- catch (e_32) {
4820
- env_32.error = e_32;
4821
- env_32.hasError = true;
4874
+ catch (e_33) {
4875
+ env_33.error = e_33;
4876
+ env_33.hasError = true;
4822
4877
  }
4823
4878
  finally {
4824
- __disposeResources(env_32);
4879
+ __disposeResources(env_33);
4825
4880
  }
4826
4881
  }
4827
4882
  }
@@ -4877,26 +4932,26 @@ describe("ChangesetReader: strict mode (column-count mismatch)", () => {
4877
4932
  const newCs = changesets[changesets.length - 1];
4878
4933
  // Strict mode ON: binary has more cols, throws.
4879
4934
  {
4880
- const env_33 = { stack: [], error: void 0, hasError: false };
4935
+ const env_34 = { stack: [], error: void 0, hasError: false };
4881
4936
  try {
4882
- const reader = __addDisposableResource(env_33, ChangesetReader.openFile({ db: b1, fileName: newCs.pathname }), false);
4937
+ const reader = __addDisposableResource(env_34, ChangesetReader.openFile({ db: b1, fileName: newCs.pathname }), false);
4883
4938
  reader.enableStrictMode();
4884
4939
  expect(() => { while (reader.step()) { /* drain */ } }).to.throw();
4885
4940
  }
4886
- catch (e_33) {
4887
- env_33.error = e_33;
4888
- env_33.hasError = true;
4941
+ catch (e_34) {
4942
+ env_34.error = e_34;
4943
+ env_34.hasError = true;
4889
4944
  }
4890
4945
  finally {
4891
- __disposeResources(env_33);
4946
+ __disposeResources(env_34);
4892
4947
  }
4893
4948
  }
4894
4949
  // Strict mode OFF: binary has more cols, does not throw.
4895
4950
  {
4896
- const env_34 = { stack: [], error: void 0, hasError: false };
4951
+ const env_35 = { stack: [], error: void 0, hasError: false };
4897
4952
  try {
4898
- const reader = __addDisposableResource(env_34, ChangesetReader.openFile({ db: b1, fileName: newCs.pathname }), false);
4899
- const pcu = __addDisposableResource(env_34, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
4953
+ const reader = __addDisposableResource(env_35, ChangesetReader.openFile({ db: b1, fileName: newCs.pathname }), false);
4954
+ const pcu = __addDisposableResource(env_35, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
4900
4955
  while (reader.step())
4901
4956
  pcu.appendFrom(reader);
4902
4957
  const instances = Array.from(pcu.instances);
@@ -4982,12 +5037,12 @@ describe("ChangesetReader: strict mode (column-count mismatch)", () => {
4982
5037
  ].sort());
4983
5038
  assert.equal(elemNew.$meta.isIndirectChange, false);
4984
5039
  }
4985
- catch (e_34) {
4986
- env_34.error = e_34;
4987
- env_34.hasError = true;
5040
+ catch (e_35) {
5041
+ env_35.error = e_35;
5042
+ env_35.hasError = true;
4988
5043
  }
4989
5044
  finally {
4990
- __disposeResources(env_34);
5045
+ __disposeResources(env_35);
4991
5046
  }
4992
5047
  }
4993
5048
  }
@@ -4998,4 +5053,564 @@ describe("ChangesetReader: strict mode (column-count mismatch)", () => {
4998
5053
  }
4999
5054
  });
5000
5055
  });
5056
+ describe("ChangesetReader: throwIfAlreadyStepped guard", () => {
5057
+ let iModel;
5058
+ let txn;
5059
+ let txnId;
5060
+ before(async () => {
5061
+ HubMock.startup("ChangesetReaderThrowGuard", KnownTestLocations.outputDir);
5062
+ const adminToken = "super manager token";
5063
+ const iTwinId = HubMock.iTwinId;
5064
+ const iModelId = await HubMock.createNewIModel({ iTwinId, iModelName: "throwGuard", accessToken: adminToken });
5065
+ iModel = await HubWrappers.downloadAndOpenBriefcase({ iTwinId, iModelId, accessToken: adminToken });
5066
+ txn = startTestTxn(iModel, "throwGuard setup");
5067
+ iModel.channels.addAllowedChannel(ChannelControl.sharedChannelName);
5068
+ await iModel.locks.acquireLocks({ shared: IModel.dictionaryId });
5069
+ IModelTestUtils.createAndInsertDrawingPartitionAndModel(txn, Code.createEmpty(), true);
5070
+ txn.saveChanges("setup");
5071
+ txnId = iModel.txns.getLastSavedTxnProps().id;
5072
+ });
5073
+ after(() => {
5074
+ txn.end();
5075
+ iModel?.close();
5076
+ HubMock.shutdown();
5077
+ });
5078
+ it("setTableNameFilters throws after step()", () => {
5079
+ const env_36 = { stack: [], error: void 0, hasError: false };
5080
+ try {
5081
+ const reader = __addDisposableResource(env_36, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5082
+ assert.isTrue(reader.step());
5083
+ expect(() => reader.setTableNameFilters(new Set(["bis_Element"]))).to.throw("filters and strict mode and batch size must be configured before the first call to step()");
5084
+ }
5085
+ catch (e_36) {
5086
+ env_36.error = e_36;
5087
+ env_36.hasError = true;
5088
+ }
5089
+ finally {
5090
+ __disposeResources(env_36);
5091
+ }
5092
+ });
5093
+ it("setOpCodeFilters throws after step()", () => {
5094
+ const env_37 = { stack: [], error: void 0, hasError: false };
5095
+ try {
5096
+ const reader = __addDisposableResource(env_37, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5097
+ assert.isTrue(reader.step());
5098
+ expect(() => reader.setOpCodeFilters(new Set(["Inserted"]))).to.throw("filters and strict mode and batch size must be configured before the first call to step()");
5099
+ }
5100
+ catch (e_37) {
5101
+ env_37.error = e_37;
5102
+ env_37.hasError = true;
5103
+ }
5104
+ finally {
5105
+ __disposeResources(env_37);
5106
+ }
5107
+ });
5108
+ it("setClassNameFilters throws after step()", () => {
5109
+ const env_38 = { stack: [], error: void 0, hasError: false };
5110
+ try {
5111
+ const reader = __addDisposableResource(env_38, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5112
+ assert.isTrue(reader.step());
5113
+ expect(() => reader.setClassNameFilters(new Set(["BisCore:Element"]))).to.throw("filters and strict mode and batch size must be configured before the first call to step()");
5114
+ }
5115
+ catch (e_38) {
5116
+ env_38.error = e_38;
5117
+ env_38.hasError = true;
5118
+ }
5119
+ finally {
5120
+ __disposeResources(env_38);
5121
+ }
5122
+ });
5123
+ it("clearTableNameFilters throws after step()", () => {
5124
+ const env_39 = { stack: [], error: void 0, hasError: false };
5125
+ try {
5126
+ const reader = __addDisposableResource(env_39, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5127
+ assert.isTrue(reader.step());
5128
+ expect(() => reader.clearTableNameFilters()).to.throw("filters and strict mode and batch size must be configured before the first call to step()");
5129
+ }
5130
+ catch (e_39) {
5131
+ env_39.error = e_39;
5132
+ env_39.hasError = true;
5133
+ }
5134
+ finally {
5135
+ __disposeResources(env_39);
5136
+ }
5137
+ });
5138
+ it("clearOpCodeFilters throws after step()", () => {
5139
+ const env_40 = { stack: [], error: void 0, hasError: false };
5140
+ try {
5141
+ const reader = __addDisposableResource(env_40, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5142
+ assert.isTrue(reader.step());
5143
+ expect(() => reader.clearOpCodeFilters()).to.throw("filters and strict mode and batch size must be configured before the first call to step()");
5144
+ }
5145
+ catch (e_40) {
5146
+ env_40.error = e_40;
5147
+ env_40.hasError = true;
5148
+ }
5149
+ finally {
5150
+ __disposeResources(env_40);
5151
+ }
5152
+ });
5153
+ it("clearClassNameFilters throws after step()", () => {
5154
+ const env_41 = { stack: [], error: void 0, hasError: false };
5155
+ try {
5156
+ const reader = __addDisposableResource(env_41, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5157
+ assert.isTrue(reader.step());
5158
+ expect(() => reader.clearClassNameFilters()).to.throw("filters and strict mode and batch size must be configured before the first call to step()");
5159
+ }
5160
+ catch (e_41) {
5161
+ env_41.error = e_41;
5162
+ env_41.hasError = true;
5163
+ }
5164
+ finally {
5165
+ __disposeResources(env_41);
5166
+ }
5167
+ });
5168
+ it("enableStrictMode throws after step()", () => {
5169
+ const env_42 = { stack: [], error: void 0, hasError: false };
5170
+ try {
5171
+ const reader = __addDisposableResource(env_42, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5172
+ assert.isTrue(reader.step());
5173
+ expect(() => reader.enableStrictMode()).to.throw("filters and strict mode and batch size must be configured before the first call to step()");
5174
+ }
5175
+ catch (e_42) {
5176
+ env_42.error = e_42;
5177
+ env_42.hasError = true;
5178
+ }
5179
+ finally {
5180
+ __disposeResources(env_42);
5181
+ }
5182
+ });
5183
+ it("disableStrictMode throws after step()", () => {
5184
+ const env_43 = { stack: [], error: void 0, hasError: false };
5185
+ try {
5186
+ const reader = __addDisposableResource(env_43, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5187
+ assert.isTrue(reader.step());
5188
+ expect(() => reader.disableStrictMode()).to.throw("filters and strict mode and batch size must be configured before the first call to step()");
5189
+ }
5190
+ catch (e_43) {
5191
+ env_43.error = e_43;
5192
+ env_43.hasError = true;
5193
+ }
5194
+ finally {
5195
+ __disposeResources(env_43);
5196
+ }
5197
+ });
5198
+ it("setBatchSize throws after step()", () => {
5199
+ const env_44 = { stack: [], error: void 0, hasError: false };
5200
+ try {
5201
+ const reader = __addDisposableResource(env_44, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5202
+ assert.isTrue(reader.step());
5203
+ expect(() => reader.setBatchSize(10)).to.throw("filters and strict mode and batch size must be configured before the first call to step()");
5204
+ }
5205
+ catch (e_44) {
5206
+ env_44.error = e_44;
5207
+ env_44.hasError = true;
5208
+ }
5209
+ finally {
5210
+ __disposeResources(env_44);
5211
+ }
5212
+ });
5213
+ it("setBatchSize throws with zero or negative value", () => {
5214
+ const env_45 = { stack: [], error: void 0, hasError: false };
5215
+ try {
5216
+ const reader = __addDisposableResource(env_45, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5217
+ expect(() => reader.setBatchSize(0)).to.throw("batchSize must be a positive integer");
5218
+ expect(() => reader.setBatchSize(-1)).to.throw("batchSize must be a positive integer");
5219
+ }
5220
+ catch (e_45) {
5221
+ env_45.error = e_45;
5222
+ env_45.hasError = true;
5223
+ }
5224
+ finally {
5225
+ __disposeResources(env_45);
5226
+ }
5227
+ });
5228
+ it("filters can be set before any step()", () => {
5229
+ const env_46 = { stack: [], error: void 0, hasError: false };
5230
+ try {
5231
+ const reader = __addDisposableResource(env_46, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5232
+ // None of these should throw before step() is called
5233
+ expect(() => reader.setTableNameFilters(new Set(["bis_Element"]))).to.not.throw();
5234
+ expect(() => reader.setOpCodeFilters(new Set(["Inserted"]))).to.not.throw();
5235
+ expect(() => reader.setClassNameFilters(new Set(["BisCore:Element"]))).to.not.throw();
5236
+ expect(() => reader.clearTableNameFilters()).to.not.throw();
5237
+ expect(() => reader.clearOpCodeFilters()).to.not.throw();
5238
+ expect(() => reader.clearClassNameFilters()).to.not.throw();
5239
+ expect(() => reader.enableStrictMode()).to.not.throw();
5240
+ expect(() => reader.disableStrictMode()).to.not.throw();
5241
+ expect(() => reader.setBatchSize(5)).to.not.throw();
5242
+ }
5243
+ catch (e_46) {
5244
+ env_46.error = e_46;
5245
+ env_46.hasError = true;
5246
+ }
5247
+ finally {
5248
+ __disposeResources(env_46);
5249
+ }
5250
+ });
5251
+ });
5252
+ describe("ChangesetReader: batched stepping behavior", () => {
5253
+ let iModel;
5254
+ let txn;
5255
+ let txnId;
5256
+ before(async () => {
5257
+ HubMock.startup("ChangesetReaderBatchStep", KnownTestLocations.outputDir);
5258
+ const adminToken = "super manager token";
5259
+ const iTwinId = HubMock.iTwinId;
5260
+ const iModelId = await HubMock.createNewIModel({ iTwinId, iModelName: "batchStep", accessToken: adminToken });
5261
+ iModel = await HubWrappers.downloadAndOpenBriefcase({ iTwinId, iModelId, accessToken: adminToken });
5262
+ txn = startTestTxn(iModel, "batchStep setup");
5263
+ iModel.channels.addAllowedChannel(ChannelControl.sharedChannelName);
5264
+ await iModel.locks.acquireLocks({ shared: IModel.dictionaryId });
5265
+ IModelTestUtils.createAndInsertDrawingPartitionAndModel(txn, Code.createEmpty(), true);
5266
+ txn.saveChanges("setup");
5267
+ txnId = iModel.txns.getLastSavedTxnProps().id;
5268
+ });
5269
+ after(() => {
5270
+ txn.end();
5271
+ iModel?.close();
5272
+ HubMock.shutdown();
5273
+ });
5274
+ it("step() returns false when no rows remain", () => {
5275
+ const env_47 = { stack: [], error: void 0, hasError: false };
5276
+ try {
5277
+ const reader = __addDisposableResource(env_47, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5278
+ // Drain all rows
5279
+ while (reader.step()) { /* consume */ }
5280
+ // The next call must also return false
5281
+ assert.isFalse(reader.step());
5282
+ }
5283
+ catch (e_47) {
5284
+ env_47.error = e_47;
5285
+ env_47.hasError = true;
5286
+ }
5287
+ finally {
5288
+ __disposeResources(env_47);
5289
+ }
5290
+ });
5291
+ it("step() returns false repeatedly after exhaustion", () => {
5292
+ const env_48 = { stack: [], error: void 0, hasError: false };
5293
+ try {
5294
+ const reader = __addDisposableResource(env_48, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5295
+ while (reader.step()) { /* consume */ }
5296
+ assert.isFalse(reader.step());
5297
+ assert.isFalse(reader.step());
5298
+ assert.isFalse(reader.step());
5299
+ }
5300
+ catch (e_48) {
5301
+ env_48.error = e_48;
5302
+ env_48.hasError = true;
5303
+ }
5304
+ finally {
5305
+ __disposeResources(env_48);
5306
+ }
5307
+ });
5308
+ it("inserted/deleted are undefined after exhaustion", () => {
5309
+ const env_49 = { stack: [], error: void 0, hasError: false };
5310
+ try {
5311
+ const reader = __addDisposableResource(env_49, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5312
+ while (reader.step()) { /* consume */ }
5313
+ assert.isUndefined(reader.inserted);
5314
+ assert.isUndefined(reader.deleted);
5315
+ }
5316
+ catch (e_49) {
5317
+ env_49.error = e_49;
5318
+ env_49.hasError = true;
5319
+ }
5320
+ finally {
5321
+ __disposeResources(env_49);
5322
+ }
5323
+ });
5324
+ it("op throws after exhaustion", () => {
5325
+ const env_50 = { stack: [], error: void 0, hasError: false };
5326
+ try {
5327
+ const reader = __addDisposableResource(env_50, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5328
+ while (reader.step()) { /* consume */ }
5329
+ expect(() => reader.op).to.throw("no current row");
5330
+ }
5331
+ catch (e_50) {
5332
+ env_50.error = e_50;
5333
+ env_50.hasError = true;
5334
+ }
5335
+ finally {
5336
+ __disposeResources(env_50);
5337
+ }
5338
+ });
5339
+ it("setBatchSize(1) produces same instances as default batch size", () => {
5340
+ const defaultInstances = [];
5341
+ {
5342
+ const env_51 = { stack: [], error: void 0, hasError: false };
5343
+ try {
5344
+ const reader = __addDisposableResource(env_51, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5345
+ const pcu = __addDisposableResource(env_51, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
5346
+ while (reader.step())
5347
+ pcu.appendFrom(reader);
5348
+ for (const inst of pcu.instances)
5349
+ defaultInstances.push(inst.ECInstanceId);
5350
+ }
5351
+ catch (e_51) {
5352
+ env_51.error = e_51;
5353
+ env_51.hasError = true;
5354
+ }
5355
+ finally {
5356
+ __disposeResources(env_51);
5357
+ }
5358
+ }
5359
+ defaultInstances.sort();
5360
+ const batchOneInstances = [];
5361
+ {
5362
+ const env_52 = { stack: [], error: void 0, hasError: false };
5363
+ try {
5364
+ const reader = __addDisposableResource(env_52, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5365
+ reader.setBatchSize(1);
5366
+ const pcu = __addDisposableResource(env_52, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
5367
+ while (reader.step())
5368
+ pcu.appendFrom(reader);
5369
+ for (const inst of pcu.instances)
5370
+ batchOneInstances.push(inst.ECInstanceId);
5371
+ }
5372
+ catch (e_52) {
5373
+ env_52.error = e_52;
5374
+ env_52.hasError = true;
5375
+ }
5376
+ finally {
5377
+ __disposeResources(env_52);
5378
+ }
5379
+ }
5380
+ batchOneInstances.sort();
5381
+ assert.deepEqual(batchOneInstances, defaultInstances);
5382
+ });
5383
+ });
5384
+ describe("ChangesetReader: lazy inserted/deleted getters", () => {
5385
+ let iModel;
5386
+ let txn;
5387
+ let txnId;
5388
+ before(async () => {
5389
+ HubMock.startup("ChangesetReaderLazyGetters", KnownTestLocations.outputDir);
5390
+ const adminToken = "super manager token";
5391
+ const iTwinId = HubMock.iTwinId;
5392
+ const iModelId = await HubMock.createNewIModel({ iTwinId, iModelName: "lazyGetters", accessToken: adminToken });
5393
+ iModel = await HubWrappers.downloadAndOpenBriefcase({ iTwinId, iModelId, accessToken: adminToken });
5394
+ txn = startTestTxn(iModel, "lazyGetters setup");
5395
+ iModel.channels.addAllowedChannel(ChannelControl.sharedChannelName);
5396
+ await iModel.locks.acquireLocks({ shared: IModel.dictionaryId });
5397
+ IModelTestUtils.createAndInsertDrawingPartitionAndModel(txn, Code.createEmpty(), true);
5398
+ txn.saveChanges("setup");
5399
+ txnId = iModel.txns.getLastSavedTxnProps().id;
5400
+ });
5401
+ after(() => {
5402
+ txn.end();
5403
+ iModel?.close();
5404
+ HubMock.shutdown();
5405
+ });
5406
+ it("inserted is undefined before step()", () => {
5407
+ const env_53 = { stack: [], error: void 0, hasError: false };
5408
+ try {
5409
+ const reader = __addDisposableResource(env_53, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5410
+ assert.isUndefined(reader.inserted);
5411
+ }
5412
+ catch (e_53) {
5413
+ env_53.error = e_53;
5414
+ env_53.hasError = true;
5415
+ }
5416
+ finally {
5417
+ __disposeResources(env_53);
5418
+ }
5419
+ });
5420
+ it("deleted is undefined before step()", () => {
5421
+ const env_54 = { stack: [], error: void 0, hasError: false };
5422
+ try {
5423
+ const reader = __addDisposableResource(env_54, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5424
+ assert.isUndefined(reader.deleted);
5425
+ }
5426
+ catch (e_54) {
5427
+ env_54.error = e_54;
5428
+ env_54.hasError = true;
5429
+ }
5430
+ finally {
5431
+ __disposeResources(env_54);
5432
+ }
5433
+ });
5434
+ it("inserted returns a fresh object on first access, cached object on subsequent access", () => {
5435
+ const env_55 = { stack: [], error: void 0, hasError: false };
5436
+ try {
5437
+ const reader = __addDisposableResource(env_55, ChangesetReader.openTxn({ db: iModel, txnId }), false);
5438
+ // Find an Inserted row
5439
+ let found = false;
5440
+ while (reader.step()) {
5441
+ if (reader.inserted) {
5442
+ const first = reader.inserted;
5443
+ const second = reader.inserted;
5444
+ assert.strictEqual(first, second);
5445
+ assert.deepEqual(first, second);
5446
+ found = true;
5447
+ break;
5448
+ }
5449
+ }
5450
+ assert.isTrue(found, "Expected at least one row with an inserted value");
5451
+ }
5452
+ catch (e_55) {
5453
+ env_55.error = e_55;
5454
+ env_55.hasError = true;
5455
+ }
5456
+ finally {
5457
+ __disposeResources(env_55);
5458
+ }
5459
+ });
5460
+ });
5461
+ describe("ChangesetReader insert-many (53 elements)", () => {
5462
+ let rwIModel;
5463
+ const elementIds = [];
5464
+ let drawingModelId;
5465
+ let drawingCategoryId;
5466
+ let txnId;
5467
+ let txn;
5468
+ const ELEMENT_COUNT = 53;
5469
+ before(async () => {
5470
+ HubMock.startup("ECChangesetInsertMany", KnownTestLocations.outputDir);
5471
+ const adminToken = "super manager token";
5472
+ const iTwinId = HubMock.iTwinId;
5473
+ const rwIModelId = await HubMock.createNewIModel({ iTwinId, iModelName: "insertMany", description: "insertMany", accessToken: adminToken });
5474
+ rwIModel = await HubWrappers.downloadAndOpenBriefcase({ iTwinId, iModelId: rwIModelId, accessToken: adminToken });
5475
+ txn = startTestTxn(rwIModel, "ChangesetReader insert-many setup");
5476
+ const schema = `<?xml version="1.0" encoding="UTF-8"?>
5477
+ <ECSchema schemaName="TestDomain" alias="ts" version="01.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1">
5478
+ <ECSchemaReference name="BisCore" version="01.00" alias="bis"/>
5479
+ <ECStructClass typeName="RichPoint" modifier="Sealed">
5480
+ <ECProperty propertyName="X" typeName="double"/>
5481
+ <ECProperty propertyName="Y" typeName="double"/>
5482
+ <ECProperty propertyName="Z" typeName="double"/>
5483
+ <ECProperty propertyName="Label" typeName="string"/>
5484
+ </ECStructClass>
5485
+ <ECEntityClass typeName="Test2dElement">
5486
+ <BaseClass>bis:GraphicalElement2d</BaseClass>
5487
+ <ECProperty propertyName="StrProp" typeName="string"/>
5488
+ <ECProperty propertyName="IntProp" typeName="int"/>
5489
+ <ECProperty propertyName="DblProp" typeName="double"/>
5490
+ <ECStructProperty propertyName="StructProp" typeName="RichPoint"/>
5491
+ <ECArrayProperty propertyName="IntArrProp" typeName="int" minOccurs="0" maxOccurs="unbounded"/>
5492
+ </ECEntityClass>
5493
+ </ECSchema>`;
5494
+ await importSchemaStrings(txn, [schema]);
5495
+ rwIModel.channels.addAllowedChannel(ChannelControl.sharedChannelName);
5496
+ await rwIModel.locks.acquireLocks({ shared: IModel.dictionaryId });
5497
+ const codeProps = Code.createEmpty();
5498
+ codeProps.value = "DrillDownDrawing";
5499
+ [, drawingModelId] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(txn, codeProps, true);
5500
+ const foundCat = DrawingCategory.queryCategoryIdByName(rwIModel, IModel.dictionaryId, "DrillDownCategory");
5501
+ drawingCategoryId = foundCat ?? DrawingCategory.insert(txn, IModel.dictionaryId, "DrillDownCategory", new SubCategoryAppearance({ color: ColorDef.fromString("rgb(255,0,0)").toJSON() }));
5502
+ txn.saveChanges("setup");
5503
+ // Wait so that LastMod on bis_Model gets a distinct timestamp before the insert txn
5504
+ await new Promise((resolve) => setTimeout(resolve, 300));
5505
+ // Txn 2: insert 53 elements
5506
+ await rwIModel.locks.acquireLocks({ shared: drawingModelId });
5507
+ for (let i = 0; i < ELEMENT_COUNT; i++) {
5508
+ const id = txn.insertElement({
5509
+ classFullName: "TestDomain:Test2dElement",
5510
+ model: drawingModelId,
5511
+ category: drawingCategoryId,
5512
+ code: Code.createEmpty(),
5513
+ StrProp: `element_${i}`,
5514
+ IntProp: i,
5515
+ DblProp: i * 1.5,
5516
+ StructProp: { X: i, Y: i + 1, Z: i + 2, Label: `pt_${i}` },
5517
+ IntArrProp: [i, i * 10, i * 100],
5518
+ });
5519
+ elementIds.push(id);
5520
+ }
5521
+ txn.saveChanges("insert 53 elements");
5522
+ txnId = rwIModel.txns.getLastSavedTxnProps().id;
5523
+ });
5524
+ after(() => {
5525
+ txn.end();
5526
+ rwIModel?.close();
5527
+ HubMock.shutdown();
5528
+ });
5529
+ it("All_Properties: returns all 53 inserted elements", () => {
5530
+ const instances = readTxn(rwIModel, txnId);
5531
+ // 53 inserted elements (New) + 1 model update (New + Old) = 55
5532
+ const elemInstances = instances.filter((i) => i.$meta.op === "Inserted" && i.$meta.stage === "New");
5533
+ assert.equal(elemInstances.length, ELEMENT_COUNT);
5534
+ // Verify the model indirect change is present
5535
+ const modelNew = instances.find((i) => i.ECInstanceId === drawingModelId && i.$meta.stage === "New");
5536
+ const modelOld = instances.find((i) => i.ECInstanceId === drawingModelId && i.$meta.stage === "Old");
5537
+ expect(modelNew).to.exist;
5538
+ expect(modelOld).to.exist;
5539
+ assert.equal(modelNew.$meta.op, "Updated");
5540
+ assert.equal(modelOld.$meta.op, "Updated");
5541
+ assert.equal(modelNew.$meta.isIndirectChange, true);
5542
+ assert.equal(modelOld.$meta.isIndirectChange, true);
5543
+ // Total: 53 elements + 2 model entries
5544
+ assert.equal(instances.length, ELEMENT_COUNT + 2);
5545
+ // Verify each element is present and has the correct props
5546
+ for (let i = 0; i < ELEMENT_COUNT; i++) {
5547
+ const elem = elemInstances.find((inst) => inst.ECInstanceId === elementIds[i]);
5548
+ expect(elem, `element ${i} not found`).to.exist;
5549
+ assert.equal(elem.$meta.op, "Inserted");
5550
+ assert.equal(elem.$meta.stage, "New");
5551
+ assert.equal(elem.$meta.propFilter, PropertyFilter.All);
5552
+ assert.equal(elem.$meta.isIndirectChange, false);
5553
+ assert.equal(elem.StrProp, `element_${i}`);
5554
+ assert.equal(elem.IntProp, i);
5555
+ assert.closeTo(elem.DblProp, i * 1.5, 1e-10);
5556
+ assert.deepEqual(elem.StructProp, { X: i, Y: i + 1, Z: i + 2, Label: `pt_${i}` });
5557
+ assert.deepEqual(elem.IntArrProp, [i, i * 10, i * 100]);
5558
+ assert.equal(elem.Model.Id, drawingModelId);
5559
+ assert.equal(elem.Category.Id, drawingCategoryId);
5560
+ }
5561
+ });
5562
+ it("BisCoreElement: returns all 53 elements without custom props", () => {
5563
+ const instances = readTxn(rwIModel, txnId, PropertyFilter.BisCoreElement, { classIdsToClassNames: true });
5564
+ const elemInstances = instances.filter((i) => i.$meta.op === "Inserted" && i.$meta.stage === "New");
5565
+ assert.equal(elemInstances.length, ELEMENT_COUNT);
5566
+ assert.equal(instances.length, ELEMENT_COUNT + 2);
5567
+ for (let i = 0; i < ELEMENT_COUNT; i++) {
5568
+ const elem = elemInstances.find((inst) => inst.ECInstanceId === elementIds[i]);
5569
+ expect(elem, `element ${i} not found`).to.exist;
5570
+ assert.equal(elem.$meta.op, "Inserted");
5571
+ assert.equal(elem.$meta.stage, "New");
5572
+ assert.equal(elem.$meta.propFilter, PropertyFilter.BisCoreElement);
5573
+ assert.equal(elem.$meta.isIndirectChange, false);
5574
+ assert.equal(elem.ECClassId, "TestDomain.Test2dElement");
5575
+ assert.equal(elem.Model.Id, drawingModelId);
5576
+ // BisCoreElement filter: no custom domain props
5577
+ assert.isUndefined(elem.StrProp);
5578
+ assert.isUndefined(elem.IntProp);
5579
+ assert.isUndefined(elem.DblProp);
5580
+ assert.isUndefined(elem.StructProp);
5581
+ assert.isUndefined(elem.IntArrProp);
5582
+ // BIS core element props are present
5583
+ expect(elem.Model).to.exist;
5584
+ expect(elem.LastMod).to.exist;
5585
+ expect(elem.CodeSpec).to.exist;
5586
+ expect(elem.CodeScope).to.exist;
5587
+ expect(elem.FederationGuid).to.exist;
5588
+ }
5589
+ });
5590
+ it("InstanceKey: returns all 53 elements with only ECInstanceId and ECClassId", () => {
5591
+ const instances = readTxn(rwIModel, txnId, PropertyFilter.InstanceKey);
5592
+ const elemInstances = instances.filter((i) => i.$meta.op === "Inserted" && i.$meta.stage === "New");
5593
+ assert.equal(elemInstances.length, ELEMENT_COUNT);
5594
+ assert.equal(instances.length, ELEMENT_COUNT + 2);
5595
+ for (let i = 0; i < ELEMENT_COUNT; i++) {
5596
+ const elem = elemInstances.find((inst) => inst.ECInstanceId === elementIds[i]);
5597
+ expect(elem, `element ${i} not found`).to.exist;
5598
+ assert.equal(elem.$meta.op, "Inserted");
5599
+ assert.equal(elem.$meta.stage, "New");
5600
+ assert.equal(elem.$meta.propFilter, PropertyFilter.InstanceKey);
5601
+ assert.equal(elem.$meta.isIndirectChange, false);
5602
+ assert.equal("TestDomain:Test2dElement", rwIModel.getClassNameFromId(elem.ECClassId));
5603
+ // InstanceKey: only ECInstanceId + ECClassId, no other props
5604
+ assert.isUndefined(elem.StrProp);
5605
+ assert.isUndefined(elem.IntProp);
5606
+ assert.isUndefined(elem.DblProp);
5607
+ assert.isUndefined(elem.StructProp);
5608
+ assert.isUndefined(elem.IntArrProp);
5609
+ assert.isUndefined(elem.Model);
5610
+ assert.isUndefined(elem.Category);
5611
+ assert.isUndefined(elem.LastMod);
5612
+ assert.deepEqual(Object.keys(elem).sort(), ["ECInstanceId", "ECClassId", "$meta"].sort());
5613
+ }
5614
+ });
5615
+ });
5001
5616
  //# sourceMappingURL=ChangesetReader.test.js.map