@itwin/core-backend 5.15.0-dev.9 → 5.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -1
- package/lib/cjs/BlobContainerService.d.ts +2 -0
- package/lib/cjs/BlobContainerService.d.ts.map +1 -1
- package/lib/cjs/BlobContainerService.js.map +1 -1
- package/lib/cjs/BriefcaseManager.d.ts +2 -1
- package/lib/cjs/BriefcaseManager.d.ts.map +1 -1
- package/lib/cjs/BriefcaseManager.js +39 -9
- package/lib/cjs/BriefcaseManager.js.map +1 -1
- package/lib/cjs/ChangeUnifier.d.ts +82 -0
- package/lib/cjs/ChangeUnifier.d.ts.map +1 -0
- package/lib/cjs/ChangeUnifier.js +163 -0
- package/lib/cjs/ChangeUnifier.js.map +1 -0
- package/lib/cjs/ChangesetReader.d.ts +21 -2
- package/lib/cjs/ChangesetReader.d.ts.map +1 -1
- package/lib/cjs/ChangesetReader.js +37 -1
- package/lib/cjs/ChangesetReader.js.map +1 -1
- package/lib/cjs/CloudSqlite.d.ts +15 -0
- package/lib/cjs/CloudSqlite.d.ts.map +1 -1
- package/lib/cjs/CloudSqlite.js.map +1 -1
- package/lib/cjs/ECDb.d.ts +1 -0
- package/lib/cjs/ECDb.d.ts.map +1 -1
- package/lib/cjs/ECDb.js +4 -1
- package/lib/cjs/ECDb.js.map +1 -1
- package/lib/cjs/EditTxn.d.ts +61 -19
- package/lib/cjs/EditTxn.d.ts.map +1 -1
- package/lib/cjs/EditTxn.js +84 -29
- package/lib/cjs/EditTxn.js.map +1 -1
- package/lib/cjs/GeoCoordConfig.d.ts +1 -0
- package/lib/cjs/GeoCoordConfig.d.ts.map +1 -1
- package/lib/cjs/GeoCoordConfig.js +12 -8
- package/lib/cjs/GeoCoordConfig.js.map +1 -1
- package/lib/cjs/IModelDb.d.ts +22 -9
- package/lib/cjs/IModelDb.d.ts.map +1 -1
- package/lib/cjs/IModelDb.js +54 -12
- package/lib/cjs/IModelDb.js.map +1 -1
- package/lib/cjs/NativeHost.d.ts.map +1 -1
- package/lib/cjs/NativeHost.js +5 -1
- package/lib/cjs/NativeHost.js.map +1 -1
- package/lib/cjs/Relationship.d.ts +28 -0
- package/lib/cjs/Relationship.d.ts.map +1 -1
- package/lib/cjs/Relationship.js +78 -0
- package/lib/cjs/Relationship.js.map +1 -1
- package/lib/cjs/SchemaSync.d.ts.map +1 -1
- package/lib/cjs/SchemaSync.js +1 -0
- package/lib/cjs/SchemaSync.js.map +1 -1
- package/lib/cjs/TxnManager.d.ts +126 -89
- package/lib/cjs/TxnManager.d.ts.map +1 -1
- package/lib/cjs/TxnManager.js +114 -76
- package/lib/cjs/TxnManager.js.map +1 -1
- package/lib/cjs/ViewStateHydrator.d.ts.map +1 -1
- package/lib/cjs/ViewStateHydrator.js.map +1 -1
- package/lib/cjs/ViewStore.d.ts.map +1 -1
- package/lib/cjs/ViewStore.js +62 -19
- package/lib/cjs/ViewStore.js.map +1 -1
- package/lib/cjs/annotations/LeaderGeometry.d.ts.map +1 -1
- package/lib/cjs/annotations/LeaderGeometry.js +3 -0
- package/lib/cjs/annotations/LeaderGeometry.js.map +1 -1
- package/lib/cjs/core-backend.d.ts +1 -0
- package/lib/cjs/core-backend.d.ts.map +1 -1
- package/lib/cjs/core-backend.js +1 -0
- package/lib/cjs/core-backend.js.map +1 -1
- package/lib/cjs/internal/BriefcaseUtils.d.ts +19 -0
- package/lib/cjs/internal/BriefcaseUtils.d.ts.map +1 -0
- package/lib/cjs/internal/BriefcaseUtils.js +65 -0
- package/lib/cjs/internal/BriefcaseUtils.js.map +1 -0
- package/lib/cjs/internal/QueryMetadataCache.d.ts +44 -0
- package/lib/cjs/internal/QueryMetadataCache.d.ts.map +1 -0
- package/lib/cjs/internal/QueryMetadataCache.js +107 -0
- package/lib/cjs/internal/QueryMetadataCache.js.map +1 -0
- package/lib/cjs/internal/Symbols.d.ts +9 -0
- package/lib/cjs/internal/Symbols.d.ts.map +1 -1
- package/lib/cjs/internal/Symbols.js +10 -1
- package/lib/cjs/internal/Symbols.js.map +1 -1
- package/lib/cjs/internal/workspace/WorkspaceImpl.d.ts.map +1 -1
- package/lib/cjs/internal/workspace/WorkspaceImpl.js +20 -6
- package/lib/cjs/internal/workspace/WorkspaceImpl.js.map +1 -1
- package/lib/cjs/rpc-impl/IModelReadRpcImpl.d.ts +6 -6
- package/lib/cjs/rpc-impl/IModelReadRpcImpl.d.ts.map +1 -1
- package/lib/cjs/rpc-impl/IModelReadRpcImpl.js.map +1 -1
- package/lib/cjs/workspace/SettingsEditor.d.ts +4 -3
- package/lib/cjs/workspace/SettingsEditor.d.ts.map +1 -1
- package/lib/cjs/workspace/SettingsEditor.js +7 -5
- package/lib/cjs/workspace/SettingsEditor.js.map +1 -1
- package/lib/esm/BlobContainerService.d.ts +2 -0
- package/lib/esm/BlobContainerService.d.ts.map +1 -1
- package/lib/esm/BlobContainerService.js.map +1 -1
- package/lib/esm/BriefcaseManager.d.ts +2 -1
- package/lib/esm/BriefcaseManager.d.ts.map +1 -1
- package/lib/esm/BriefcaseManager.js +40 -10
- package/lib/esm/BriefcaseManager.js.map +1 -1
- package/lib/esm/ChangeUnifier.d.ts +82 -0
- package/lib/esm/ChangeUnifier.d.ts.map +1 -0
- package/lib/esm/ChangeUnifier.js +159 -0
- package/lib/esm/ChangeUnifier.js.map +1 -0
- package/lib/esm/ChangesetReader.d.ts +21 -2
- package/lib/esm/ChangesetReader.d.ts.map +1 -1
- package/lib/esm/ChangesetReader.js +38 -2
- package/lib/esm/ChangesetReader.js.map +1 -1
- package/lib/esm/CloudSqlite.d.ts +15 -0
- package/lib/esm/CloudSqlite.d.ts.map +1 -1
- package/lib/esm/CloudSqlite.js.map +1 -1
- package/lib/esm/ECDb.d.ts +1 -0
- package/lib/esm/ECDb.d.ts.map +1 -1
- package/lib/esm/ECDb.js +4 -1
- package/lib/esm/ECDb.js.map +1 -1
- package/lib/esm/EditTxn.d.ts +61 -19
- package/lib/esm/EditTxn.d.ts.map +1 -1
- package/lib/esm/EditTxn.js +85 -30
- package/lib/esm/EditTxn.js.map +1 -1
- package/lib/esm/GeoCoordConfig.d.ts +1 -0
- package/lib/esm/GeoCoordConfig.d.ts.map +1 -1
- package/lib/esm/GeoCoordConfig.js +12 -8
- package/lib/esm/GeoCoordConfig.js.map +1 -1
- package/lib/esm/IModelDb.d.ts +22 -9
- package/lib/esm/IModelDb.d.ts.map +1 -1
- package/lib/esm/IModelDb.js +56 -14
- package/lib/esm/IModelDb.js.map +1 -1
- package/lib/esm/NativeHost.d.ts.map +1 -1
- package/lib/esm/NativeHost.js +5 -1
- package/lib/esm/NativeHost.js.map +1 -1
- package/lib/esm/Relationship.d.ts +28 -0
- package/lib/esm/Relationship.d.ts.map +1 -1
- package/lib/esm/Relationship.js +78 -0
- package/lib/esm/Relationship.js.map +1 -1
- package/lib/esm/SchemaSync.d.ts.map +1 -1
- package/lib/esm/SchemaSync.js +1 -0
- package/lib/esm/SchemaSync.js.map +1 -1
- package/lib/esm/TxnManager.d.ts +126 -89
- package/lib/esm/TxnManager.d.ts.map +1 -1
- package/lib/esm/TxnManager.js +116 -78
- package/lib/esm/TxnManager.js.map +1 -1
- package/lib/esm/ViewStateHydrator.d.ts.map +1 -1
- package/lib/esm/ViewStateHydrator.js.map +1 -1
- package/lib/esm/ViewStore.d.ts.map +1 -1
- package/lib/esm/ViewStore.js +62 -19
- package/lib/esm/ViewStore.js.map +1 -1
- package/lib/esm/annotations/LeaderGeometry.d.ts.map +1 -1
- package/lib/esm/annotations/LeaderGeometry.js +3 -0
- package/lib/esm/annotations/LeaderGeometry.js.map +1 -1
- package/lib/esm/core-backend.d.ts +1 -0
- package/lib/esm/core-backend.d.ts.map +1 -1
- package/lib/esm/core-backend.js +1 -0
- package/lib/esm/core-backend.js.map +1 -1
- package/lib/esm/internal/BriefcaseUtils.d.ts +19 -0
- package/lib/esm/internal/BriefcaseUtils.d.ts.map +1 -0
- package/lib/esm/internal/BriefcaseUtils.js +61 -0
- package/lib/esm/internal/BriefcaseUtils.js.map +1 -0
- package/lib/esm/internal/QueryMetadataCache.d.ts +44 -0
- package/lib/esm/internal/QueryMetadataCache.d.ts.map +1 -0
- package/lib/esm/internal/QueryMetadataCache.js +103 -0
- package/lib/esm/internal/QueryMetadataCache.js.map +1 -0
- package/lib/esm/internal/Symbols.d.ts +9 -0
- package/lib/esm/internal/Symbols.d.ts.map +1 -1
- package/lib/esm/internal/Symbols.js +9 -0
- package/lib/esm/internal/Symbols.js.map +1 -1
- package/lib/esm/internal/workspace/WorkspaceImpl.d.ts.map +1 -1
- package/lib/esm/internal/workspace/WorkspaceImpl.js +20 -6
- package/lib/esm/internal/workspace/WorkspaceImpl.js.map +1 -1
- package/lib/esm/rpc-impl/IModelReadRpcImpl.d.ts +6 -6
- package/lib/esm/rpc-impl/IModelReadRpcImpl.d.ts.map +1 -1
- package/lib/esm/rpc-impl/IModelReadRpcImpl.js.map +1 -1
- package/lib/esm/test/annotations/LeaderGeometry.test.js +15 -1
- package/lib/esm/test/annotations/LeaderGeometry.test.js.map +1 -1
- package/lib/esm/test/ecdb/ConcurrentQuery.test.js +144 -30
- package/lib/esm/test/ecdb/ConcurrentQuery.test.js.map +1 -1
- package/lib/esm/test/ecdb/QueryMetadataCache.test.d.ts +2 -0
- package/lib/esm/test/ecdb/QueryMetadataCache.test.d.ts.map +1 -0
- package/lib/esm/test/ecdb/QueryMetadataCache.test.js +630 -0
- package/lib/esm/test/ecdb/QueryMetadataCache.test.js.map +1 -0
- package/lib/esm/test/element/DeleteDefinitionElements.test.js +403 -249
- package/lib/esm/test/element/DeleteDefinitionElements.test.js.map +1 -1
- package/lib/esm/test/element/RelationshipBatchLookup.test.d.ts +2 -0
- package/lib/esm/test/element/RelationshipBatchLookup.test.d.ts.map +1 -0
- package/lib/esm/test/element/RelationshipBatchLookup.test.js +202 -0
- package/lib/esm/test/element/RelationshipBatchLookup.test.js.map +1 -0
- package/lib/esm/test/hubaccess/BriefcaseManager.test.js +187 -1
- package/lib/esm/test/hubaccess/BriefcaseManager.test.js.map +1 -1
- package/lib/esm/test/hubaccess/SchemaImportOrphanColumns.test.d.ts +2 -0
- package/lib/esm/test/hubaccess/SchemaImportOrphanColumns.test.d.ts.map +1 -0
- package/lib/esm/test/hubaccess/SchemaImportOrphanColumns.test.js +142 -0
- package/lib/esm/test/hubaccess/SchemaImportOrphanColumns.test.js.map +1 -0
- package/lib/esm/test/imodel/IModelGeolocation.test.js +8 -6
- package/lib/esm/test/imodel/IModelGeolocation.test.js.map +1 -1
- package/lib/esm/test/imodel/IModelProps.test.d.ts +2 -0
- package/lib/esm/test/imodel/IModelProps.test.d.ts.map +1 -0
- package/lib/esm/test/imodel/IModelProps.test.js +411 -0
- package/lib/esm/test/imodel/IModelProps.test.js.map +1 -0
- package/lib/esm/test/schema/SchemaViewPropertyOverrides.test.d.ts +2 -0
- package/lib/esm/test/schema/SchemaViewPropertyOverrides.test.d.ts.map +1 -0
- package/lib/esm/test/schema/SchemaViewPropertyOverrides.test.js +240 -0
- package/lib/esm/test/schema/SchemaViewPropertyOverrides.test.js.map +1 -0
- package/lib/esm/test/schema/SchemaViewValidation.test.js +2 -8
- package/lib/esm/test/schema/SchemaViewValidation.test.js.map +1 -1
- package/lib/esm/test/standalone/BriefcaseManagerDelete.test.d.ts +2 -0
- package/lib/esm/test/standalone/BriefcaseManagerDelete.test.d.ts.map +1 -0
- package/lib/esm/test/standalone/BriefcaseManagerDelete.test.js +159 -0
- package/lib/esm/test/standalone/BriefcaseManagerDelete.test.js.map +1 -0
- package/lib/esm/test/standalone/ChangeUnifier.test.d.ts +2 -0
- package/lib/esm/test/standalone/ChangeUnifier.test.d.ts.map +1 -0
- package/lib/esm/test/standalone/ChangeUnifier.test.js +732 -0
- package/lib/esm/test/standalone/ChangeUnifier.test.js.map +1 -0
- package/lib/esm/test/standalone/DeleteElements.test.js +962 -83
- package/lib/esm/test/standalone/DeleteElements.test.js.map +1 -1
- package/lib/esm/test/standalone/EditTxn.test.js +72 -1
- package/lib/esm/test/standalone/EditTxn.test.js.map +1 -1
- package/lib/esm/test/standalone/GeoCoordConfig.test.d.ts +2 -0
- package/lib/esm/test/standalone/GeoCoordConfig.test.d.ts.map +1 -0
- package/lib/esm/test/standalone/GeoCoordConfig.test.js +158 -0
- package/lib/esm/test/standalone/GeoCoordConfig.test.js.map +1 -0
- package/lib/esm/test/standalone/ViewStoreDb.test.js +53 -0
- package/lib/esm/test/standalone/ViewStoreDb.test.js.map +1 -1
- package/lib/esm/test/workspace/SettingsContainers.test.d.ts +2 -0
- package/lib/esm/test/workspace/SettingsContainers.test.d.ts.map +1 -0
- package/lib/esm/test/workspace/SettingsContainers.test.js +95 -0
- package/lib/esm/test/workspace/SettingsContainers.test.js.map +1 -0
- package/lib/esm/workspace/SettingsEditor.d.ts +4 -3
- package/lib/esm/workspace/SettingsEditor.d.ts.map +1 -1
- package/lib/esm/workspace/SettingsEditor.js +7 -5
- package/lib/esm/workspace/SettingsEditor.js.map +1 -1
- 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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
for (const
|
|
72
|
-
assertDeleted(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
|
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
|
|
246
|
-
executeTestCase("deep gap chain: A
|
|
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
|
|
287
|
-
executeTestCase("sibling scope - delete only scope element,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
360
|
-
// childA1 is the code scope of rootB.
|
|
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
|
-
|
|
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
|
-
//
|
|
395
|
-
//
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
});
|