@itwin/core-backend 5.15.0-dev.19 → 5.15.0-dev.20

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.
@@ -4,14 +4,38 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import { assert } from "chai";
6
6
  import * as path from "path";
7
- import { Id64 } from "@itwin/core-bentley";
8
- import { Point3d } from "@itwin/core-geometry";
7
+ import { DbResult, Id64 } from "@itwin/core-bentley";
8
+ import { Point3d, Range3d } from "@itwin/core-geometry";
9
9
  import { Code, CodeScopeSpec, ImageSourceFormat, IModel, QueryBinder, QueryRowFormat, SubCategoryAppearance } from "@itwin/core-common";
10
- import { EditTxn } from "../../EditTxn";
11
- import { CategorySelector, ChannelControl, DefinitionContainer, DefinitionModel, DisplayStyle2d, DisplayStyle3d, DocumentListModel, Drawing, DrawingCategory, DrawingGraphic, DrawingViewDefinition, GenericGraphicalType2d, GenericPhysicalType, GeometryPart, GraphicalElement2dIsOfType, IModelJsFs, InformationPartitionElement, ModelSelector, OrthographicViewDefinition, PhysicalElementIsOfType, PhysicalModel, PhysicalPartition, RenderMaterialElement, SnapshotDb, SpatialCategory, SubCategory, Subject, Texture, } from "../../core-backend";
10
+ import { BulkDeleteElementsStatus, BulkDeleteFailureReason, EditTxn } from "../../EditTxn";
11
+ import { _nativeDb, CategorySelector, ChannelControl, DefinitionContainer, DefinitionModel, DisplayStyle2d, DisplayStyle3d, DocumentListModel, Drawing, DrawingCategory, DrawingGraphic, DrawingViewDefinition, GenericGraphicalType2d, GenericPhysicalType, GeometryPart, GraphicalElement2dIsOfType, IModelJsFs, InformationPartitionElement, ModelSelector, OrthographicViewDefinition, PhysicalElementIsOfType, PhysicalModel, PhysicalPartition, RenderMaterialElement, SnapshotDb, SpatialCategory, SubCategory, Subject, Texture, } from "../../core-backend";
12
12
  import { ExtensiveTestScenario, IModelTestUtils } from "../IModelTestUtils";
13
13
  import { KnownTestLocations } from "../KnownTestLocations";
14
14
  import { withEditTxn } from "../TestEditTxn";
15
+ const success = { status: BulkDeleteElementsStatus.Success, sqlDeleteStatus: DbResult.BE_SQLITE_OK, failures: [] };
16
+ /** Nothing is deleted; `failures` lists the direct violators (one entry per element, `ReferencedByForeignKey` wins over `DefinitionInUse`). */
17
+ const constraintViolation = (...failures) => ({ status: BulkDeleteElementsStatus.ConstraintViolation, failures });
18
+ /** The element is a DefinitionElement still used (e.g. via a GeometryStream) by elements outside the delete set. */
19
+ const definitionInUse = (elementId) => ({ elementId, reason: BulkDeleteFailureReason.DefinitionInUse });
20
+ /** The element itself is referenced from outside the delete set via its Code scope. */
21
+ const codeScopeViolator = (elementId) => referencedByForeignKey(elementId);
22
+ /** A row outside the delete set references the element via a hard NO ACTION foreign key. */
23
+ const referencedByForeignKey = (elementId) => ({ elementId, reason: BulkDeleteFailureReason.ReferencedByForeignKey });
24
+ /** A Category still assigned to a GeometricElement3d outside the delete set (hard FK, so not `DefinitionInUse`). */
25
+ const categoryInUse3d = (elementId) => referencedByForeignKey(elementId);
26
+ /** A Category still assigned to a GeometricElement2d outside the delete set (hard FK, so not `DefinitionInUse`). */
27
+ const categoryInUse2d = (elementId) => referencedByForeignKey(elementId);
28
+ /** Assert the result matches `expected` exactly, then check that each id in `deletedIds` is gone and each id in `retainedIds` is still present. */
29
+ const assertResult = (result, expected, deletedIds, retainedIds, iModelDb) => {
30
+ assert.equal(result.status, expected.status, `Bulk element deletion status is ${BulkDeleteElementsStatus[result.status]}; Expected: ${BulkDeleteElementsStatus[expected.status]}. Actual failures: ${JSON.stringify(result.failures)}`);
31
+ assert.equal(result.sqlDeleteStatus, expected.sqlDeleteStatus ?? DbResult.BE_SQLITE_OK, `Bulk element deletion internal sql status is ${result.sqlDeleteStatus}; Expected: ${expected.sqlDeleteStatus ?? DbResult.BE_SQLITE_OK}`);
32
+ assert.equal(result.failures.length, expected.failures.length, `Actual failures don't match the expected. Actual: ${JSON.stringify(result.failures)}; Expected: ${JSON.stringify(expected.failures)}`);
33
+ assert.deepEqualInAnyOrder(result.failures.map(({ elementId, reason }) => ({ elementId, reason })), expected.failures);
34
+ for (const retainedId of retainedIds)
35
+ assert.isDefined(iModelDb.elements.tryGetElement(retainedId), `${retainedId} should have been retained`);
36
+ for (const deletedId of deletedIds)
37
+ assert.isUndefined(iModelDb.elements.tryGetElement(deletedId), `${deletedId} should have been deleted`);
38
+ };
15
39
  describe("DeleteDefinitionElements", () => {
16
40
  const outputDir = path.join(KnownTestLocations.outputDir, "DeleteDefinitionElements");
17
41
  let seedDb;
@@ -21,6 +45,7 @@ describe("DeleteDefinitionElements", () => {
21
45
  let physicalModelId;
22
46
  let spatialCategoryId;
23
47
  let subCategoryId;
48
+ let filteredSubCategoryId;
24
49
  let spatialCategorySelectorId;
25
50
  let modelSelectorId;
26
51
  let displayStyle3dId;
@@ -55,6 +80,7 @@ describe("DeleteDefinitionElements", () => {
55
80
  physicalModelId = seedDb.elements.queryElementIdByCode(PhysicalPartition.createCode(seedDb, subjectId, "Physical"));
56
81
  spatialCategoryId = seedDb.elements.queryElementIdByCode(SpatialCategory.createCode(seedDb, definitionModelId, "SpatialCategory"));
57
82
  subCategoryId = seedDb.elements.queryElementIdByCode(SubCategory.createCode(seedDb, spatialCategoryId, "SubCategory"));
83
+ filteredSubCategoryId = seedDb.elements.queryElementIdByCode(SubCategory.createCode(seedDb, spatialCategoryId, "FilteredSubCategory"));
58
84
  spatialCategorySelectorId = seedDb.elements.queryElementIdByCode(CategorySelector.createCode(seedDb, definitionModelId, "SpatialCategories"));
59
85
  modelSelectorId = seedDb.elements.queryElementIdByCode(ModelSelector.createCode(seedDb, definitionModelId, "SpatialModels"));
60
86
  displayStyle3dId = seedDb.elements.queryElementIdByCode(DisplayStyle3d.createCode(seedDb, definitionModelId, "DisplayStyle3d"));
@@ -282,183 +308,168 @@ describe("DeleteDefinitionElements", () => {
282
308
  return txn.insertElement(props);
283
309
  };
284
310
  /**
285
- * Call deleteElements, verify the returned failed-set, then check which elements survived vs. were deleted.
311
+ * Failures reported when the used `spatialCategoryId` is requested on its own. Validation runs once over the expanded set
312
+ * (no pruning between the FK pass and the definition-usage pass), so the used SpatialCategory is reported via its hard FK and
313
+ * its used non-default SubCategories (pulled into the set as children) are reported as `DefinitionInUse` as well.
314
+ */
315
+ const usedSpatialCategoryViolations = () => [
316
+ categoryInUse3d(spatialCategoryId),
317
+ definitionInUse(subCategoryId),
318
+ definitionInUse(filteredSubCategoryId),
319
+ ];
320
+ /**
321
+ * Call deleteElements, compare the returned result object against `expected`, then check which elements survived vs. were deleted.
286
322
  * Rolls back changes with abandonChanges afterwards.
287
323
  */
288
- const executeTestCase = (txn, label, idsToDelete, expectedDeleted, expectedFailed, expectedRetained, abandonChanges = true) => {
289
- const failed = txn.deleteElements(idsToDelete);
290
- assert.sameMembers(Array.from(failed.failedIds), expectedFailed, `[${label}] failed set mismatch`);
291
- for (const id of expectedDeleted)
292
- assert.isUndefined(iModelDb.elements.tryGetElement(id), `error reading element`);
293
- for (const id of expectedRetained)
294
- assert.isDefined(iModelDb.elements.tryGetElement(id), `[${label}] ${id} should have been retained`);
324
+ const executeTestCase = (txn, _label, idsToDelete, deletedIds, expected, retainedIds, abandonChanges = true) => {
325
+ const result = txn.deleteElements(idsToDelete);
326
+ assertResult(result, expected, deletedIds, retainedIds, iModelDb);
295
327
  if (abandonChanges)
296
328
  txn.abandonChanges();
297
329
  };
298
330
  it("should delete only if not used with deleteElements", async () => {
299
- let usedDefinitionElementIds;
300
331
  const txn = new EditTxn(iModelDb, "delete definition elements");
301
332
  txn.start();
302
- // make sure deleteElements only skips Elements that are being used (subjectId)
303
- usedDefinitionElementIds = txn.deleteElements([physicalObjectId1, physicalObjectId2, physicalObjectId3, subjectId]).failedIds;
304
- assert.equal(usedDefinitionElementIds.size, 1);
305
- assert.isUndefined(iModelDb.elements.tryGetElement(physicalObjectId1));
306
- assert.isUndefined(iModelDb.elements.tryGetElement(physicalObjectId2));
307
- assert.isUndefined(iModelDb.elements.tryGetElement(physicalObjectId3));
308
- assert.isDefined(iModelDb.elements.tryGetElement(subjectId));
309
- // Reset the db state
310
- txn.abandonChanges();
311
- // make sure deleteElements throws for invalid Ids
312
- assert.throws(() => txn.deleteElements([Id64.invalid, Id64.invalid]));
313
- // deleteElement/deleteElements for a used GeometryPart should fail
314
- assert.throws(() => txn.deleteElement(geometryPartId));
315
- usedDefinitionElementIds = txn.deleteElements([geometryPartId]).failedIds;
316
- assert.isTrue(usedDefinitionElementIds.has(geometryPartId));
317
- assert.isDefined(iModelDb.elements.tryGetElement(geometryPartId));
318
- // deleteElements for an unused GeometryPart should succeed, delete should still fail
319
- const unusedGeometryPartProps = {
320
- classFullName: GeometryPart.classFullName,
321
- model: definitionModelId,
322
- code: GeometryPart.createCode(iModelDb, definitionModelId, "Unused GeometryPart"),
323
- geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)),
324
- };
325
- // Reset the db state
326
- txn.abandonChanges();
327
- const unusedGeometryPartId = txn.insertElement(unusedGeometryPartProps);
328
- assert.isTrue(Id64.isValidId64(unusedGeometryPartId));
329
- assert.throws(() => txn.deleteElement(unusedGeometryPartId));
330
- usedDefinitionElementIds = txn.deleteElements([unusedGeometryPartId]).failedIds;
331
- assert.equal(usedDefinitionElementIds.size, 0);
332
- assert.isUndefined(iModelDb.elements.tryGetElement(unusedGeometryPartId));
333
- txn.abandonChanges();
334
- // deleteElement/deleteElements for a used RenderMaterial should fail
335
- assert.throws(() => txn.deleteElement(renderMaterialId));
336
- usedDefinitionElementIds = txn.deleteElements([renderMaterialId]).failedIds;
337
- assert.isTrue(usedDefinitionElementIds.has(renderMaterialId));
338
- assert.isDefined(iModelDb.elements.tryGetElement(renderMaterialId));
339
- txn.abandonChanges();
340
- // deleteElements for an unused RenderMaterial should succeed, delete should still fail
341
- const unusedRenderMaterialId = RenderMaterialElement.insert(txn, definitionModelId, "Unused RenderMaterial", { paletteName: "PaletteName" });
342
- assert.isTrue(Id64.isValidId64(unusedRenderMaterialId));
343
- assert.throws(() => txn.deleteElement(unusedRenderMaterialId));
344
- usedDefinitionElementIds = txn.deleteElements([unusedRenderMaterialId]).failedIds;
345
- assert.equal(usedDefinitionElementIds.size, 0);
346
- assert.isUndefined(iModelDb.elements.tryGetElement(unusedRenderMaterialId));
347
- txn.abandonChanges();
348
- // deleteElement/deleteElements for a used Texture should fail
349
- assert.throws(() => txn.deleteElement(textureId));
350
- usedDefinitionElementIds = txn.deleteElements([textureId]).failedIds;
351
- assert.isTrue(usedDefinitionElementIds.has(textureId));
352
- assert.isDefined(iModelDb.elements.tryGetElement(textureId));
353
- txn.abandonChanges();
354
- // deleteElements for an unused Texture should succeed, delete should still fail
355
- const unusedTextureId = IModelTestUtils.insertTextureElement(txn, definitionModelId, "Unused Texture");
356
- assert.isTrue(Id64.isValidId64(unusedTextureId));
357
- assert.throws(() => txn.deleteElement(unusedTextureId));
358
- usedDefinitionElementIds = txn.deleteElements([unusedTextureId]).failedIds;
359
- assert.equal(usedDefinitionElementIds.size, 0);
360
- assert.isUndefined(iModelDb.elements.tryGetElement(unusedTextureId));
361
- txn.abandonChanges();
362
- // deleteElement/deleteElements for a used SpatialCategory should fail
363
- assert.throws(() => txn.deleteElement(spatialCategoryId));
364
- usedDefinitionElementIds = txn.deleteElements([spatialCategoryId]).failedIds;
365
- assert.isTrue(usedDefinitionElementIds.has(spatialCategoryId));
366
- assert.isDefined(iModelDb.elements.tryGetElement(spatialCategoryId));
367
- txn.abandonChanges();
368
- // deleteElement/deleteElements for a default SubCategory should fail
369
- const spatialCategory = iModelDb.elements.getElement(spatialCategoryId, SpatialCategory);
370
- const defaultSpatialSubCategoryId = spatialCategory.myDefaultSubCategoryId();
371
- assert.throws(() => txn.deleteElement(defaultSpatialSubCategoryId));
372
- usedDefinitionElementIds = txn.deleteElements([defaultSpatialSubCategoryId]).failedIds;
373
- assert.isTrue(usedDefinitionElementIds.has(defaultSpatialSubCategoryId));
374
- assert.isDefined(iModelDb.elements.tryGetElement(defaultSpatialSubCategoryId));
375
- txn.abandonChanges();
376
- // deleteElement/deleteElements for a used, non-default SubCategory should fail
377
- assert.throws(() => txn.deleteElement(subCategoryId));
378
- usedDefinitionElementIds = txn.deleteElements([subCategoryId]).failedIds;
379
- assert.isTrue(usedDefinitionElementIds.has(subCategoryId));
380
- assert.isDefined(iModelDb.elements.tryGetElement(subCategoryId));
381
- txn.abandonChanges();
382
- // deleteElements for an unused SubCategory should succeed, delete should still fail
383
- const unusedSubCategoryId = SubCategory.insert(txn, spatialCategoryId, "Unused SubCategory", {});
384
- assert.isTrue(Id64.isValidId64(unusedSubCategoryId));
385
- assert.throws(() => txn.deleteElement(unusedSubCategoryId));
386
- usedDefinitionElementIds = txn.deleteElements([unusedSubCategoryId]).failedIds;
387
- assert.equal(usedDefinitionElementIds.size, 0);
388
- assert.isUndefined(iModelDb.elements.tryGetElement(unusedSubCategoryId));
389
- txn.abandonChanges();
390
- // deleteElements for an unused SpatialCategory should succeed, delete should still fail
391
- const unusedSpatialCategoryId = SpatialCategory.insert(txn, definitionModelId, "Unused SpatialCategory", {});
392
- assert.isTrue(Id64.isValidId64(unusedSpatialCategoryId));
393
- const unusedSpatialCategory = iModelDb.elements.getElement(unusedSpatialCategoryId, SpatialCategory);
394
- const unusedSpatialCategoryDefaultSubCategoryId = unusedSpatialCategory.myDefaultSubCategoryId();
395
- assert.isTrue(Id64.isValidId64(unusedSpatialCategoryDefaultSubCategoryId));
396
- assert.throws(() => txn.deleteElement(unusedSpatialCategoryId));
397
- assert.throws(() => txn.deleteElement(unusedSpatialCategoryDefaultSubCategoryId));
398
- usedDefinitionElementIds = txn.deleteElements([unusedSpatialCategoryId]).failedIds;
399
- assert.equal(usedDefinitionElementIds.size, 0);
400
- assert.isUndefined(iModelDb.elements.tryGetElement(unusedSpatialCategoryId));
401
- assert.isUndefined(iModelDb.elements.tryGetElement(unusedSpatialCategoryDefaultSubCategoryId));
402
- txn.abandonChanges();
403
- // deleteElement/deleteElements of a used DrawingCategory should fail
404
- assert.throws(() => txn.deleteElement(drawingCategoryId));
405
- usedDefinitionElementIds = txn.deleteElements([drawingCategoryId]).failedIds;
406
- assert.isTrue(usedDefinitionElementIds.has(drawingCategoryId));
407
- assert.isDefined(iModelDb.elements.tryGetElement(drawingCategoryId));
408
- txn.abandonChanges();
409
- // deleteElements for an unused DrawingCategory should succeed, delete should still fail
410
- const unusedDrawingCategoryId = DrawingCategory.insert(txn, definitionModelId, "Unused DrawingCategory", {});
411
- assert.isTrue(Id64.isValidId64(unusedDrawingCategoryId));
412
- const unusedDrawingCategory = iModelDb.elements.getElement(unusedDrawingCategoryId, DrawingCategory);
413
- const unusedDrawingSubCategoryId = unusedDrawingCategory.myDefaultSubCategoryId();
414
- assert.isTrue(Id64.isValidId64(unusedDrawingSubCategoryId));
415
- assert.throws(() => txn.deleteElement(unusedDrawingSubCategoryId));
416
- usedDefinitionElementIds = txn.deleteElements([unusedDrawingCategoryId]).failedIds;
417
- assert.equal(usedDefinitionElementIds.size, 0);
418
- assert.isUndefined(iModelDb.elements.tryGetElement(unusedDrawingCategoryId));
419
- assert.isUndefined(iModelDb.elements.tryGetElement(unusedDrawingSubCategoryId));
420
- txn.abandonChanges();
421
- // deleteElement/deleteElements of DefinitionElements used by an existing SpatialViewDefinition should fail
422
- assert.throws(() => txn.deleteElement(spatialCategorySelectorId));
423
- assert.throws(() => txn.deleteElement(modelSelectorId));
424
- assert.throws(() => txn.deleteElement(displayStyle3dId));
425
- usedDefinitionElementIds = txn.deleteElements([spatialCategorySelectorId, modelSelectorId, displayStyle3dId]).failedIds;
426
- assert.isTrue(usedDefinitionElementIds.has(spatialCategorySelectorId));
427
- assert.isTrue(usedDefinitionElementIds.has(modelSelectorId));
428
- assert.isTrue(usedDefinitionElementIds.has(displayStyle3dId));
429
- assert.isDefined(iModelDb.elements.tryGetElement(spatialCategorySelectorId));
430
- assert.isDefined(iModelDb.elements.tryGetElement(modelSelectorId));
431
- assert.isDefined(iModelDb.elements.tryGetElement(displayStyle3dId));
432
- assert.isDefined(iModelDb.elements.tryGetElement(viewId));
433
- txn.abandonChanges();
434
- // deleteElements should succeed when the list includes the SpatialViewDefinition as the only thing referencing other DefinitionElements, delete should still fail
435
- assert.throws(() => txn.deleteElement(viewId));
436
- usedDefinitionElementIds = txn.deleteElements([viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId]).failedIds;
437
- assert.equal(usedDefinitionElementIds.size, 0);
438
- assert.isUndefined(iModelDb.elements.tryGetElement(spatialCategorySelectorId));
439
- assert.isUndefined(iModelDb.elements.tryGetElement(modelSelectorId));
440
- assert.isUndefined(iModelDb.elements.tryGetElement(displayStyle3dId));
441
- assert.isUndefined(iModelDb.elements.tryGetElement(viewId));
442
- txn.abandonChanges();
443
- // deleteElement/deleteElements of DefinitionElements used by an existing DrawingViewDefinition should fail
444
- assert.throws(() => txn.deleteElement(drawingCategorySelectorId));
445
- assert.throws(() => txn.deleteElement(displayStyle2dId));
446
- usedDefinitionElementIds = txn.deleteElements([drawingCategorySelectorId, displayStyle2dId]).failedIds;
447
- assert.isTrue(usedDefinitionElementIds.has(drawingCategorySelectorId));
448
- assert.isTrue(usedDefinitionElementIds.has(displayStyle2dId));
449
- assert.isDefined(iModelDb.elements.tryGetElement(drawingCategorySelectorId));
450
- assert.isDefined(iModelDb.elements.tryGetElement(displayStyle2dId));
451
- assert.isDefined(iModelDb.elements.tryGetElement(drawingViewId));
452
- txn.abandonChanges();
453
- // deleteElements should succeed when the list includes the DrawingViewDefinition as the only thing referencing other DefinitionElements, delete should still fail
454
- assert.throws(() => txn.deleteElement(drawingViewId));
455
- usedDefinitionElementIds = txn.deleteElements([drawingViewId, drawingCategorySelectorId, displayStyle2dId]).failedIds;
456
- assert.equal(usedDefinitionElementIds.size, 0);
457
- assert.isUndefined(iModelDb.elements.tryGetElement(drawingCategorySelectorId));
458
- assert.isUndefined(iModelDb.elements.tryGetElement(displayStyle2dId));
459
- assert.isUndefined(iModelDb.elements.tryGetElement(drawingViewId));
460
- txn.end("abandon");
461
- iModelDb.close();
333
+ /** Run deleteElements and compare the returned result object against `expected`, then check deleted / retained ids. */
334
+ const expectDelete = (idsToDelete, expected, deletedIds, retainedIds) => assertResult(txn.deleteElements(idsToDelete), expected, deletedIds, retainedIds, iModelDb);
335
+ try {
336
+ // Deleting the Subject expands the delete set to everything beneath it (partitions, sub-models and their contents).
337
+ // Every element in this iModel lives under that Subject, so all usages of the categories / GeometryPart / RenderMaterial
338
+ // in the Definition model are *intra-set* and must not block the deletion: everything is deleted.
339
+ expectDelete([physicalObjectId1, physicalObjectId2, physicalObjectId3, subjectId], success, [physicalObjectId1, physicalObjectId2, physicalObjectId3, subjectId, definitionModelId, physicalModelId, spatialCategoryId, geometryPartId, renderMaterialId], []);
340
+ // Reset the db state
341
+ txn.abandonChanges();
342
+ // make sure deleteElements throws for invalid Ids
343
+ assert.throws(() => txn.deleteElements([Id64.invalid, Id64.invalid]));
344
+ // deleteElement/deleteElements for a used GeometryPart should fail
345
+ assert.throws(() => txn.deleteElement(geometryPartId));
346
+ expectDelete([geometryPartId], constraintViolation(definitionInUse(geometryPartId)), [], [geometryPartId]);
347
+ // deleteElements for an unused GeometryPart should succeed, delete should still fail
348
+ const unusedGeometryPartProps = {
349
+ classFullName: GeometryPart.classFullName,
350
+ model: definitionModelId,
351
+ code: GeometryPart.createCode(iModelDb, definitionModelId, "Unused GeometryPart"),
352
+ geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)),
353
+ };
354
+ // Reset the db state
355
+ txn.abandonChanges();
356
+ const unusedGeometryPartId = txn.insertElement(unusedGeometryPartProps);
357
+ assert.isTrue(Id64.isValidId64(unusedGeometryPartId));
358
+ assert.throws(() => txn.deleteElement(unusedGeometryPartId));
359
+ expectDelete([unusedGeometryPartId], success, [unusedGeometryPartId], []);
360
+ txn.abandonChanges();
361
+ // deleteElement/deleteElements for a used RenderMaterial should fail
362
+ assert.throws(() => txn.deleteElement(renderMaterialId));
363
+ expectDelete([renderMaterialId], constraintViolation(definitionInUse(renderMaterialId)), [], [renderMaterialId]);
364
+ txn.abandonChanges();
365
+ // deleteElements for an unused RenderMaterial should succeed, delete should still fail
366
+ const unusedRenderMaterialId = RenderMaterialElement.insert(txn, definitionModelId, "Unused RenderMaterial", { paletteName: "PaletteName" });
367
+ assert.isTrue(Id64.isValidId64(unusedRenderMaterialId));
368
+ assert.throws(() => txn.deleteElement(unusedRenderMaterialId));
369
+ expectDelete([unusedRenderMaterialId], success, [unusedRenderMaterialId], []);
370
+ txn.abandonChanges();
371
+ // deleteElement/deleteElements for a used Texture should fail
372
+ assert.throws(() => txn.deleteElement(textureId));
373
+ expectDelete([textureId], constraintViolation(definitionInUse(textureId)), [], [textureId]);
374
+ txn.abandonChanges();
375
+ // deleteElements for an unused Texture should succeed, delete should still fail
376
+ const unusedTextureId = IModelTestUtils.insertTextureElement(txn, definitionModelId, "Unused Texture");
377
+ assert.isTrue(Id64.isValidId64(unusedTextureId));
378
+ assert.throws(() => txn.deleteElement(unusedTextureId));
379
+ expectDelete([unusedTextureId], success, [unusedTextureId], []);
380
+ txn.abandonChanges();
381
+ // deleteElement/deleteElements for a used SpatialCategory should fail (hard FK from GeometricElement3d.CategoryId, so not DefinitionInUse).
382
+ // Its used non-default SubCategories are pulled into the set as children and are reported as well.
383
+ assert.throws(() => txn.deleteElement(spatialCategoryId));
384
+ expectDelete([spatialCategoryId], constraintViolation(...usedSpatialCategoryViolations()), [], [spatialCategoryId, subCategoryId, filteredSubCategoryId]);
385
+ txn.abandonChanges();
386
+ // deleteElement/deleteElements for a default SubCategory should fail
387
+ const spatialCategory = iModelDb.elements.getElement(spatialCategoryId, SpatialCategory);
388
+ const defaultSpatialSubCategoryId = spatialCategory.myDefaultSubCategoryId();
389
+ assert.throws(() => txn.deleteElement(defaultSpatialSubCategoryId));
390
+ expectDelete([defaultSpatialSubCategoryId], constraintViolation(definitionInUse(defaultSpatialSubCategoryId)), [], [defaultSpatialSubCategoryId]);
391
+ txn.abandonChanges();
392
+ // deleteElement/deleteElements for a used, non-default SubCategory should fail
393
+ assert.throws(() => txn.deleteElement(subCategoryId));
394
+ expectDelete([subCategoryId], constraintViolation(definitionInUse(subCategoryId)), [], [subCategoryId]);
395
+ txn.abandonChanges();
396
+ // deleteElements for an unused SubCategory should succeed, delete should still fail
397
+ const unusedSubCategoryId = SubCategory.insert(txn, spatialCategoryId, "Unused SubCategory", {});
398
+ assert.isTrue(Id64.isValidId64(unusedSubCategoryId));
399
+ assert.throws(() => txn.deleteElement(unusedSubCategoryId));
400
+ expectDelete([unusedSubCategoryId], success, [unusedSubCategoryId], []);
401
+ txn.abandonChanges();
402
+ // deleteElements for an unused SpatialCategory should succeed, delete should still fail
403
+ const unusedSpatialCategoryId = SpatialCategory.insert(txn, definitionModelId, "Unused SpatialCategory", {});
404
+ assert.isTrue(Id64.isValidId64(unusedSpatialCategoryId));
405
+ const unusedSpatialCategory = iModelDb.elements.getElement(unusedSpatialCategoryId, SpatialCategory);
406
+ const unusedSpatialCategoryDefaultSubCategoryId = unusedSpatialCategory.myDefaultSubCategoryId();
407
+ assert.isTrue(Id64.isValidId64(unusedSpatialCategoryDefaultSubCategoryId));
408
+ assert.throws(() => txn.deleteElement(unusedSpatialCategoryId));
409
+ assert.throws(() => txn.deleteElement(unusedSpatialCategoryDefaultSubCategoryId));
410
+ expectDelete([unusedSpatialCategoryId], success, [unusedSpatialCategoryId, unusedSpatialCategoryDefaultSubCategoryId], []);
411
+ txn.abandonChanges();
412
+ // deleteElement/deleteElements of a used DrawingCategory should fail (hard FK from GeometricElement2d.CategoryId, so not DefinitionInUse)
413
+ assert.throws(() => txn.deleteElement(drawingCategoryId));
414
+ expectDelete([drawingCategoryId], constraintViolation(categoryInUse2d(drawingCategoryId)), [], [drawingCategoryId]);
415
+ txn.abandonChanges();
416
+ // deleteElements for an unused DrawingCategory should succeed, delete should still fail
417
+ const unusedDrawingCategoryId = DrawingCategory.insert(txn, definitionModelId, "Unused DrawingCategory", {});
418
+ assert.isTrue(Id64.isValidId64(unusedDrawingCategoryId));
419
+ const unusedDrawingCategory = iModelDb.elements.getElement(unusedDrawingCategoryId, DrawingCategory);
420
+ const unusedDrawingSubCategoryId = unusedDrawingCategory.myDefaultSubCategoryId();
421
+ assert.isTrue(Id64.isValidId64(unusedDrawingSubCategoryId));
422
+ assert.throws(() => txn.deleteElement(unusedDrawingSubCategoryId));
423
+ expectDelete([unusedDrawingCategoryId], success, [unusedDrawingCategoryId, unusedDrawingSubCategoryId], []);
424
+ txn.abandonChanges();
425
+ // deleteElement/deleteElements of DefinitionElements used by an existing SpatialViewDefinition should fail
426
+ assert.throws(() => txn.deleteElement(spatialCategorySelectorId));
427
+ assert.throws(() => txn.deleteElement(modelSelectorId));
428
+ assert.throws(() => txn.deleteElement(displayStyle3dId));
429
+ expectDelete([spatialCategorySelectorId, modelSelectorId, displayStyle3dId], constraintViolation(definitionInUse(spatialCategorySelectorId), definitionInUse(modelSelectorId), definitionInUse(displayStyle3dId)), [], [spatialCategorySelectorId, modelSelectorId, displayStyle3dId, viewId]);
430
+ txn.abandonChanges();
431
+ // deleteElements should succeed when the list includes the SpatialViewDefinition as the only thing referencing other DefinitionElements, delete should still fail
432
+ assert.throws(() => txn.deleteElement(viewId));
433
+ expectDelete([viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId], success, [viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId], []);
434
+ txn.abandonChanges();
435
+ // deleteElement/deleteElements of DefinitionElements used by an existing DrawingViewDefinition should fail
436
+ assert.throws(() => txn.deleteElement(drawingCategorySelectorId));
437
+ assert.throws(() => txn.deleteElement(displayStyle2dId));
438
+ expectDelete([drawingCategorySelectorId, displayStyle2dId], constraintViolation(definitionInUse(drawingCategorySelectorId), definitionInUse(displayStyle2dId)), [], [drawingCategorySelectorId, displayStyle2dId, drawingViewId]);
439
+ txn.abandonChanges();
440
+ // deleteElements should succeed when the list includes the DrawingViewDefinition as the only thing referencing other DefinitionElements, delete should still fail
441
+ assert.throws(() => txn.deleteElement(drawingViewId));
442
+ expectDelete([drawingViewId, drawingCategorySelectorId, displayStyle2dId], success, [drawingViewId, drawingCategorySelectorId, displayStyle2dId], []);
443
+ }
444
+ finally {
445
+ if (txn.isActive)
446
+ txn.end("abandon");
447
+ }
448
+ });
449
+ it("deleting a Subject is rejected when a definition beneath it is used from outside its subtree", () => {
450
+ const txn = new EditTxn(iModelDb, "delete subject with outside user");
451
+ txn.start();
452
+ try {
453
+ // A PhysicalObject in a model that is NOT under the Subject uses the SpatialCategory. The category is a hard FK violator
454
+ // (bis_GeometricElement3d.CategoryId, so ReferencedByForeignKey rather than DefinitionInUse). Its SubCategories are only
455
+ // used by elements inside the Subject's subtree, so they are not reported. The whole call is rejected - the requested
456
+ // PhysicalObjects survive as well, even though nothing references them.
457
+ const outsideModelId = PhysicalModel.insert(txn, IModel.rootSubjectId, "OutsideSubject");
458
+ const outsideUserProps = {
459
+ classFullName: "Generic:PhysicalObject",
460
+ model: outsideModelId,
461
+ category: spatialCategoryId,
462
+ code: Code.createEmpty(),
463
+ placement: { origin: Point3d.create(0, 0, 0).toJSON(), angles: {} },
464
+ };
465
+ const outsideUserId = txn.insertElement(outsideUserProps);
466
+ txn.saveChanges();
467
+ executeTestCase(txn, "subject rejected by outside user of its category", [physicalObjectId1, physicalObjectId2, physicalObjectId3, subjectId], [], constraintViolation(categoryInUse3d(spatialCategoryId)), [physicalObjectId1, physicalObjectId2, physicalObjectId3, subjectId, definitionModelId, physicalModelId, spatialCategoryId, IModel.getDefaultSubCategoryId(spatialCategoryId), outsideUserId]);
468
+ }
469
+ finally {
470
+ if (txn.isActive)
471
+ txn.end("abandon");
472
+ }
462
473
  });
463
474
  describe("basic tests", () => {
464
475
  let txn;
@@ -471,28 +482,25 @@ describe("DeleteDefinitionElements", () => {
471
482
  });
472
483
  it("deletes a single unused definition element", () => {
473
484
  const element = insertDefinitionElement(txn);
474
- executeTestCase(txn, "single unused definition element", [element], [element], [], []);
485
+ executeTestCase(txn, "single unused definition element", [element], [element], success, []);
475
486
  });
476
487
  it("deletes multiple unused definition elements in one call", () => {
477
488
  const element1 = insertDefinitionElement(txn);
478
489
  const element2 = insertDefinitionElement(txn);
479
490
  const element3 = insertDefinitionElement(txn);
480
- executeTestCase(txn, "multiple unused definition elements", [element1, element2, element3], [element1, element2, element3], [], []);
491
+ executeTestCase(txn, "multiple unused definition elements", [element1, element2, element3], [element1, element2, element3], success, []);
481
492
  });
482
- it("returns empty failed-set when given an empty array", () => {
483
- const failed = txn.deleteElements([]).failedIds;
484
- assert.isEmpty(Array.from(failed), "empty input should return empty failed set");
493
+ it("returns a Success result when given an empty array", () => {
494
+ executeTestCase(txn, "empty input", [], [], success, []);
485
495
  });
486
496
  it("throws for invalid ids", () => {
487
497
  assert.throws(() => txn.deleteElements([Id64.invalid, Id64.invalid]));
488
498
  });
489
- it("keeps an in-use definition element in the failed set and does not delete it", () => {
499
+ it("reports an in-use definition element as a failure and does not delete it", () => {
490
500
  // geometryPartId is referenced by PhysicalObject1's geometry stream
491
- const failed = txn.deleteElements([geometryPartId]);
492
- assert.isTrue(failed.failedIds.has(geometryPartId), "in-use definition element must be in failed set");
493
- assert.isDefined(iModelDb.elements.tryGetElement(geometryPartId), "in-use definition element must not be deleted");
501
+ executeTestCase(txn, "in-use GeometryPart", [geometryPartId], [], constraintViolation(definitionInUse(geometryPartId)), [geometryPartId]);
494
502
  });
495
- it("returns only the in-use definition element in the failed set when mixing used and unused parts", () => {
503
+ it("reports only the in-use definition element as a failure when mixing used and unused parts, and deletes nothing", () => {
496
504
  const unusedElement = insertDefinitionElement(txn);
497
505
  const usedElement = insertDefinitionElement(txn);
498
506
  // Reference usedElement from a physical element geometry stream
@@ -505,27 +513,23 @@ describe("DeleteDefinitionElements", () => {
505
513
  geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1), spatialCategoryId, undefined, undefined, usedElement),
506
514
  };
507
515
  const physElemId = txn.insertElement(physElemProps);
508
- executeTestCase(txn, "used + unused definition elements", [unusedElement, usedElement], [unusedElement], [usedElement], [usedElement, physElemId]);
516
+ executeTestCase(txn, "used + unused definition elements", [unusedElement, usedElement], [], constraintViolation(definitionInUse(usedElement)), [unusedElement, usedElement, physElemId]);
509
517
  });
510
- it("keeps an in-use RenderMaterial in the failed set and does not delete it", () => {
511
- const failed = txn.deleteElements([renderMaterialId]);
512
- assert.isTrue(failed.failedIds.has(renderMaterialId), "in-use RenderMaterial must be in failed set");
513
- assert.isDefined(iModelDb.elements.tryGetElement(renderMaterialId), "in-use RenderMaterial must not be deleted");
518
+ it("reports an in-use RenderMaterial as a failure and does not delete it", () => {
519
+ executeTestCase(txn, "in-use RenderMaterial", [renderMaterialId], [], constraintViolation(definitionInUse(renderMaterialId)), [renderMaterialId]);
514
520
  });
515
521
  it("deletes an unused RenderMaterial", () => {
516
522
  const unusedRenderMaterialId = RenderMaterialElement.insert(txn, definitionModelId, "Unused RenderMaterial", { paletteName: "PaletteName" });
517
- executeTestCase(txn, "unused RenderMaterial", [unusedRenderMaterialId], [unusedRenderMaterialId], [], []);
523
+ executeTestCase(txn, "unused RenderMaterial", [unusedRenderMaterialId], [unusedRenderMaterialId], success, []);
518
524
  });
519
- it("keeps an in-use Texture in the failed set and does not delete it", () => {
520
- const failed = txn.deleteElements([textureId]);
521
- assert.isTrue(failed.failedIds.has(textureId), "in-use Texture must be in failed set");
522
- assert.isDefined(iModelDb.elements.tryGetElement(textureId), "in-use Texture must not be deleted");
525
+ it("reports an in-use Texture as a failure and does not delete it", () => {
526
+ executeTestCase(txn, "in-use Texture", [textureId], [], constraintViolation(definitionInUse(textureId)), [textureId]);
523
527
  });
524
528
  it("deletes an unused Texture", () => {
525
529
  const unusedTextureId = IModelTestUtils.insertTextureElement(txn, definitionModelId, "Unused Texture");
526
- executeTestCase(txn, "unused Texture", [unusedTextureId], [unusedTextureId], [], []);
530
+ executeTestCase(txn, "unused Texture", [unusedTextureId], [unusedTextureId], success, []);
527
531
  });
528
- it("returns the full input as the failed set when every element is an in-use definition element", () => {
532
+ it("reports every element as a failure when all are in-use definition elements", () => {
529
533
  const usedElement1 = insertDefinitionElement(txn);
530
534
  const usedElement2 = insertDefinitionElement(txn);
531
535
  const usedElement3 = insertDefinitionElement(txn);
@@ -543,11 +547,33 @@ describe("DeleteDefinitionElements", () => {
543
547
  assert.isTrue(Id64.isValid(txn.insertElement(physElemProps)));
544
548
  physElemProps.geom = IModelTestUtils.createBox(Point3d.create(1, 1, 1), spatialCategoryId, undefined, undefined, usedElement3);
545
549
  assert.isTrue(Id64.isValid(txn.insertElement(physElemProps)));
546
- const failedToDelete = txn.deleteElements([usedElement1, usedElement2, usedElement3]);
547
- assert.equal(failedToDelete.failedIds.size, 3, "all in-use definition elements should be in failed set");
548
- assert.isTrue(failedToDelete.failedIds.has(usedElement1));
549
- assert.isTrue(failedToDelete.failedIds.has(usedElement2));
550
- assert.isTrue(failedToDelete.failedIds.has(usedElement3));
550
+ // Every requested element is in use - all three are reported and nothing is deleted.
551
+ executeTestCase(txn, "all in-use", [usedElement1, usedElement2, usedElement3], [], constraintViolation(definitionInUse(usedElement1), definitionInUse(usedElement2), definitionInUse(usedElement3)), [usedElement1, usedElement2, usedElement3]);
552
+ });
553
+ it("a definition used only by elements inside the delete set is not DefinitionInUse", () => {
554
+ const insertPhysicalObject = (model, category) => {
555
+ const props = {
556
+ classFullName: "Generic:PhysicalObject",
557
+ model,
558
+ category,
559
+ code: Code.createEmpty(),
560
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
561
+ };
562
+ return txn.insertElement(props);
563
+ };
564
+ // Case 1: the only user of the category is requested directly.
565
+ const directCategory = SpatialCategory.insert(txn, definitionModelId, "DirectUserCategory", new SubCategoryAppearance());
566
+ const directUser = insertPhysicalObject(physicalModelId, directCategory);
567
+ // Case 2: the only user of the category lives in a sub-model that is reached through the partition cascade, i.e. it is in the
568
+ // *expanded* delete set but not in the requested ids.
569
+ const cascadeCategory = SpatialCategory.insert(txn, definitionModelId, "CascadeUserCategory", new SubCategoryAppearance());
570
+ const partitionId = PhysicalModel.insert(txn, subjectId, "CascadeUserModel");
571
+ const cascadeUser = insertPhysicalObject(partitionId, cascadeCategory);
572
+ txn.saveChanges();
573
+ const directSubCategory = IModel.getDefaultSubCategoryId(directCategory);
574
+ const cascadeSubCategory = IModel.getDefaultSubCategoryId(cascadeCategory);
575
+ executeTestCase(txn, "category + its direct user", [directCategory, directUser], [directCategory, directSubCategory, directUser], success, [cascadeCategory, cascadeSubCategory, cascadeUser]);
576
+ executeTestCase(txn, "category + partition containing its only user", [cascadeCategory, partitionId], [cascadeCategory, cascadeSubCategory, partitionId, cascadeUser], success, [directCategory, directSubCategory, directUser]);
551
577
  });
552
578
  });
553
579
  describe("category and subcategory tests", () => {
@@ -559,53 +585,47 @@ describe("DeleteDefinitionElements", () => {
559
585
  afterEach(() => {
560
586
  txn.end("abandon");
561
587
  });
562
- it("keeps an in-use SpatialCategory in the failed set and does not delete it", () => {
563
- const failed = txn.deleteElements([spatialCategoryId]);
564
- assert.isTrue(failed.failedIds.has(spatialCategoryId), "in-use SpatialCategory must be in failed set");
565
- assert.isDefined(iModelDb.elements.tryGetElement(spatialCategoryId), "in-use SpatialCategory must not be deleted");
588
+ it("reports an in-use SpatialCategory as an FK violator (plus its used SubCategories) and does not delete it", () => {
589
+ executeTestCase(txn, "in-use SpatialCategory", [spatialCategoryId], [], constraintViolation(...usedSpatialCategoryViolations()), [spatialCategoryId, subCategoryId, filteredSubCategoryId]);
566
590
  });
567
- it("keeps the default SubCategory in the failed set when its parent SpatialCategory is in use", () => {
591
+ it("reports the default SubCategory as a failure when its parent SpatialCategory is in use", () => {
568
592
  const spatialCategory = iModelDb.elements.getElement(spatialCategoryId, SpatialCategory);
569
593
  const defaultSubCategoryId = spatialCategory.myDefaultSubCategoryId();
570
- const failed = txn.deleteElements([defaultSubCategoryId]);
571
- assert.isTrue(failed.failedIds.has(defaultSubCategoryId), "default SubCategory must be in failed set");
572
- assert.isDefined(iModelDb.elements.tryGetElement(defaultSubCategoryId), "default SubCategory must not be deleted");
594
+ executeTestCase(txn, "default SubCategory", [defaultSubCategoryId], [], constraintViolation(definitionInUse(defaultSubCategoryId)), [defaultSubCategoryId]);
573
595
  });
574
- it("keeps a used non-default SubCategory in the failed set and does not delete it", () => {
575
- const failed = txn.deleteElements([subCategoryId]);
576
- assert.isTrue(failed.failedIds.has(subCategoryId), "in-use SubCategory must be in failed set");
577
- assert.isDefined(iModelDb.elements.tryGetElement(subCategoryId), "in-use SubCategory must not be deleted");
596
+ it("reports a used non-default SubCategory as a failure and does not delete it", () => {
597
+ executeTestCase(txn, "in-use SubCategory", [subCategoryId], [], constraintViolation(definitionInUse(subCategoryId)), [subCategoryId]);
578
598
  });
579
599
  it("deletes an unused non-default SubCategory", () => {
580
600
  const unusedSubCategoryId = SubCategory.insert(txn, spatialCategoryId, "Unused SubCategory", {});
581
- executeTestCase(txn, "unused SubCategory", [unusedSubCategoryId], [unusedSubCategoryId], [], [spatialCategoryId]);
601
+ executeTestCase(txn, "unused SubCategory", [unusedSubCategoryId], [unusedSubCategoryId], success, [spatialCategoryId]);
582
602
  });
583
603
  it("deletes an unused SpatialCategory together with its default SubCategory", () => {
584
604
  const unusedCategoryId = SpatialCategory.insert(txn, definitionModelId, "Unused SpatialCategory", {});
585
605
  const unusedCategory = iModelDb.elements.getElement(unusedCategoryId, SpatialCategory);
586
606
  const unusedDefaultSubCategoryId = unusedCategory.myDefaultSubCategoryId();
587
- executeTestCase(txn, "unused SpatialCategory + default SubCategory", [unusedCategoryId], [unusedCategoryId, unusedDefaultSubCategoryId], [], []);
607
+ executeTestCase(txn, "unused SpatialCategory + default SubCategory", [unusedCategoryId], [unusedCategoryId, unusedDefaultSubCategoryId], success, []);
588
608
  });
589
609
  it("deletes an unused DrawingCategory together with its default SubCategory", () => {
590
610
  const unusedDrawingCategoryId = DrawingCategory.insert(txn, definitionModelId, "Unused DrawingCategory", {});
591
611
  const unusedDrawingCategory = iModelDb.elements.getElement(unusedDrawingCategoryId, DrawingCategory);
592
612
  const unusedDrawingSubCategoryId = unusedDrawingCategory.myDefaultSubCategoryId();
593
- executeTestCase(txn, "unused DrawingCategory + default SubCategory", [unusedDrawingCategoryId], [unusedDrawingCategoryId, unusedDrawingSubCategoryId], [], []);
613
+ executeTestCase(txn, "unused DrawingCategory + default SubCategory", [unusedDrawingCategoryId], [unusedDrawingCategoryId, unusedDrawingSubCategoryId], success, []);
594
614
  });
595
- it("keeps CategorySelector, ModelSelector, DisplayStyle3d in the failed set when their view still exists", () => {
596
- executeTestCase(txn, "view-related still referenced", [spatialCategorySelectorId, modelSelectorId, displayStyle3dId], [], [spatialCategorySelectorId, modelSelectorId, displayStyle3dId], [spatialCategorySelectorId, modelSelectorId, displayStyle3dId, viewId]);
615
+ it("reports CategorySelector, ModelSelector, DisplayStyle3d as failures when their view still exists", () => {
616
+ executeTestCase(txn, "view-related still referenced", [spatialCategorySelectorId, modelSelectorId, displayStyle3dId], [], constraintViolation(definitionInUse(spatialCategorySelectorId), definitionInUse(modelSelectorId), definitionInUse(displayStyle3dId)), [spatialCategorySelectorId, modelSelectorId, displayStyle3dId, viewId]);
597
617
  });
598
618
  it("deletes CategorySelector, ModelSelector, DisplayStyle3d and the SpatialViewDefinition in one call", () => {
599
- executeTestCase(txn, "view + view-related", [viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId], [viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId], [], []);
619
+ executeTestCase(txn, "view + view-related", [viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId], [viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId], success, []);
600
620
  });
601
621
  it("deletes DrawingCategorySelector, DisplayStyle2d and the DrawingViewDefinition in one call", () => {
602
- executeTestCase(txn, "drawing view + drawing view-related", [drawingViewId, drawingCategorySelectorId, displayStyle2dId], [drawingViewId, drawingCategorySelectorId, displayStyle2dId], [], []);
622
+ executeTestCase(txn, "drawing view + drawing view-related", [drawingViewId, drawingCategorySelectorId, displayStyle2dId], [drawingViewId, drawingCategorySelectorId, displayStyle2dId], success, []);
603
623
  });
604
624
  it("deletes orphaned CategorySelector and DisplayStyle2d (no view references them)", () => {
605
625
  // Insert a standalone CategorySelector and DisplayStyle2d that are not referenced by any view
606
626
  const orphanCategorySelectorId = CategorySelector.insert(txn, definitionModelId, "OrphanCategorySelector", [drawingCategoryId]);
607
627
  const orphanDisplayStyle2dId = DisplayStyle2d.insert(txn, definitionModelId, "OrphanDisplayStyle2d");
608
- executeTestCase(txn, "orphaned view-related elements", [orphanCategorySelectorId, orphanDisplayStyle2dId], [orphanCategorySelectorId, orphanDisplayStyle2dId], [], []);
628
+ executeTestCase(txn, "orphaned view-related elements", [orphanCategorySelectorId, orphanDisplayStyle2dId], [orphanCategorySelectorId, orphanDisplayStyle2dId], success, []);
609
629
  });
610
630
  });
611
631
  describe("Element hierarchy", () => {
@@ -621,13 +641,13 @@ describe("DeleteDefinitionElements", () => {
621
641
  const parentPart = insertDefinitionElement(txn);
622
642
  const childPart = insertDefinitionElement(txn, parentPart);
623
643
  // Only pass the parent - the child must be pulled in automatically
624
- executeTestCase(txn, "parent + child definition elements", [parentPart], [parentPart, childPart], [], []);
644
+ executeTestCase(txn, "parent + child definition elements", [parentPart], [parentPart, childPart], success, []);
625
645
  });
626
646
  it("deletes a parent DefinitionElement and its child explicitly passed", () => {
627
647
  const parentPart = insertDefinitionElement(txn);
628
648
  const childPart = insertDefinitionElement(txn, parentPart);
629
649
  // Pass both parent and child explicitly
630
- executeTestCase(txn, "parent + child definition elements", [parentPart, childPart], [parentPart, childPart], [], []);
650
+ executeTestCase(txn, "parent + child definition elements", [parentPart, childPart], [parentPart, childPart], success, []);
631
651
  });
632
652
  // Intra-set code scope
633
653
  // Both partA and partB are in the input set; partB uses partA as its CodeScope.
@@ -644,7 +664,7 @@ describe("DeleteDefinitionElements", () => {
644
664
  };
645
665
  const partB = txn.insertElement(partBProps);
646
666
  // Both unused externally; the intra-set code-scope link must not block deletion
647
- executeTestCase(txn, "intra-set code scope", [partA, partB], [partA, partB], [], []);
667
+ executeTestCase(txn, "intra-set code scope", [partA, partB], [partA, partB], success, []);
648
668
  });
649
669
  // Multi-level parent-child hierarchy
650
670
  // Passing only the root must automatically pull in all descendants (leaf-first).
@@ -655,14 +675,14 @@ describe("DeleteDefinitionElements", () => {
655
675
  const child = insertDefinitionElement(txn, root);
656
676
  const grandchild = insertDefinitionElement(txn, child);
657
677
  // Only supply the root - all descendants must be included and deleted leaf-first
658
- executeTestCase(txn, "3-level hierarchy via root only", [root], [root, child, grandchild], [], []);
678
+ executeTestCase(txn, "3-level hierarchy via root only", [root], [root, child, grandchild], success, []);
659
679
  });
660
680
  it("deletes a 3-level parent-child hierarchy by passing all elements explicitly", () => {
661
681
  const root = insertDefinitionElement(txn);
662
682
  const child = insertDefinitionElement(txn, root);
663
683
  const grandchild = insertDefinitionElement(txn, child);
664
684
  // Pass all elements explicitly
665
- executeTestCase(txn, "3-level hierarchy with all elements", [root, child, grandchild], [root, child, grandchild], [], []);
685
+ executeTestCase(txn, "3-level hierarchy with all elements", [root, child, grandchild], [root, child, grandchild], success, []);
666
686
  });
667
687
  // Mixed inputs
668
688
  it("deletes a mixed batch of unused DefinitionElements of different types in one call", () => {
@@ -670,7 +690,7 @@ describe("DeleteDefinitionElements", () => {
670
690
  const unusedRenderMaterial = RenderMaterialElement.insert(txn, definitionModelId, "BatchRenderMaterial", { paletteName: "P" });
671
691
  const unusedTexture = IModelTestUtils.insertTextureElement(txn, definitionModelId, "BatchTexture");
672
692
  const unusedSubCategory = SubCategory.insert(txn, spatialCategoryId, "BatchSubCategory", {});
673
- executeTestCase(txn, "mixed unused batch", [unusedPart, unusedRenderMaterial, unusedTexture, unusedSubCategory], [unusedPart, unusedRenderMaterial, unusedTexture, unusedSubCategory], [], []);
693
+ executeTestCase(txn, "mixed unused batch", [unusedPart, unusedRenderMaterial, unusedTexture, unusedSubCategory], [unusedPart, unusedRenderMaterial, unusedTexture, unusedSubCategory], success, []);
674
694
  });
675
695
  // Mixed inputs with intra scope violations and parent child hierarchies
676
696
  it("deletes a mixed batch of used and unused DefinitionElements in a hierarchy with conflicting code scopes with child", () => {
@@ -686,7 +706,7 @@ describe("DeleteDefinitionElements", () => {
686
706
  };
687
707
  const scopedPart = txn.insertElement(scopedPartProps);
688
708
  // Pass both used and unused parts, with a child that has a code scope reference
689
- executeTestCase(txn, "mixed used and unused batch", [usedPart, unusedPart, scopedPart], [usedPart, unusedPart, childPart, scopedPart], [], []);
709
+ executeTestCase(txn, "mixed used and unused batch", [usedPart, unusedPart, scopedPart], [usedPart, unusedPart, childPart, scopedPart], success, []);
690
710
  });
691
711
  // Mixed inputs with intra scope violations and parent child hierarchies
692
712
  it("deletes a mixed batch of used and unused DefinitionElements in a hierarchy with conflicting code scopes", () => {
@@ -701,8 +721,9 @@ describe("DeleteDefinitionElements", () => {
701
721
  geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)),
702
722
  };
703
723
  const scopedPart = txn.insertElement(scopedPartProps);
704
- // Pass both used and unused parts, with a child that has a code scope reference
705
- executeTestCase(txn, "mixed used and unused batch", [usedPart, unusedPart], [unusedPart], [usedPart], [usedPart, childPart, scopedPart]);
724
+ // Pass both used and unused parts; usedPart is the code scope of scopedPart (outside the set) so it is a direct FK violator
725
+ // and the whole call is rejected (unusedPart survives too).
726
+ executeTestCase(txn, "mixed used and unused batch", [usedPart, unusedPart], [], constraintViolation(codeScopeViolator(usedPart)), [usedPart, unusedPart, childPart, scopedPart]);
706
727
  });
707
728
  });
708
729
  describe("sub-model hierarchy", () => {
@@ -729,18 +750,18 @@ describe("DeleteDefinitionElements", () => {
729
750
  const containerId = insertDefinitionContainer();
730
751
  const partA = insertDefinitionElement(txn, undefined, containerId);
731
752
  const partB = insertDefinitionElement(txn, undefined, containerId);
732
- executeTestCase(txn, "unused elements in container sub-model", [partA, partB], [partA, partB], [], [], false);
753
+ executeTestCase(txn, "unused elements in container sub-model", [partA, partB], [partA, partB], success, [], false);
733
754
  assertModelExists(containerId, "DefinitionContainer must survive when container is not in the input set");
734
755
  });
735
756
  it("deletes a DefinitionContainer and all definition elements inside it", () => {
736
757
  const containerId = insertDefinitionContainer();
737
758
  const defElem1 = insertDefinitionElement(txn, undefined, containerId);
738
759
  const defElem2 = insertDefinitionElement(txn, undefined, containerId);
739
- executeTestCase(txn, "container + sub-model contents deleted via container id", [containerId], [containerId, defElem1, defElem2], [], []);
760
+ executeTestCase(txn, "container + sub-model contents deleted via container id", [containerId], [containerId, defElem1, defElem2], success, []);
740
761
  // Sub-model row is deleted once the container element is successfully purged.
741
762
  assertModelDeleted(containerId, "DefinitionModel sub-model row must be deleted after container is purged");
742
763
  });
743
- it("keeps an in-use element inside a DefinitionContainer sub-model in the failed set", () => {
764
+ it("reports an in-use element inside a DefinitionContainer sub-model as a failure", () => {
744
765
  const containerId = insertDefinitionContainer();
745
766
  const usedPart = insertDefinitionElement(txn, undefined, containerId);
746
767
  const unusedPart = insertDefinitionElement(txn, undefined, containerId);
@@ -754,10 +775,10 @@ describe("DeleteDefinitionElements", () => {
754
775
  geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1), spatialCategoryId, undefined, undefined, usedPart),
755
776
  };
756
777
  const physElemId = txn.insertElement(physElemProps);
757
- executeTestCase(txn, "one used + one unused in container sub-model", [usedPart, unusedPart], [unusedPart], [usedPart], [usedPart, physElemId], false);
758
- assertModelExists(containerId, "DefinitionContainer sub-model row must survive when an element inside it is blocked");
778
+ executeTestCase(txn, "one used + one unused in container sub-model", [usedPart, unusedPart], [], constraintViolation(definitionInUse(usedPart)), [usedPart, unusedPart, physElemId], false);
779
+ assertModelExists(containerId, "DefinitionContainer sub-model row must survive when the call is rejected");
759
780
  });
760
- it("parent blocked in failed set when its child inside a DefinitionContainer sub-model is in use", () => {
781
+ it("call rejected when the child of a requested parent inside a DefinitionContainer sub-model is in use", () => {
761
782
  const containerId = insertDefinitionContainer();
762
783
  const parent = insertDefinitionElement(txn, undefined, containerId);
763
784
  const child = insertDefinitionElement(txn, parent, containerId);
@@ -771,8 +792,9 @@ describe("DeleteDefinitionElements", () => {
771
792
  geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1), spatialCategoryId, undefined, undefined, child),
772
793
  };
773
794
  const physElemId = txn.insertElement(physElemProps);
774
- executeTestCase(txn, "parent blocked when child inside container sub-model is in use", [parent], [], [parent], [parent, child, physElemId], false);
775
- assertModelExists(containerId, "DefinitionContainer sub-model row must survive when elements inside it are blocked");
795
+ // child (pulled into the set via parent) is a DefinitionInUse violator; only the direct violator is reported.
796
+ executeTestCase(txn, "parent rejected when child inside container sub-model is in use", [parent], [], constraintViolation(definitionInUse(child)), [parent, child, physElemId], false);
797
+ assertModelExists(containerId, "DefinitionContainer sub-model row must survive when the call is rejected");
776
798
  });
777
799
  it("deletes two elements inside a DefinitionContainer sub-model related by intra-delete-set code scope", () => {
778
800
  const containerId = insertDefinitionContainer();
@@ -785,10 +807,10 @@ describe("DeleteDefinitionElements", () => {
785
807
  geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)),
786
808
  };
787
809
  const partB = txn.insertElement(partBProps);
788
- executeTestCase(txn, "intra-set code scope in container sub-model", [partA, partB], [partA, partB], [], [], false);
810
+ executeTestCase(txn, "intra-set code scope in container sub-model", [partA, partB], [partA, partB], success, [], false);
789
811
  assertModelExists(containerId, "DefinitionContainer sub-model row must survive when container is not in the input set");
790
812
  });
791
- it("element inside a DefinitionContainer sub-model blocked when used as external code scope", () => {
813
+ it("element inside a DefinitionContainer sub-model rejects the call when used as external code scope", () => {
792
814
  const containerId = insertDefinitionContainer();
793
815
  const scopeSpecId = iModelDb.codeSpecs.insert(txn, "ExtScopeSpec", CodeScopeSpec.Type.RelatedElement);
794
816
  const partA = insertDefinitionElement(txn, undefined, containerId);
@@ -801,8 +823,8 @@ describe("DeleteDefinitionElements", () => {
801
823
  geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)),
802
824
  };
803
825
  const externalId = txn.insertElement(externalProps);
804
- executeTestCase(txn, "external code scope blocks element inside container sub-model", [partA, partB], [partB], [partA], [partA, externalId], false);
805
- assertModelExists(containerId, "DefinitionContainer sub-model row must survive when an element inside it is blocked");
826
+ executeTestCase(txn, "external code scope rejects element inside container sub-model", [partA, partB], [], constraintViolation(codeScopeViolator(partA)), [partA, partB, externalId], false);
827
+ assertModelExists(containerId, "DefinitionContainer sub-model row must survive when the call is rejected");
806
828
  });
807
829
  it("cross-container intra-set code scope: both elements deleted cleanly", () => {
808
830
  const container1 = insertDefinitionContainer();
@@ -816,11 +838,11 @@ describe("DeleteDefinitionElements", () => {
816
838
  geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)),
817
839
  };
818
840
  const partB = txn.insertElement(partBProps);
819
- executeTestCase(txn, "cross-container intra-set code scope", [partA, partB], [partA, partB], [], [], false);
841
+ executeTestCase(txn, "cross-container intra-set code scope", [partA, partB], [partA, partB], success, [], false);
820
842
  assertModelExists(container1, "container1 sub-model row must survive when container is not in the input set");
821
843
  assertModelExists(container2, "container2 sub-model row must survive when container is not in the input set");
822
844
  });
823
- it("mixed elements where in-use ones get blocked/ignored, unused get deleted", () => {
845
+ it("mixed elements where in-use ones reject the call and nothing is deleted", () => {
824
846
  const containerId = insertDefinitionContainer();
825
847
  const topUnused = insertDefinitionElement(txn, undefined, definitionModelId);
826
848
  const topUsed = insertDefinitionElement(txn, undefined, definitionModelId);
@@ -839,8 +861,141 @@ describe("DeleteDefinitionElements", () => {
839
861
  };
840
862
  const topUsedPhysElem = makeUser(topUsed);
841
863
  const nestedUsedPhysElem = makeUser(nestedUsed);
842
- executeTestCase(txn, "mixed top-level and container sub-model: used blocked, unused deleted", [topUnused, topUsed, nestedUnused, nestedUsed], [topUnused, nestedUnused], [topUsed, nestedUsed], [topUsed, nestedUsed, topUsedPhysElem, nestedUsedPhysElem], false);
843
- assertModelExists(containerId, "DefinitionContainer sub-model row must survive when an element inside it is blocked");
864
+ executeTestCase(txn, "mixed top-level and container sub-model: used reported, nothing deleted", [topUnused, topUsed, nestedUnused, nestedUsed], [], constraintViolation(definitionInUse(topUsed), definitionInUse(nestedUsed)), [topUnused, topUsed, nestedUnused, nestedUsed, topUsedPhysElem, nestedUsedPhysElem], false);
865
+ assertModelExists(containerId, "DefinitionContainer sub-model row must survive when the call is rejected");
866
+ });
867
+ });
868
+ describe("definition and its users deleted together", () => {
869
+ let txn;
870
+ beforeEach(() => {
871
+ txn = new EditTxn(iModelDb, "delete definition + users");
872
+ txn.start();
873
+ });
874
+ afterEach(() => {
875
+ txn.end("abandon");
876
+ });
877
+ const insertPhysicalObject = (opts = {}) => {
878
+ const category = opts.categoryId ?? spatialCategoryId;
879
+ // A material is only recorded in the geometry stream together with a sub-category.
880
+ const subCategory = opts.subCategoryId ?? (opts.renderMaterialId ? IModel.getDefaultSubCategoryId(category) : undefined);
881
+ return txn.insertElement({
882
+ classFullName: "Generic:PhysicalObject",
883
+ model: physicalModelId,
884
+ category,
885
+ code: Code.createEmpty(),
886
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
887
+ geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1), category, subCategory, opts.renderMaterialId, opts.geometryPartId),
888
+ });
889
+ };
890
+ it("a GeometryPart with two users is still in use when only one user is in the delete set", () => {
891
+ const part = insertDefinitionElement(txn);
892
+ const user1 = insertPhysicalObject({ geometryPartId: part });
893
+ const user2 = insertPhysicalObject({ geometryPartId: part });
894
+ executeTestCase(txn, "part + one of two users", [part, user1], [], constraintViolation(definitionInUse(part)), [part, user1, user2]);
895
+ });
896
+ it("a DisplayStyle shared by two views is reported in use when only one view is in the delete set", () => {
897
+ const categorySelectorB = CategorySelector.insert(txn, definitionModelId, "CategorySelectorB", [spatialCategoryId]);
898
+ const modelSelectorB = ModelSelector.insert(txn, definitionModelId, "ModelSelectorB", [physicalModelId]);
899
+ const viewB = OrthographicViewDefinition.insert(txn, definitionModelId, "ViewB", modelSelectorB, categorySelectorB, displayStyle3dId, new Range3d(0, 0, 0, 1, 1, 1));
900
+ executeTestCase(txn, "view A + its selectors + shared style", [viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId], [], constraintViolation(definitionInUse(displayStyle3dId)), [viewId, spatialCategorySelectorId, modelSelectorId, displayStyle3dId, viewB, categorySelectorB, modelSelectorB], false);
901
+ // Deleting both views releases the shared style.
902
+ executeTestCase(txn, "both views + everything they reference", [viewId, viewB, spatialCategorySelectorId, modelSelectorId, categorySelectorB, modelSelectorB, displayStyle3dId], [viewId, viewB, spatialCategorySelectorId, modelSelectorId, categorySelectorB, modelSelectorB, displayStyle3dId], success, []);
903
+ });
904
+ it("an element referenced through several foreign keys is reported once", () => {
905
+ // A fresh category used by a physical object (CategoryId FK) AND used as the Code scope of a GeometryPart (CodeScopeId FK).
906
+ const category = SpatialCategory.insert(txn, definitionModelId, "DoublyReferencedCategory", {});
907
+ const user = insertPhysicalObject({ categoryId: category });
908
+ const scopeSpecId = iModelDb.codeSpecs.insert(txn, "CategoryScopedSpec", CodeScopeSpec.Type.RelatedElement);
909
+ const scopedPart = txn.insertElement({
910
+ classFullName: GeometryPart.classFullName,
911
+ model: definitionModelId,
912
+ code: { spec: scopeSpecId, scope: category, value: "scoped-part" },
913
+ geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)),
914
+ });
915
+ const result = txn.deleteElements([category]);
916
+ assert.equal(result.status, BulkDeleteElementsStatus.ConstraintViolation);
917
+ assert.equal(result.failures.length, 1, `expected a single failure, got ${JSON.stringify(result.failures)}`);
918
+ assert.equal(result.failures[0].elementId, category);
919
+ assert.equal(result.failures[0].reason, BulkDeleteFailureReason.ReferencedByForeignKey);
920
+ for (const id of [category, user, scopedPart])
921
+ assert.isDefined(iModelDb.elements.tryGetElement(id));
922
+ });
923
+ });
924
+ describe("all-or-nothing semantics with definition elements", () => {
925
+ let txn;
926
+ beforeEach(() => {
927
+ txn = new EditTxn(iModelDb, "all-or-nothing");
928
+ txn.start();
929
+ });
930
+ afterEach(() => {
931
+ txn.end("abandon");
932
+ });
933
+ it("DefinitionInUse is a constraint violation: nothing is deleted, including unused elements in the same call", () => {
934
+ const unusedPart = insertDefinitionElement(txn);
935
+ const result = txn.deleteElements([geometryPartId, unusedPart]);
936
+ assertResult(result, constraintViolation(definitionInUse(geometryPartId)), [], [geometryPartId, unusedPart], iModelDb);
937
+ });
938
+ it("a used Category is a constraint violation and every violator in the expanded set is reported", () => {
939
+ const unusedPart = insertDefinitionElement(txn);
940
+ const result = txn.deleteElements([spatialCategoryId, unusedPart]);
941
+ assertResult(result, constraintViolation(...usedSpatialCategoryViolations()), [], [spatialCategoryId, subCategoryId, filteredSubCategoryId, unusedPart], iModelDb);
942
+ });
943
+ it("reports the in-use cascaded child (not the requested parent) when the violator is a child", () => {
944
+ const parent = insertDefinitionElement(txn);
945
+ const child = insertDefinitionElement(txn, parent);
946
+ const user = txn.insertElement({
947
+ classFullName: "Generic:PhysicalObject",
948
+ model: physicalModelId,
949
+ category: spatialCategoryId,
950
+ code: Code.createEmpty(),
951
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
952
+ geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1), spatialCategoryId, undefined, undefined, child),
953
+ });
954
+ const result = txn.deleteElements([parent]);
955
+ assertResult(result, constraintViolation(definitionInUse(child)), [], [parent, child, user], iModelDb);
956
+ });
957
+ it("deprecated skipFKConstraintValidations flag is ignored and DefinitionInUse is still detected", () => {
958
+ // There is no way to bypass the definition usage check: geometry-stream references are not SQLite foreign keys, so
959
+ // skipping validation would silently leave dangling references. The deprecated flag is ignored.
960
+ const unusedPart = insertDefinitionElement(txn);
961
+ for (const skipFKConstraintValidations of [true, false]) {
962
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
963
+ const result = txn.deleteElements([geometryPartId, unusedPart], { skipFKConstraintValidations });
964
+ assertResult(result, constraintViolation(definitionInUse(geometryPartId)), [], [geometryPartId, unusedPart], iModelDb);
965
+ }
966
+ });
967
+ it("ConstraintViolation for a used Category (hard FK) leaves the txn clean", () => {
968
+ txn.saveChanges();
969
+ const result = txn.deleteElements([spatialCategoryId]);
970
+ assertResult(result, constraintViolation(...usedSpatialCategoryViolations()), [], [spatialCategoryId], iModelDb);
971
+ assert.isFalse(iModelDb[_nativeDb].hasUnsavedChanges(), "a rejected delete must not dirty the txn");
972
+ });
973
+ });
974
+ describe("cache coherence for cascaded definition elements", () => {
975
+ let txn;
976
+ beforeEach(() => {
977
+ txn = new EditTxn(iModelDb, "cache");
978
+ txn.start();
979
+ });
980
+ afterEach(() => {
981
+ txn.end("abandon");
982
+ });
983
+ it("a cached default SubCategory is not readable after its Category is deleted", () => {
984
+ const category = SpatialCategory.insert(txn, definitionModelId, "CachedCategory", {});
985
+ const defaultSubCategory = iModelDb.elements.getElement(category, SpatialCategory).myDefaultSubCategoryId();
986
+ assert.isDefined(iModelDb.elements.getElement(defaultSubCategory)); // prime the cache
987
+ executeTestCase(txn, "category with cached default subcategory", [category], [category, defaultSubCategory], success, [], false);
988
+ assert.isUndefined(iModelDb.elements.tryGetElementProps(defaultSubCategory), "cached default SubCategory props must not be served after deletion");
989
+ });
990
+ it("cached contents of a deleted DefinitionContainer are not readable afterwards", () => {
991
+ const codeSpecId = iModelDb.codeSpecs.insert(txn, "CachedContainerSpec", CodeScopeSpec.Type.Model);
992
+ const containerId = DefinitionContainer.insert(txn, definitionModelId, new Code({ spec: codeSpecId, scope: definitionModelId, value: "CachedContainer" }));
993
+ const inner = insertDefinitionElement(txn, undefined, containerId);
994
+ assert.isDefined(iModelDb.elements.getElement(inner));
995
+ assert.isDefined(iModelDb.models.getModelProps(containerId));
996
+ executeTestCase(txn, "container with cached contents", [containerId], [containerId, inner], success, [], false);
997
+ assert.isUndefined(iModelDb.elements.tryGetElementProps(inner));
998
+ assert.isUndefined(iModelDb.models.tryGetModelProps(containerId));
844
999
  });
845
1000
  });
846
1001
  });
@@ -961,10 +1116,9 @@ describe("DeleteDefinitionElements", () => {
961
1116
  it("TypeDefinitionId is NOT NULLed when deleteElements deletes the type and NULLs the reference", () => {
962
1117
  const physTypeId = insertPhysicalType();
963
1118
  const physObjId = insertPhysicalObjectWithType(physTypeId);
964
- const failed = txn.deleteElements([physTypeId]);
965
- assert.isFalse(failed.failedIds.has(physTypeId), "physTypeId should NOT be in the failed set — TypeDefinitionId refs do not block deleteElements");
966
- assert.isUndefined(iModelDb.elements.tryGetElement(physTypeId), "PhysicalType should be deleted");
967
- assert.isDefined(iModelDb.elements.tryGetElement(physObjId), "PhysicalObject should still exist");
1119
+ const result = txn.deleteElements([physTypeId]);
1120
+ // TypeDefinitionId refs do not block deleteElements - the type is deleted, the referencing object survives, and no failures are reported.
1121
+ assertResult(result, success, [physTypeId], [physObjId], iModelDb);
968
1122
  assert.isUndefined(getTypeDefinitionId3d(physObjId), "TypeDefinitionId should be NULLed since the type was deleted");
969
1123
  });
970
1124
  });