@itwin/core-backend 5.15.0-dev.9 → 5.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/lib/cjs/BlobContainerService.d.ts +2 -0
  3. package/lib/cjs/BlobContainerService.d.ts.map +1 -1
  4. package/lib/cjs/BlobContainerService.js.map +1 -1
  5. package/lib/cjs/BriefcaseManager.d.ts +2 -1
  6. package/lib/cjs/BriefcaseManager.d.ts.map +1 -1
  7. package/lib/cjs/BriefcaseManager.js +39 -9
  8. package/lib/cjs/BriefcaseManager.js.map +1 -1
  9. package/lib/cjs/ChangeUnifier.d.ts +82 -0
  10. package/lib/cjs/ChangeUnifier.d.ts.map +1 -0
  11. package/lib/cjs/ChangeUnifier.js +163 -0
  12. package/lib/cjs/ChangeUnifier.js.map +1 -0
  13. package/lib/cjs/ChangesetReader.d.ts +21 -2
  14. package/lib/cjs/ChangesetReader.d.ts.map +1 -1
  15. package/lib/cjs/ChangesetReader.js +37 -1
  16. package/lib/cjs/ChangesetReader.js.map +1 -1
  17. package/lib/cjs/CloudSqlite.d.ts +15 -0
  18. package/lib/cjs/CloudSqlite.d.ts.map +1 -1
  19. package/lib/cjs/CloudSqlite.js.map +1 -1
  20. package/lib/cjs/ECDb.d.ts +1 -0
  21. package/lib/cjs/ECDb.d.ts.map +1 -1
  22. package/lib/cjs/ECDb.js +4 -1
  23. package/lib/cjs/ECDb.js.map +1 -1
  24. package/lib/cjs/EditTxn.d.ts +61 -19
  25. package/lib/cjs/EditTxn.d.ts.map +1 -1
  26. package/lib/cjs/EditTxn.js +84 -29
  27. package/lib/cjs/EditTxn.js.map +1 -1
  28. package/lib/cjs/GeoCoordConfig.d.ts +1 -0
  29. package/lib/cjs/GeoCoordConfig.d.ts.map +1 -1
  30. package/lib/cjs/GeoCoordConfig.js +12 -8
  31. package/lib/cjs/GeoCoordConfig.js.map +1 -1
  32. package/lib/cjs/IModelDb.d.ts +22 -9
  33. package/lib/cjs/IModelDb.d.ts.map +1 -1
  34. package/lib/cjs/IModelDb.js +54 -12
  35. package/lib/cjs/IModelDb.js.map +1 -1
  36. package/lib/cjs/NativeHost.d.ts.map +1 -1
  37. package/lib/cjs/NativeHost.js +5 -1
  38. package/lib/cjs/NativeHost.js.map +1 -1
  39. package/lib/cjs/Relationship.d.ts +28 -0
  40. package/lib/cjs/Relationship.d.ts.map +1 -1
  41. package/lib/cjs/Relationship.js +78 -0
  42. package/lib/cjs/Relationship.js.map +1 -1
  43. package/lib/cjs/SQLiteDb.d.ts +52 -5
  44. package/lib/cjs/SQLiteDb.d.ts.map +1 -1
  45. package/lib/cjs/SQLiteDb.js +40 -5
  46. package/lib/cjs/SQLiteDb.js.map +1 -1
  47. package/lib/cjs/SchemaSync.d.ts.map +1 -1
  48. package/lib/cjs/SchemaSync.js +1 -0
  49. package/lib/cjs/SchemaSync.js.map +1 -1
  50. package/lib/cjs/TxnManager.d.ts +126 -89
  51. package/lib/cjs/TxnManager.d.ts.map +1 -1
  52. package/lib/cjs/TxnManager.js +114 -76
  53. package/lib/cjs/TxnManager.js.map +1 -1
  54. package/lib/cjs/ViewStateHydrator.d.ts.map +1 -1
  55. package/lib/cjs/ViewStateHydrator.js.map +1 -1
  56. package/lib/cjs/ViewStore.d.ts.map +1 -1
  57. package/lib/cjs/ViewStore.js +62 -19
  58. package/lib/cjs/ViewStore.js.map +1 -1
  59. package/lib/cjs/annotations/LeaderGeometry.d.ts.map +1 -1
  60. package/lib/cjs/annotations/LeaderGeometry.js +3 -0
  61. package/lib/cjs/annotations/LeaderGeometry.js.map +1 -1
  62. package/lib/cjs/core-backend.d.ts +1 -0
  63. package/lib/cjs/core-backend.d.ts.map +1 -1
  64. package/lib/cjs/core-backend.js +1 -0
  65. package/lib/cjs/core-backend.js.map +1 -1
  66. package/lib/cjs/internal/BriefcaseUtils.d.ts +19 -0
  67. package/lib/cjs/internal/BriefcaseUtils.d.ts.map +1 -0
  68. package/lib/cjs/internal/BriefcaseUtils.js +65 -0
  69. package/lib/cjs/internal/BriefcaseUtils.js.map +1 -0
  70. package/lib/cjs/internal/QueryMetadataCache.d.ts +44 -0
  71. package/lib/cjs/internal/QueryMetadataCache.d.ts.map +1 -0
  72. package/lib/cjs/internal/QueryMetadataCache.js +107 -0
  73. package/lib/cjs/internal/QueryMetadataCache.js.map +1 -0
  74. package/lib/cjs/internal/Symbols.d.ts +9 -0
  75. package/lib/cjs/internal/Symbols.d.ts.map +1 -1
  76. package/lib/cjs/internal/Symbols.js +10 -1
  77. package/lib/cjs/internal/Symbols.js.map +1 -1
  78. package/lib/cjs/internal/workspace/WorkspaceImpl.d.ts.map +1 -1
  79. package/lib/cjs/internal/workspace/WorkspaceImpl.js +20 -6
  80. package/lib/cjs/internal/workspace/WorkspaceImpl.js.map +1 -1
  81. package/lib/cjs/rpc-impl/IModelReadRpcImpl.d.ts +6 -6
  82. package/lib/cjs/rpc-impl/IModelReadRpcImpl.d.ts.map +1 -1
  83. package/lib/cjs/rpc-impl/IModelReadRpcImpl.js.map +1 -1
  84. package/lib/cjs/workspace/SettingsEditor.d.ts +4 -3
  85. package/lib/cjs/workspace/SettingsEditor.d.ts.map +1 -1
  86. package/lib/cjs/workspace/SettingsEditor.js +7 -5
  87. package/lib/cjs/workspace/SettingsEditor.js.map +1 -1
  88. package/lib/esm/BlobContainerService.d.ts +2 -0
  89. package/lib/esm/BlobContainerService.d.ts.map +1 -1
  90. package/lib/esm/BlobContainerService.js.map +1 -1
  91. package/lib/esm/BriefcaseManager.d.ts +2 -1
  92. package/lib/esm/BriefcaseManager.d.ts.map +1 -1
  93. package/lib/esm/BriefcaseManager.js +40 -10
  94. package/lib/esm/BriefcaseManager.js.map +1 -1
  95. package/lib/esm/ChangeUnifier.d.ts +82 -0
  96. package/lib/esm/ChangeUnifier.d.ts.map +1 -0
  97. package/lib/esm/ChangeUnifier.js +159 -0
  98. package/lib/esm/ChangeUnifier.js.map +1 -0
  99. package/lib/esm/ChangesetReader.d.ts +21 -2
  100. package/lib/esm/ChangesetReader.d.ts.map +1 -1
  101. package/lib/esm/ChangesetReader.js +38 -2
  102. package/lib/esm/ChangesetReader.js.map +1 -1
  103. package/lib/esm/CloudSqlite.d.ts +15 -0
  104. package/lib/esm/CloudSqlite.d.ts.map +1 -1
  105. package/lib/esm/CloudSqlite.js.map +1 -1
  106. package/lib/esm/ECDb.d.ts +1 -0
  107. package/lib/esm/ECDb.d.ts.map +1 -1
  108. package/lib/esm/ECDb.js +4 -1
  109. package/lib/esm/ECDb.js.map +1 -1
  110. package/lib/esm/EditTxn.d.ts +61 -19
  111. package/lib/esm/EditTxn.d.ts.map +1 -1
  112. package/lib/esm/EditTxn.js +85 -30
  113. package/lib/esm/EditTxn.js.map +1 -1
  114. package/lib/esm/GeoCoordConfig.d.ts +1 -0
  115. package/lib/esm/GeoCoordConfig.d.ts.map +1 -1
  116. package/lib/esm/GeoCoordConfig.js +12 -8
  117. package/lib/esm/GeoCoordConfig.js.map +1 -1
  118. package/lib/esm/IModelDb.d.ts +22 -9
  119. package/lib/esm/IModelDb.d.ts.map +1 -1
  120. package/lib/esm/IModelDb.js +56 -14
  121. package/lib/esm/IModelDb.js.map +1 -1
  122. package/lib/esm/NativeHost.d.ts.map +1 -1
  123. package/lib/esm/NativeHost.js +5 -1
  124. package/lib/esm/NativeHost.js.map +1 -1
  125. package/lib/esm/Relationship.d.ts +28 -0
  126. package/lib/esm/Relationship.d.ts.map +1 -1
  127. package/lib/esm/Relationship.js +78 -0
  128. package/lib/esm/Relationship.js.map +1 -1
  129. package/lib/esm/SQLiteDb.d.ts +52 -5
  130. package/lib/esm/SQLiteDb.d.ts.map +1 -1
  131. package/lib/esm/SQLiteDb.js +40 -5
  132. package/lib/esm/SQLiteDb.js.map +1 -1
  133. package/lib/esm/SchemaSync.d.ts.map +1 -1
  134. package/lib/esm/SchemaSync.js +1 -0
  135. package/lib/esm/SchemaSync.js.map +1 -1
  136. package/lib/esm/TxnManager.d.ts +126 -89
  137. package/lib/esm/TxnManager.d.ts.map +1 -1
  138. package/lib/esm/TxnManager.js +116 -78
  139. package/lib/esm/TxnManager.js.map +1 -1
  140. package/lib/esm/ViewStateHydrator.d.ts.map +1 -1
  141. package/lib/esm/ViewStateHydrator.js.map +1 -1
  142. package/lib/esm/ViewStore.d.ts.map +1 -1
  143. package/lib/esm/ViewStore.js +62 -19
  144. package/lib/esm/ViewStore.js.map +1 -1
  145. package/lib/esm/annotations/LeaderGeometry.d.ts.map +1 -1
  146. package/lib/esm/annotations/LeaderGeometry.js +3 -0
  147. package/lib/esm/annotations/LeaderGeometry.js.map +1 -1
  148. package/lib/esm/core-backend.d.ts +1 -0
  149. package/lib/esm/core-backend.d.ts.map +1 -1
  150. package/lib/esm/core-backend.js +1 -0
  151. package/lib/esm/core-backend.js.map +1 -1
  152. package/lib/esm/internal/BriefcaseUtils.d.ts +19 -0
  153. package/lib/esm/internal/BriefcaseUtils.d.ts.map +1 -0
  154. package/lib/esm/internal/BriefcaseUtils.js +61 -0
  155. package/lib/esm/internal/BriefcaseUtils.js.map +1 -0
  156. package/lib/esm/internal/QueryMetadataCache.d.ts +44 -0
  157. package/lib/esm/internal/QueryMetadataCache.d.ts.map +1 -0
  158. package/lib/esm/internal/QueryMetadataCache.js +103 -0
  159. package/lib/esm/internal/QueryMetadataCache.js.map +1 -0
  160. package/lib/esm/internal/Symbols.d.ts +9 -0
  161. package/lib/esm/internal/Symbols.d.ts.map +1 -1
  162. package/lib/esm/internal/Symbols.js +9 -0
  163. package/lib/esm/internal/Symbols.js.map +1 -1
  164. package/lib/esm/internal/workspace/WorkspaceImpl.d.ts.map +1 -1
  165. package/lib/esm/internal/workspace/WorkspaceImpl.js +20 -6
  166. package/lib/esm/internal/workspace/WorkspaceImpl.js.map +1 -1
  167. package/lib/esm/rpc-impl/IModelReadRpcImpl.d.ts +6 -6
  168. package/lib/esm/rpc-impl/IModelReadRpcImpl.d.ts.map +1 -1
  169. package/lib/esm/rpc-impl/IModelReadRpcImpl.js.map +1 -1
  170. package/lib/esm/test/annotations/LeaderGeometry.test.js +15 -1
  171. package/lib/esm/test/annotations/LeaderGeometry.test.js.map +1 -1
  172. package/lib/esm/test/ecdb/ConcurrentQuery.test.js +144 -30
  173. package/lib/esm/test/ecdb/ConcurrentQuery.test.js.map +1 -1
  174. package/lib/esm/test/ecdb/QueryMetadataCache.test.d.ts +2 -0
  175. package/lib/esm/test/ecdb/QueryMetadataCache.test.d.ts.map +1 -0
  176. package/lib/esm/test/ecdb/QueryMetadataCache.test.js +630 -0
  177. package/lib/esm/test/ecdb/QueryMetadataCache.test.js.map +1 -0
  178. package/lib/esm/test/element/DeleteDefinitionElements.test.js +403 -249
  179. package/lib/esm/test/element/DeleteDefinitionElements.test.js.map +1 -1
  180. package/lib/esm/test/element/RelationshipBatchLookup.test.d.ts +2 -0
  181. package/lib/esm/test/element/RelationshipBatchLookup.test.d.ts.map +1 -0
  182. package/lib/esm/test/element/RelationshipBatchLookup.test.js +202 -0
  183. package/lib/esm/test/element/RelationshipBatchLookup.test.js.map +1 -0
  184. package/lib/esm/test/hubaccess/BriefcaseManager.test.js +187 -1
  185. package/lib/esm/test/hubaccess/BriefcaseManager.test.js.map +1 -1
  186. package/lib/esm/test/hubaccess/SchemaImportOrphanColumns.test.d.ts +2 -0
  187. package/lib/esm/test/hubaccess/SchemaImportOrphanColumns.test.d.ts.map +1 -0
  188. package/lib/esm/test/hubaccess/SchemaImportOrphanColumns.test.js +142 -0
  189. package/lib/esm/test/hubaccess/SchemaImportOrphanColumns.test.js.map +1 -0
  190. package/lib/esm/test/imodel/IModelGeolocation.test.js +8 -6
  191. package/lib/esm/test/imodel/IModelGeolocation.test.js.map +1 -1
  192. package/lib/esm/test/imodel/IModelProps.test.d.ts +2 -0
  193. package/lib/esm/test/imodel/IModelProps.test.d.ts.map +1 -0
  194. package/lib/esm/test/imodel/IModelProps.test.js +411 -0
  195. package/lib/esm/test/imodel/IModelProps.test.js.map +1 -0
  196. package/lib/esm/test/schema/SchemaViewPropertyOverrides.test.d.ts +2 -0
  197. package/lib/esm/test/schema/SchemaViewPropertyOverrides.test.d.ts.map +1 -0
  198. package/lib/esm/test/schema/SchemaViewPropertyOverrides.test.js +240 -0
  199. package/lib/esm/test/schema/SchemaViewPropertyOverrides.test.js.map +1 -0
  200. package/lib/esm/test/schema/SchemaViewValidation.test.js +2 -8
  201. package/lib/esm/test/schema/SchemaViewValidation.test.js.map +1 -1
  202. package/lib/esm/test/standalone/BriefcaseManagerDelete.test.d.ts +2 -0
  203. package/lib/esm/test/standalone/BriefcaseManagerDelete.test.d.ts.map +1 -0
  204. package/lib/esm/test/standalone/BriefcaseManagerDelete.test.js +159 -0
  205. package/lib/esm/test/standalone/BriefcaseManagerDelete.test.js.map +1 -0
  206. package/lib/esm/test/standalone/ChangeUnifier.test.d.ts +2 -0
  207. package/lib/esm/test/standalone/ChangeUnifier.test.d.ts.map +1 -0
  208. package/lib/esm/test/standalone/ChangeUnifier.test.js +732 -0
  209. package/lib/esm/test/standalone/ChangeUnifier.test.js.map +1 -0
  210. package/lib/esm/test/standalone/DeleteElements.test.js +962 -83
  211. package/lib/esm/test/standalone/DeleteElements.test.js.map +1 -1
  212. package/lib/esm/test/standalone/EditTxn.test.js +72 -1
  213. package/lib/esm/test/standalone/EditTxn.test.js.map +1 -1
  214. package/lib/esm/test/standalone/GeoCoordConfig.test.d.ts +2 -0
  215. package/lib/esm/test/standalone/GeoCoordConfig.test.d.ts.map +1 -0
  216. package/lib/esm/test/standalone/GeoCoordConfig.test.js +158 -0
  217. package/lib/esm/test/standalone/GeoCoordConfig.test.js.map +1 -0
  218. package/lib/esm/test/standalone/SQLiteDb.test.d.ts.map +1 -1
  219. package/lib/esm/test/standalone/SQLiteDb.test.js +422 -1
  220. package/lib/esm/test/standalone/SQLiteDb.test.js.map +1 -1
  221. package/lib/esm/test/standalone/ViewStoreDb.test.js +53 -0
  222. package/lib/esm/test/standalone/ViewStoreDb.test.js.map +1 -1
  223. package/lib/esm/test/workspace/SettingsContainers.test.d.ts +2 -0
  224. package/lib/esm/test/workspace/SettingsContainers.test.d.ts.map +1 -0
  225. package/lib/esm/test/workspace/SettingsContainers.test.js +95 -0
  226. package/lib/esm/test/workspace/SettingsContainers.test.js.map +1 -0
  227. package/lib/esm/workspace/SettingsEditor.d.ts +4 -3
  228. package/lib/esm/workspace/SettingsEditor.d.ts.map +1 -1
  229. package/lib/esm/workspace/SettingsEditor.js +7 -5
  230. package/lib/esm/workspace/SettingsEditor.js.map +1 -1
  231. package/package.json +18 -18
@@ -1,10 +1,53 @@
1
1
  import { assert } from "chai";
2
- import { DbResult, Id64 } from "@itwin/core-bentley";
3
- import { Code, CodeScopeSpec, IModel, SubCategoryAppearance } from "@itwin/core-common";
4
- import { BulkDeleteElementsStatus, ChannelControl, EditTxn, IModelJsFs, PhysicalModel, SnapshotDb, SpatialCategory, Subject } from "../../core-backend";
2
+ import { DbResult, Id64, IModelStatus } from "@itwin/core-bentley";
3
+ import { Point3d } from "@itwin/core-geometry";
4
+ import { Code, CodeScopeSpec, IModel, QueryBinder, SubCategoryAppearance } from "@itwin/core-common";
5
+ import { _nativeDb, BulkDeleteElementsStatus, BulkDeleteFailureReason, ChannelControl, DefinitionModel, EditTxn, ElementDrivesElement, ElementGroupsMembers, ExternalSourceAspect, GeometryPart, Group, GroupInformationPartition, GroupModel, IModelJsFs, ModelSelector, PhysicalModel, RenderMaterialElement, SnapshotDb, SpatialCategory, StandaloneDb, SubCategory, Subject, SubjectOwnsPartitionElements, } from "../../core-backend";
5
6
  import { IModelTestUtils } from "../IModelTestUtils";
6
7
  import { KnownTestLocations } from "../KnownTestLocations";
7
8
  import { withEditTxn } from "../TestEditTxn";
9
+ const success = { status: BulkDeleteElementsStatus.Success, sqlDeleteStatus: DbResult.BE_SQLITE_OK, failures: [], failedIds: new Set() };
10
+ /**
11
+ * Nothing is deleted; `failures` lists the *direct* violators only. The bulk delete validates the expanded delete set exactly
12
+ * once (no pruning fixpoint), so an element that would only become a violator after another violator is excluded is NOT reported.
13
+ */
14
+ const constraintViolation = (failedIds, ...failures) => ({ status: BulkDeleteElementsStatus.ConstraintViolation, sqlDeleteStatus: DbResult.BE_SQLITE_OK, failures, failedIds });
15
+ /** The element itself is referenced from outside the delete set via its Code scope. */
16
+ const codeScopeViolator = (elementId) => ({ elementId, reason: BulkDeleteFailureReason.ReferencedByForeignKey });
17
+ /** A Category still assigned to a GeometricElement3d outside the delete set (hard FK, so not `DefinitionInUse`). */
18
+ const categoryInUse3d = (elementId) => ({ elementId, reason: BulkDeleteFailureReason.ReferencedByForeignKey });
19
+ /** A DefinitionElement still used (e.g. via a GeometryStream) by elements outside the delete set. No SQLite foreign key is involved. */
20
+ const definitionInUse = (elementId) => ({ elementId, reason: BulkDeleteFailureReason.DefinitionInUse });
21
+ const assertNoFkViolations = (db) => {
22
+ const rows = db.withSqliteStatement("PRAGMA foreign_key_check", (stmt) => {
23
+ const out = [];
24
+ while (stmt.step() === DbResult.BE_SQLITE_ROW)
25
+ out.push(`${stmt.getValueString(0)} rowid=${stmt.getValueInteger(1)} -> ${stmt.getValueString(2)} fk#${stmt.getValueInteger(3)}`);
26
+ return out;
27
+ });
28
+ assert.isEmpty(rows, `PRAGMA foreign_key_check reported violations after bulk delete: ${rows.join("; ")}`);
29
+ };
30
+ /**
31
+ * Minimal domain schema with a navigation property on an Element subclass. Navigation properties declared in domain
32
+ * schemas are NOT backed by a SQLite foreign key (only BisCore can declare FK-enforced nav props), so the bulk delete
33
+ * has to detect and NULL them itself rather than relying on `PRAGMA foreign_key_list`.
34
+ */
35
+ const navTestSchemaXml = `<?xml version="1.0" encoding="UTF-8"?>
36
+ <ECSchema schemaName="DeleteElementsNavTest" alias="dent" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.2">
37
+ <ECSchemaReference name="BisCore" version="01.00.00" alias="bis"/>
38
+ <ECEntityClass typeName="NavObject">
39
+ <BaseClass>bis:PhysicalElement</BaseClass>
40
+ <ECNavigationProperty propertyName="Target" relationshipName="NavObjectRefersToElement" direction="Forward"/>
41
+ </ECEntityClass>
42
+ <ECRelationshipClass typeName="NavObjectRefersToElement" strength="referencing" modifier="Sealed">
43
+ <Source multiplicity="(0..*)" roleLabel="refers to" polymorphic="true">
44
+ <Class class="NavObject"/>
45
+ </Source>
46
+ <Target multiplicity="(0..1)" roleLabel="is referred to by" polymorphic="true">
47
+ <Class class="bis:Element"/>
48
+ </Target>
49
+ </ECRelationshipClass>
50
+ </ECSchema>`;
8
51
  describe("deleteElements (native bulk delete API)", () => {
9
52
  let seedDb;
10
53
  let iModelDb;
@@ -16,6 +59,7 @@ describe("deleteElements (native bulk delete API)", () => {
16
59
  IModelJsFs.recursiveMkDirSync(KnownTestLocations.outputDir);
17
60
  const seedFile = IModelTestUtils.prepareOutputFile("DeleteElements", "seed.bim");
18
61
  seedDb = SnapshotDb.createEmpty(seedFile, { rootSubject: { name: "DeleteElements" } });
62
+ await seedDb.importSchemaStrings([navTestSchemaXml]);
19
63
  await withEditTxn(seedDb, "create elements", async (editTxn) => {
20
64
  modelId = PhysicalModel.insert(editTxn, IModel.rootSubjectId, "TestModel");
21
65
  categoryId = SpatialCategory.insert(editTxn, IModel.dictionaryId, "TestCategory", new SubCategoryAppearance());
@@ -60,44 +104,31 @@ describe("deleteElements (native bulk delete API)", () => {
60
104
  /** Assert that the element with the given id exists or has been deleted. */
61
105
  const assertExists = (id, msg) => assert.isDefined(iModelDb.elements.tryGetElement(id), msg);
62
106
  const assertDeleted = (id, msg) => assert.isUndefined(iModelDb.elements.tryGetElement(id), msg);
63
- // Run deleteElements, then verify the returned failed set, check each id in `deleted` is gone and each id in `retained` is still present.
64
- const executeTestCase = (label, idsToDelete, deleted, retained, expectedFailed = []) => {
65
- const resultStatus = txn.deleteElements(idsToDelete);
66
- if (expectedFailed.length === 0)
67
- assert.equal(resultStatus.status, BulkDeleteElementsStatus.Success);
68
- else
69
- assert.equal(resultStatus.status, (expectedFailed.length === idsToDelete.length) ? BulkDeleteElementsStatus.DeletionFailed : BulkDeleteElementsStatus.PartialSuccess);
70
- assert.sameMembers(Array.from(resultStatus.failedIds), expectedFailed, `[${label}] failed set mismatch`);
71
- for (const id of deleted)
72
- assertDeleted(id, `[${label}] ${id} should have been deleted`);
73
- for (const id of retained)
74
- assertExists(id, `[${label}] ${id} should have been retained`);
75
- txn.abandonChanges();
76
- // Verify that the same test case produces the same result when using the deprecated iModelDb.elements.deleteElements API, to ensure that the deprecation does not cause any regressions.
77
- executeTestCaseDeprecated(label, idsToDelete, deleted, retained, expectedFailed);
107
+ /** Assert the result matches `expected` and that elements in the delete set are the only ones reported; then check which elements were deleted / retained. */
108
+ const assertResult = (result, expected, deletedIds, retainedIds) => {
109
+ assert.equal(result.status, expected.status, `Bulk element deletion status is ${BulkDeleteElementsStatus[result.status]}; Expected: ${BulkDeleteElementsStatus[expected.status]}. Actual failures: ${JSON.stringify(result.failures)}`);
110
+ 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}`);
111
+ 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)}`);
112
+ assert.deepEqualInAnyOrder(result.failures.map(({ elementId, reason }) => ({ elementId, reason })), expected.failures);
113
+ for (const retainedId of retainedIds)
114
+ assertExists(retainedId);
115
+ for (const deletedId of deletedIds)
116
+ assertDeleted(deletedId);
117
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
118
+ assert.deepEqual(result.failedIds, expected.failedIds ?? new Set());
119
+ assertNoFkViolations(iModelDb);
78
120
  };
79
- const executeTestCaseDeprecated = (label, idsToDelete, deleted, retained, expectedFailed = []) => {
80
- const previousEnforcement = EditTxn.implicitWriteEnforcement;
81
- EditTxn.implicitWriteEnforcement = "allow";
82
- try {
83
- // eslint-disable-next-line @typescript-eslint/no-deprecated
84
- const resultStatus = iModelDb.elements.deleteElements(idsToDelete);
85
- if (expectedFailed.length === 0)
86
- assert.equal(resultStatus.status, BulkDeleteElementsStatus.Success);
87
- else
88
- assert.equal(resultStatus.status, (expectedFailed.length === idsToDelete.length) ? BulkDeleteElementsStatus.DeletionFailed : BulkDeleteElementsStatus.PartialSuccess);
89
- assert.sameMembers(Array.from(resultStatus.failedIds), expectedFailed, `[${label}] failed set mismatch`);
90
- for (const id of deleted)
91
- assertDeleted(id, `[${label}] ${id} should have been deleted`);
92
- for (const id of retained)
93
- assertExists(id, `[${label}] ${id} should have been retained`);
94
- }
95
- finally {
96
- // eslint-disable-next-line @typescript-eslint/no-deprecated
97
- iModelDb.abandonChanges();
98
- EditTxn.implicitWriteEnforcement = previousEnforcement;
99
- }
121
+ // Run deleteElements, compare the returned result object against `expected`, then check each id in `deleted` is gone and each id in `retained` is still present.
122
+ const executeTestCase = (_label, idsToDelete, deletedIds, retainedIds, expected = success) => {
123
+ const result = txn.deleteElements(idsToDelete);
124
+ assertResult(result, expected, deletedIds, retainedIds);
125
+ txn.abandonChanges();
100
126
  };
127
+ /**
128
+ * Run a delete that must be rejected with `ConstraintViolation`: nothing is deleted, so every id in `retained` must still exist,
129
+ * and `failures` / `failedIds` must list exactly the given direct violators. Changes are rolled back afterwards.
130
+ */
131
+ const expectRejected = (label, idsToDelete, retained, failedIds, ...failures) => executeTestCase(label, idsToDelete, [], retained, constraintViolation(failedIds, ...failures));
101
132
  /**
102
133
  * Shared hierarchy used throughout the parent-child tests:
103
134
  *
@@ -143,7 +174,7 @@ describe("deleteElements (native bulk delete API)", () => {
143
174
  const result1 = txn.deleteElements([el1]);
144
175
  const result2 = txn.deleteElements([el2]);
145
176
  for (const result of [result1, result2]) {
146
- assert.equal(result.failedIds.size, 0);
177
+ assert.isEmpty(result.failures);
147
178
  assert.equal(result.status, BulkDeleteElementsStatus.Success, "deleteElements call must succeed");
148
179
  assert.equal(result.sqlDeleteStatus, DbResult.BE_SQLITE_OK);
149
180
  }
@@ -207,7 +238,7 @@ describe("deleteElements (native bulk delete API)", () => {
207
238
  const childA = insertElement({ parentId: rootA });
208
239
  const rootB = insertElement({ codeScope: childA, codeValue: "rootB-code" });
209
240
  executeTestCase("depth-1 child scopes unrelated root - delete child+root directly", [childA, rootB], [childA, rootB], [rootA]);
210
- executeTestCase("depth-1 child scopes unrelated root - delete child only", [childA], [], [rootA, childA, rootB], [childA]);
241
+ executeTestCase("depth-1 child scopes unrelated root - delete child only", [childA], [], [rootA, childA, rootB], constraintViolation(new Set([childA]), codeScopeViolator(childA)));
211
242
  executeTestCase("depth-1 child scopes unrelated root - delete root only", [rootB], [rootB], [rootA, childA]);
212
243
  });
213
244
  it("grandchild is the code scope for an unrelated root", () => {
@@ -230,8 +261,8 @@ describe("deleteElements (native bulk delete API)", () => {
230
261
  const rootA = insertElement();
231
262
  const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" });
232
263
  const rootC = insertElement({ codeScope: rootB, codeValue: "rootC-code" });
233
- executeTestCase("scope chain single A", [rootA], [], [rootA, rootB, rootC], [rootA]);
234
- executeTestCase("scope chain single B", [rootB], [], [rootA, rootB, rootC], [rootB]);
264
+ executeTestCase("scope chain single A", [rootA], [], [rootA, rootB, rootC], constraintViolation(new Set([rootA]), codeScopeViolator(rootA)));
265
+ executeTestCase("scope chain single B", [rootB], [], [rootA, rootB, rootC], constraintViolation(new Set([rootB]), codeScopeViolator(rootB)));
235
266
  executeTestCase("scope chain single C", [rootC], [rootC], [rootA, rootB]);
236
267
  executeTestCase("scope chain forward", [rootA, rootB, rootC], [rootA, rootB, rootC], []);
237
268
  executeTestCase("scope chain reversed", [rootC, rootB, rootA], [rootA, rootB, rootC], []);
@@ -242,17 +273,8 @@ describe("deleteElements (native bulk delete API)", () => {
242
273
  const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" });
243
274
  const rootC = insertElement({ codeScope: rootB, codeValue: "rootC-code" });
244
275
  const rootD = insertElement({ codeScope: rootC, codeValue: "rootD-code" });
245
- // Only A and D in the delete set. B is external -> A ignored. D's scope (C) is not being deleted -> D is safe.
246
- executeTestCase("deep gap chain: A ignored, D deleted", [rootA, rootD], [rootD], [rootA, rootB, rootC], [rootA]);
247
- });
248
- it("scope chain delete with skipping constraint validation should fail", () => {
249
- const rootA = insertElement();
250
- const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" });
251
- const rootC = insertElement({ codeScope: rootB, codeValue: "rootC-code" });
252
- const rootD = insertElement({ codeScope: rootC, codeValue: "rootD-code" });
253
- const resultStatus = txn.deleteElements([rootA, rootD], { skipFKConstraintValidations: true });
254
- assert.equal(resultStatus.status, BulkDeleteElementsStatus.DeletionFailed);
255
- assert.equal(resultStatus.sqlDeleteStatus, DbResult.BE_SQLITE_CONSTRAINT_FOREIGNKEY);
276
+ // Only A and D in the delete set. B is external -> A is a violator, which rejects the whole call even though D itself is safe.
277
+ executeTestCase("deep gap chain: A violates, nothing deleted", [rootA, rootD], [], [rootA, rootB, rootC, rootD], constraintViolation(new Set([rootA]), codeScopeViolator(rootA)));
256
278
  });
257
279
  it("two elements using the same scope", () => {
258
280
  // A is the code scope for both B and C independently.
@@ -283,23 +305,115 @@ describe("deleteElements (native bulk delete API)", () => {
283
305
  executeTestCase("sibling scope - delete both directly", [childA, childB], [childA, childB], [parent]);
284
306
  // Delete only the scoped child - its scope (childA) is not being deleted, safe to delete.
285
307
  executeTestCase("sibling scope - delete only scoped child", [childB], [childB], [parent, childA]);
286
- // Delete only the scope element - childB is external -> childA ignored.
287
- executeTestCase("sibling scope - delete only scope element, ignored due to external childB", [childA], [], [parent, childA, childB], [childA]);
308
+ // Delete only the scope element - childB is external -> childA is a violator.
309
+ executeTestCase("sibling scope - delete only scope element, rejected due to external childB", [childA], [], [parent, childA, childB], constraintViolation(new Set([childA]), codeScopeViolator(childA)));
310
+ });
311
+ });
312
+ describe("all-or-nothing semantics and validation rounds", () => {
313
+ /** A -> B -> C -> D code-scope chain (each element is the code scope of the next). */
314
+ const insertScopeChain = () => {
315
+ const rootA = insertElement();
316
+ const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" });
317
+ const rootC = insertElement({ codeScope: rootB, codeValue: "rootC-code" });
318
+ const rootD = insertElement({ codeScope: rootC, codeValue: "rootD-code" });
319
+ return { rootA, rootB, rootC, rootD };
320
+ };
321
+ it("reports each FK violator with the referencing table.column and deletes nothing", () => {
322
+ const { rootA, rootB, rootC, rootD } = insertScopeChain();
323
+ const all = [rootA, rootB, rootC, rootD];
324
+ // A is scoped by B (external) -> violator. D is safe, but the call is all-or-nothing.
325
+ executeTestCase("Delete A and D", [rootA, rootD], [], all, constraintViolation(new Set([rootA]), codeScopeViolator(rootA)));
326
+ // Only the *direct* violators are reported: A is scoped by B which is in the set, so only B (scoped by external C) is reported.
327
+ executeTestCase("Delete A and B", [rootA, rootB], [], all, constraintViolation(new Set([rootB]), codeScopeViolator(rootB)));
328
+ executeTestCase("Delete B and C", [rootB, rootC], [], all, constraintViolation(new Set([rootC]), codeScopeViolator(rootC)));
329
+ executeTestCase("Delete A, B and D", [rootA, rootB, rootD], [], all, constraintViolation(new Set([rootB]), codeScopeViolator(rootB)));
330
+ // Tail of the chain has no external dependents.
331
+ executeTestCase("Delete C and D", [rootC, rootD], [rootC, rootD], [rootA, rootB], success);
332
+ executeTestCase("Delete all the roots", all, all, [], success);
333
+ });
334
+ it("succeeds when the whole chain is in the delete set, regardless of input order", () => {
335
+ const { rootA, rootB, rootC, rootD } = insertScopeChain();
336
+ executeTestCase("forward", [rootA, rootB, rootC, rootD], [rootA, rootB, rootC, rootD], []);
337
+ executeTestCase("reversed", [rootD, rootC, rootB, rootA], [rootA, rootB, rootC, rootD], []);
338
+ });
339
+ it("an unrelated deletable element does not rescue a call that contains a violator", () => {
340
+ const { rootA, rootB, rootC, rootD } = insertScopeChain();
341
+ const unrelated = insertElement();
342
+ expectRejected("violator + unrelated", [rootA, rootD, unrelated], [rootA, rootB, rootC, rootD, unrelated], new Set([rootA]), codeScopeViolator(rootA));
343
+ });
344
+ it("reports only first-round violators (no pruning fixpoint) - known limitation", () => {
345
+ // Chain A <- B <- C <- D; delete [A, B, D]. C is external and scopes B, so B is a violator. A is scoped by B, which is
346
+ // *in* the set, so A is not a violator in the single validation round that runs. If the caller drops B from the input and
347
+ // retries, A will then be reported. Pinned here so a change in the native validation strategy is deliberate.
348
+ const { rootA, rootB, rootC, rootD } = insertScopeChain();
349
+ const result = txn.deleteElements([rootA, rootB, rootD]);
350
+ assertResult(result, constraintViolation(new Set([rootB]), codeScopeViolator(rootB)), [], [rootA, rootB, rootC, rootD]);
351
+ assert.isFalse(result.failures.some((f) => f.elementId === rootA), "transitive violators (A) are not reported; update this test if the native fixpoint changes");
352
+ txn.abandonChanges();
353
+ const retry = txn.deleteElements([rootA, rootD]);
354
+ assertResult(retry, constraintViolation(new Set([rootA]), codeScopeViolator(rootA)), [], [rootA, rootB, rootC, rootD]);
355
+ });
356
+ it("a rejected delete does not evict anything from the element cache", () => {
357
+ const { rootA, rootD } = insertScopeChain();
358
+ // Prime the cache.
359
+ const before = iModelDb.elements.getElement(rootA);
360
+ expectRejected("rejected delete keeps cache", [rootA, rootD], [rootA, rootD], new Set([rootA]), codeScopeViolator(rootA));
361
+ const after = iModelDb.elements.getElement(rootA);
362
+ assert.strictEqual(after.id, before.id);
363
+ assertExists(rootD, "rootD must still exist");
364
+ });
365
+ it("ConstraintViolation leaves no side effects behind (aspects, subclass rows, sub-models)", async () => {
366
+ // Validation runs before anything is modified, so unlike DeletionFailed nothing needs to be abandoned.
367
+ const { rootA, rootB, rootC, rootD } = insertScopeChain();
368
+ const aspectId = txn.insertAspect({
369
+ classFullName: ExternalSourceAspect.classFullName,
370
+ element: { id: rootD },
371
+ scope: { id: IModel.rootSubjectId },
372
+ identifier: "src-rootD",
373
+ kind: "Element",
374
+ });
375
+ const partitionId = PhysicalModel.insert(txn, IModel.rootSubjectId, "NoSideEffects");
376
+ txn.saveChanges();
377
+ const aspectExists = async () => {
378
+ const reader = iModelDb.createQueryReader("SELECT 1 FROM bis.ElementMultiAspect WHERE ECInstanceId=?", new QueryBinder().bindId(1, aspectId));
379
+ return reader.step();
380
+ };
381
+ const geomRowExists = (id) => iModelDb.withPreparedSqliteStatement("SELECT 1 FROM bis_GeometricElement3d WHERE ElementId=?", (stmt) => { stmt.bindId(1, id); return stmt.step() === DbResult.BE_SQLITE_ROW; });
382
+ const result = txn.deleteElements([rootA, rootD, partitionId]);
383
+ assertResult(result, constraintViolation(new Set([rootA]), codeScopeViolator(rootA)), [], [rootA, rootB, rootC, rootD, partitionId]);
384
+ assert.isTrue(await aspectExists(), "aspect must not be touched");
385
+ assert.isTrue(geomRowExists(rootD), "subclass rows must not be touched");
386
+ assert.isDefined(iModelDb.models.tryGetModelProps(partitionId), "sub-model must not be touched");
387
+ assert.isFalse(iModelDb[_nativeDb].hasUnsavedChanges(), "a rejected delete must not dirty the txn");
388
+ });
389
+ it("deprecated skipFKConstraintValidations flag is ignored (either value)", () => {
390
+ const { rootA, rootB, rootC, rootD } = insertScopeChain();
391
+ for (const skipFKConstraintValidations of [true, false]) {
392
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
393
+ const result = txn.deleteElements([rootA, rootD], { skipFKConstraintValidations });
394
+ assertResult(result, constraintViolation(new Set([rootA]), codeScopeViolator(rootA)), [], [rootA, rootB, rootC, rootD]);
395
+ // ...and succeeds when nothing is referenced from outside the set.
396
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
397
+ const ok = txn.deleteElements([rootA, rootB, rootC, rootD], { skipFKConstraintValidations });
398
+ assertResult(ok, success, [rootA, rootB, rootC, rootD], []);
399
+ txn.abandonChanges();
400
+ }
288
401
  });
289
402
  });
290
- describe("Code scope violations to test delete set element pruning", () => {
403
+ describe("Code scope violations from outside the delete set", () => {
291
404
  it("root is code scope for an external element", () => {
292
405
  const rootA = insertElement();
293
406
  const external = insertElement({ codeScope: rootA, codeValue: "ext-code" });
294
407
  const rootB = insertElement();
295
- executeTestCase("external scopes root", [rootA, rootB], [rootB], [rootA, external], [rootA]);
408
+ expectRejected("external scopes root", [rootA, rootB], [rootA, rootB, external], new Set([rootA]), codeScopeViolator(rootA));
296
409
  });
297
410
  it("depth-1 child is code scope for external", () => {
298
411
  const rootA = insertElement();
299
412
  const childA = insertElement({ parentId: rootA });
300
413
  const external = insertElement({ codeScope: childA, codeValue: "ext-code" });
301
414
  const rootB = insertElement();
302
- executeTestCase("external scopes depth-1 child - parent subtree ignored", [rootA, rootB], [rootB], [rootA, childA, external], [rootA]);
415
+ // The cascaded child is the violator; the requested parent is not itself reported.
416
+ expectRejected("external scopes depth-1 child", [rootA, rootB], [rootA, childA, external, rootB], new Set([childA]), codeScopeViolator(childA));
303
417
  });
304
418
  it("depth-2 grandchild is code scope for external", () => {
305
419
  const rootA = insertElement();
@@ -307,27 +421,27 @@ describe("deleteElements (native bulk delete API)", () => {
307
421
  const grandchildA = insertElement({ parentId: childA });
308
422
  const external = insertElement({ codeScope: grandchildA, codeValue: "ext-code" });
309
423
  const rootB = insertElement();
310
- executeTestCase("external scopes depth-2 grandchild - grandparent subtree ignored", [rootA, rootB], [rootB], [rootA, childA, grandchildA, external], [rootA]);
424
+ expectRejected("external scopes depth-2 grandchild", [rootA, rootB], [rootA, childA, grandchildA, external, rootB], new Set([grandchildA]), codeScopeViolator(grandchildA));
311
425
  });
312
426
  it("only the child is passed for deletion", () => {
313
427
  const rootA = insertElement();
314
428
  const childA = insertElement({ parentId: rootA });
315
429
  const external = insertElement({ codeScope: childA, codeValue: "ext-code" });
316
- executeTestCase("external scopes requested child", [childA], [], [rootA, childA, external], [childA]);
430
+ expectRejected("external scopes requested child", [childA], [rootA, childA, external], new Set([childA]), codeScopeViolator(childA));
317
431
  });
318
432
  it("root has both an external scope dependent AND an intra-set scope dependent", () => {
319
433
  const rootA = insertElement();
320
434
  const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" });
321
435
  const external = insertElement({ codeScope: rootA, codeValue: "ext-code" });
322
- executeTestCase("root ignored due to external; sibling still deleted", [rootA, rootB], [rootB], [rootA, external], [rootA]);
436
+ expectRejected("root rejected due to external", [rootA, rootB], [rootA, rootB, external], new Set([rootA]), codeScopeViolator(rootA));
323
437
  });
324
- it("two independent external scope violations", () => {
438
+ it("two independent external scope violations are both reported", () => {
325
439
  const rootA = insertElement();
326
440
  const rootB = insertElement();
327
441
  const extX = insertElement({ codeScope: rootA, codeValue: "extX" });
328
442
  const extY = insertElement({ codeScope: rootB, codeValue: "extY" });
329
443
  const rootC = insertElement();
330
- executeTestCase("two independent violations", [rootA, rootB, rootC], [rootC], [rootA, rootB, extX, extY], [rootA, rootB]);
444
+ expectRejected("two independent violations", [rootA, rootB, rootC], [rootA, rootB, rootC, extX, extY], new Set([rootA, rootB]), codeScopeViolator(rootA), codeScopeViolator(rootB));
331
445
  });
332
446
  });
333
447
  describe("mixed element hierarchy and code scope violations", () => {
@@ -356,14 +470,13 @@ describe("deleteElements (native bulk delete API)", () => {
356
470
  // Only childA1 and rootB - rootA is NOT in the delete set.
357
471
  executeTestCase("depth-1 child scopes root - delete child + scoped root directly", [childA1, rootB], [childA1, rootB, childB1], [rootA]);
358
472
  });
359
- it("depth-1 child scopes an unrelated root - deleting only the child cascades into the scoped root's subtree", () => {
360
- // childA1 is the code scope of rootB. When childA1 is deleted, rootB loses its scope
361
- // element -> rootB (and its children) must also be deleted.
473
+ it("depth-1 child scopes an unrelated root - deleting only the child is rejected because the scoped root is outside the set", () => {
474
+ // childA1 is the code scope of rootB. rootB is not in the delete set, so childA1 is still referenced and the call is rejected.
362
475
  const rootA = insertElement();
363
476
  const childA1 = insertElement({ parentId: rootA });
364
477
  const rootB = insertElement({ codeScope: childA1, codeValue: "rootB-code" });
365
478
  const childB1 = insertElement({ parentId: rootB });
366
- executeTestCase("delete child only - scoped root also removed", [childA1], [], [rootA, childA1, rootB, childB1], [childA1]);
479
+ expectRejected("delete child only - scoped root is external", [childA1], [rootA, childA1, rootB, childB1], new Set([childA1]), codeScopeViolator(childA1));
367
480
  });
368
481
  it("root scopes a depth-1 child in sibling tree - delete both roots, all descendants removed", () => {
369
482
  const rootA = insertElement();
@@ -391,17 +504,17 @@ describe("deleteElements (native bulk delete API)", () => {
391
504
  executeTestCase("depth-2 grandchild scopes root - delete grandchild + root directly", [grandchildA, rootB], [grandchildA, rootB, childB], [rootA, childA]);
392
505
  });
393
506
  it("external element scopes a depth-2 grandchild, mixed with an unrelated deletion", () => {
394
- // Unique case: grandchild has an external scope violation, but an unrelated element from
395
- // another tree (childB) is also requested and has no violation - it should be deleted.
507
+ // The grandchild has an external scope violation. An unrelated element from another tree (childB) is also requested and
508
+ // has no violation, but the call is all-or-nothing so it survives as well.
396
509
  const rootA = insertElement();
397
510
  const childA = insertElement({ parentId: rootA });
398
511
  const grandchildA = insertElement({ parentId: childA });
399
512
  const rootB = insertElement();
400
513
  const childB = insertElement({ parentId: rootB });
401
514
  const external = insertElement({ codeScope: grandchildA, codeValue: "ext-code" });
402
- executeTestCase("external scopes depth-2 grandchild, unrelated childB deleted", [grandchildA, rootA, childB], [childB], [rootA, childA, grandchildA, rootB, external], [rootA, grandchildA]);
515
+ expectRejected("external scopes depth-2 grandchild, unrelated childB retained", [grandchildA, rootA, childB], [rootA, childA, grandchildA, rootB, childB, external], new Set([grandchildA]), codeScopeViolator(grandchildA));
403
516
  });
404
- it("two trees: one has external scope violation, other is deleted cleanly", () => {
517
+ it("two trees: one has external scope violation, neither is deleted", () => {
405
518
  const rootA = insertElement();
406
519
  const childA = insertElement({ parentId: rootA });
407
520
  const gcA = insertElement({ parentId: childA });
@@ -410,7 +523,7 @@ describe("deleteElements (native bulk delete API)", () => {
410
523
  const childB1 = insertElement({ parentId: rootB });
411
524
  const childB2 = insertElement({ parentId: rootB });
412
525
  const gcB = insertElement({ parentId: childB1 });
413
- executeTestCase("one tree ignored, other fully deleted", [rootA, rootB], [rootB, childB1, childB2, gcB], [rootA, childA, gcA, external], [rootA]);
526
+ expectRejected("one tree violates, both retained", [rootA, rootB], [rootA, childA, gcA, external, rootB, childB1, childB2, gcB], new Set([childA]), codeScopeViolator(childA));
414
527
  });
415
528
  });
416
529
  describe("sub-model hierarchy", () => {
@@ -444,12 +557,13 @@ describe("deleteElements (native bulk delete API)", () => {
444
557
  const unrelated = insertElement();
445
558
  const result = txn.deleteElements([partitionId]);
446
559
  assert.equal(result.status, BulkDeleteElementsStatus.Success);
447
- assert.isEmpty(result.failedIds);
560
+ assert.isEmpty(result.failures);
448
561
  assertDeleted(partitionId, "partition element should be deleted");
449
562
  assertModelDeleted(partitionId, "sub-model should be deleted");
450
563
  assertDeleted(elem1, "elem1 inside sub-model should be deleted");
451
564
  assertDeleted(elem2, "elem2 inside sub-model should be deleted");
452
565
  assertExists(unrelated, "unrelated element should be retained");
566
+ assertNoFkViolations(iModelDb);
453
567
  });
454
568
  /**
455
569
  * Scenario: Subject -> partition (parent-child) -> modeled-element.
@@ -472,6 +586,7 @@ describe("deleteElements (native bulk delete API)", () => {
472
586
  assertModelDeleted(childPartitionId, "child partition sub-model should be deleted");
473
587
  assertDeleted(elem1, "elem1 inside child partition sub-model should be deleted");
474
588
  assertExists(unrelated, "unrelated element should be retained");
589
+ assertNoFkViolations(iModelDb);
475
590
  });
476
591
  /**
477
592
  * Scenario: sub-model elements have their own children; all should be removed transitively.
@@ -499,6 +614,7 @@ describe("deleteElements (native bulk delete API)", () => {
499
614
  assertDeleted(grandchildOfElem1, "grandchild of elem1 should be deleted");
500
615
  assertDeleted(elem2, "elem2 should be deleted");
501
616
  assertExists(unrelated, "unrelated should be retained");
617
+ assertNoFkViolations(iModelDb);
502
618
  });
503
619
  /**
504
620
  * Scenario: element inside a sub-model is a code scope for an element outside the delete set. The whole partition subtree must be ignored from the delete set.
@@ -509,22 +625,17 @@ describe("deleteElements (native bulk delete API)", () => {
509
625
  * external (not in delete set)
510
626
  * unrelated
511
627
  */
512
- it("partition ignored when a sub-model element is a code scope for an external element", () => {
628
+ it("partition rejected when a sub-model element is a code scope for an external element", () => {
513
629
  const partitionId = insertSubModel();
514
630
  const scopingElem = insertElementInModel(partitionId);
515
631
  const otherElem = insertElementInModel(partitionId);
516
632
  const external = insertElement({ codeScope: scopingElem, codeValue: "ext-code" });
517
633
  const unrelated = insertElement();
518
- // `external` is NOT in the delete set and uses scopingElem as its code scope -> the
519
- // entire partition subtree (including the sub-model) must be ignored from the delete set.
634
+ // `external` is NOT in the delete set and uses scopingElem as its code scope -> the cascaded sub-model member is a
635
+ // violator and the whole call is rejected, including the unrelated element.
520
636
  const result = txn.deleteElements([partitionId, unrelated]);
521
- assert.equal(result.status, BulkDeleteElementsStatus.PartialSuccess);
522
- assertExists(partitionId, "partition should be ignored (retained)");
637
+ assertResult(result, constraintViolation(new Set([scopingElem]), codeScopeViolator(scopingElem)), [], [partitionId, scopingElem, otherElem, external, unrelated]);
523
638
  assertModelExists(partitionId, "sub-model should be retained");
524
- assertExists(scopingElem, "scopingElem should be retained");
525
- assertExists(otherElem, "otherElem should be retained");
526
- assertExists(external, "external should be retained");
527
- assertDeleted(unrelated, "unrelated should still be deleted");
528
639
  });
529
640
  /**
530
641
  * Scenario: the sub-model element of one partition is used as a code scope for an element
@@ -557,6 +668,7 @@ describe("deleteElements (native bulk delete API)", () => {
557
668
  assertModelDeleted(p2, "sub-model of p2 should be deleted");
558
669
  assertDeleted(dependentElem, "dependentElem should be deleted");
559
670
  assertDeleted(dependentId, "dependentId should be deleted");
671
+ assertNoFkViolations(iModelDb);
560
672
  });
561
673
  /**
562
674
  * Scenario: three-level Subject cascade: deleting a grandparent Subject removes a child
@@ -584,6 +696,7 @@ describe("deleteElements (native bulk delete API)", () => {
584
696
  assertDeleted(innerElem1, "innerElem1 should be deleted");
585
697
  assertDeleted(innerElem2, "innerElem2 should be deleted");
586
698
  assertExists(unrelated, "unrelated should be retained");
699
+ assertNoFkViolations(iModelDb);
587
700
  });
588
701
  /**
589
702
  * Scenario: empty sub-model: partition with no elements in its sub-model.
@@ -599,6 +712,7 @@ describe("deleteElements (native bulk delete API)", () => {
599
712
  assertDeleted(partitionId, "partition should be deleted");
600
713
  assertModelDeleted(partitionId, "empty sub-model should be deleted");
601
714
  assertExists(unrelated, "unrelated should be retained");
715
+ assertNoFkViolations(iModelDb);
602
716
  });
603
717
  /**
604
718
  * Scenario: a regular element has two children, one of which is a partition with a
@@ -625,6 +739,7 @@ describe("deleteElements (native bulk delete API)", () => {
625
739
  assertDeleted(subElem1, "subElem1 should be deleted");
626
740
  assertDeleted(subElem2, "subElem2 should be deleted");
627
741
  assertExists(unrelated, "unrelated should be retained");
742
+ assertNoFkViolations(iModelDb);
628
743
  });
629
744
  /**
630
745
  * Scenario: a regular element's grandchild is a partition. Passing the partition's
@@ -651,6 +766,7 @@ describe("deleteElements (native bulk delete API)", () => {
651
766
  assertModelDeleted(partitionId, "sub-model should be deleted");
652
767
  assertDeleted(subElem1, "subElem1 should be deleted");
653
768
  assertDeleted(subElem2, "subElem2 should be deleted");
769
+ assertNoFkViolations(iModelDb);
654
770
  });
655
771
  it("deleting a partition element directly (not via its regular parent) cascades into the sub-model; parent survives", () => {
656
772
  const subjectA = Subject.insert(txn, IModel.rootSubjectId, `DirectPartSubject-${++partitionCounter}`);
@@ -664,6 +780,769 @@ describe("deleteElements (native bulk delete API)", () => {
664
780
  assertModelDeleted(partitionId, "sub-model should be deleted");
665
781
  assertDeleted(subElem1, "subElem1 should be deleted");
666
782
  assertDeleted(subElem2, "subElem2 should be deleted");
783
+ assertNoFkViolations(iModelDb);
784
+ });
785
+ });
786
+ /**
787
+ * Members of a sub-model that are *also* listed explicitly in the input. Native expands the delete set from the element itself
788
+ * (children + sub-model contents), so a violator inside the sub-model must reject the call regardless of whether it was
789
+ * requested or cascaded, and a rejected call must never delete the model row out from under explicitly-requested members.
790
+ *
791
+ * [P:defPartition] [M:defPartition] -> catA (used by wall1, outside the set), catB (unused)
792
+ * wall1 (in modelId, category = catA)
793
+ */
794
+ describe("explicit sub-model members", () => {
795
+ let defPartition;
796
+ let catA, catB;
797
+ let subA, subB;
798
+ let wall1;
799
+ beforeEach(() => {
800
+ defPartition = DefinitionModel.insert(txn, IModel.rootSubjectId, "DefPartition");
801
+ catA = SpatialCategory.insert(txn, defPartition, "CatA", new SubCategoryAppearance());
802
+ catB = SpatialCategory.insert(txn, defPartition, "CatB", new SubCategoryAppearance());
803
+ subA = IModel.getDefaultSubCategoryId(catA);
804
+ subB = IModel.getDefaultSubCategoryId(catB);
805
+ wall1 = txn.insertElement({
806
+ classFullName: "Generic:PhysicalObject", model: modelId, category: catA, code: Code.createEmpty(),
807
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
808
+ });
809
+ txn.saveChanges();
810
+ });
811
+ const assertPartitionIntact = () => {
812
+ assert.isDefined(iModelDb.models.tryGetModelProps(defPartition), "sub-model row must survive when the call is rejected");
813
+ assert.equal(iModelDb.elements.getElement(catA).model, defPartition, "catA must still live in the sub-model");
814
+ };
815
+ it("Only the modeled element requested: violator inside rejects it, nothing deleted", () => {
816
+ expectRejected("partition only", [defPartition], [defPartition, catA, subA, catB, subB, wall1], new Set([catA]), categoryInUse3d(catA));
817
+ assertPartitionIntact();
818
+ });
819
+ it("Modeled element + members requested explicitly: nothing deleted, only the direct violator is reported", () => {
820
+ // The reviewer's case: pre-fix native treated catB as a root (it was requested) and deleted it together with the model row
821
+ // while catA survived, leaving catA pointing at a model that no longer existed.
822
+ expectRejected("partition + members", [defPartition, catA, catB], [defPartition, catA, subA, catB, subB, wall1], new Set([catA]), categoryInUse3d(catA));
823
+ assertPartitionIntact();
824
+ });
825
+ it("Only members requested (modeled element not listed): the in-use one rejects the call, the model survives", () => {
826
+ expectRejected("members only", [catA, catB], [defPartition, catA, subA, catB, subB, wall1], new Set([catA]), categoryInUse3d(catA));
827
+ assertPartitionIntact();
828
+ });
829
+ it("Only the unused member requested: deleted, the model survives", () => {
830
+ executeTestCase("unused member only", [catB], [catB, subB], [defPartition, catA, subA, wall1]);
831
+ assertPartitionIntact();
832
+ });
833
+ it("Once the external user is gone, modeled element + members delete cleanly", () => {
834
+ txn.deleteElement(wall1);
835
+ txn.saveChanges();
836
+ const result = txn.deleteElements([defPartition, catA, catB]);
837
+ assertResult(result, success, [defPartition, catA, subA, catB, subB], []);
838
+ assert.isUndefined(iModelDb.models.tryGetModelProps(defPartition), "sub-model row must be deleted with its modeled element");
839
+ });
840
+ it("Requested member deep in a parent chain inside the sub-model rejects the call", () => {
841
+ const partition = PhysicalModel.insert(txn, IModel.rootSubjectId, "ChainPartition");
842
+ const insertIn = (parentId) => txn.insertElement({
843
+ classFullName: "Generic:PhysicalObject", model: partition, category: categoryId, code: Code.createEmpty(),
844
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
845
+ ...(parentId ? { parent: { id: parentId, relClassName: "BisCore:ElementOwnsChildElements" } } : {}),
846
+ });
847
+ const p = insertIn();
848
+ const c = insertIn(p);
849
+ const g = insertIn(c);
850
+ txn.saveChanges();
851
+ const external = insertElement({ codeScope: g, codeValue: "ext-g" });
852
+ // g is requested *and* a cascaded grandchild of a sub-model member; it is a violator either way.
853
+ expectRejected("partition + deep member", [partition, g], [partition, p, c, g, external], new Set([g]), codeScopeViolator(g));
854
+ assert.isDefined(iModelDb.models.tryGetModelProps(partition));
855
+ });
856
+ });
857
+ /**
858
+ * `DefinitionInUse` is reported for definitions referenced from geometry streams or other non-FK usages. Every category test
859
+ * elsewhere in this file is caught by the `CategoryId` *foreign key* path first, so these are the only tests that reach
860
+ * `DefinitionElementUsageInfo`. The RenderMaterial / GeometryPart cases force a full geometry-stream scan natively; fine on
861
+ * a tiny db, do not copy this pattern into a performance test.
862
+ *
863
+ * [M:defModel] -> cat (-> sub, non-default), mat, part
864
+ * elem (in modelId, category = cat): geometry uses sub, mat and part
865
+ */
866
+ describe("DefinitionInUse via geometry streams", () => {
867
+ let defModel;
868
+ let cat, sub, defaultSub, mat, part;
869
+ let elem;
870
+ beforeEach(() => {
871
+ defModel = DefinitionModel.insert(txn, IModel.rootSubjectId, "Defs");
872
+ cat = SpatialCategory.insert(txn, defModel, "Cat", new SubCategoryAppearance());
873
+ defaultSub = IModel.getDefaultSubCategoryId(cat);
874
+ sub = SubCategory.insert(txn, cat, "Sub", new SubCategoryAppearance());
875
+ mat = RenderMaterialElement.insert(txn, defModel, "Mat", { paletteName: "p" });
876
+ part = txn.insertElement({
877
+ classFullName: GeometryPart.classFullName, model: defModel, code: GeometryPart.createCode(iModelDb, defModel, "Part"),
878
+ geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1)),
879
+ });
880
+ elem = txn.insertElement({
881
+ classFullName: "Generic:PhysicalObject", model: modelId, category: cat, code: Code.createEmpty(),
882
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
883
+ geom: IModelTestUtils.createBox(Point3d.create(1, 1, 1), cat, sub, mat, part),
884
+ });
885
+ txn.saveChanges();
886
+ });
887
+ it("A non-default SubCategory referenced from a geometry stream is DefinitionInUse", () => {
888
+ expectRejected("sub-category in geometry", [sub], [cat, defaultSub, sub, elem], new Set([sub]), definitionInUse(sub));
889
+ });
890
+ it("A in-use Category rejects the call that also requests its SubCategory (nothing deleted)", () => {
891
+ // Pre-fix native pruned only *upwards* from the violator: `sub` was deleted while `cat` survived. With all-or-nothing
892
+ // semantics the sub-category must survive together with its category.
893
+ const elem2 = txn.insertElement({
894
+ classFullName: "Generic:PhysicalObject", model: modelId, category: cat, code: Code.createEmpty(),
895
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
896
+ });
897
+ txn.saveChanges();
898
+ // Remove the geometry-stream user so the only thing keeping `cat` alive is the CategoryId FK from elem2.
899
+ txn.deleteElement(elem);
900
+ txn.saveChanges();
901
+ expectRejected("category + sub-category", [cat, sub], [cat, defaultSub, sub, elem2], new Set([cat]), categoryInUse3d(cat));
902
+ });
903
+ it("A RenderMaterial referenced from a geometry stream is DefinitionInUse", () => {
904
+ expectRejected("material in geometry", [mat], [mat, elem], new Set([mat]), definitionInUse(mat));
905
+ });
906
+ it("A GeometryPart referenced from a geometry stream is DefinitionInUse", () => {
907
+ expectRejected("part in geometry", [part], [part, elem], new Set([part]), definitionInUse(part));
908
+ });
909
+ it("Definitions whose only user is in the delete set are deleted with it", () => {
910
+ executeTestCase("definitions + their user", [mat, part, sub, elem], [mat, part, sub, elem], [cat, defaultSub]);
911
+ });
912
+ });
913
+ describe("input validation and txn state", () => {
914
+ it("throws when the EditTxn is not active", () => {
915
+ const el = insertElement();
916
+ txn.end("abandon");
917
+ assert.throws(() => txn.deleteElements([el]), "EditTxn is not active");
918
+ // Re-activate so afterEach can end it.
919
+ txn = new EditTxn(iModelDb, "delete elements");
920
+ txn.start();
921
+ assertExists(el, "nothing may be deleted by an inactive txn");
922
+ });
923
+ it("throws before touching the iModel when any id is invalid, even if other ids are deletable", () => {
924
+ const rootA = insertElement();
925
+ const rootB = insertElement();
926
+ assert.throws(() => txn.deleteElements([rootA, "0x", rootB]));
927
+ assert.throws(() => txn.deleteElements([rootA, "", rootB]));
928
+ assertExists(rootA);
929
+ assertExists(rootB);
930
+ });
931
+ it("well-formed ids that do not exist are ignored", () => {
932
+ const rootA = insertElement();
933
+ const missing = "0xffffffffffff";
934
+ assert.isUndefined(iModelDb.elements.tryGetElement(missing));
935
+ executeTestCase("missing id", [rootA, missing], [rootA], [], success);
936
+ });
937
+ it("second deletion of an already-deleted element is a no-op", () => {
938
+ const rootA = insertElement();
939
+ const rootB = insertElement();
940
+ assert.equal(txn.deleteElements([rootA]).status, BulkDeleteElementsStatus.Success);
941
+ assertDeleted(rootA);
942
+ // rootA no longer exists; the call must still succeed and delete rootB.
943
+ const result = txn.deleteElements([rootA, rootB]);
944
+ assertResult(result, success, [rootA, rootB], []);
945
+ });
946
+ it("does not auto-save: abandonChanges restores every deleted element", () => {
947
+ const rootA = insertElement();
948
+ const childA = insertElement({ parentId: rootA });
949
+ const result = txn.deleteElements([rootA]);
950
+ assertResult(result, success, [rootA, childA], []);
951
+ txn.abandonChanges();
952
+ assertExists(rootA, "abandon must restore rootA");
953
+ assertExists(childA, "abandon must restore childA");
954
+ });
955
+ it("an element referenced through several foreign keys is reported exactly once", () => {
956
+ // A fresh category referenced from outside the delete set via two distinct NO ACTION foreign keys:
957
+ // - bis_GeometricElement3d.CategoryId (a PhysicalObject assigned to it)
958
+ // - bis_Element.CodeScopeId (another element using it as code scope)
959
+ const multiRefCategory = SpatialCategory.insert(txn, IModel.dictionaryId, "MultiRefCategory", new SubCategoryAppearance());
960
+ const usesAsCategoryProps = {
961
+ classFullName: "Generic:PhysicalObject",
962
+ model: modelId,
963
+ category: multiRefCategory,
964
+ code: Code.createEmpty(),
965
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
966
+ };
967
+ const usesAsCategory = txn.insertElement(usesAsCategoryProps);
968
+ const usesAsCodeScope = insertElement({ codeScope: multiRefCategory, codeValue: "scoped-by-category" });
969
+ txn.saveChanges();
970
+ const result = txn.deleteElements([multiRefCategory]);
971
+ assert.equal(result.status, BulkDeleteElementsStatus.ConstraintViolation);
972
+ assert.equal(result.failures.length, 1, `expected a single failure, got ${JSON.stringify(result.failures)}`);
973
+ const [failure] = result.failures;
974
+ assert.equal(failure.elementId, multiRefCategory);
975
+ assert.equal(failure.reason, BulkDeleteFailureReason.ReferencedByForeignKey);
976
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
977
+ assert.deepEqual(result.failedIds, new Set([multiRefCategory]));
978
+ for (const id of [multiRefCategory, usesAsCategory, usesAsCodeScope])
979
+ assertExists(id);
980
+ // Once both referencing elements are in the delete set the category is no longer a violator.
981
+ const retry = txn.deleteElements([multiRefCategory, usesAsCategory, usesAsCodeScope]);
982
+ assertResult(retry, success, [multiRefCategory, usesAsCategory, usesAsCodeScope], []);
983
+ });
984
+ });
985
+ describe("cache coherence", () => {
986
+ /** Load an element (or model) so that it is held in the in-memory cache. */
987
+ const prime = (ids) => {
988
+ for (const id of ids)
989
+ assert.isDefined(iModelDb.elements.getElement(id));
990
+ };
991
+ it("cascaded descendants that were cached are no longer readable after their ancestor is deleted", () => {
992
+ const parent = insertElement();
993
+ const child = insertElement({ parentId: parent });
994
+ const grandchild = insertElement({ parentId: child });
995
+ prime([parent, child, grandchild]);
996
+ const result = txn.deleteElements([parent]);
997
+ assertResult(result, success, [parent, child, grandchild], []);
998
+ assert.isUndefined(iModelDb.elements.tryGetElementProps(child), "cached child props must not be served after deletion");
999
+ assert.isUndefined(iModelDb.elements.tryGetElementProps(grandchild), "cached grandchild props must not be served after deletion");
1000
+ assert.throws(() => iModelDb.elements.getElement(child));
1001
+ });
1002
+ it("cached sub-model contents and the model itself are no longer readable after the modeled element is deleted", () => {
1003
+ const partitionId = PhysicalModel.insert(txn, IModel.rootSubjectId, "CachedSubModelPartition");
1004
+ const inner = txn.insertElement({
1005
+ classFullName: "Generic:PhysicalObject",
1006
+ model: partitionId,
1007
+ category: categoryId,
1008
+ code: Code.createEmpty(),
1009
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
1010
+ });
1011
+ txn.saveChanges();
1012
+ prime([partitionId, inner]);
1013
+ assert.isDefined(iModelDb.models.getModelProps(partitionId));
1014
+ assert.isDefined(iModelDb.models.getModel(partitionId));
1015
+ const result = txn.deleteElements([partitionId]);
1016
+ assertResult(result, success, [partitionId, inner], []);
1017
+ assert.isUndefined(iModelDb.models.tryGetModelProps(partitionId), "cached model props must not be served after deletion");
1018
+ assert.isUndefined(iModelDb.models.tryGetModel(partitionId), "cached model must not be served after deletion");
1019
+ assert.isUndefined(iModelDb.elements.tryGetElementProps(inner), "cached sub-model element must not be served after deletion");
1020
+ });
1021
+ it("on ConstraintViolation, every cached element stays readable", () => {
1022
+ const rootA = insertElement();
1023
+ const external = insertElement({ codeScope: rootA, codeValue: "ext" });
1024
+ const rootB = insertElement();
1025
+ const childB = insertElement({ parentId: rootB });
1026
+ prime([rootA, external, rootB, childB]);
1027
+ const result = txn.deleteElements([rootA, rootB]);
1028
+ assertResult(result, constraintViolation(new Set([rootA]), codeScopeViolator(rootA)), [], [rootA, external, rootB, childB]);
1029
+ for (const id of [rootA, external, rootB, childB])
1030
+ assert.strictEqual(iModelDb.elements.getElement(id).id, id);
1031
+ });
1032
+ it("a cached parent reflects the removal of its children", () => {
1033
+ const parent = insertElement();
1034
+ const child = insertElement({ parentId: parent });
1035
+ prime([parent, child]);
1036
+ assert.deepEqual(iModelDb.elements.queryChildren(parent), [child]);
1037
+ const result = txn.deleteElements([child]);
1038
+ assertResult(result, success, [child], [parent]);
1039
+ assert.isEmpty(iModelDb.elements.queryChildren(parent));
1040
+ assert.strictEqual(iModelDb.elements.getElement(parent).id, parent);
1041
+ });
1042
+ });
1043
+ describe("related data cleanup", () => {
1044
+ const countAspects = async (elementId) => {
1045
+ const reader = iModelDb.createQueryReader("SELECT COUNT(*) FROM bis.ElementMultiAspect WHERE Element.Id=?", new QueryBinder().bindId(1, elementId), { usePrimaryConn: true });
1046
+ assert.isTrue(await reader.step());
1047
+ return reader.current.toArray()[0];
1048
+ };
1049
+ const insertAspect = (elementId, scopeId = IModel.rootSubjectId) => txn.insertAspect({
1050
+ classFullName: ExternalSourceAspect.classFullName,
1051
+ element: { id: elementId },
1052
+ scope: { id: scopeId },
1053
+ identifier: `src-${elementId}`,
1054
+ kind: "Element",
1055
+ });
1056
+ it("aspects owned by deleted elements (including cascaded descendants) are removed", async () => {
1057
+ const parent = insertElement();
1058
+ const child = insertElement({ parentId: parent });
1059
+ const survivor = insertElement();
1060
+ insertAspect(parent);
1061
+ insertAspect(child);
1062
+ insertAspect(child);
1063
+ const survivorAspect = insertAspect(survivor);
1064
+ assert.equal(await countAspects(parent), 1);
1065
+ assert.equal(await countAspects(child), 2);
1066
+ const result = txn.deleteElements([parent]);
1067
+ assertResult(result, success, [parent, child], [survivor]);
1068
+ assert.equal(await countAspects(parent), 0, "aspects of the deleted parent must be removed");
1069
+ assert.equal(await countAspects(child), 0, "aspects of the cascaded child must be removed");
1070
+ assert.equal(await countAspects(survivor), 1, "aspects of unrelated elements must be untouched");
1071
+ assert.isDefined(iModelDb.elements.getAspect(survivorAspect));
1072
+ });
1073
+ it("an element used as the Scope of another element's ExternalSourceAspect", () => {
1074
+ const scopeElem = insertElement();
1075
+ const owner = insertElement();
1076
+ const aspectId = insertAspect(owner, scopeElem);
1077
+ // Deleting both the scope element and the aspect owner is fine: the aspect goes away with its owner.
1078
+ const result = txn.deleteElements([scopeElem, owner]);
1079
+ assertResult(result, success, [scopeElem, owner], []);
1080
+ assert.throws(() => iModelDb.elements.getAspect(aspectId));
1081
+ });
1082
+ it("Aspects owned by cascaded sub-model members are removed", async () => {
1083
+ // Existing aspect tests only cover parent/child cascades; sub-model members reach the delete set through a different path.
1084
+ const partitionId = PhysicalModel.insert(txn, IModel.rootSubjectId, "AspectPartition");
1085
+ const insertMember = () => txn.insertElement({
1086
+ classFullName: "Generic:PhysicalObject", model: partitionId, category: categoryId, code: Code.createEmpty(),
1087
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
1088
+ });
1089
+ const member1 = insertMember();
1090
+ const member2 = insertMember();
1091
+ insertAspect(member1);
1092
+ insertAspect(member2);
1093
+ insertAspect(member2);
1094
+ txn.saveChanges();
1095
+ assert.equal(await countAspects(member1), 1);
1096
+ assert.equal(await countAspects(member2), 2);
1097
+ const result = txn.deleteElements([partitionId]);
1098
+ assertResult(result, success, [partitionId, member1, member2], []);
1099
+ assert.equal(await countAspects(member1), 0, "aspects of cascaded sub-model member must be removed");
1100
+ assert.equal(await countAspects(member2), 0, "aspects of cascaded sub-model member must be removed");
1101
+ });
1102
+ describe("domain-schema navigation properties (no FK constraint)", () => {
1103
+ const navObjectClass = "DeleteElementsNavTest:NavObject";
1104
+ const navRelClass = "DeleteElementsNavTest:NavObjectRefersToElement";
1105
+ /** Insert a `NavObject` that optionally points at `targetId` through its (non-FK) `Target` navigation property. */
1106
+ const insertNavObject = (targetId, parentId) => {
1107
+ const id = txn.insertElement({
1108
+ classFullName: navObjectClass, model: modelId, category: categoryId, code: Code.createEmpty(),
1109
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
1110
+ ...(targetId ? { target: { id: targetId, relClassName: navRelClass } } : {}),
1111
+ ...(parentId ? { parent: { id: parentId, relClassName: "BisCore:ElementOwnsChildElements" } } : {}),
1112
+ });
1113
+ assert.isNotEmpty(id);
1114
+ txn.saveChanges();
1115
+ return id;
1116
+ };
1117
+ /** Returns the raw `Target.Id` stored for `navObjectId`, or `undefined` if the column is NULL. */
1118
+ const queryTargetId = async (navObjectId) => {
1119
+ const reader = iModelDb.createQueryReader("SELECT Target.Id FROM dent.NavObject WHERE ECInstanceId=?", new QueryBinder().bindId(1, navObjectId), { usePrimaryConn: true });
1120
+ assert.isTrue(await reader.step(), `NavObject ${navObjectId} must still exist`);
1121
+ const value = reader.current.toArray()[0];
1122
+ return value === undefined || value === null ? undefined : value;
1123
+ };
1124
+ it("the nav property is not FK-enforced, so the reference alone must not block the delete", async () => {
1125
+ const target = insertElement();
1126
+ const referrer = insertNavObject(target);
1127
+ assert.equal(await queryTargetId(referrer), target);
1128
+ // Sanity check the premise of this test suite: the domain nav property has no SQLite FK behind it.
1129
+ const fkColumns = iModelDb.withSqliteStatement("PRAGMA foreign_key_list(dent_NavObject)", (stmt) => {
1130
+ const cols = [];
1131
+ while (stmt.step() === DbResult.BE_SQLITE_ROW)
1132
+ cols.push(stmt.getValueString(3));
1133
+ return cols;
1134
+ });
1135
+ assert.notInclude(fkColumns, "TargetId", "domain-schema nav props must not be FK-enforced; otherwise this test exercises the wrong code path");
1136
+ const result = txn.deleteElements([target]);
1137
+ assertResult(result, success, [target], [referrer]);
1138
+ });
1139
+ it("nav property on a surviving element is set to NULL when its target is deleted", async () => {
1140
+ const target = insertElement();
1141
+ const referrer = insertNavObject(target);
1142
+ const unrelatedTarget = insertElement();
1143
+ const unrelatedReferrer = insertNavObject(unrelatedTarget);
1144
+ const result = txn.deleteElements([target]);
1145
+ assertResult(result, success, [target], [referrer, unrelatedTarget, unrelatedReferrer]);
1146
+ assert.isUndefined(await queryTargetId(referrer), "the dangling Target reference must be NULLed, not left pointing at a deleted element");
1147
+ assert.equal(await queryTargetId(unrelatedReferrer), unrelatedTarget, "references to elements outside the delete set must be untouched");
1148
+ // The surviving referrer must still be readable through the element API with the nav prop cleared.
1149
+ const props = iModelDb.elements.getElementProps(referrer);
1150
+ assert.isTrue(props.target === undefined || props.target.id === undefined || !Id64.isValid(props.target.id), `expected cleared nav prop, got ${JSON.stringify(props.target)}`);
1151
+ });
1152
+ it("a cached surviving referrer does not keep a stale nav property after its target is deleted", () => {
1153
+ const target = insertElement();
1154
+ const referrer = insertNavObject(target);
1155
+ const cached = iModelDb.elements.getElementProps(referrer);
1156
+ assert.equal(cached.target?.id, target, "premise: the cached referrer points at the target");
1157
+ assertResult(txn.deleteElements([target]), success, [target], [referrer]);
1158
+ const reloaded = iModelDb.elements.getElementProps(referrer);
1159
+ assert.isUndefined(reloaded.target?.id, "referrer must be reloaded, not served stale from the cache");
1160
+ });
1161
+ it("updating a surviving referrer from props read before the delete cannot resurrect the dangling reference", async () => {
1162
+ const target = insertElement();
1163
+ const referrer = insertNavObject(target);
1164
+ iModelDb.elements.getElementProps(referrer);
1165
+ const result = txn.deleteElements([target]);
1166
+ assertResult(result, success, [target], [referrer]);
1167
+ const reloaded = iModelDb.elements.getElementProps(referrer);
1168
+ reloaded.userLabel = "edited-after-delete";
1169
+ txn.updateElement(reloaded);
1170
+ txn.saveChanges();
1171
+ assert.isUndefined(await queryTargetId(referrer), "an update from freshly loaded props must not write the deleted target id back");
1172
+ assert.equal(iModelDb.elements.getElementProps(referrer).userLabel, "edited-after-delete");
1173
+ });
1174
+ it("nav property pointing at a cascaded descendant is NULLed too", async () => {
1175
+ const parent = insertElement();
1176
+ const child = insertElement({ parentId: parent });
1177
+ const referrer = insertNavObject(child);
1178
+ const result = txn.deleteElements([parent]);
1179
+ assertResult(result, success, [parent, child], [referrer]);
1180
+ assert.isUndefined(await queryTargetId(referrer), "a reference to a cascaded child must be NULLed");
1181
+ });
1182
+ it("multiple referrers of the same target are all NULLed", async () => {
1183
+ const target = insertElement();
1184
+ const referrers = [insertNavObject(target), insertNavObject(target), insertNavObject(target)];
1185
+ const result = txn.deleteElements([target]);
1186
+ assertResult(result, success, [target], referrers);
1187
+ for (const referrer of referrers)
1188
+ assert.isUndefined(await queryTargetId(referrer));
1189
+ });
1190
+ it("referrer inside the delete set is simply deleted along with its target", async () => {
1191
+ const target = insertElement();
1192
+ const referrer = insertNavObject(target);
1193
+ const childReferrer = insertNavObject(target, target); // child of target, so cascaded into the set
1194
+ const result = txn.deleteElements([target, referrer]);
1195
+ assertResult(result, success, [target, referrer, childReferrer], []);
1196
+ });
1197
+ it("NULLing is rolled back when the txn is abandoned", async () => {
1198
+ const target = insertElement();
1199
+ const referrer = insertNavObject(target);
1200
+ const result = txn.deleteElements([target]);
1201
+ assertResult(result, success, [target], [referrer]);
1202
+ assert.isUndefined(await queryTargetId(referrer));
1203
+ txn.abandonChanges();
1204
+ assertExists(target);
1205
+ assert.equal(await queryTargetId(referrer), target, "abandoning the txn must restore the nav property");
1206
+ });
1207
+ });
1208
+ describe("ElementDrivesElement link table", () => {
1209
+ const countDrives = async () => {
1210
+ const reader = iModelDb.createQueryReader("SELECT COUNT(*) FROM bis.ElementDrivesElement", undefined, { usePrimaryConn: true });
1211
+ assert.isTrue(await reader.step());
1212
+ return reader.current.toArray()[0];
1213
+ };
1214
+ const insertDrives = (sourceId, targetId) => txn.insertRelationship({ classFullName: ElementDrivesElement.classFullName, sourceId, targetId, status: 0, priority: 0 });
1215
+ it("deleting the source removes the row; the target survives", async () => {
1216
+ const a = insertElement();
1217
+ const b = insertElement();
1218
+ insertDrives(a, b);
1219
+ txn.saveChanges();
1220
+ assert.equal(await countDrives(), 1);
1221
+ const result = txn.deleteElements([a]);
1222
+ assertResult(result, success, [a], [b]);
1223
+ assert.equal(await countDrives(), 0, "ElementDrivesElement row referencing the deleted source must be removed");
1224
+ });
1225
+ it("deleting the target removes the row; the source survives", async () => {
1226
+ const a = insertElement();
1227
+ const b = insertElement();
1228
+ insertDrives(a, b);
1229
+ txn.saveChanges();
1230
+ assert.equal(await countDrives(), 1);
1231
+ const result = txn.deleteElements([b]);
1232
+ assertResult(result, success, [b], [a]);
1233
+ assert.equal(await countDrives(), 0, "ElementDrivesElement row referencing the deleted target must be removed");
1234
+ });
1235
+ });
1236
+ describe("ElementRefersToElements link table", () => {
1237
+ let groupModelId;
1238
+ let groupCounter = 0;
1239
+ beforeEach(() => {
1240
+ const partitionId = txn.insertElement({
1241
+ classFullName: GroupInformationPartition.classFullName,
1242
+ model: IModel.repositoryModelId,
1243
+ parent: new SubjectOwnsPartitionElements(IModel.rootSubjectId),
1244
+ code: GroupInformationPartition.createCode(iModelDb, IModel.rootSubjectId, `GroupPartition-${++groupCounter}`),
1245
+ });
1246
+ groupModelId = txn.insertModel({ classFullName: GroupModel.classFullName, modeledElement: { id: partitionId } });
1247
+ txn.saveChanges();
1248
+ });
1249
+ const insertGroup = () => txn.insertElement({ classFullName: Group.classFullName, model: groupModelId, code: Code.createEmpty() });
1250
+ const countMemberships = async (groupId) => {
1251
+ const reader = iModelDb.createQueryReader("SELECT COUNT(*) FROM bis.ElementGroupsMembers WHERE SourceECInstanceId=?", new QueryBinder().bindId(1, groupId), { usePrimaryConn: true });
1252
+ assert.isTrue(await reader.step());
1253
+ return reader.current.toArray()[0];
1254
+ };
1255
+ it("deleting a member removes only its membership row; the group and other members survive", async () => {
1256
+ const group = insertGroup();
1257
+ const member1 = insertElement();
1258
+ const member2 = insertElement();
1259
+ const rel1 = txn.insertRelationship(ElementGroupsMembers.create(iModelDb, group, member1, 0).toJSON());
1260
+ const rel2 = txn.insertRelationship(ElementGroupsMembers.create(iModelDb, group, member2, 1).toJSON());
1261
+ txn.saveChanges();
1262
+ assert.equal(await countMemberships(group), 2);
1263
+ const result = txn.deleteElements([member1]);
1264
+ assertResult(result, success, [member1], [group, member2]);
1265
+ assert.equal(await countMemberships(group), 1);
1266
+ assert.isUndefined(iModelDb.relationships.tryGetInstanceProps(ElementGroupsMembers.classFullName, rel1));
1267
+ assert.isDefined(iModelDb.relationships.tryGetInstanceProps(ElementGroupsMembers.classFullName, rel2));
1268
+ });
1269
+ it("deleting the group removes all of its membership rows; the members survive", async () => {
1270
+ const group = insertGroup();
1271
+ const member1 = insertElement();
1272
+ const member2 = insertElement();
1273
+ txn.insertRelationship(ElementGroupsMembers.create(iModelDb, group, member1, 0).toJSON());
1274
+ txn.insertRelationship(ElementGroupsMembers.create(iModelDb, group, member2, 1).toJSON());
1275
+ txn.saveChanges();
1276
+ const result = txn.deleteElements([group]);
1277
+ assertResult(result, success, [group], [member1, member2]);
1278
+ assert.equal(await countMemberships(group), 0);
1279
+ });
1280
+ it("deleting a parent whose cascaded child is a group member removes the membership row", () => {
1281
+ const group = insertGroup();
1282
+ const parent = insertElement();
1283
+ const child = insertElement({ parentId: parent });
1284
+ const rel = txn.insertRelationship(ElementGroupsMembers.create(iModelDb, group, child, 0).toJSON());
1285
+ txn.saveChanges();
1286
+ const result = txn.deleteElements([parent]);
1287
+ assertResult(result, success, [parent, child], [group]);
1288
+ assert.isUndefined(iModelDb.relationships.tryGetInstanceProps(ElementGroupsMembers.classFullName, rel));
1289
+ });
1290
+ });
1291
+ describe("ModelSelectorRefersToModels link table", () => {
1292
+ let selectorCounter = 0;
1293
+ /** Insert a partition + PhysicalModel and return the partition id (which doubles as the model id). */
1294
+ const insertPhysicalModel = (name) => {
1295
+ const id = PhysicalModel.insert(txn, IModel.rootSubjectId, name);
1296
+ txn.saveChanges();
1297
+ return id;
1298
+ };
1299
+ const insertModelSelector = (models) => {
1300
+ const id = ModelSelector.insert(txn, IModel.dictionaryId, `Selector-${++selectorCounter}`, models);
1301
+ txn.saveChanges();
1302
+ return id;
1303
+ };
1304
+ /** Count link-table rows, optionally restricted to a given selector (source) or model (target). */
1305
+ const countRows = async (filter = {}) => {
1306
+ const clauses = [];
1307
+ const binder = new QueryBinder();
1308
+ let idx = 1;
1309
+ if (filter.sourceId) {
1310
+ clauses.push(`SourceECInstanceId=?`);
1311
+ binder.bindId(idx++, filter.sourceId);
1312
+ }
1313
+ if (filter.targetId) {
1314
+ clauses.push(`TargetECInstanceId=?`);
1315
+ binder.bindId(idx++, filter.targetId);
1316
+ }
1317
+ const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
1318
+ const reader = iModelDb.createQueryReader(`SELECT COUNT(*) FROM bis.ModelSelectorRefersToModels${where}`, binder, { usePrimaryConn: true });
1319
+ assert.isTrue(await reader.step());
1320
+ return reader.current.toArray()[0];
1321
+ };
1322
+ /** Rows whose source (ModelSelector) or target (modeled element) no longer exists in bis.Element. */
1323
+ const countDanglingRows = async () => {
1324
+ const reader = iModelDb.createQueryReader(`SELECT COUNT(*) FROM bis.ModelSelectorRefersToModels r
1325
+ WHERE NOT EXISTS (SELECT 1 FROM bis.Element e WHERE e.ECInstanceId = r.SourceECInstanceId)
1326
+ OR NOT EXISTS (SELECT 1 FROM bis.Element e WHERE e.ECInstanceId = r.TargetECInstanceId)`, undefined, { usePrimaryConn: true });
1327
+ assert.isTrue(await reader.step());
1328
+ return reader.current.toArray()[0];
1329
+ };
1330
+ it("deleting a referenced model (target) removes only its rows; the selector and other models survive", async () => {
1331
+ const modelA = insertPhysicalModel("MsTargetA");
1332
+ const modelB = insertPhysicalModel("MsTargetB");
1333
+ const selector = insertModelSelector([modelA, modelB]);
1334
+ assert.equal(await countRows({ sourceId: selector }), 2);
1335
+ const result = txn.deleteElements([modelA]);
1336
+ assertResult(result, success, [modelA], [modelB, selector]);
1337
+ assert.equal(await countRows({ targetId: modelA }), 0, "rows targeting the deleted model must be removed");
1338
+ assert.equal(await countRows({ sourceId: selector, targetId: modelB }), 1, "rows targeting a surviving model must be untouched");
1339
+ assert.equal(await countDanglingRows(), 0);
1340
+ });
1341
+ it("deleting the ModelSelector itself (source) removes all of its rows; the referenced models survive", async () => {
1342
+ // Regression: cleanup used to only match on TargetId, so deleting the selector left its rows dangling on the source side.
1343
+ const modelA = insertPhysicalModel("MsSourceA");
1344
+ const modelB = insertPhysicalModel("MsSourceB");
1345
+ const selector = insertModelSelector([modelA, modelB]);
1346
+ assert.equal(await countRows({ sourceId: selector }), 2);
1347
+ const result = txn.deleteElements([selector]);
1348
+ assertResult(result, success, [selector], [modelA, modelB]);
1349
+ assert.equal(await countRows({ sourceId: selector }), 0, "rows sourced from the deleted ModelSelector must be removed");
1350
+ assert.equal(await countDanglingRows(), 0, "no ModelSelectorRefersToModels row may reference a deleted element");
1351
+ assertNoFkViolations(iModelDb);
1352
+ });
1353
+ it("deleting one selector leaves another selector's rows for the same models untouched", async () => {
1354
+ const modelA = insertPhysicalModel("MsSharedA");
1355
+ const selector1 = insertModelSelector([modelA]);
1356
+ const selector2 = insertModelSelector([modelA]);
1357
+ assert.equal(await countRows({ targetId: modelA }), 2);
1358
+ const result = txn.deleteElements([selector1]);
1359
+ assertResult(result, success, [selector1], [selector2, modelA]);
1360
+ assert.equal(await countRows({ sourceId: selector1 }), 0);
1361
+ assert.equal(await countRows({ sourceId: selector2, targetId: modelA }), 1, "the surviving selector's row must be untouched");
1362
+ assert.equal(await countDanglingRows(), 0);
1363
+ });
1364
+ it("deleting both a selector and one of its models in the same call removes every affected row", async () => {
1365
+ const modelA = insertPhysicalModel("MsBothA");
1366
+ const modelB = insertPhysicalModel("MsBothB");
1367
+ const selector = insertModelSelector([modelA, modelB]);
1368
+ const otherSelector = insertModelSelector([modelB]);
1369
+ assert.equal(await countRows(), 3);
1370
+ const result = txn.deleteElements([selector, modelA]);
1371
+ assertResult(result, success, [selector, modelA], [modelB, otherSelector]);
1372
+ assert.equal(await countRows({ sourceId: selector }), 0);
1373
+ assert.equal(await countRows({ targetId: modelA }), 0);
1374
+ assert.equal(await countRows({ sourceId: otherSelector, targetId: modelB }), 1);
1375
+ assert.equal(await countDanglingRows(), 0);
1376
+ });
1377
+ it("link-table cleanup is rolled back when the txn is abandoned", async () => {
1378
+ const modelA = insertPhysicalModel("MsUndoA");
1379
+ const selector = insertModelSelector([modelA]);
1380
+ assert.equal(await countRows({ sourceId: selector }), 1);
1381
+ const result = txn.deleteElements([selector]);
1382
+ assertResult(result, success, [selector], [modelA]);
1383
+ assert.equal(await countRows({ sourceId: selector }), 0);
1384
+ txn.abandonChanges();
1385
+ assertExists(selector);
1386
+ assert.equal(await countRows({ sourceId: selector, targetId: modelA }), 1, "abandoning the txn must restore the link-table row");
1387
+ assert.isTrue((iModelDb.elements.getElement(selector)).models.includes(modelA));
1388
+ });
1389
+ });
1390
+ });
1391
+ describe("rejection with hierarchy", () => {
1392
+ it("reports the cascaded child violator (not the requested ancestor) and deletes nothing", () => {
1393
+ const rootA = insertElement();
1394
+ const childA = insertElement({ parentId: rootA });
1395
+ const external = insertElement({ codeScope: childA, codeValue: "ext" });
1396
+ const unrelated = insertElement();
1397
+ expectRejected("violator is a cascaded child", [rootA, unrelated], [rootA, childA, external, unrelated], new Set([childA]), codeScopeViolator(childA));
1398
+ });
1399
+ it("sub-model partition rejected by an element inside its sub-model", () => {
1400
+ const partitionId = PhysicalModel.insert(txn, IModel.rootSubjectId, "RejectedPartition");
1401
+ const inner = txn.insertElement({
1402
+ classFullName: "Generic:PhysicalObject",
1403
+ model: partitionId,
1404
+ category: categoryId,
1405
+ code: Code.createEmpty(),
1406
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
1407
+ });
1408
+ txn.saveChanges();
1409
+ const external = insertElement({ codeScope: inner, codeValue: "ext" });
1410
+ expectRejected("violator is inside the sub-model", [partitionId], [partitionId, inner, external], new Set([inner]), codeScopeViolator(inner));
1411
+ assert.isDefined(iModelDb.models.tryGetModelProps(partitionId), "sub-model must survive a rejection");
1412
+ });
1413
+ });
1414
+ describe("undo / redo (StandaloneDb)", () => {
1415
+ let sdb;
1416
+ let sModelId;
1417
+ let sCategoryId;
1418
+ before(() => {
1419
+ sdb = StandaloneDb.createEmpty(IModelTestUtils.prepareOutputFile("DeleteElements", "UndoRedo.bim"), {
1420
+ rootSubject: { name: "DeleteElementsUndo" },
1421
+ enableTransactions: true,
1422
+ });
1423
+ sdb.channels.addAllowedChannel(ChannelControl.sharedChannelName);
1424
+ withEditTxn(sdb, "setup", (t) => {
1425
+ sModelId = PhysicalModel.insert(t, IModel.rootSubjectId, "UndoModel");
1426
+ sCategoryId = SpatialCategory.insert(t, IModel.dictionaryId, "UndoCategory", new SubCategoryAppearance());
1427
+ });
1428
+ });
1429
+ after(() => {
1430
+ if (sdb?.isOpen)
1431
+ sdb.close();
1432
+ });
1433
+ const insertInStandalone = (t, parentId) => t.insertElement({
1434
+ classFullName: "Generic:PhysicalObject",
1435
+ model: sModelId,
1436
+ category: sCategoryId,
1437
+ code: Code.createEmpty(),
1438
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
1439
+ ...(parentId ? { parent: { id: parentId, relClassName: "BisCore:ElementOwnsChildElements" } } : {}),
1440
+ });
1441
+ it("a bulk delete is recorded as a single undoable txn and can be reversed and reinstated", () => {
1442
+ const { parent, child, partition, inner, survivor } = withEditTxn(sdb, "insert", (t) => {
1443
+ const p = insertInStandalone(t);
1444
+ const c = insertInStandalone(t, p);
1445
+ const part = PhysicalModel.insert(t, IModel.rootSubjectId, "UndoPartition");
1446
+ const i = t.insertElement({
1447
+ classFullName: "Generic:PhysicalObject", model: part, category: sCategoryId, code: Code.createEmpty(),
1448
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
1449
+ });
1450
+ return { parent: p, child: c, partition: part, inner: i, survivor: insertInStandalone(t) };
1451
+ });
1452
+ withEditTxn(sdb, "bulk delete", (t) => {
1453
+ const result = t.deleteElements([parent, partition]);
1454
+ assert.equal(result.status, BulkDeleteElementsStatus.Success);
1455
+ assert.isEmpty(result.failures);
1456
+ });
1457
+ assertNoFkViolations(sdb);
1458
+ for (const id of [parent, child, partition, inner])
1459
+ assert.isUndefined(sdb.elements.tryGetElement(id));
1460
+ assert.isUndefined(sdb.models.tryGetModelProps(partition));
1461
+ assert.isDefined(sdb.elements.tryGetElement(survivor));
1462
+ // Undo: everything the bulk delete removed (requested, cascaded children, sub-model + contents) comes back in one step.
1463
+ assert.isTrue(sdb.txns.isUndoPossible);
1464
+ assert.equal(sdb.txns.reverseSingleTxn(), IModelStatus.Success);
1465
+ for (const id of [parent, child, partition, inner, survivor])
1466
+ assert.isDefined(sdb.elements.tryGetElement(id), `${id} must be restored by undo`);
1467
+ assert.isDefined(sdb.models.tryGetModelProps(partition), "sub-model must be restored by undo");
1468
+ assertNoFkViolations(sdb);
1469
+ // Redo: deleted again.
1470
+ assert.isTrue(sdb.txns.isRedoPossible);
1471
+ assert.equal(sdb.txns.reinstateTxn(), IModelStatus.Success);
1472
+ for (const id of [parent, child, partition, inner])
1473
+ assert.isUndefined(sdb.elements.tryGetElement(id), `${id} must be deleted again by redo`);
1474
+ assert.isUndefined(sdb.models.tryGetModelProps(partition));
1475
+ assert.isDefined(sdb.elements.tryGetElement(survivor));
1476
+ assertNoFkViolations(sdb);
1477
+ });
1478
+ it("a rejected (ConstraintViolation) delete records nothing to undo", () => {
1479
+ const { rootA, external, rootB } = withEditTxn(sdb, "insert", (t) => {
1480
+ const a = insertInStandalone(t);
1481
+ const specId = sdb.codeSpecs.insert(t, "UndoScopeSpec", CodeScopeSpec.Type.RelatedElement);
1482
+ const ext = t.insertElement({
1483
+ classFullName: "Generic:PhysicalObject", model: sModelId, category: sCategoryId,
1484
+ code: { spec: specId, scope: a, value: "ext" },
1485
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
1486
+ });
1487
+ return { rootA: a, external: ext, rootB: insertInStandalone(t) };
1488
+ });
1489
+ withEditTxn(sdb, "rejected delete", (t) => {
1490
+ const result = t.deleteElements([rootA, rootB]);
1491
+ assert.equal(result.status, BulkDeleteElementsStatus.ConstraintViolation);
1492
+ assert.isFalse(sdb[_nativeDb].hasUnsavedChanges(), "a rejected delete must not dirty the txn");
1493
+ });
1494
+ for (const id of [rootA, external, rootB])
1495
+ assert.isDefined(sdb.elements.tryGetElement(id));
1496
+ assertNoFkViolations(sdb);
1497
+ // The only undoable txn is the insert above; undoing it must remove all three, proving the rejected delete left no txn behind.
1498
+ assert.isTrue(sdb.txns.isUndoPossible);
1499
+ assert.equal(sdb.txns.reverseSingleTxn(), IModelStatus.Success);
1500
+ for (const id of [rootA, external, rootB])
1501
+ assert.isUndefined(sdb.elements.tryGetElement(id), `${id} must be removed by undoing the insert txn`);
1502
+ assert.equal(sdb.txns.reinstateTxn(), IModelStatus.Success);
1503
+ for (const id of [rootA, external, rootB])
1504
+ assert.isDefined(sdb.elements.tryGetElement(id));
1505
+ });
1506
+ });
1507
+ describe("robustness / negative path tests", () => {
1508
+ it("deleting the root Subject does not delete anything", () => {
1509
+ // The root Subject is the modeled element of the RepositoryModel, which in turn contains the root Subject: a cycle in
1510
+ // the expansion graph. The call must terminate; it must never remove the root of the iModel.
1511
+ const sentinel = insertElement();
1512
+ const result = txn.deleteElements([IModel.rootSubjectId]);
1513
+ assert.notEqual(result.status, BulkDeleteElementsStatus.Success, "root Subject must not be deletable");
1514
+ assert.notEqual(result.status, BulkDeleteElementsStatus.DeletionFailed, "root Subject delete must be rejected by validation, not by a SQLite error");
1515
+ assertExists(IModel.rootSubjectId, "root Subject must survive");
1516
+ assert.isDefined(iModelDb.models.tryGetModelProps(IModel.repositoryModelId), "RepositoryModel must survive");
1517
+ assert.isDefined(iModelDb.models.tryGetModelProps(IModel.dictionaryId), "DictionaryModel must survive");
1518
+ assertExists(modelId, "seed partition must survive");
1519
+ assertExists(categoryId, "seed category must survive");
1520
+ assertExists(sentinel, "unrelated element must survive");
1521
+ assertNoFkViolations(iModelDb);
1522
+ });
1523
+ it("plain element, parent tree, partition, in-use category and unused material in one call", () => {
1524
+ const plainElem = insertElement();
1525
+ const parent = insertElement();
1526
+ const child1 = insertElement({ parentId: parent });
1527
+ const child2 = insertElement({ parentId: parent });
1528
+ const partition = PhysicalModel.insert(txn, IModel.rootSubjectId, "TestPartition");
1529
+ const inner = txn.insertElement({
1530
+ classFullName: "Generic:PhysicalObject", model: partition, category: categoryId, code: Code.createEmpty(),
1531
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
1532
+ });
1533
+ const violatorCat = SpatialCategory.insert(txn, IModel.dictionaryId, "TestCategory2", new SubCategoryAppearance());
1534
+ const violatorSub = IModel.getDefaultSubCategoryId(violatorCat);
1535
+ const externalUser = txn.insertElement({
1536
+ classFullName: "Generic:PhysicalObject", model: modelId, category: violatorCat, code: Code.createEmpty(),
1537
+ placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } },
1538
+ });
1539
+ const unusedMat = RenderMaterialElement.insert(txn, IModel.dictionaryId, "TestMaterial", { paletteName: "p" });
1540
+ txn.saveChanges();
1541
+ // One in-use category rejects the whole call: the plain element, the tree, the partition and the unused material all survive.
1542
+ expectRejected("test rejection with FK violators", [plainElem, parent, partition, violatorCat, unusedMat], [plainElem, parent, child1, child2, partition, inner, unusedMat, violatorCat, violatorSub, externalUser], new Set([violatorCat]), categoryInUse3d(violatorCat));
1543
+ assert.isDefined(iModelDb.models.tryGetModelProps(partition), "partition sub-model must be untouched");
1544
+ // Without the violator everything else goes in one call.
1545
+ executeTestCase("success without violators", [plainElem, parent, partition, unusedMat], [plainElem, parent, child1, child2, partition, inner, unusedMat], [violatorCat, violatorSub, externalUser]);
667
1546
  });
668
1547
  });
669
1548
  });