From a559cdc38581830c222200f1fffb866cbca8f3da Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:47:23 +0530 Subject: [PATCH 01/23] First set of changes for a bulk element deletion API --- .../iModelPlatform/DgnCore/DgnElement.cpp | 6 +- .../iModelPlatform/DgnCore/DgnElements.cpp | 199 +++++++ .../PublicAPI/DgnPlatform/DgnElement.h | 7 + iModelJsNodeAddon/IModelJsNative.cpp | 16 + iModelJsNodeAddon/IModelJsNative.h | 1 + iModelJsNodeAddon/JsInteropDgnDb.cpp | 12 + .../api_package/ts/src/NativeLibrary.ts | 1 + .../api_package/ts/src/test/DgnDb.test.ts | 545 +++++++++++++++++- .../iModelJsNodeAddon.PartFile.xml | 8 - 9 files changed, 785 insertions(+), 10 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElement.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElement.cpp index 2426c76fcd..2e4f648fba 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElement.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElement.cpp @@ -1046,7 +1046,11 @@ void DgnElement::_OnDeleted() const CallJsPostHandler("onDeleted"); CallAppData(OnDeletedCaller()); GetDgnDb().Elements().DropFromPool(*this); - deleteLinkTableRelationships(GetDgnDb(), GetElementId()); + + // For a bulk delete operation, the relationship classes will be handled separately + if (GetDgnDb().Elements().IsBulkOperation()) + deleteLinkTableRelationships(GetDgnDb(), GetElementId()); + DgnModelPtr model = GetModel(); if (model.IsValid()) model->_OnDeletedElement(m_elementId); diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index 28880469d6..7bc217aeae 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -685,6 +685,205 @@ DgnDbStatus DgnElements::Delete(DgnElementCR elementIn) return DgnDbStatus::Success; } +DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, const bool skipValidation, const bool skipHandlerCallbacks) + { + DgnDb::VerifyClientThread(); + + if (elementIds.empty()) + return {}; + + DgnElementIdSet validatedElementIds = elementIds; + DgnElementIdSet failedToDeleteElements; + + // Validate the input set + if (!skipValidation) + { + std::for_each(elementIds.begin(), elementIds.end(), [&] (const DgnElementId elementId) + { + if (elementId.IsValid()) + { + if (const auto element = GetElement(elementId); element.IsValid()) + validatedElementIds.insert(elementId); + else + failedToDeleteElements.insert(elementId); + } + }); + } + + // Expand the input set to include all descendants, then detect any elements that must be excluded + // because an external element uses one of them as a CodeScope (deleting would leave a dangling FK). + ECSqlStatement expandAndValidate; + if (ECSqlStatus::Success != expandAndValidate.Prepare(m_dgndb, R"sql( + WITH RECURSIVE + -- Expand input roots to their full descendant hierarchy + fullDeleteSet(id) AS ( + SELECT ECInstanceId FROM bis.Element WHERE InVirtualSet(?, ECInstanceId) + UNION ALL + SELECT e.ECInstanceId FROM bis.Element e + INNER JOIN fullDeleteSet p ON e.Parent.Id = p.id + ), + -- Elements outside the delete set that use a delete-set element as their CodeScope + violatingScopes(id) AS ( + SELECT DISTINCT CodeScope.Id FROM bis.Element + WHERE CodeScope.Id IN (SELECT id FROM fullDeleteSet) + AND ECInstanceId NOT IN (SELECT id FROM fullDeleteSet) + ), + -- For each violator, find its highest ancestor that is still inside the delete set + subtreeRoots(id, parentId) AS ( + SELECT ECInstanceId, Parent.Id FROM bis.Element + WHERE ECInstanceId IN (SELECT id FROM violatingScopes) + UNION ALL + SELECT e.ECInstanceId, e.Parent.Id FROM bis.Element e + INNER JOIN subtreeRoots s ON e.ECInstanceId = s.parentId + WHERE s.parentId IN (SELECT id FROM fullDeleteSet) + ), + -- Walk DOWN from each root to collect the entire subtree that must be excluded + subtree(id) AS ( + SELECT id FROM subtreeRoots + WHERE parentId IS NULL OR parentId NOT IN (SELECT id FROM fullDeleteSet) + UNION ALL + SELECT e.ECInstanceId FROM bis.Element e + INNER JOIN subtree s ON e.Parent.Id = s.id + ) + SELECT fds.id AS id, 0 AS isScopeViolation FROM fullDeleteSet fds + UNION ALL + SELECT DISTINCT s.id AS id, 1 AS isScopeViolation FROM subtree s + )sql")) + return elementIds; + + const auto inputVS = std::make_shared>(BeIdSet(validatedElementIds.GetBeIdSet())); + expandAndValidate.BindVirtualSet(1, inputVS); + + while (expandAndValidate.Step() == BE_SQLITE_ROW) + { + const auto id = expandAndValidate.GetValueId(0); + const bool isScopeViolation = expandAndValidate.GetValueInt(1) != 0; + if (isScopeViolation) + failedToDeleteElements.insert(id); + else + validatedElementIds.insert(id); + } + + for (const auto& id : failedToDeleteElements) + validatedElementIds.erase(id); + + if (!failedToDeleteElements.empty()) + LOG.warningv("deleteElements: Skipping elements as them or their subtrees contain code scopes for elements outside the delete set: %s", failedToDeleteElements.ToString().c_str()); + + // Prep the elements, handlers and the Db for a bulk deletion + SetBulkOperation(true); + m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = true"); + + // Call the pre-delete handlers. Remove the elements that get veto'd off the delete list from the handlers + if (!skipHandlerCallbacks) + { + std::vector idsToRemove; + for (const auto& elementId : validatedElementIds) + { + const auto element = GetElement(elementId); + if (!element.IsValid()) + { + idsToRemove.push_back(elementId); + continue; + } + + if (element->_OnDelete() != DgnDbStatus::Success) + { + idsToRemove.push_back(elementId); + continue; + } + + auto parent = GetElement(element->m_parent.m_id); + if (parent.IsValid() && parent->_OnChildDelete(*element) != DgnDbStatus::Success) + { + idsToRemove.push_back(elementId); + continue; + } + } + for (const auto& id : idsToRemove) + validatedElementIds.erase(id); + } + + // Get all the elements from leaves to root elements to be deleted with a single bulk operation + ECSqlStatement rootIdsToDelete; + if (ECSqlStatus::Success != rootIdsToDelete.Prepare(m_dgndb, R"sql( + WITH RECURSIVE + deleteSet(id, parentid) AS ( + SELECT ECInstanceId, Parent.Id FROM bis.Element WHERE InVirtualSet(?, ECInstanceId) + ), + elementTree(id, depth) AS ( + SELECT ECInstanceId, 0 + FROM bis.Element + WHERE InVirtualSet(?, ECInstanceId) AND (Parent.Id IS NULL OR Parent.Id NOT IN (SELECT id FROM deleteSet)) + + UNION ALL + + SELECT e.ECInstanceId, t.depth + 1 + FROM bis.Element e + INNER JOIN elementTree t + ON e.Parent.Id = t.id + ) + SELECT id FROM elementTree ORDER BY depth DESC + )sql")) + return elementIds; + + const auto finalDeleteSetVS = std::make_shared>(BeIdSet(validatedElementIds.GetBeIdSet())); + rootIdsToDelete.BindVirtualSet(1, finalDeleteSetVS); + rootIdsToDelete.BindVirtualSet(2, finalDeleteSetVS); + + DgnElementIdSet finalList; + std::vector elementsToDelete; + + while (rootIdsToDelete.Step() == BE_SQLITE_ROW) + { + if (const auto elementToDelete = GetElement(rootIdsToDelete.GetValueId(0)); elementToDelete.IsValid()) + { + elementsToDelete.push_back(elementToDelete); + finalList.insert(elementToDelete->GetElementId()); + } + } + + // Clear up the relationships + bulkDeleteLinkTableRelationships(finalList); + + // Delete the elements + CachedStatementPtr statement = GetStatement("DELETE FROM " BIS_TABLE(BIS_CLASS_Element) " WHERE InVirtualSet(?, Id)"); + statement->BindVirtualSet(1, finalList); + statement->Step(); + + // Call the post delete handlers + if (!skipHandlerCallbacks) + { + std::for_each(elementsToDelete.begin(), elementsToDelete.end(), [&](const DgnElementCPtr& element) + { + element->_OnDeleted(); + auto parent = GetElement(element->m_parent.m_id); + if (parent.IsValid() && finalList.find(element->m_parent.m_id) == finalList.end()) + parent->_OnChildDeleted(*element); + }); + } + + // Reset the db state + SetBulkOperation(false); + m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = false"); + + return failedToDeleteElements; + } + +void DgnElements::bulkDeleteLinkTableRelationships(const DgnElementIdSet elementIds) + { + for (const auto& table : { BIS_TABLE(BIS_REL_ElementRefersToElements), BIS_TABLE(BIS_REL_ElementDrivesElement) }) + { + BeAssert(m_dgndb.TableExists(table)); + + Utf8PrintfString sql("DELETE FROM %s WHERE InVirtualSet(?, SourceId) OR InVirtualSet(?, TargetId)", table); + CachedStatementPtr statement = GetStatement(sql.c_str()); + statement->BindVirtualSet(1, elementIds); + statement->BindVirtualSet(2, elementIds); + statement->Step(); + } + } + //--------------------------------------------------------------------------------------- // @bsimethod //--------------------------------------------------------------------------------------- diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index 71f40fcdc3..df91e54112 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -3882,6 +3882,7 @@ struct DgnElements : DgnDbTable mutable T_ClassParamsMap m_classParams; // information about custom-handled properties mutable AutoHandledPropertyUpdaterCache m_updaterCache; mutable std::map> m_jsonSelectAdapterCache; + bool m_isBulkOperation = false; void Destroy(); void AddToPool(DgnElementCR) const; @@ -4029,6 +4030,12 @@ struct DgnElements : DgnDbTable //! @note This function can only be safely invoked from the client thread. DGNPLATFORM_EXPORT DgnDbStatus Delete(DgnElementCR element); + DGNPLATFORM_EXPORT void SetBulkOperation(const bool isBulk) { m_isBulkOperation = isBulk; } + DGNPLATFORM_EXPORT bool IsBulkOperation() const { return m_isBulkOperation; } + DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds, const bool skipValidation = false, const bool skipHandlerCallbacks = false); + + void bulkDeleteLinkTableRelationships(const DgnElementIdSet elementIds); + //! Delete a DgnElement from this DgnDb by DgnElementId. //! @return DgnDbStatus::Success if the element was deleted, error status otherwise. //! @note This method is merely a shortcut to #GetElement and then #Delete diff --git a/iModelJsNodeAddon/IModelJsNative.cpp b/iModelJsNodeAddon/IModelJsNative.cpp index e3416fe8a8..eee3005bd1 100644 --- a/iModelJsNodeAddon/IModelJsNative.cpp +++ b/iModelJsNodeAddon/IModelJsNative.cpp @@ -1740,6 +1740,21 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps JsInterop::DeleteElement(db, elemIdStr); } + Napi::Value DeleteElements(NapiInfoCR info) { + auto& db = GetOpenedDb(info); + if (ARGUMENT_IS_NOT_PRESENT(0) || !info[0].IsArray()) { + THROW_JS_TYPE_EXCEPTION("Invalid argument given to deleteElements"); + } + + auto elemIds = JsInterop::DeleteElements(db, info[0].As(), false, false); + uint32_t index = 0; + auto ret = Napi::Array::New(Env(), elemIds.size()); + for (auto elemId : elemIds) + ret.Set(index++, Napi::String::New(Env(), elemId.ToHexStr().c_str())); + + return ret; + } + Napi::Value QueryDefinitionElementUsage(NapiInfoCR info) { auto& db = GetOpenedDb(info); @@ -3118,6 +3133,7 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps InstanceMethod("createIModel", &NativeDgnDb::CreateIModel), InstanceMethod("deleteAllTxns", &NativeDgnDb::DeleteAllTxns), InstanceMethod("deleteElement", &NativeDgnDb::DeleteElement), + InstanceMethod("deleteElements", &NativeDgnDb::DeleteElements), InstanceMethod("deleteElementAspect", &NativeDgnDb::DeleteElementAspect), InstanceMethod("deleteLinkTableRelationship", &NativeDgnDb::DeleteLinkTableRelationship), InstanceMethod("deleteLinkTableRelationships", &NativeDgnDb::DeleteLinkTableRelationships), diff --git a/iModelJsNodeAddon/IModelJsNative.h b/iModelJsNodeAddon/IModelJsNative.h index aaf5151e2d..6edcb7a5e0 100644 --- a/iModelJsNodeAddon/IModelJsNative.h +++ b/iModelJsNodeAddon/IModelJsNative.h @@ -511,6 +511,7 @@ struct JsInterop { static Napi::String InsertElement(DgnDbR db, Napi::Object props, Napi::Value options); static void UpdateElement(DgnDbR db, Napi::Object); static void DeleteElement(DgnDbR db, Utf8StringCR eidStr); + static DgnElementIdSet DeleteElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipValidation = false, const bool skipHandlerCallbacks = false); static DgnDbStatus SimplifyElementGeometry(DgnDbR db, Napi::Object simplifyArgs); static InlineGeometryPartsResult InlineGeometryParts(DgnDbR db); static Napi::String InsertElementAspect(DgnDbR db, Napi::Object aspectProps); diff --git a/iModelJsNodeAddon/JsInteropDgnDb.cpp b/iModelJsNodeAddon/JsInteropDgnDb.cpp index 93020de173..4850374ad2 100644 --- a/iModelJsNodeAddon/JsInteropDgnDb.cpp +++ b/iModelJsNodeAddon/JsInteropDgnDb.cpp @@ -684,6 +684,18 @@ void JsInterop::DeleteElement(DgnDbR dgndb, Utf8StringCR eidStr) { THROW_JS_DGN_DB_EXCEPTION(Env(), "error deleting element", stat); } +DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipValidation, const bool skipHandlerCallbacks) { + DgnElementIdSet elementIdSet; + for (auto i = 0U; i < elementIds.Length(); ++i) { + Napi::Value arrayItem = elementIds[i]; + + auto val = BeInt64Id::FromString(arrayItem.As().Utf8Value().c_str()); + DgnElementId elementId(val.GetValue()); + elementIdSet.insert(elementId); + } + return dgndb.Elements().DeleteElements(elementIdSet, skipValidation, skipHandlerCallbacks); +} + /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ diff --git a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts index 1177eaad57..51ae91f898 100644 --- a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts +++ b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts @@ -624,6 +624,7 @@ export declare namespace IModelJsNative { public createIModel(fileName: string, props: CreateEmptyStandaloneIModelProps): void; public deleteAllTxns(): void; public deleteElement(elemIdJson: string): void; + public deleteElements(elementIds: Id64Array): Id64Array; public deleteElementAspect(aspectIdJson: string): void; public deleteLinkTableRelationship(props: RelationshipProps): DbResult; public deleteLinkTableRelationships(props: ReadonlyArray): DbResult; diff --git a/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts b/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts index 96059066d7..db0fb30e76 100644 --- a/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts +++ b/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts @@ -17,7 +17,7 @@ import { copyFile, dbFileName, getAssetsDir, getOutputDir, iModelJsNative } from if (os.platform() === "linux") process.env.LINUX_MINIDUMP_ENABLED = "yes"; -describe("basic tests", () => { +describe.only("basic tests", () => { let dgndb: IModelJsNative.DgnDb; @@ -1613,4 +1613,547 @@ describe("basic tests", () => { }); db.closeFile(); }); + + describe.only("Bulk Element Deletion", () => { + let db: IModelJsNative.DgnDb; + let modelId: Id64String; + let categoryId: Id64String; + let codeSpecId: Id64String; + + // Testcase Generation Helpers + const insertElement = (opts: { parentId?: Id64String; codeScope?: Id64String; codeValue?: string } = {}): Id64String => { + const { parentId, codeScope, codeValue } = opts; + const props: PhysicalElementProps = { + classFullName: "Generic:PhysicalObject", + model: modelId, + category: categoryId, + code: codeScope && codeValue ? { spec: codeSpecId, scope: codeScope, value: codeValue } : Code.createEmpty(), + placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } }, + ...(parentId ? { parent: { id: parentId, relClassName: "BisCore:ElementOwnsChildElements" } } : {}), + }; + const id = db.insertElement(props); + assert.isNotEmpty(id, "insertElement must return a valid ID"); + return id; + }; + + /** Assert that the element with the given id exists or has been deleted. */ + const assertExists = (id: Id64String, msg: string) => assert.isDefined(db.getElement({ id }), msg); + const assertDeleted = (id: Id64String, msg: string) => assert.throws(() => db.getElement({ id }), undefined, msg); + + /** + * Run deleteElements, then verify each id in `deleted` is gone and each id in `retained` is still present. + */ + const executeTestCase = (label: string, idsToDelete: Id64String[], deleted: Id64String[], retained: Id64String[]) => { + db.deleteElements(idsToDelete); + + for (const id of deleted) + assertDeleted(id, `error reading element`); + + for (const id of retained) + assertExists(id, `[${label}] ${id} should have been retained`); + + db.abandonChanges(); + }; + + beforeEach(() => { + const seedUri = path.join(getAssetsDir(), "test.bim"); + const testFile = path.join(getAssetsDir(), "deleteElements-test.bim"); + if (fs.existsSync(testFile)) + fs.unlinkSync(testFile); + fs.copyFileSync(seedUri, testFile); + + db = new iModelJsNative.DgnDb(); + db.openIModel(testFile, OpenMode.ReadWrite); + + const modelStmt = new iModelJsNative.ECSqlStatement(); + modelStmt.prepare(db, "SELECT ECInstanceId FROM bis.PhysicalModel LIMIT 1"); + modelId = DbResult.BE_SQLITE_ROW === modelStmt.step() ? modelStmt.getValue(0).getId() : ""; + modelStmt.dispose(); + assert.isNotEmpty(modelId, "Expected a PhysicalModel"); + + const catStmt = new iModelJsNative.ECSqlStatement(); + catStmt.prepare(db, "SELECT ECInstanceId FROM bis.SpatialCategory LIMIT 1"); + categoryId = DbResult.BE_SQLITE_ROW === catStmt.step() ? catStmt.getValue(0).getId() : ""; + catStmt.dispose(); + assert.isNotEmpty(categoryId, "Expected a SpatialCategory"); + + codeSpecId = db.insertCodeSpec("TestScopeSpec", { scopeSpec: { type: 4 /* RelatedElement */ } }); + assert.isNotEmpty(codeSpecId); + }); + + afterEach(() => { + db.closeFile(); + }); + + /** + * Shared hierarchy used throughout the parent-child tests: + * + * parentA parentB standalone + * ├─ childA1 ├─ childB1 └─ childS1 + * │ └─ grandchildA1 └─ childB2 + * ├─ childA2 + * │ └─ grandchildA2 + * └─ childA3 + */ + describe("parent-child hierarchy", () => { + let parentA: Id64String, childA1: Id64String, grandchildA1: Id64String; + let childA2: Id64String, grandchildA2: Id64String, childA3: Id64String; + let parentB: Id64String, childB1: Id64String, childB2: Id64String; + let standalone: Id64String, childS1: Id64String; + let all: Id64String[]; + + beforeEach(() => { + parentA = insertElement(); + childA1 = insertElement({ parentId: parentA }); + grandchildA1 = insertElement({ parentId: childA1 }); + childA2 = insertElement({ parentId: parentA }); + grandchildA2 = insertElement({ parentId: childA2 }); + childA3 = insertElement({ parentId: parentA }); + parentB = insertElement(); + childB1 = insertElement({ parentId: parentB }); + childB2 = insertElement({ parentId: parentB }); + standalone = insertElement(); + childS1 = insertElement({ parentId: standalone }); + db.saveChanges(); + all = [parentA, childA1, grandchildA1, childA2, grandchildA2, childA3, + parentB, childB1, childB2, standalone, childS1]; + }); + + it("delete a root element", () => { + executeTestCase("root cascades", + [parentA], + [parentA, childA1, grandchildA1, childA2, grandchildA2, childA3], + [parentB, childB1, childB2, standalone, childS1]); + }); + + it("explicitly delete the whole tree", () => { + executeTestCase("redundant descendants in input", + [parentA, childA1, grandchildA1, childA2], + [parentA, childA1, grandchildA1, childA2, grandchildA2, childA3], + [parentB, childB1, childB2, standalone, childS1]); + }); + + it("deleting all roots removes every element", () => { + executeTestCase("delete all roots", + [parentA, parentB, standalone], + all, + []); + }); + + it("empty input set is a no-op", () => { + executeTestCase("empty set", + [], + [], + all); + }); + + it("deleting a child removes its subtree but leaves the parent", () => { + executeTestCase("delete depth-1 child", + [childA1], + [childA1, grandchildA1], + [parentA, childA2, grandchildA2, childA3, parentB, childB1, childB2, standalone, childS1]); + }); + + it("deleting two mid-tree siblings leaves their parent and unrelated siblings", () => { + executeTestCase("delete two depth-1 siblings", + [childA1, childA2], + [childA1, grandchildA1, childA2, grandchildA2], + [parentA, childA3, parentB, childB1, childB2, standalone, childS1]); + }); + + it("deleting a child from one tree and a child from another tree", () => { + executeTestCase("cross-tree mid-tree delete", + [childA1, childB2], + [childA1, grandchildA1, childB2], + [parentA, childA2, grandchildA2, childA3, parentB, childB1, standalone, childS1]); + }); + + it("deleting mid-tree nodes mixed with a root", () => { + executeTestCase("mid-tree + roots mixed", + [childA1, childA3, parentB, standalone], + [childA1, grandchildA1, childA3, parentB, childB1, childB2, standalone, childS1], + [parentA, childA2, grandchildA2]); + }); + + it("deleting only grandchildren leaves all ancestors", () => { + executeTestCase("delete leaves only", + [grandchildA1, grandchildA2], + [grandchildA1, grandchildA2], + [parentA, childA1, childA2, childA3, parentB, childB1, childB2, standalone, childS1]); + }); + + it("deleting leaves from different subtrees simultaneously", () => { + executeTestCase("leaves from multiple subtrees", + [grandchildA1, childB1, childS1], + [grandchildA1, childB1, childS1], + [parentA, childA1, childA2, grandchildA2, childA3, parentB, childB2, standalone]); + }); + + it("deleting root, mid-tree and leaf", () => { + executeTestCase("root + child + grandchild + leaf", + [childA1, grandchildA2, parentB, childS1], + [childA1, grandchildA1, grandchildA2, parentB, childB1, childB2, childS1], + [parentA, childA2, childA3, standalone]); + }); + + it("parent and its grandchild", () => { + executeTestCase("parent + grandchild redundant", + [parentA, grandchildA1], + [parentA, childA1, grandchildA1, childA2, grandchildA2, childA3], + [parentB, childB1, childB2, standalone, childS1]); + }); + }); + + describe("intra-set code scope", () => { + it("scope and scoped element both roots - order-agnostic", () => { + const rootA = insertElement(); + const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); + db.saveChanges(); + // Forward order: scope element first + executeTestCase("rootA -> rootB forward", [rootA, rootB], [rootA, rootB], []); + // Reverse order: scoped element first + executeTestCase("rootA -> rootB reverse", [rootB, rootA], [rootA, rootB], []); + }); + + it("child element is the code scope for an unrelated root", () => { + const rootA = insertElement(); + const childA = insertElement({ parentId: rootA }); + const rootB = insertElement({ codeScope: childA, codeValue: "rootB-code" }); + db.saveChanges(); + executeTestCase("depth-1 child scopes unrelated root - delete child+root directly", + [childA, rootB], + [childA, rootB], + [rootA]); + executeTestCase("depth-1 child scopes unrelated root - delete child only", + [childA], + [], + [rootA, childA, rootB]); + executeTestCase("depth-1 child scopes unrelated root - delete root only", + [rootB], + [rootB], + [rootA, childA]); + }); + + it("grandchild is the code scope for an unrelated root", () => { + const rootA = insertElement(); + const childA = insertElement({ parentId: rootA }); + const grandchildA = insertElement({ parentId: childA }); + const rootB = insertElement({ codeScope: grandchildA, codeValue: "rootB-code" }); + db.saveChanges(); + executeTestCase("depth-2 grandchild scopes unrelated root - delete both roots", + [rootA, rootB], + [rootA, childA, grandchildA, rootB], + []); + executeTestCase("depth-2 grandchild scopes unrelated root - delete grandchild+root directly", + [grandchildA, rootB], + [grandchildA, rootB], + [rootA, childA]); + }); + + it("root element scopes a child in another subtree", () => { + const rootA = insertElement(); + const rootB = insertElement(); + const childB = insertElement({ parentId: rootB, codeScope: rootA, codeValue: "childB-code" }); + db.saveChanges(); + executeTestCase("root scopes depth-1 child in sibling tree", + [rootA, rootB], + [rootA, rootB, childB], + []); + }); + + it("child scopes a sibling child", () => { + const rootA = insertElement(); + const childA = insertElement({ parentId: rootA }); + const rootB = insertElement(); + const childB = insertElement({ parentId: rootB, codeScope: childA, codeValue: "childB-code" }); + db.saveChanges(); + executeTestCase("depth-1 child scopes depth-1 sibling", + [childA, childB], + [childA, childB], + [rootA, rootB]); + }); + + it("scope chain 1", () => { + const rootA = insertElement(); + const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); + const rootC = insertElement({ codeScope: rootB, codeValue: "rootC-code" }); + db.saveChanges(); + executeTestCase("scope chain forward", [rootA, rootB, rootC], [rootA, rootB, rootC], []); + executeTestCase("scope chain reversed", [rootC, rootB, rootA], [rootA, rootB, rootC], []); + executeTestCase("scope chain middle-first", [rootB, rootA, rootC], [rootA, rootB, rootC], []); + }); + + it("scope chain 2", () => { + // A -> B -> C: B is not in the delete set but depends on A (external violation). + // A should be pruned. C depends on B which is not being deleted, so C is standalone-deletable. + const rootA = insertElement(); + const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); + const rootC = insertElement({ codeScope: rootB, codeValue: "rootC-code" }); + db.saveChanges(); + // rootB is NOT in the delete set but uses rootA as scope -> external violation -> rootA pruned + // rootC is in the delete set and its scope (rootB) is not being deleted -> rootC is safe to delete + executeTestCase("scope chain: delete A and C, B external violation prunes A", + [rootA, rootC], + [rootC], + [rootA, rootB]); + }); + + it("two elements using the same scope", () => { + // A is the code scope for both B and C independently. + // A + // / \ + // B C (code scope, not parent-child) + const rootA = insertElement(); + const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); + const rootC = insertElement({ codeScope: rootA, codeValue: "rootC-code" }); + db.saveChanges(); + executeTestCase("delete all three", + [rootA, rootB, rootC], + [rootA, rootB, rootC], + []); + executeTestCase("delete only B and C", + [rootB, rootC], + [rootB, rootC], + [rootA]); + }); + + it("parent is also the code scope of its own child", () => { + const rootP = insertElement(); + const childC = insertElement({ parentId: rootP, codeScope: rootP, codeValue: "childC-code" }); + db.saveChanges(); + executeTestCase("parent is code scope of child - delete parent", + [rootP], + [rootP, childC], + []); + }); + }); + + describe("external code scope violation - pruning", () => { + it("root is code scope for an external element", () => { + const rootA = insertElement(); + const external = insertElement({ codeScope: rootA, codeValue: "ext-code" }); + const rootB = insertElement(); + db.saveChanges(); + executeTestCase("external scopes root", + [rootA, rootB], + [rootB], + [rootA, external]); + }); + + it("depth-1 child is code scope for external", () => { + const rootA = insertElement(); + const childA = insertElement({ parentId: rootA }); + const external = insertElement({ codeScope: childA, codeValue: "ext-code" }); + const rootB = insertElement(); + db.saveChanges(); + executeTestCase("external scopes depth-1 child - parent subtree pruned", + [rootA, rootB], + [rootB], + [rootA, childA, external]); + }); + + it("depth-2 grandchild is code scope for external", () => { + const rootA = insertElement(); + const childA = insertElement({ parentId: rootA }); + const grandchildA = insertElement({ parentId: childA }); + const external = insertElement({ codeScope: grandchildA, codeValue: "ext-code" }); + const rootB = insertElement(); + db.saveChanges(); + executeTestCase("external scopes depth-2 grandchild - grandparent subtree pruned", + [rootA, rootB], + [rootB], + [rootA, childA, grandchildA, external]); + }); + + it("only the child is passed for deletion", () => { + const rootA = insertElement(); + const childA = insertElement({ parentId: rootA }); + const external = insertElement({ codeScope: childA, codeValue: "ext-code" }); + db.saveChanges(); + executeTestCase("external scopes requested child", + [childA], + [], + [rootA, childA, external]); + }); + + it("root has both an external scope dependent AND an intra-set scope dependent", () => { + const rootA = insertElement(); + const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); + const external = insertElement({ codeScope: rootA, codeValue: "ext-code" }); + db.saveChanges(); + executeTestCase("root pruned due to external; sibling still deleted", + [rootA, rootB], + [rootB], + [rootA, external]); + }); + + it("two independent external scope violations", () => { + const rootA = insertElement(); + const rootB = insertElement(); + const extX = insertElement({ codeScope: rootA, codeValue: "extX" }); + const extY = insertElement({ codeScope: rootB, codeValue: "extY" }); + const rootC = insertElement(); + db.saveChanges(); + executeTestCase("two independent violations", + [rootA, rootB, rootC], + [rootC], + [rootA, rootB, extX, extY]); + }); + }); + + describe("mixed parent-child hierarchy and code scope", () => { + it("root scopes another root - delete both roots, all descendants removed", () => { + const rootA = insertElement(); + const childA1 = insertElement({ parentId: rootA }); + const childA2 = insertElement({ parentId: rootA }); + const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); + const childB1 = insertElement({ parentId: rootB }); + db.saveChanges(); + executeTestCase("root scopes root - delete both roots", + [rootA, rootB], + [rootA, childA1, childA2, rootB, childB1], + []); + }); + + it("depth-1 child scopes an unrelated root", () => { + const rootA = insertElement(); + const childA1 = insertElement({ parentId: rootA }); + const rootB = insertElement({ codeScope: childA1, codeValue: "rootB-code" }); + const childB1 = insertElement({ parentId: rootB }); + db.saveChanges(); + executeTestCase("depth-1 child scopes root - delete both via parents", + [rootA, rootB], + [rootA, childA1, rootB, childB1], + []); + // Reverse input order - result must be identical + executeTestCase("depth-1 child scopes root - reverse input order", + [rootB, rootA], + [rootA, childA1, rootB, childB1], + []); + }); + + it("depth-1 child scopes an unrelated root - delete child and root directly (parent survives)", () => { + const rootA = insertElement(); + const childA1 = insertElement({ parentId: rootA }); + const rootB = insertElement({ codeScope: childA1, codeValue: "rootB-code" }); + const childB1 = insertElement({ parentId: rootB }); + db.saveChanges(); + // Only childA1 and rootB - rootA is NOT in the delete set. + executeTestCase("depth-1 child scopes root - delete child + scoped root directly", + [childA1, rootB], + [childA1, rootB, childB1], + [rootA]); + }); + + it("depth-1 child scopes an unrelated root - deleting only the child cascades into the scoped root's subtree", () => { + // childA1 is the code scope of rootB. When childA1 is deleted, rootB loses its scope + // element -> rootB (and its children) must also be deleted. + const rootA = insertElement(); + const childA1 = insertElement({ parentId: rootA }); + const rootB = insertElement({ codeScope: childA1, codeValue: "rootB-code" }); + const childB1 = insertElement({ parentId: rootB }); + db.saveChanges(); + executeTestCase("delete child only - scoped root also removed", + [childA1], + [], + [rootA, childA1, rootB, childB1]); + }); + + it("root scopes a depth-1 child in sibling tree - delete both roots, all descendants removed", () => { + const rootA = insertElement(); + const childA1 = insertElement({ parentId: rootA }); + const rootB = insertElement(); + const childB1 = insertElement({ parentId: rootB, codeScope: rootA, codeValue: "childB1-code" }); + db.saveChanges(); + executeTestCase("root scopes depth-1 child - delete both roots", + [rootA, rootB], + [rootA, childA1, rootB, childB1], + []); + }); + + it("depth-1 child scopes a depth-1 child in sibling tree - delete both children directly (parents survive)", () => { + const rootA = insertElement(); + const childA1 = insertElement({ parentId: rootA }); + const rootB = insertElement(); + const childB1 = insertElement({ parentId: rootB, codeScope: childA1, codeValue: "childB1-code" }); + const childB2 = insertElement({ parentId: rootB }); + db.saveChanges(); + executeTestCase("sibling-child scope - delete both children directly", + [childA1, childB1], + [childA1, childB1], + [rootA, rootB, childB2]); + }); + + it("depth-2 grandchild scopes an unrelated root - delete grandparent + scoped root", () => { + const rootA = insertElement(); + const childA = insertElement({ parentId: rootA }); + const grandchildA = insertElement({ parentId: childA }); + const rootB = insertElement({ codeScope: grandchildA, codeValue: "rootB-code" }); + const childB = insertElement({ parentId: rootB }); + db.saveChanges(); + executeTestCase("depth-2 grandchild scopes root - delete both roots", + [rootA, rootB], + [rootA, childA, grandchildA, rootB, childB], + []); + // Delete grandchild and scoped root directly (rootA and childA survive) + executeTestCase("depth-2 grandchild scopes root - delete grandchild + root directly", + [grandchildA, rootB], + [grandchildA, rootB, childB], + [rootA, childA]); + }); + + it("external element scopes a depth-1 child", () => { + const rootA = insertElement(); + const childA1 = insertElement({ parentId: rootA }); + const rootB = insertElement(); + const childB1 = insertElement({ parentId: rootB }); + const external = insertElement({ codeScope: childA1, codeValue: "ext-code" }); + db.saveChanges(); + executeTestCase("external scopes depth-1 child", + [rootA, rootB], + [rootB, childB1], + [rootA, childA1, external]); + }); + + it("external element scopes a depth-2 grandchild", () => { + const rootA = insertElement(); + const childA = insertElement({ parentId: rootA }); + const grandchildA = insertElement({ parentId: childA }); + const rootB = insertElement(); + const childB = insertElement({ parentId: rootB }); + const external = insertElement({ codeScope: grandchildA, codeValue: "ext-code" }); + db.saveChanges(); + executeTestCase("external scopes depth-2 grandchild order 1", + [rootA, rootB], + [rootB, childB], + [rootA, childA, grandchildA, external]); + + executeTestCase("external scopes depth-2 grandchild order 2", + [grandchildA], + [], + [rootA, childA, grandchildA, rootB, childB, external]); + + executeTestCase("external scopes depth-2 grandchild order 3", + [grandchildA, rootA, childB], + [childB], + [rootA, childA, grandchildA, rootB, external]); + }); + + it("two trees: one has external scope violation, other is deleted cleanly", () => { + const rootA = insertElement(); + const childA = insertElement({ parentId: rootA }); + const gcA = insertElement({ parentId: childA }); + const external = insertElement({ codeScope: childA, codeValue: "ext-code" }); + const rootB = insertElement(); + const childB1 = insertElement({ parentId: rootB }); + const childB2 = insertElement({ parentId: rootB }); + const gcB = insertElement({ parentId: childB1 }); + db.saveChanges(); + executeTestCase("one tree pruned, other fully deleted", + [rootA, rootB], + [rootB, childB1, childB2, gcB], + [rootA, childA, gcA, external]); + }); + }); + }); }); diff --git a/iModelJsNodeAddon/iModelJsNodeAddon.PartFile.xml b/iModelJsNodeAddon/iModelJsNodeAddon.PartFile.xml index 56dfb99ddc..78015c39ed 100644 --- a/iModelJsNodeAddon/iModelJsNodeAddon.PartFile.xml +++ b/iModelJsNodeAddon/iModelJsNodeAddon.PartFile.xml @@ -9,7 +9,6 @@ - @@ -64,9 +63,6 @@ GeoCoordAssetsLarge - - - @@ -159,8 +155,4 @@ - - - - From cc121f6a5e6668745ce0ef27e1c34925bf4d56e9 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:40:11 +0530 Subject: [PATCH 02/23] Added basic api path to delete definition elements --- .../iModelPlatform/DgnCore/DgnElements.cpp | 63 ++++++++++++++++--- .../PublicAPI/DgnPlatform/DgnElement.h | 11 ++-- iModelJsNodeAddon/IModelJsNative.cpp | 18 +++++- iModelJsNodeAddon/IModelJsNative.h | 1 + iModelJsNodeAddon/JsInteropDgnDb.cpp | 11 ++++ .../api_package/ts/src/NativeLibrary.ts | 1 + 6 files changed, 90 insertions(+), 15 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index 7bc217aeae..c223ec143c 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -685,6 +685,9 @@ DgnDbStatus DgnElements::Delete(DgnElementCR elementIn) return DgnDbStatus::Success; } +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, const bool skipValidation, const bool skipHandlerCallbacks) { DgnDb::VerifyClientThread(); @@ -717,23 +720,23 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c WITH RECURSIVE -- Expand input roots to their full descendant hierarchy fullDeleteSet(id) AS ( - SELECT ECInstanceId FROM bis.Element WHERE InVirtualSet(?, ECInstanceId) + SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_Element) " WHERE InVirtualSet(?, ECInstanceId) UNION ALL - SELECT e.ECInstanceId FROM bis.Element e + SELECT e.ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_Element) " e INNER JOIN fullDeleteSet p ON e.Parent.Id = p.id ), -- Elements outside the delete set that use a delete-set element as their CodeScope violatingScopes(id) AS ( - SELECT DISTINCT CodeScope.Id FROM bis.Element + SELECT DISTINCT CodeScope.Id FROM " BIS_SCHEMA(BIS_CLASS_Element) " WHERE CodeScope.Id IN (SELECT id FROM fullDeleteSet) AND ECInstanceId NOT IN (SELECT id FROM fullDeleteSet) ), -- For each violator, find its highest ancestor that is still inside the delete set subtreeRoots(id, parentId) AS ( - SELECT ECInstanceId, Parent.Id FROM bis.Element + SELECT ECInstanceId, Parent.Id FROM " BIS_SCHEMA(BIS_CLASS_Element) " WHERE ECInstanceId IN (SELECT id FROM violatingScopes) UNION ALL - SELECT e.ECInstanceId, e.Parent.Id FROM bis.Element e + SELECT e.ECInstanceId, e.Parent.Id FROM " BIS_SCHEMA(BIS_CLASS_Element) " e INNER JOIN subtreeRoots s ON e.ECInstanceId = s.parentId WHERE s.parentId IN (SELECT id FROM fullDeleteSet) ), @@ -742,7 +745,7 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c SELECT id FROM subtreeRoots WHERE parentId IS NULL OR parentId NOT IN (SELECT id FROM fullDeleteSet) UNION ALL - SELECT e.ECInstanceId FROM bis.Element e + SELECT e.ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_Element) " e INNER JOIN subtree s ON e.Parent.Id = s.id ) SELECT fds.id AS id, 0 AS isScopeViolation FROM fullDeleteSet fds @@ -809,17 +812,17 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c if (ECSqlStatus::Success != rootIdsToDelete.Prepare(m_dgndb, R"sql( WITH RECURSIVE deleteSet(id, parentid) AS ( - SELECT ECInstanceId, Parent.Id FROM bis.Element WHERE InVirtualSet(?, ECInstanceId) + SELECT ECInstanceId, Parent.Id FROM " BIS_SCHEMA(BIS_CLASS_Element) " WHERE InVirtualSet(?, ECInstanceId) ), elementTree(id, depth) AS ( SELECT ECInstanceId, 0 - FROM bis.Element + FROM " BIS_SCHEMA(BIS_CLASS_Element) " WHERE InVirtualSet(?, ECInstanceId) AND (Parent.Id IS NULL OR Parent.Id NOT IN (SELECT id FROM deleteSet)) UNION ALL SELECT e.ECInstanceId, t.depth + 1 - FROM bis.Element e + FROM " BIS_SCHEMA(BIS_CLASS_Element) " e INNER JOIN elementTree t ON e.Parent.Id = t.id ) @@ -870,6 +873,48 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c return failedToDeleteElements; } +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& elementIds) + { + DgnDb::VerifyClientThread(); + + if (elementIds.empty()) + return {}; + + DgnElementIdSet definitionElementIds; + DgnElementIdSet nonDefinitionElementIds; + + ECSqlStatement classifyStmt; + if (ECSqlStatus::Success != classifyStmt.Prepare(m_dgndb, R"sql( + SELECT e.ECInstanceId, CASE WHEN d.ECInstanceId IS NULL THEN 0 ELSE 1 END + FROM " BIS_SCHEMA(BIS_CLASS_Element) " e + LEFT JOIN " BIS_SCHEMA(BIS_CLASS_DefinitionElement) " d ON d.ECInstanceId = e.ECInstanceId + WHERE InVirtualSet(?, e.ECInstanceId) + )sql")) + return elementIds; + + classifyStmt.BindVirtualSet(1, std::make_shared>(BeIdSet(elementIds.GetBeIdSet()))); + + while (classifyStmt.Step() == BE_SQLITE_ROW) + { + const auto id = classifyStmt.GetValueId(0); + if (classifyStmt.GetValueInt(1) != 0) + definitionElementIds.insert(id); + else + nonDefinitionElementIds.insert(id); + } + + if (!nonDefinitionElementIds.empty()) + LOG.warningv("deleteDefinitionElements: Elements %s are not DefinitionElements and cannot be deleted with this API.", nonDefinitionElementIds.ToString().c_str()); + + if (definitionElementIds.empty()) + return nonDefinitionElementIds; + + return definitionElementIds; + } + void DgnElements::bulkDeleteLinkTableRelationships(const DgnElementIdSet elementIds) { for (const auto& table : { BIS_TABLE(BIS_REL_ElementRefersToElements), BIS_TABLE(BIS_REL_ElementDrivesElement) }) diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index df91e54112..cfc21f8914 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -3906,6 +3906,10 @@ struct DgnElements : DgnDbTable // *** WIP_SCHEMA_IMPORT - temporary work-around needed because ECClass objects are deleted when a schema is imported void ClearECCaches(); + + void SetBulkOperation(const bool isBulk) { m_isBulkOperation = isBulk; } + bool IsBulkOperation() const { return m_isBulkOperation; } + void bulkDeleteLinkTableRelationships(const DgnElementIdSet elementIds); public: DGNPLATFORM_EXPORT BeSQLite::SnappyFromMemory& GetSnappyFrom() {return m_snappyFrom;} // NB: Not to be used during loading of a GeometricElement or GeometryPart! @@ -4029,12 +4033,9 @@ struct DgnElements : DgnDbTable //! @return DgnDbStatus::Success if the element was deleted, error status otherwise. //! @note This function can only be safely invoked from the client thread. DGNPLATFORM_EXPORT DgnDbStatus Delete(DgnElementCR element); - - DGNPLATFORM_EXPORT void SetBulkOperation(const bool isBulk) { m_isBulkOperation = isBulk; } - DGNPLATFORM_EXPORT bool IsBulkOperation() const { return m_isBulkOperation; } + DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds, const bool skipValidation = false, const bool skipHandlerCallbacks = false); - - void bulkDeleteLinkTableRelationships(const DgnElementIdSet elementIds); + DGNPLATFORM_EXPORT DgnElementIdSet DeleteDefinitionElements(const DgnElementIdSet& elementIds); //! Delete a DgnElement from this DgnDb by DgnElementId. //! @return DgnDbStatus::Success if the element was deleted, error status otherwise. diff --git a/iModelJsNodeAddon/IModelJsNative.cpp b/iModelJsNodeAddon/IModelJsNative.cpp index eee3005bd1..9ebd719662 100644 --- a/iModelJsNodeAddon/IModelJsNative.cpp +++ b/iModelJsNodeAddon/IModelJsNative.cpp @@ -1749,7 +1749,22 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps auto elemIds = JsInterop::DeleteElements(db, info[0].As(), false, false); uint32_t index = 0; auto ret = Napi::Array::New(Env(), elemIds.size()); - for (auto elemId : elemIds) + for (const auto& elemId : elemIds) + ret.Set(index++, Napi::String::New(Env(), elemId.ToHexStr().c_str())); + + return ret; + } + + Napi::Value DeleteDefinitionElements(NapiInfoCR info) { + auto& db = GetOpenedDb(info); + if (ARGUMENT_IS_NOT_PRESENT(0) || !info[0].IsArray()) { + THROW_JS_TYPE_EXCEPTION("Invalid argument given to deleteDefinitionElements"); + } + + auto elemIds = JsInterop::DeleteDefinitionElements(db, info[0].As()); + uint32_t index = 0; + auto ret = Napi::Array::New(Env(), elemIds.size()); + for (const auto& elemId : elemIds) ret.Set(index++, Napi::String::New(Env(), elemId.ToHexStr().c_str())); return ret; @@ -3134,6 +3149,7 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps InstanceMethod("deleteAllTxns", &NativeDgnDb::DeleteAllTxns), InstanceMethod("deleteElement", &NativeDgnDb::DeleteElement), InstanceMethod("deleteElements", &NativeDgnDb::DeleteElements), + InstanceMethod("deleteDefinitionElements", &NativeDgnDb::DeleteDefinitionElements), InstanceMethod("deleteElementAspect", &NativeDgnDb::DeleteElementAspect), InstanceMethod("deleteLinkTableRelationship", &NativeDgnDb::DeleteLinkTableRelationship), InstanceMethod("deleteLinkTableRelationships", &NativeDgnDb::DeleteLinkTableRelationships), diff --git a/iModelJsNodeAddon/IModelJsNative.h b/iModelJsNodeAddon/IModelJsNative.h index 6edcb7a5e0..35eef5b932 100644 --- a/iModelJsNodeAddon/IModelJsNative.h +++ b/iModelJsNodeAddon/IModelJsNative.h @@ -512,6 +512,7 @@ struct JsInterop { static void UpdateElement(DgnDbR db, Napi::Object); static void DeleteElement(DgnDbR db, Utf8StringCR eidStr); static DgnElementIdSet DeleteElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipValidation = false, const bool skipHandlerCallbacks = false); + static DgnElementIdSet DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds); static DgnDbStatus SimplifyElementGeometry(DgnDbR db, Napi::Object simplifyArgs); static InlineGeometryPartsResult InlineGeometryParts(DgnDbR db); static Napi::String InsertElementAspect(DgnDbR db, Napi::Object aspectProps); diff --git a/iModelJsNodeAddon/JsInteropDgnDb.cpp b/iModelJsNodeAddon/JsInteropDgnDb.cpp index 4850374ad2..8a5bf6d3ec 100644 --- a/iModelJsNodeAddon/JsInteropDgnDb.cpp +++ b/iModelJsNodeAddon/JsInteropDgnDb.cpp @@ -696,6 +696,17 @@ DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds, return dgndb.Elements().DeleteElements(elementIdSet, skipValidation, skipHandlerCallbacks); } +DgnElementIdSet JsInterop::DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds) { + DgnElementIdSet elementIdSet; + for (auto i = 0U; i < elementIds.Length(); ++i) { + Napi::Value arrayItem = elementIds[i]; + auto val = BeInt64Id::FromString(arrayItem.As().Utf8Value().c_str()); + DgnElementId elementId(val.GetValue()); + elementIdSet.insert(elementId); + } + return dgndb.Elements().DeleteDefinitionElements(elementIdSet); +} + /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ diff --git a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts index 51ae91f898..26e7f8fc4e 100644 --- a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts +++ b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts @@ -625,6 +625,7 @@ export declare namespace IModelJsNative { public deleteAllTxns(): void; public deleteElement(elemIdJson: string): void; public deleteElements(elementIds: Id64Array): Id64Array; + public deleteDefinitionElements(elementIds: Id64Array): Id64Array; public deleteElementAspect(aspectIdJson: string): void; public deleteLinkTableRelationship(props: RelationshipProps): DbResult; public deleteLinkTableRelationships(props: ReadonlyArray): DbResult; From b30fe30ad588b2b6fe5d7656b75b430fcb433cad Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:25:21 +0530 Subject: [PATCH 03/23] Reverted accidental commit --- .../iModelPlatform/DgnCore/DgnElements.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index c223ec143c..b693d81bef 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -720,23 +720,23 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c WITH RECURSIVE -- Expand input roots to their full descendant hierarchy fullDeleteSet(id) AS ( - SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_Element) " WHERE InVirtualSet(?, ECInstanceId) + SELECT ECInstanceId FROM bis.Element WHERE InVirtualSet(?, ECInstanceId) UNION ALL - SELECT e.ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_Element) " e + SELECT e.ECInstanceId FROM bis.Element e INNER JOIN fullDeleteSet p ON e.Parent.Id = p.id ), -- Elements outside the delete set that use a delete-set element as their CodeScope violatingScopes(id) AS ( - SELECT DISTINCT CodeScope.Id FROM " BIS_SCHEMA(BIS_CLASS_Element) " + SELECT DISTINCT CodeScope.Id FROM bis.Element WHERE CodeScope.Id IN (SELECT id FROM fullDeleteSet) AND ECInstanceId NOT IN (SELECT id FROM fullDeleteSet) ), -- For each violator, find its highest ancestor that is still inside the delete set subtreeRoots(id, parentId) AS ( - SELECT ECInstanceId, Parent.Id FROM " BIS_SCHEMA(BIS_CLASS_Element) " + SELECT ECInstanceId, Parent.Id FROM bis.Element WHERE ECInstanceId IN (SELECT id FROM violatingScopes) UNION ALL - SELECT e.ECInstanceId, e.Parent.Id FROM " BIS_SCHEMA(BIS_CLASS_Element) " e + SELECT e.ECInstanceId, e.Parent.Id FROM bis.Element e INNER JOIN subtreeRoots s ON e.ECInstanceId = s.parentId WHERE s.parentId IN (SELECT id FROM fullDeleteSet) ), @@ -745,7 +745,7 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c SELECT id FROM subtreeRoots WHERE parentId IS NULL OR parentId NOT IN (SELECT id FROM fullDeleteSet) UNION ALL - SELECT e.ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_Element) " e + SELECT e.ECInstanceId FROM bis.Element e INNER JOIN subtree s ON e.Parent.Id = s.id ) SELECT fds.id AS id, 0 AS isScopeViolation FROM fullDeleteSet fds @@ -812,17 +812,17 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c if (ECSqlStatus::Success != rootIdsToDelete.Prepare(m_dgndb, R"sql( WITH RECURSIVE deleteSet(id, parentid) AS ( - SELECT ECInstanceId, Parent.Id FROM " BIS_SCHEMA(BIS_CLASS_Element) " WHERE InVirtualSet(?, ECInstanceId) + SELECT ECInstanceId, Parent.Id FROM bis.Element WHERE InVirtualSet(?, ECInstanceId) ), elementTree(id, depth) AS ( SELECT ECInstanceId, 0 - FROM " BIS_SCHEMA(BIS_CLASS_Element) " + FROM bis.Element WHERE InVirtualSet(?, ECInstanceId) AND (Parent.Id IS NULL OR Parent.Id NOT IN (SELECT id FROM deleteSet)) UNION ALL SELECT e.ECInstanceId, t.depth + 1 - FROM " BIS_SCHEMA(BIS_CLASS_Element) " e + FROM bis.Element e INNER JOIN elementTree t ON e.Parent.Id = t.id ) From c138bca6ae989d2731c07a8af192a09c1c7ce9af Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:55:59 +0530 Subject: [PATCH 04/23] Updated bulk element deletion APIs --- .../DgnCore/DefinitionElementUsageInfo.cpp | 117 +++++-- .../iModelPlatform/DgnCore/DgnElements.cpp | 298 ++++++++++-------- .../PublicAPI/DgnPlatform/DgnElement.h | 21 +- iModelJsNodeAddon/IModelJsNative.cpp | 6 +- iModelJsNodeAddon/IModelJsNative.h | 4 +- iModelJsNodeAddon/JsInteropDgnDb.cpp | 17 +- .../api_package/ts/src/test/DgnDb.test.ts | 8 +- 7 files changed, 283 insertions(+), 188 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DefinitionElementUsageInfo.cpp b/iModelCore/iModelPlatform/DgnCore/DefinitionElementUsageInfo.cpp index 6038fb75fd..46ff3d5988 100644 --- a/iModelCore/iModelPlatform/DgnCore/DefinitionElementUsageInfo.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DefinitionElementUsageInfo.cpp @@ -7,12 +7,13 @@ /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -DefinitionElementUsageInfoPtr DefinitionElementUsageInfo::Create(DgnDbR db, BeSQLite::IdSet const& elementIds) +DefinitionElementUsageInfoPtr DefinitionElementUsageInfo::Create(DgnDbR db, BeSQLite::IdSet const& elementIds, std::shared_ptr> excludeIds) { DefinitionElementUsageInfoPtr context = new DefinitionElementUsageInfo(db); if (!context.IsValid()) return nullptr; + context->m_excludeIds = excludeIds; context->Initialize(elementIds); context->QueryUsage(); return context; @@ -215,9 +216,14 @@ void DefinitionElementUsageInfo::QueryUsage() +---------------+---------------+---------------+---------------+---------------+------*/ bool DefinitionElementUsageInfo::IsSpatialCategoryUsed(DgnCategoryId categoryId) const { - Utf8CP sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_GeometricElement3d) " WHERE Category.Id=? LIMIT 1"; - CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql); + Utf8String sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_GeometricElement3d) " WHERE Category.Id=?"; + if (m_excludeIds) + sql.append(" AND NOT InVirtualSet(?, ECInstanceId)"); + sql.append(" LIMIT 1"); + CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql.c_str()); statement->BindId(1, categoryId); + if (m_excludeIds) + statement->BindVirtualSet(2, m_excludeIds); return BE_SQLITE_ROW == statement->Step(); } @@ -226,9 +232,14 @@ bool DefinitionElementUsageInfo::IsSpatialCategoryUsed(DgnCategoryId categoryId) +---------------+---------------+---------------+---------------+---------------+------*/ bool DefinitionElementUsageInfo::IsDrawingCategoryUsed(DgnCategoryId categoryId) const { - Utf8CP sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_GeometricElement2d) " WHERE Category.Id=? LIMIT 1"; - CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql); + Utf8String sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_GeometricElement2d) " WHERE Category.Id=?"; + if (m_excludeIds) + sql.append(" AND NOT InVirtualSet(?, ECInstanceId)"); + sql.append(" LIMIT 1"); + CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql.c_str()); statement->BindId(1, categoryId); + if (m_excludeIds) + statement->BindVirtualSet(2, m_excludeIds); return BE_SQLITE_ROW == statement->Step(); } @@ -237,9 +248,14 @@ bool DefinitionElementUsageInfo::IsDrawingCategoryUsed(DgnCategoryId categoryId) +---------------+---------------+---------------+---------------+---------------+------*/ bool DefinitionElementUsageInfo::IsDisplayStyleUsed(DgnElementId displayStyleId) const { - Utf8CP sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_ViewDefinition) " WHERE DisplayStyle.Id=? LIMIT 1"; - CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql); + Utf8String sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_ViewDefinition) " WHERE DisplayStyle.Id=?"; + if (m_excludeIds) + sql.append(" AND NOT InVirtualSet(?, ECInstanceId)"); + sql.append(" LIMIT 1"); + CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql.c_str()); statement->BindId(1, displayStyleId); + if (m_excludeIds) + statement->BindVirtualSet(2, m_excludeIds); return BE_SQLITE_ROW == statement->Step(); } @@ -248,9 +264,14 @@ bool DefinitionElementUsageInfo::IsDisplayStyleUsed(DgnElementId displayStyleId) +---------------+---------------+---------------+---------------+---------------+------*/ bool DefinitionElementUsageInfo::IsCategorySelectorUsed(DgnElementId categorySelectorId) const { - Utf8CP sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_ViewDefinition) " WHERE CategorySelector.Id=? LIMIT 1"; - CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql); + Utf8String sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_ViewDefinition) " WHERE CategorySelector.Id=?"; + if (m_excludeIds) + sql.append(" AND NOT InVirtualSet(?, ECInstanceId)"); + sql.append(" LIMIT 1"); + CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql.c_str()); statement->BindId(1, categorySelectorId); + if (m_excludeIds) + statement->BindVirtualSet(2, m_excludeIds); return BE_SQLITE_ROW == statement->Step(); } @@ -259,9 +280,14 @@ bool DefinitionElementUsageInfo::IsCategorySelectorUsed(DgnElementId categorySel +---------------+---------------+---------------+---------------+---------------+------*/ bool DefinitionElementUsageInfo::IsModelSelectorUsed(DgnElementId modelSelectorId) const { - Utf8CP sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_SpatialViewDefinition) " WHERE ModelSelector.Id=? LIMIT 1"; - CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql); + Utf8String sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_SpatialViewDefinition) " WHERE ModelSelector.Id=?"; + if (m_excludeIds) + sql.append(" AND NOT InVirtualSet(?, ECInstanceId)"); + sql.append(" LIMIT 1"); + CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql.c_str()); statement->BindId(1, modelSelectorId); + if (m_excludeIds) + statement->BindVirtualSet(2, m_excludeIds); return BE_SQLITE_ROW == statement->Step(); } @@ -272,24 +298,39 @@ bool DefinitionElementUsageInfo::IsViewDefinitionUsed(DgnViewId viewDefinitionId { if (m_db.Schemas().GetClass(BIS_ECSCHEMA_NAME, BIS_CLASS_SectionDrawing)->GetPropertyP("SpatialView") != nullptr) { - Utf8CP sectionDrawingSql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_SectionDrawing) " WHERE SpatialView.Id=? LIMIT 1"; - CachedECSqlStatementPtr sectionDrawingStatement = m_db.GetPreparedECSqlStatement(sectionDrawingSql); + Utf8String sectionDrawingSql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_SectionDrawing) " WHERE SpatialView.Id=?"; + if (m_excludeIds) + sectionDrawingSql.append(" AND NOT InVirtualSet(?, ECInstanceId)"); + sectionDrawingSql.append(" LIMIT 1"); + CachedECSqlStatementPtr sectionDrawingStatement = m_db.GetPreparedECSqlStatement(sectionDrawingSql.c_str()); sectionDrawingStatement->BindId(1, viewDefinitionId); + if (m_excludeIds) + sectionDrawingStatement->BindVirtualSet(2, m_excludeIds); if (BE_SQLITE_ROW == sectionDrawingStatement->Step()) return true; } - Utf8CP viewAttachmentSql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_ViewAttachment) " WHERE View.Id=? LIMIT 1"; - CachedECSqlStatementPtr viewAttachmentStatement = m_db.GetPreparedECSqlStatement(viewAttachmentSql); + Utf8String viewAttachmentSql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_ViewAttachment) " WHERE View.Id=?"; + if (m_excludeIds) + viewAttachmentSql.append(" AND NOT InVirtualSet(?, ECInstanceId)"); + viewAttachmentSql.append(" LIMIT 1"); + CachedECSqlStatementPtr viewAttachmentStatement = m_db.GetPreparedECSqlStatement(viewAttachmentSql.c_str()); viewAttachmentStatement->BindId(1, viewDefinitionId); + if (m_excludeIds) + viewAttachmentStatement->BindVirtualSet(2, m_excludeIds); if (BE_SQLITE_ROW == viewAttachmentStatement->Step()) return true; if (m_db.Schemas().GetClassId(BIS_ECSCHEMA_NAME, BIS_CLASS_SectionDrawingLocation).IsValid()) { - Utf8CP sectionDrawingLocationSql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_SectionDrawingLocation) " WHERE SectionView.Id=? LIMIT 1"; - CachedECSqlStatementPtr sectionDrawingLocationStatement = m_db.GetPreparedECSqlStatement(sectionDrawingLocationSql); + Utf8String sectionDrawingLocationSql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_SectionDrawingLocation) " WHERE SectionView.Id=?"; + if (m_excludeIds) + sectionDrawingLocationSql.append(" AND NOT InVirtualSet(?, ECInstanceId)"); + sectionDrawingLocationSql.append(" LIMIT 1"); + CachedECSqlStatementPtr sectionDrawingLocationStatement = m_db.GetPreparedECSqlStatement(sectionDrawingLocationSql.c_str()); sectionDrawingLocationStatement->BindId(1, viewDefinitionId); + if (m_excludeIds) + sectionDrawingLocationStatement->BindVirtualSet(2, m_excludeIds); if (BE_SQLITE_ROW == sectionDrawingLocationStatement->Step()) return true; } @@ -315,12 +356,15 @@ void DefinitionElementUsageInfo::ScanGeometricElement3ds(std::shared_ptrBindVirtualSet(1, categoriesToScan); - } + statement->BindVirtualSet(bindIdx++, categoriesToScan); + if (m_excludeIds) + statement->BindVirtualSet(bindIdx, m_excludeIds); while (BE_SQLITE_ROW == statement->Step()) { @@ -337,12 +381,15 @@ void DefinitionElementUsageInfo::ScanGeometricElement2ds(std::shared_ptrBindVirtualSet(1, categoriesToScan); - } + statement->BindVirtualSet(bindIdx++, categoriesToScan); + if (m_excludeIds) + statement->BindVirtualSet(bindIdx, m_excludeIds); while (BE_SQLITE_ROW == statement->Step()) { @@ -367,8 +414,12 @@ void DefinitionElementUsageInfo::ScanGeometricElement(DgnElementId elementId) +---------------+---------------+---------------+---------------+---------------+------*/ void DefinitionElementUsageInfo::ScanGeometryParts() { - Utf8CP sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_GeometryPart) " WHERE GeometryStream IS NOT NULL"; - CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql); + Utf8String sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_GeometryPart) " WHERE GeometryStream IS NOT NULL"; + if (m_excludeIds) + sql.append(" AND NOT InVirtualSet(?, ECInstanceId)"); + CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql.c_str()); + if (m_excludeIds) + statement->BindVirtualSet(1, m_excludeIds); while (BE_SQLITE_ROW == statement->Step()) { DgnElementId elementId = statement->GetValueId(0); @@ -383,8 +434,12 @@ void DefinitionElementUsageInfo::ScanGeometryParts() +---------------+---------------+---------------+---------------+---------------+------*/ void DefinitionElementUsageInfo::ScanLineStyles() { - Utf8CP sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_LineStyle); - CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql); + Utf8String sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_LineStyle); + if (m_excludeIds) + sql.append(" WHERE NOT InVirtualSet(?, ECInstanceId)"); + CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql.c_str()); + if (m_excludeIds) + statement->BindVirtualSet(1, m_excludeIds); while (BE_SQLITE_ROW == statement->Step()) { DgnElementId lineStyleElementId = statement->GetValueId(0); @@ -460,8 +515,12 @@ void DefinitionElementUsageInfo::ScanLineStyleComponent(LsComponentCP lineStyleC +---------------+---------------+---------------+---------------+---------------+------*/ void DefinitionElementUsageInfo::ScanDisplayStyles() { - Utf8CP sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_DisplayStyle3d); - CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql); + Utf8String sql = "SELECT ECInstanceId FROM " BIS_SCHEMA(BIS_CLASS_DisplayStyle3d); + if (m_excludeIds) + sql.append(" WHERE NOT InVirtualSet(?, ECInstanceId)"); + CachedECSqlStatementPtr statement = m_db.GetPreparedECSqlStatement(sql.c_str()); + if (m_excludeIds) + statement->BindVirtualSet(1, m_excludeIds); while (BE_SQLITE_ROW == statement->Step()) { DgnElementId displayStyleId = statement->GetValueId(0); diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index b693d81bef..d680b07ce4 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -685,10 +685,77 @@ DgnDbStatus DgnElements::Delete(DgnElementCR elementIn) return DgnDbStatus::Success; } +namespace + { + DgnElementIdSet ExpandElementHierarchyAndValidate(DgnDbR db, DgnElementIdSet& elementIds) + { + const auto expandAndValidateSqlStr = R"sql( + WITH RECURSIVE + -- Expand input roots to their full descendant hierarchy + fullDeleteSet(id) AS ( + SELECT ECInstanceId FROM bis.Element WHERE InVirtualSet(?, ECInstanceId) + UNION ALL + SELECT e.ECInstanceId FROM bis.Element e + INNER JOIN fullDeleteSet p ON e.Parent.Id = p.id + ), + -- Elements outside the delete set that use a delete-set element as their CodeScope + violatingScopes(id) AS ( + SELECT DISTINCT CodeScope.Id FROM bis.Element + WHERE CodeScope.Id IN (SELECT id FROM fullDeleteSet) + AND ECInstanceId NOT IN (SELECT id FROM fullDeleteSet) + ), + -- For each violator, find its highest ancestor that is still inside the delete set + subtreeRoots(id, parentId) AS ( + SELECT ECInstanceId, Parent.Id FROM bis.Element + WHERE ECInstanceId IN (SELECT id FROM violatingScopes) + UNION ALL + SELECT e.ECInstanceId, e.Parent.Id FROM bis.Element e + INNER JOIN subtreeRoots s ON e.ECInstanceId = s.parentId + WHERE s.parentId IN (SELECT id FROM fullDeleteSet) + ), + -- Walk DOWN from each root to collect the entire subtree that must be excluded + subtree(id) AS ( + SELECT id FROM subtreeRoots + WHERE parentId IS NULL OR parentId NOT IN (SELECT id FROM fullDeleteSet) + UNION ALL + SELECT e.ECInstanceId FROM bis.Element e + INNER JOIN subtree s ON e.Parent.Id = s.id + ) + SELECT fds.id AS id, FALSE AS isCodeScopeViolation FROM fullDeleteSet fds + UNION ALL + SELECT DISTINCT s.id AS id, TRUE AS isCodeScopeViolation FROM subtree s + )sql"; + + ECSqlStatement expandAndValidate; + if (ECSqlStatus::Success != expandAndValidate.Prepare(db, expandAndValidateSqlStr)) + return elementIds; + + expandAndValidate.BindVirtualSet(1, std::make_shared>(BeIdSet(elementIds.GetBeIdSet()))); + + DgnElementIdSet failedToDeleteElements; + while (expandAndValidate.Step() == BE_SQLITE_ROW) + { + const auto id = expandAndValidate.GetValueId(0); + + if (const bool isCodeScopeViolation = expandAndValidate.GetValueBoolean(1); isCodeScopeViolation) + { + failedToDeleteElements.insert(id); + elementIds.erase(id); + } + else + { + elementIds.insert(id); + } + } + + return failedToDeleteElements; + } + } + /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, const bool skipValidation, const bool skipHandlerCallbacks) +DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, const bool skipHandlerCallbacks) { DgnDb::VerifyClientThread(); @@ -696,88 +763,19 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c return {}; DgnElementIdSet validatedElementIds = elementIds; - DgnElementIdSet failedToDeleteElements; - - // Validate the input set - if (!skipValidation) - { - std::for_each(elementIds.begin(), elementIds.end(), [&] (const DgnElementId elementId) - { - if (elementId.IsValid()) - { - if (const auto element = GetElement(elementId); element.IsValid()) - validatedElementIds.insert(elementId); - else - failedToDeleteElements.insert(elementId); - } - }); - } // Expand the input set to include all descendants, then detect any elements that must be excluded // because an external element uses one of them as a CodeScope (deleting would leave a dangling FK). - ECSqlStatement expandAndValidate; - if (ECSqlStatus::Success != expandAndValidate.Prepare(m_dgndb, R"sql( - WITH RECURSIVE - -- Expand input roots to their full descendant hierarchy - fullDeleteSet(id) AS ( - SELECT ECInstanceId FROM bis.Element WHERE InVirtualSet(?, ECInstanceId) - UNION ALL - SELECT e.ECInstanceId FROM bis.Element e - INNER JOIN fullDeleteSet p ON e.Parent.Id = p.id - ), - -- Elements outside the delete set that use a delete-set element as their CodeScope - violatingScopes(id) AS ( - SELECT DISTINCT CodeScope.Id FROM bis.Element - WHERE CodeScope.Id IN (SELECT id FROM fullDeleteSet) - AND ECInstanceId NOT IN (SELECT id FROM fullDeleteSet) - ), - -- For each violator, find its highest ancestor that is still inside the delete set - subtreeRoots(id, parentId) AS ( - SELECT ECInstanceId, Parent.Id FROM bis.Element - WHERE ECInstanceId IN (SELECT id FROM violatingScopes) - UNION ALL - SELECT e.ECInstanceId, e.Parent.Id FROM bis.Element e - INNER JOIN subtreeRoots s ON e.ECInstanceId = s.parentId - WHERE s.parentId IN (SELECT id FROM fullDeleteSet) - ), - -- Walk DOWN from each root to collect the entire subtree that must be excluded - subtree(id) AS ( - SELECT id FROM subtreeRoots - WHERE parentId IS NULL OR parentId NOT IN (SELECT id FROM fullDeleteSet) - UNION ALL - SELECT e.ECInstanceId FROM bis.Element e - INNER JOIN subtree s ON e.Parent.Id = s.id - ) - SELECT fds.id AS id, 0 AS isScopeViolation FROM fullDeleteSet fds - UNION ALL - SELECT DISTINCT s.id AS id, 1 AS isScopeViolation FROM subtree s - )sql")) - return elementIds; - - const auto inputVS = std::make_shared>(BeIdSet(validatedElementIds.GetBeIdSet())); - expandAndValidate.BindVirtualSet(1, inputVS); - - while (expandAndValidate.Step() == BE_SQLITE_ROW) - { - const auto id = expandAndValidate.GetValueId(0); - const bool isScopeViolation = expandAndValidate.GetValueInt(1) != 0; - if (isScopeViolation) - failedToDeleteElements.insert(id); - else - validatedElementIds.insert(id); - } - - for (const auto& id : failedToDeleteElements) - validatedElementIds.erase(id); - + DgnElementIdSet failedToDeleteElements = ExpandElementHierarchyAndValidate(m_dgndb, validatedElementIds); if (!failedToDeleteElements.empty()) LOG.warningv("deleteElements: Skipping elements as them or their subtrees contain code scopes for elements outside the delete set: %s", failedToDeleteElements.ToString().c_str()); // Prep the elements, handlers and the Db for a bulk deletion SetBulkOperation(true); - m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = true"); - // Call the pre-delete handlers. Remove the elements that get veto'd off the delete list from the handlers + // Call the pre-delete handlers. Remove the elements that get veto'd off the delete list from the handlers. + // We need to save these elements to avoid a re-load when calling the post-delete handlers + std::vector elementsToDelete; if (!skipHandlerCallbacks) { std::vector idsToRemove; @@ -790,93 +788,95 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c continue; } + // Call the pre-delete handler if (element->_OnDelete() != DgnDbStatus::Success) { idsToRemove.push_back(elementId); continue; } + // Ask the parent if it's okay to delete the child. + // Also, skip parent callback if the parent itself is also being deleted. auto parent = GetElement(element->m_parent.m_id); - if (parent.IsValid() && parent->_OnChildDelete(*element) != DgnDbStatus::Success) + if (parent.IsValid() && + validatedElementIds.find(element->m_parent.m_id) == validatedElementIds.end() && + parent->_OnChildDelete(*element) != DgnDbStatus::Success) { idsToRemove.push_back(elementId); continue; } - } - for (const auto& id : idsToRemove) - validatedElementIds.erase(id); - } - - // Get all the elements from leaves to root elements to be deleted with a single bulk operation - ECSqlStatement rootIdsToDelete; - if (ECSqlStatus::Success != rootIdsToDelete.Prepare(m_dgndb, R"sql( - WITH RECURSIVE - deleteSet(id, parentid) AS ( - SELECT ECInstanceId, Parent.Id FROM bis.Element WHERE InVirtualSet(?, ECInstanceId) - ), - elementTree(id, depth) AS ( - SELECT ECInstanceId, 0 - FROM bis.Element - WHERE InVirtualSet(?, ECInstanceId) AND (Parent.Id IS NULL OR Parent.Id NOT IN (SELECT id FROM deleteSet)) - - UNION ALL - - SELECT e.ECInstanceId, t.depth + 1 - FROM bis.Element e - INNER JOIN elementTree t - ON e.Parent.Id = t.id - ) - SELECT id FROM elementTree ORDER BY depth DESC - )sql")) - return elementIds; - - const auto finalDeleteSetVS = std::make_shared>(BeIdSet(validatedElementIds.GetBeIdSet())); - rootIdsToDelete.BindVirtualSet(1, finalDeleteSetVS); - rootIdsToDelete.BindVirtualSet(2, finalDeleteSetVS); - - DgnElementIdSet finalList; - std::vector elementsToDelete; - while (rootIdsToDelete.Step() == BE_SQLITE_ROW) - { - if (const auto elementToDelete = GetElement(rootIdsToDelete.GetValueId(0)); elementToDelete.IsValid()) - { - elementsToDelete.push_back(elementToDelete); - finalList.insert(elementToDelete->GetElementId()); + elementsToDelete.push_back(element); } + std::for_each(idsToRemove.begin(), idsToRemove.end(), [&validatedElementIds](const DgnElementId elementId){ validatedElementIds.erase(elementId); }); + + elementsToDelete.erase( + std::remove_if( + elementsToDelete.begin(), + elementsToDelete.end(), + [&validatedElementIds](const DgnElementCPtr& el) { return validatedElementIds.find(el->GetElementId()) == validatedElementIds.end(); } + ), elementsToDelete.end()); } - // Clear up the relationships - bulkDeleteLinkTableRelationships(finalList); + // Since we have already handled all the external code scope violations, + // defer the FK integrity check for all the intra set violations as all of them are being deleted anyway + m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = true"); + + // Clear up the link-table relationships in bulk + DeleteLinkTableRelationships(m_dgndb, validatedElementIds); // Delete the elements CachedStatementPtr statement = GetStatement("DELETE FROM " BIS_TABLE(BIS_CLASS_Element) " WHERE InVirtualSet(?, Id)"); - statement->BindVirtualSet(1, finalList); + statement->BindVirtualSet(1, validatedElementIds); statement->Step(); + // Evict all deleted elements from the MRU cache so stale pointers are not returned to callers + for (const auto& elementId : validatedElementIds) + m_mruCache->DropElement(elementId); + // Call the post delete handlers if (!skipHandlerCallbacks) { std::for_each(elementsToDelete.begin(), elementsToDelete.end(), [&](const DgnElementCPtr& element) { element->_OnDeleted(); - auto parent = GetElement(element->m_parent.m_id); - if (parent.IsValid() && finalList.find(element->m_parent.m_id) == finalList.end()) - parent->_OnChildDeleted(*element); + // Notify parent only if it is not itself being deleted + if (element->m_parent.m_id.IsValid() && validatedElementIds.find(element->m_parent.m_id) == validatedElementIds.end()) + { + auto parent = GetElement(element->m_parent.m_id); + if (parent.IsValid()) + parent->_OnChildDeleted(*element); + } }); } // Reset the db state - SetBulkOperation(false); m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = false"); + SetBulkOperation(false); + if (!failedToDeleteElements.empty()) + LOG.warningv("deleteElements: Failed to delete elements: %s", failedToDeleteElements.ToString().c_str()); return failedToDeleteElements; } +/* static */ +void DgnElements::DeleteLinkTableRelationships(DgnDbR db, const DgnElementIdSet& elementIds) + { + for (const auto& table : { BIS_TABLE(BIS_REL_ElementRefersToElements), BIS_TABLE(BIS_REL_ElementDrivesElement) }) + { + BeAssert(db.TableExists(table)); + + auto statement = db.GetCachedStatement(Utf8PrintfString("DELETE FROM %s WHERE InVirtualSet(?, SourceId) OR InVirtualSet(?, TargetId)", table).c_str()); + statement->BindVirtualSet(1, elementIds); + statement->BindVirtualSet(2, elementIds); + statement->Step(); + } + } + /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& elementIds) +DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& elementIds, bool skipHandlerCallbacks) { DgnDb::VerifyClientThread(); @@ -889,8 +889,8 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele ECSqlStatement classifyStmt; if (ECSqlStatus::Success != classifyStmt.Prepare(m_dgndb, R"sql( SELECT e.ECInstanceId, CASE WHEN d.ECInstanceId IS NULL THEN 0 ELSE 1 END - FROM " BIS_SCHEMA(BIS_CLASS_Element) " e - LEFT JOIN " BIS_SCHEMA(BIS_CLASS_DefinitionElement) " d ON d.ECInstanceId = e.ECInstanceId + FROM bis.Element e + LEFT JOIN bis.DefinitionElement d ON d.ECInstanceId = e.ECInstanceId WHERE InVirtualSet(?, e.ECInstanceId) )sql")) return elementIds; @@ -910,23 +910,43 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele LOG.warningv("deleteDefinitionElements: Elements %s are not DefinitionElements and cannot be deleted with this API.", nonDefinitionElementIds.ToString().c_str()); if (definitionElementIds.empty()) - return nonDefinitionElementIds; + return {}; - return definitionElementIds; - } + // Get the usage info for all the elements except for the ones in elementIds. + // This will give us all the dependencies that exist outside the user supplied set. + // For any intra set dependencies, since we are bulk deleting, they will all get deleted anyway. + auto usageInfo = DefinitionElementUsageInfo::Create(m_dgndb, definitionElementIds, std::make_shared>(BeIdSet(definitionElementIds.GetBeIdSet()))); + if (!usageInfo.IsValid()) + return definitionElementIds; -void DgnElements::bulkDeleteLinkTableRelationships(const DgnElementIdSet elementIds) - { - for (const auto& table : { BIS_TABLE(BIS_REL_ElementRefersToElements), BIS_TABLE(BIS_REL_ElementDrivesElement) }) - { - BeAssert(m_dgndb.TableExists(table)); + DgnElementIdSet toBeDeleted; + DgnElementIdSet cannotBeDeleted; - Utf8PrintfString sql("DELETE FROM %s WHERE InVirtualSet(?, SourceId) OR InVirtualSet(?, TargetId)", table); - CachedStatementPtr statement = GetStatement(sql.c_str()); - statement->BindVirtualSet(1, elementIds); - statement->BindVirtualSet(2, elementIds); - statement->Step(); - } + for (const auto& elementId : definitionElementIds) + { + if (usageInfo->GetUsedIds().Contains(elementId)) + cannotBeDeleted.insert(elementId); + else + toBeDeleted.insert(elementId); + } + + if (toBeDeleted.empty()) + { + if (!cannotBeDeleted.empty()) + LOG.warningv("deleteDefinitionElements: Skipping elements that are in use: %s", cannotBeDeleted.ToString().c_str()); + return cannotBeDeleted; + } + + m_dgndb.BeginPurgeOperation(); + const auto failedToDeleteIds = DeleteElements(toBeDeleted, skipHandlerCallbacks); + m_dgndb.EndPurgeOperation(); + + cannotBeDeleted.insert(failedToDeleteIds.begin(), failedToDeleteIds.end()); + + if (!cannotBeDeleted.empty()) + LOG.warningv("deleteDefinitionElements: Skipping elements that are in use or blocked: %s", cannotBeDeleted.ToString().c_str()); + + return cannotBeDeleted; } //--------------------------------------------------------------------------------------- diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index cfc21f8914..2d898bc058 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -278,6 +278,7 @@ struct EXPORT_VTABLE_ATTRIBUTE DefinitionElementUsageInfo : RefCountedBase BeSQLite::IdSet m_textureIds; BeSQLite::IdSet m_otherDefinitionElementIds; BeSQLite::IdSet m_usedIds; + std::shared_ptr> m_excludeIds; BE_JSON_NAME(spatialCategoryIds) BE_JSON_NAME(drawingCategoryIds) @@ -316,8 +317,9 @@ struct EXPORT_VTABLE_ATTRIBUTE DefinitionElementUsageInfo : RefCountedBase public: //! Generate usage information for the specified set of DefinitionElementIds - DGNPLATFORM_EXPORT static DefinitionElementUsageInfoPtr Create(DgnDbR db, BeSQLite::IdSet const& definitionElementIds); + DGNPLATFORM_EXPORT static DefinitionElementUsageInfoPtr Create(DgnDbR db, BeSQLite::IdSet const& definitionElementIds, std::shared_ptr> excludeIds = nullptr); DGNPLATFORM_EXPORT void ToJson(BeJsValue) const; + DGNPLATFORM_EXPORT DgnElementIdSet GetUsedIds() const { return m_usedIds; } }; //======================================================================================= @@ -3909,7 +3911,7 @@ struct DgnElements : DgnDbTable void SetBulkOperation(const bool isBulk) { m_isBulkOperation = isBulk; } bool IsBulkOperation() const { return m_isBulkOperation; } - void bulkDeleteLinkTableRelationships(const DgnElementIdSet elementIds); + static void DeleteLinkTableRelationships(DgnDbR db, const DgnElementIdSet& elementIds); public: DGNPLATFORM_EXPORT BeSQLite::SnappyFromMemory& GetSnappyFrom() {return m_snappyFrom;} // NB: Not to be used during loading of a GeometricElement or GeometryPart! @@ -4034,8 +4036,19 @@ struct DgnElements : DgnDbTable //! @note This function can only be safely invoked from the client thread. DGNPLATFORM_EXPORT DgnDbStatus Delete(DgnElementCR element); - DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds, const bool skipValidation = false, const bool skipHandlerCallbacks = false); - DGNPLATFORM_EXPORT DgnElementIdSet DeleteDefinitionElements(const DgnElementIdSet& elementIds); + //! Delete multiple DgnElements from this DgnDb. + //! @param[in] elementIds The element set to delete. + //! @param[in] skipHandlerCallbacks Skip any domain handler callbacks before and after deletion. Defaults to false. + //! @return A DgnElementIdSet of elements that failed to delete. + //! @note This function can only be safely invoked from the client thread. + DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds, const bool skipHandlerCallbacks = false); + + //! Delete multiple definition elements from this DgnDb. + //! @param[in] elementIds The set of definition elements to delete. + //! @param[in] skipHandlerCallbacks Skip any domain handler callbacks before and after deletion. Defaults to false. + //! @return A DgnElementIdSet of definition elements that failed to delete. + //! @note This function can only be safely invoked from the client thread. + DGNPLATFORM_EXPORT DgnElementIdSet DeleteDefinitionElements(const DgnElementIdSet& elementIds, const bool skipHandlerCallbacks = false); //! Delete a DgnElement from this DgnDb by DgnElementId. //! @return DgnDbStatus::Success if the element was deleted, error status otherwise. diff --git a/iModelJsNodeAddon/IModelJsNative.cpp b/iModelJsNodeAddon/IModelJsNative.cpp index 9ebd719662..2f959255a4 100644 --- a/iModelJsNodeAddon/IModelJsNative.cpp +++ b/iModelJsNodeAddon/IModelJsNative.cpp @@ -1745,8 +1745,9 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps if (ARGUMENT_IS_NOT_PRESENT(0) || !info[0].IsArray()) { THROW_JS_TYPE_EXCEPTION("Invalid argument given to deleteElements"); } + OPTIONAL_ARGUMENT_BOOL(1, skipHandlerCallbacks, false); - auto elemIds = JsInterop::DeleteElements(db, info[0].As(), false, false); + auto elemIds = JsInterop::DeleteElements(db, info[0].As(), skipHandlerCallbacks); uint32_t index = 0; auto ret = Napi::Array::New(Env(), elemIds.size()); for (const auto& elemId : elemIds) @@ -1760,8 +1761,9 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps if (ARGUMENT_IS_NOT_PRESENT(0) || !info[0].IsArray()) { THROW_JS_TYPE_EXCEPTION("Invalid argument given to deleteDefinitionElements"); } + OPTIONAL_ARGUMENT_BOOL(1, skipHandlerCallbacks, false); - auto elemIds = JsInterop::DeleteDefinitionElements(db, info[0].As()); + auto elemIds = JsInterop::DeleteDefinitionElements(db, info[0].As(), skipHandlerCallbacks); uint32_t index = 0; auto ret = Napi::Array::New(Env(), elemIds.size()); for (const auto& elemId : elemIds) diff --git a/iModelJsNodeAddon/IModelJsNative.h b/iModelJsNodeAddon/IModelJsNative.h index 35eef5b932..af3827b6ee 100644 --- a/iModelJsNodeAddon/IModelJsNative.h +++ b/iModelJsNodeAddon/IModelJsNative.h @@ -511,8 +511,8 @@ struct JsInterop { static Napi::String InsertElement(DgnDbR db, Napi::Object props, Napi::Value options); static void UpdateElement(DgnDbR db, Napi::Object); static void DeleteElement(DgnDbR db, Utf8StringCR eidStr); - static DgnElementIdSet DeleteElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipValidation = false, const bool skipHandlerCallbacks = false); - static DgnElementIdSet DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds); + static DgnElementIdSet DeleteElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipHandlerCallbacks = false); + static DgnElementIdSet DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipHandlerCallbacks = false); static DgnDbStatus SimplifyElementGeometry(DgnDbR db, Napi::Object simplifyArgs); static InlineGeometryPartsResult InlineGeometryParts(DgnDbR db); static Napi::String InsertElementAspect(DgnDbR db, Napi::Object aspectProps); diff --git a/iModelJsNodeAddon/JsInteropDgnDb.cpp b/iModelJsNodeAddon/JsInteropDgnDb.cpp index 8a5bf6d3ec..950c5a9d1d 100644 --- a/iModelJsNodeAddon/JsInteropDgnDb.cpp +++ b/iModelJsNodeAddon/JsInteropDgnDb.cpp @@ -684,27 +684,28 @@ void JsInterop::DeleteElement(DgnDbR dgndb, Utf8StringCR eidStr) { THROW_JS_DGN_DB_EXCEPTION(Env(), "error deleting element", stat); } -DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipValidation, const bool skipHandlerCallbacks) { +DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipHandlerCallbacks) { DgnElementIdSet elementIdSet; for (auto i = 0U; i < elementIds.Length(); ++i) { Napi::Value arrayItem = elementIds[i]; auto val = BeInt64Id::FromString(arrayItem.As().Utf8Value().c_str()); - DgnElementId elementId(val.GetValue()); - elementIdSet.insert(elementId); + if (val.IsValid()) + elementIdSet.insert(DgnElementId(val.GetValue())); } - return dgndb.Elements().DeleteElements(elementIdSet, skipValidation, skipHandlerCallbacks); + return dgndb.Elements().DeleteElements(elementIdSet, skipHandlerCallbacks); } -DgnElementIdSet JsInterop::DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds) { +DgnElementIdSet JsInterop::DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipHandlerCallbacks) { DgnElementIdSet elementIdSet; for (auto i = 0U; i < elementIds.Length(); ++i) { Napi::Value arrayItem = elementIds[i]; + auto val = BeInt64Id::FromString(arrayItem.As().Utf8Value().c_str()); - DgnElementId elementId(val.GetValue()); - elementIdSet.insert(elementId); + if (val.IsValid()) + elementIdSet.insert(DgnElementId(val.GetValue())); } - return dgndb.Elements().DeleteDefinitionElements(elementIdSet); + return dgndb.Elements().DeleteDefinitionElements(elementIdSet, skipHandlerCallbacks); } /*---------------------------------------------------------------------------------**//** diff --git a/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts b/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts index db0fb30e76..693153fd92 100644 --- a/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts +++ b/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts @@ -1643,14 +1643,14 @@ describe.only("basic tests", () => { /** * Run deleteElements, then verify each id in `deleted` is gone and each id in `retained` is still present. */ - const executeTestCase = (label: string, idsToDelete: Id64String[], deleted: Id64String[], retained: Id64String[]) => { + const executeTestCase = (label: string, idsToDelete: Id64Array, deleted: Id64Array, retained: Id64Array) => { db.deleteElements(idsToDelete); for (const id of deleted) - assertDeleted(id, `error reading element`); + assertDeleted(id, `error reading element`); for (const id of retained) - assertExists(id, `[${label}] ${id} should have been retained`); + assertExists(id, `[${label}] ${id} should have been retained`); db.abandonChanges(); }; @@ -1700,7 +1700,7 @@ describe.only("basic tests", () => { let childA2: Id64String, grandchildA2: Id64String, childA3: Id64String; let parentB: Id64String, childB1: Id64String, childB2: Id64String; let standalone: Id64String, childS1: Id64String; - let all: Id64String[]; + let all: Id64Array; beforeEach(() => { parentA = insertElement(); From 780ec74b3b6bd9a11e9d3a1480e68790c9c717e4 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:42:50 +0530 Subject: [PATCH 05/23] Moved tests over to itwinjs repo --- .../api_package/ts/src/test/DgnDb.test.ts | 545 +----------------- 1 file changed, 1 insertion(+), 544 deletions(-) diff --git a/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts b/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts index 693153fd92..96059066d7 100644 --- a/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts +++ b/iModelJsNodeAddon/api_package/ts/src/test/DgnDb.test.ts @@ -17,7 +17,7 @@ import { copyFile, dbFileName, getAssetsDir, getOutputDir, iModelJsNative } from if (os.platform() === "linux") process.env.LINUX_MINIDUMP_ENABLED = "yes"; -describe.only("basic tests", () => { +describe("basic tests", () => { let dgndb: IModelJsNative.DgnDb; @@ -1613,547 +1613,4 @@ describe.only("basic tests", () => { }); db.closeFile(); }); - - describe.only("Bulk Element Deletion", () => { - let db: IModelJsNative.DgnDb; - let modelId: Id64String; - let categoryId: Id64String; - let codeSpecId: Id64String; - - // Testcase Generation Helpers - const insertElement = (opts: { parentId?: Id64String; codeScope?: Id64String; codeValue?: string } = {}): Id64String => { - const { parentId, codeScope, codeValue } = opts; - const props: PhysicalElementProps = { - classFullName: "Generic:PhysicalObject", - model: modelId, - category: categoryId, - code: codeScope && codeValue ? { spec: codeSpecId, scope: codeScope, value: codeValue } : Code.createEmpty(), - placement: { origin: [0, 0, 0], angles: { yaw: 0, pitch: 0, roll: 0 } }, - ...(parentId ? { parent: { id: parentId, relClassName: "BisCore:ElementOwnsChildElements" } } : {}), - }; - const id = db.insertElement(props); - assert.isNotEmpty(id, "insertElement must return a valid ID"); - return id; - }; - - /** Assert that the element with the given id exists or has been deleted. */ - const assertExists = (id: Id64String, msg: string) => assert.isDefined(db.getElement({ id }), msg); - const assertDeleted = (id: Id64String, msg: string) => assert.throws(() => db.getElement({ id }), undefined, msg); - - /** - * Run deleteElements, then verify each id in `deleted` is gone and each id in `retained` is still present. - */ - const executeTestCase = (label: string, idsToDelete: Id64Array, deleted: Id64Array, retained: Id64Array) => { - db.deleteElements(idsToDelete); - - for (const id of deleted) - assertDeleted(id, `error reading element`); - - for (const id of retained) - assertExists(id, `[${label}] ${id} should have been retained`); - - db.abandonChanges(); - }; - - beforeEach(() => { - const seedUri = path.join(getAssetsDir(), "test.bim"); - const testFile = path.join(getAssetsDir(), "deleteElements-test.bim"); - if (fs.existsSync(testFile)) - fs.unlinkSync(testFile); - fs.copyFileSync(seedUri, testFile); - - db = new iModelJsNative.DgnDb(); - db.openIModel(testFile, OpenMode.ReadWrite); - - const modelStmt = new iModelJsNative.ECSqlStatement(); - modelStmt.prepare(db, "SELECT ECInstanceId FROM bis.PhysicalModel LIMIT 1"); - modelId = DbResult.BE_SQLITE_ROW === modelStmt.step() ? modelStmt.getValue(0).getId() : ""; - modelStmt.dispose(); - assert.isNotEmpty(modelId, "Expected a PhysicalModel"); - - const catStmt = new iModelJsNative.ECSqlStatement(); - catStmt.prepare(db, "SELECT ECInstanceId FROM bis.SpatialCategory LIMIT 1"); - categoryId = DbResult.BE_SQLITE_ROW === catStmt.step() ? catStmt.getValue(0).getId() : ""; - catStmt.dispose(); - assert.isNotEmpty(categoryId, "Expected a SpatialCategory"); - - codeSpecId = db.insertCodeSpec("TestScopeSpec", { scopeSpec: { type: 4 /* RelatedElement */ } }); - assert.isNotEmpty(codeSpecId); - }); - - afterEach(() => { - db.closeFile(); - }); - - /** - * Shared hierarchy used throughout the parent-child tests: - * - * parentA parentB standalone - * ├─ childA1 ├─ childB1 └─ childS1 - * │ └─ grandchildA1 └─ childB2 - * ├─ childA2 - * │ └─ grandchildA2 - * └─ childA3 - */ - describe("parent-child hierarchy", () => { - let parentA: Id64String, childA1: Id64String, grandchildA1: Id64String; - let childA2: Id64String, grandchildA2: Id64String, childA3: Id64String; - let parentB: Id64String, childB1: Id64String, childB2: Id64String; - let standalone: Id64String, childS1: Id64String; - let all: Id64Array; - - beforeEach(() => { - parentA = insertElement(); - childA1 = insertElement({ parentId: parentA }); - grandchildA1 = insertElement({ parentId: childA1 }); - childA2 = insertElement({ parentId: parentA }); - grandchildA2 = insertElement({ parentId: childA2 }); - childA3 = insertElement({ parentId: parentA }); - parentB = insertElement(); - childB1 = insertElement({ parentId: parentB }); - childB2 = insertElement({ parentId: parentB }); - standalone = insertElement(); - childS1 = insertElement({ parentId: standalone }); - db.saveChanges(); - all = [parentA, childA1, grandchildA1, childA2, grandchildA2, childA3, - parentB, childB1, childB2, standalone, childS1]; - }); - - it("delete a root element", () => { - executeTestCase("root cascades", - [parentA], - [parentA, childA1, grandchildA1, childA2, grandchildA2, childA3], - [parentB, childB1, childB2, standalone, childS1]); - }); - - it("explicitly delete the whole tree", () => { - executeTestCase("redundant descendants in input", - [parentA, childA1, grandchildA1, childA2], - [parentA, childA1, grandchildA1, childA2, grandchildA2, childA3], - [parentB, childB1, childB2, standalone, childS1]); - }); - - it("deleting all roots removes every element", () => { - executeTestCase("delete all roots", - [parentA, parentB, standalone], - all, - []); - }); - - it("empty input set is a no-op", () => { - executeTestCase("empty set", - [], - [], - all); - }); - - it("deleting a child removes its subtree but leaves the parent", () => { - executeTestCase("delete depth-1 child", - [childA1], - [childA1, grandchildA1], - [parentA, childA2, grandchildA2, childA3, parentB, childB1, childB2, standalone, childS1]); - }); - - it("deleting two mid-tree siblings leaves their parent and unrelated siblings", () => { - executeTestCase("delete two depth-1 siblings", - [childA1, childA2], - [childA1, grandchildA1, childA2, grandchildA2], - [parentA, childA3, parentB, childB1, childB2, standalone, childS1]); - }); - - it("deleting a child from one tree and a child from another tree", () => { - executeTestCase("cross-tree mid-tree delete", - [childA1, childB2], - [childA1, grandchildA1, childB2], - [parentA, childA2, grandchildA2, childA3, parentB, childB1, standalone, childS1]); - }); - - it("deleting mid-tree nodes mixed with a root", () => { - executeTestCase("mid-tree + roots mixed", - [childA1, childA3, parentB, standalone], - [childA1, grandchildA1, childA3, parentB, childB1, childB2, standalone, childS1], - [parentA, childA2, grandchildA2]); - }); - - it("deleting only grandchildren leaves all ancestors", () => { - executeTestCase("delete leaves only", - [grandchildA1, grandchildA2], - [grandchildA1, grandchildA2], - [parentA, childA1, childA2, childA3, parentB, childB1, childB2, standalone, childS1]); - }); - - it("deleting leaves from different subtrees simultaneously", () => { - executeTestCase("leaves from multiple subtrees", - [grandchildA1, childB1, childS1], - [grandchildA1, childB1, childS1], - [parentA, childA1, childA2, grandchildA2, childA3, parentB, childB2, standalone]); - }); - - it("deleting root, mid-tree and leaf", () => { - executeTestCase("root + child + grandchild + leaf", - [childA1, grandchildA2, parentB, childS1], - [childA1, grandchildA1, grandchildA2, parentB, childB1, childB2, childS1], - [parentA, childA2, childA3, standalone]); - }); - - it("parent and its grandchild", () => { - executeTestCase("parent + grandchild redundant", - [parentA, grandchildA1], - [parentA, childA1, grandchildA1, childA2, grandchildA2, childA3], - [parentB, childB1, childB2, standalone, childS1]); - }); - }); - - describe("intra-set code scope", () => { - it("scope and scoped element both roots - order-agnostic", () => { - const rootA = insertElement(); - const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); - db.saveChanges(); - // Forward order: scope element first - executeTestCase("rootA -> rootB forward", [rootA, rootB], [rootA, rootB], []); - // Reverse order: scoped element first - executeTestCase("rootA -> rootB reverse", [rootB, rootA], [rootA, rootB], []); - }); - - it("child element is the code scope for an unrelated root", () => { - const rootA = insertElement(); - const childA = insertElement({ parentId: rootA }); - const rootB = insertElement({ codeScope: childA, codeValue: "rootB-code" }); - db.saveChanges(); - executeTestCase("depth-1 child scopes unrelated root - delete child+root directly", - [childA, rootB], - [childA, rootB], - [rootA]); - executeTestCase("depth-1 child scopes unrelated root - delete child only", - [childA], - [], - [rootA, childA, rootB]); - executeTestCase("depth-1 child scopes unrelated root - delete root only", - [rootB], - [rootB], - [rootA, childA]); - }); - - it("grandchild is the code scope for an unrelated root", () => { - const rootA = insertElement(); - const childA = insertElement({ parentId: rootA }); - const grandchildA = insertElement({ parentId: childA }); - const rootB = insertElement({ codeScope: grandchildA, codeValue: "rootB-code" }); - db.saveChanges(); - executeTestCase("depth-2 grandchild scopes unrelated root - delete both roots", - [rootA, rootB], - [rootA, childA, grandchildA, rootB], - []); - executeTestCase("depth-2 grandchild scopes unrelated root - delete grandchild+root directly", - [grandchildA, rootB], - [grandchildA, rootB], - [rootA, childA]); - }); - - it("root element scopes a child in another subtree", () => { - const rootA = insertElement(); - const rootB = insertElement(); - const childB = insertElement({ parentId: rootB, codeScope: rootA, codeValue: "childB-code" }); - db.saveChanges(); - executeTestCase("root scopes depth-1 child in sibling tree", - [rootA, rootB], - [rootA, rootB, childB], - []); - }); - - it("child scopes a sibling child", () => { - const rootA = insertElement(); - const childA = insertElement({ parentId: rootA }); - const rootB = insertElement(); - const childB = insertElement({ parentId: rootB, codeScope: childA, codeValue: "childB-code" }); - db.saveChanges(); - executeTestCase("depth-1 child scopes depth-1 sibling", - [childA, childB], - [childA, childB], - [rootA, rootB]); - }); - - it("scope chain 1", () => { - const rootA = insertElement(); - const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); - const rootC = insertElement({ codeScope: rootB, codeValue: "rootC-code" }); - db.saveChanges(); - executeTestCase("scope chain forward", [rootA, rootB, rootC], [rootA, rootB, rootC], []); - executeTestCase("scope chain reversed", [rootC, rootB, rootA], [rootA, rootB, rootC], []); - executeTestCase("scope chain middle-first", [rootB, rootA, rootC], [rootA, rootB, rootC], []); - }); - - it("scope chain 2", () => { - // A -> B -> C: B is not in the delete set but depends on A (external violation). - // A should be pruned. C depends on B which is not being deleted, so C is standalone-deletable. - const rootA = insertElement(); - const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); - const rootC = insertElement({ codeScope: rootB, codeValue: "rootC-code" }); - db.saveChanges(); - // rootB is NOT in the delete set but uses rootA as scope -> external violation -> rootA pruned - // rootC is in the delete set and its scope (rootB) is not being deleted -> rootC is safe to delete - executeTestCase("scope chain: delete A and C, B external violation prunes A", - [rootA, rootC], - [rootC], - [rootA, rootB]); - }); - - it("two elements using the same scope", () => { - // A is the code scope for both B and C independently. - // A - // / \ - // B C (code scope, not parent-child) - const rootA = insertElement(); - const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); - const rootC = insertElement({ codeScope: rootA, codeValue: "rootC-code" }); - db.saveChanges(); - executeTestCase("delete all three", - [rootA, rootB, rootC], - [rootA, rootB, rootC], - []); - executeTestCase("delete only B and C", - [rootB, rootC], - [rootB, rootC], - [rootA]); - }); - - it("parent is also the code scope of its own child", () => { - const rootP = insertElement(); - const childC = insertElement({ parentId: rootP, codeScope: rootP, codeValue: "childC-code" }); - db.saveChanges(); - executeTestCase("parent is code scope of child - delete parent", - [rootP], - [rootP, childC], - []); - }); - }); - - describe("external code scope violation - pruning", () => { - it("root is code scope for an external element", () => { - const rootA = insertElement(); - const external = insertElement({ codeScope: rootA, codeValue: "ext-code" }); - const rootB = insertElement(); - db.saveChanges(); - executeTestCase("external scopes root", - [rootA, rootB], - [rootB], - [rootA, external]); - }); - - it("depth-1 child is code scope for external", () => { - const rootA = insertElement(); - const childA = insertElement({ parentId: rootA }); - const external = insertElement({ codeScope: childA, codeValue: "ext-code" }); - const rootB = insertElement(); - db.saveChanges(); - executeTestCase("external scopes depth-1 child - parent subtree pruned", - [rootA, rootB], - [rootB], - [rootA, childA, external]); - }); - - it("depth-2 grandchild is code scope for external", () => { - const rootA = insertElement(); - const childA = insertElement({ parentId: rootA }); - const grandchildA = insertElement({ parentId: childA }); - const external = insertElement({ codeScope: grandchildA, codeValue: "ext-code" }); - const rootB = insertElement(); - db.saveChanges(); - executeTestCase("external scopes depth-2 grandchild - grandparent subtree pruned", - [rootA, rootB], - [rootB], - [rootA, childA, grandchildA, external]); - }); - - it("only the child is passed for deletion", () => { - const rootA = insertElement(); - const childA = insertElement({ parentId: rootA }); - const external = insertElement({ codeScope: childA, codeValue: "ext-code" }); - db.saveChanges(); - executeTestCase("external scopes requested child", - [childA], - [], - [rootA, childA, external]); - }); - - it("root has both an external scope dependent AND an intra-set scope dependent", () => { - const rootA = insertElement(); - const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); - const external = insertElement({ codeScope: rootA, codeValue: "ext-code" }); - db.saveChanges(); - executeTestCase("root pruned due to external; sibling still deleted", - [rootA, rootB], - [rootB], - [rootA, external]); - }); - - it("two independent external scope violations", () => { - const rootA = insertElement(); - const rootB = insertElement(); - const extX = insertElement({ codeScope: rootA, codeValue: "extX" }); - const extY = insertElement({ codeScope: rootB, codeValue: "extY" }); - const rootC = insertElement(); - db.saveChanges(); - executeTestCase("two independent violations", - [rootA, rootB, rootC], - [rootC], - [rootA, rootB, extX, extY]); - }); - }); - - describe("mixed parent-child hierarchy and code scope", () => { - it("root scopes another root - delete both roots, all descendants removed", () => { - const rootA = insertElement(); - const childA1 = insertElement({ parentId: rootA }); - const childA2 = insertElement({ parentId: rootA }); - const rootB = insertElement({ codeScope: rootA, codeValue: "rootB-code" }); - const childB1 = insertElement({ parentId: rootB }); - db.saveChanges(); - executeTestCase("root scopes root - delete both roots", - [rootA, rootB], - [rootA, childA1, childA2, rootB, childB1], - []); - }); - - it("depth-1 child scopes an unrelated root", () => { - const rootA = insertElement(); - const childA1 = insertElement({ parentId: rootA }); - const rootB = insertElement({ codeScope: childA1, codeValue: "rootB-code" }); - const childB1 = insertElement({ parentId: rootB }); - db.saveChanges(); - executeTestCase("depth-1 child scopes root - delete both via parents", - [rootA, rootB], - [rootA, childA1, rootB, childB1], - []); - // Reverse input order - result must be identical - executeTestCase("depth-1 child scopes root - reverse input order", - [rootB, rootA], - [rootA, childA1, rootB, childB1], - []); - }); - - it("depth-1 child scopes an unrelated root - delete child and root directly (parent survives)", () => { - const rootA = insertElement(); - const childA1 = insertElement({ parentId: rootA }); - const rootB = insertElement({ codeScope: childA1, codeValue: "rootB-code" }); - const childB1 = insertElement({ parentId: rootB }); - db.saveChanges(); - // Only childA1 and rootB - rootA is NOT in the delete set. - executeTestCase("depth-1 child scopes root - delete child + scoped root directly", - [childA1, rootB], - [childA1, rootB, childB1], - [rootA]); - }); - - it("depth-1 child scopes an unrelated root - deleting only the child cascades into the scoped root's subtree", () => { - // childA1 is the code scope of rootB. When childA1 is deleted, rootB loses its scope - // element -> rootB (and its children) must also be deleted. - const rootA = insertElement(); - const childA1 = insertElement({ parentId: rootA }); - const rootB = insertElement({ codeScope: childA1, codeValue: "rootB-code" }); - const childB1 = insertElement({ parentId: rootB }); - db.saveChanges(); - executeTestCase("delete child only - scoped root also removed", - [childA1], - [], - [rootA, childA1, rootB, childB1]); - }); - - it("root scopes a depth-1 child in sibling tree - delete both roots, all descendants removed", () => { - const rootA = insertElement(); - const childA1 = insertElement({ parentId: rootA }); - const rootB = insertElement(); - const childB1 = insertElement({ parentId: rootB, codeScope: rootA, codeValue: "childB1-code" }); - db.saveChanges(); - executeTestCase("root scopes depth-1 child - delete both roots", - [rootA, rootB], - [rootA, childA1, rootB, childB1], - []); - }); - - it("depth-1 child scopes a depth-1 child in sibling tree - delete both children directly (parents survive)", () => { - const rootA = insertElement(); - const childA1 = insertElement({ parentId: rootA }); - const rootB = insertElement(); - const childB1 = insertElement({ parentId: rootB, codeScope: childA1, codeValue: "childB1-code" }); - const childB2 = insertElement({ parentId: rootB }); - db.saveChanges(); - executeTestCase("sibling-child scope - delete both children directly", - [childA1, childB1], - [childA1, childB1], - [rootA, rootB, childB2]); - }); - - it("depth-2 grandchild scopes an unrelated root - delete grandparent + scoped root", () => { - const rootA = insertElement(); - const childA = insertElement({ parentId: rootA }); - const grandchildA = insertElement({ parentId: childA }); - const rootB = insertElement({ codeScope: grandchildA, codeValue: "rootB-code" }); - const childB = insertElement({ parentId: rootB }); - db.saveChanges(); - executeTestCase("depth-2 grandchild scopes root - delete both roots", - [rootA, rootB], - [rootA, childA, grandchildA, rootB, childB], - []); - // Delete grandchild and scoped root directly (rootA and childA survive) - executeTestCase("depth-2 grandchild scopes root - delete grandchild + root directly", - [grandchildA, rootB], - [grandchildA, rootB, childB], - [rootA, childA]); - }); - - it("external element scopes a depth-1 child", () => { - const rootA = insertElement(); - const childA1 = insertElement({ parentId: rootA }); - const rootB = insertElement(); - const childB1 = insertElement({ parentId: rootB }); - const external = insertElement({ codeScope: childA1, codeValue: "ext-code" }); - db.saveChanges(); - executeTestCase("external scopes depth-1 child", - [rootA, rootB], - [rootB, childB1], - [rootA, childA1, external]); - }); - - it("external element scopes a depth-2 grandchild", () => { - const rootA = insertElement(); - const childA = insertElement({ parentId: rootA }); - const grandchildA = insertElement({ parentId: childA }); - const rootB = insertElement(); - const childB = insertElement({ parentId: rootB }); - const external = insertElement({ codeScope: grandchildA, codeValue: "ext-code" }); - db.saveChanges(); - executeTestCase("external scopes depth-2 grandchild order 1", - [rootA, rootB], - [rootB, childB], - [rootA, childA, grandchildA, external]); - - executeTestCase("external scopes depth-2 grandchild order 2", - [grandchildA], - [], - [rootA, childA, grandchildA, rootB, childB, external]); - - executeTestCase("external scopes depth-2 grandchild order 3", - [grandchildA, rootA, childB], - [childB], - [rootA, childA, grandchildA, rootB, external]); - }); - - it("two trees: one has external scope violation, other is deleted cleanly", () => { - const rootA = insertElement(); - const childA = insertElement({ parentId: rootA }); - const gcA = insertElement({ parentId: childA }); - const external = insertElement({ codeScope: childA, codeValue: "ext-code" }); - const rootB = insertElement(); - const childB1 = insertElement({ parentId: rootB }); - const childB2 = insertElement({ parentId: rootB }); - const gcB = insertElement({ parentId: childB1 }); - db.saveChanges(); - executeTestCase("one tree pruned, other fully deleted", - [rootA, rootB], - [rootB, childB1, childB2, gcB], - [rootA, childA, gcA, external]); - }); - }); - }); }); From bd01a7ec4fd6d85f535cb7a6751f485977fc3b81 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:18:44 +0530 Subject: [PATCH 06/23] Updated native library --- iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts index 26e7f8fc4e..c99829a79b 100644 --- a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts +++ b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts @@ -624,8 +624,8 @@ export declare namespace IModelJsNative { public createIModel(fileName: string, props: CreateEmptyStandaloneIModelProps): void; public deleteAllTxns(): void; public deleteElement(elemIdJson: string): void; - public deleteElements(elementIds: Id64Array): Id64Array; - public deleteDefinitionElements(elementIds: Id64Array): Id64Array; + public deleteElements(elementIds: Id64Array, skipHandlerCallbacks?: boolean): Id64Array; + public deleteDefinitionElements(elementIds: Id64Array, skipHandlerCallbacks?: boolean): Id64Array; public deleteElementAspect(aspectIdJson: string): void; public deleteLinkTableRelationship(props: RelationshipProps): DbResult; public deleteLinkTableRelationships(props: ReadonlyArray): DbResult; From 2f4af239856fb6de265c0dfda20287a5262c146f Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:50:32 +0530 Subject: [PATCH 07/23] Re-worked the code scope violation detection logic, reverted partfile changes made during dev --- .../iModelPlatform/DgnCore/DgnElements.cpp | 138 +++++++++++------- .../iModelJsNodeAddon.PartFile.xml | 8 + 2 files changed, 93 insertions(+), 53 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index d680b07ce4..0a26f3e9ad 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -687,65 +687,95 @@ DgnDbStatus DgnElements::Delete(DgnElementCR elementIn) namespace { - DgnElementIdSet ExpandElementHierarchyAndValidate(DgnDbR db, DgnElementIdSet& elementIds) + void ExpandElementHierarchy(DgnDbR db, DgnElementIdSet& elementIds) { - const auto expandAndValidateSqlStr = R"sql( + constexpr auto expandHierarchySql = R"sql( WITH RECURSIVE - -- Expand input roots to their full descendant hierarchy fullDeleteSet(id) AS ( - SELECT ECInstanceId FROM bis.Element WHERE InVirtualSet(?, ECInstanceId) + SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) UNION ALL - SELECT e.ECInstanceId FROM bis.Element e - INNER JOIN fullDeleteSet p ON e.Parent.Id = p.id - ), - -- Elements outside the delete set that use a delete-set element as their CodeScope - violatingScopes(id) AS ( - SELECT DISTINCT CodeScope.Id FROM bis.Element - WHERE CodeScope.Id IN (SELECT id FROM fullDeleteSet) - AND ECInstanceId NOT IN (SELECT id FROM fullDeleteSet) - ), - -- For each violator, find its highest ancestor that is still inside the delete set - subtreeRoots(id, parentId) AS ( - SELECT ECInstanceId, Parent.Id FROM bis.Element - WHERE ECInstanceId IN (SELECT id FROM violatingScopes) - UNION ALL - SELECT e.ECInstanceId, e.Parent.Id FROM bis.Element e - INNER JOIN subtreeRoots s ON e.ECInstanceId = s.parentId - WHERE s.parentId IN (SELECT id FROM fullDeleteSet) - ), - -- Walk DOWN from each root to collect the entire subtree that must be excluded + SELECT e.Id FROM bis_Element e + INNER JOIN fullDeleteSet p ON e.ParentId = p.id + ) + SELECT id FROM fullDeleteSet + )sql"; + + Statement expandHierarchyStmt; + if (BE_SQLITE_OK != expandHierarchyStmt.Prepare(db, expandHierarchySql)) + return; + + expandHierarchyStmt.BindVirtualSet(1, elementIds); + while (expandHierarchyStmt.Step() == BE_SQLITE_ROW) + elementIds.insert(expandHierarchyStmt.GetValueId(0)); + } + + DgnElementIdSet FindViolatingCodeScopeIds(DgnDbR db, const DgnElementIdSet& elementIds) + { + constexpr auto violatingCodeScopesSql = R"sql( + SELECT DISTINCT CodeScopeId FROM bis_Element + WHERE InVirtualSet(?, CodeScopeId) + AND NOT InVirtualSet(?, Id) + )sql"; + + Statement violatingCodeScopesStmt; + if (BE_SQLITE_OK != violatingCodeScopesStmt.Prepare(db, violatingCodeScopesSql)) + return {}; + + violatingCodeScopesStmt.BindVirtualSet(1, elementIds); + violatingCodeScopesStmt.BindVirtualSet(2, elementIds); + + DgnElementIdSet violatingScopeIds; + while (violatingCodeScopesStmt.Step() == BE_SQLITE_ROW) + violatingScopeIds.insert(violatingCodeScopesStmt.GetValueId(0)); + + return violatingScopeIds; + } + + DgnElementIdSet CollectSubtreesToExclude(DgnDbR db, const DgnElementIdSet& violatingScopeIds) + { + constexpr auto elementSubtreeSql = R"sql( + WITH RECURSIVE subtree(id) AS ( - SELECT id FROM subtreeRoots - WHERE parentId IS NULL OR parentId NOT IN (SELECT id FROM fullDeleteSet) + SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) UNION ALL - SELECT e.ECInstanceId FROM bis.Element e - INNER JOIN subtree s ON e.Parent.Id = s.id + SELECT e.Id FROM bis_Element e + INNER JOIN subtree s ON e.ParentId = s.id ) - SELECT fds.id AS id, FALSE AS isCodeScopeViolation FROM fullDeleteSet fds - UNION ALL - SELECT DISTINCT s.id AS id, TRUE AS isCodeScopeViolation FROM subtree s + SELECT id FROM subtree )sql"; - ECSqlStatement expandAndValidate; - if (ECSqlStatus::Success != expandAndValidate.Prepare(db, expandAndValidateSqlStr)) - return elementIds; + Statement elementSubtreeStmt; + if (BE_SQLITE_OK != elementSubtreeStmt.Prepare(db, elementSubtreeSql)) + return {}; + + elementSubtreeStmt.BindVirtualSet(1, violatingScopeIds); + + DgnElementIdSet subtreeIds; + while (elementSubtreeStmt.Step() == BE_SQLITE_ROW) + subtreeIds.insert(elementSubtreeStmt.GetValueId(0)); + + return subtreeIds; + } + + DgnElementIdSet ExpandElementHierarchyAndValidateCodeScopes(DgnDbR db, DgnElementIdSet& elementIds) + { + // Expand input roots to their full descendant hierarchy to get all affected child elements + ExpandElementHierarchy(db, elementIds); - expandAndValidate.BindVirtualSet(1, std::make_shared>(BeIdSet(elementIds.GetBeIdSet()))); + // Elements outside the delete set that use a delete-set element as their CodeScope will lead to FK violations. + DgnElementIdSet violatingScopeIds = FindViolatingCodeScopeIds(db, elementIds); + if (violatingScopeIds.empty()) + return {}; + // For each violator, find its highest ancestor that is still inside the delete set + DgnElementIdSet subtreesToExclude = CollectSubtreesToExclude(db, violatingScopeIds); + + // Remove excluded subtrees from the delete set and return them as the failed set. DgnElementIdSet failedToDeleteElements; - while (expandAndValidate.Step() == BE_SQLITE_ROW) + for (const auto& id : subtreesToExclude) { - const auto id = expandAndValidate.GetValueId(0); - - if (const bool isCodeScopeViolation = expandAndValidate.GetValueBoolean(1); isCodeScopeViolation) - { - failedToDeleteElements.insert(id); - elementIds.erase(id); - } - else - { - elementIds.insert(id); - } + failedToDeleteElements.insert(id); + elementIds.erase(id); } return failedToDeleteElements; @@ -766,7 +796,7 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c // Expand the input set to include all descendants, then detect any elements that must be excluded // because an external element uses one of them as a CodeScope (deleting would leave a dangling FK). - DgnElementIdSet failedToDeleteElements = ExpandElementHierarchyAndValidate(m_dgndb, validatedElementIds); + DgnElementIdSet failedToDeleteElements = ExpandElementHierarchyAndValidateCodeScopes(m_dgndb, validatedElementIds); if (!failedToDeleteElements.empty()) LOG.warningv("deleteElements: Skipping elements as them or their subtrees contain code scopes for elements outside the delete set: %s", failedToDeleteElements.ToString().c_str()); @@ -886,13 +916,15 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele DgnElementIdSet definitionElementIds; DgnElementIdSet nonDefinitionElementIds; + constexpr auto classifySql = R"sql( + SELECT e.ECInstanceId, CASE WHEN d.ECInstanceId IS NULL THEN 0 ELSE 1 END + FROM bis.Element e + LEFT JOIN bis.DefinitionElement d ON d.ECInstanceId = e.ECInstanceId + WHERE InVirtualSet(?, e.ECInstanceId) + )sql"; + ECSqlStatement classifyStmt; - if (ECSqlStatus::Success != classifyStmt.Prepare(m_dgndb, R"sql( - SELECT e.ECInstanceId, CASE WHEN d.ECInstanceId IS NULL THEN 0 ELSE 1 END - FROM bis.Element e - LEFT JOIN bis.DefinitionElement d ON d.ECInstanceId = e.ECInstanceId - WHERE InVirtualSet(?, e.ECInstanceId) - )sql")) + if (ECSqlStatus::Success != classifyStmt.Prepare(m_dgndb, classifySql)) return elementIds; classifyStmt.BindVirtualSet(1, std::make_shared>(BeIdSet(elementIds.GetBeIdSet()))); diff --git a/iModelJsNodeAddon/iModelJsNodeAddon.PartFile.xml b/iModelJsNodeAddon/iModelJsNodeAddon.PartFile.xml index 78015c39ed..56dfb99ddc 100644 --- a/iModelJsNodeAddon/iModelJsNodeAddon.PartFile.xml +++ b/iModelJsNodeAddon/iModelJsNodeAddon.PartFile.xml @@ -9,6 +9,7 @@ + @@ -63,6 +64,9 @@ GeoCoordAssetsLarge + + + @@ -155,4 +159,8 @@ + + + + From 1535fd8f38a7e3995ef0b3f514a08fce62ed0ec7 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:07:10 +0530 Subject: [PATCH 08/23] Updated function arg, bugfix, reworked the logic a bit --- .../iModelPlatform/DgnCore/DgnElements.cpp | 109 ++++++++++-------- .../PublicAPI/DgnPlatform/DgnElement.h | 2 +- iModelJsNodeAddon/IModelJsNative.cpp | 9 +- iModelJsNodeAddon/IModelJsNative.h | 5 +- iModelJsNodeAddon/JsInteropDgnDb.cpp | 12 +- .../api_package/ts/src/NativeLibrary.ts | 4 +- 6 files changed, 83 insertions(+), 58 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index 0a26f3e9ad..c98cdf56b9 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -731,15 +731,25 @@ namespace return violatingScopeIds; } - DgnElementIdSet CollectSubtreesToExclude(DgnDbR db, const DgnElementIdSet& violatingScopeIds) + DgnElementIdSet CollectSubtreesToExclude(DgnDbR db, const DgnElementIdSet& violatingScopeIds, const DgnElementIdSet& expandedDeleteSet) { constexpr auto elementSubtreeSql = R"sql( WITH RECURSIVE + subtreeRoots(id, parentId) AS ( + SELECT Id, ParentId FROM bis_Element + WHERE InVirtualSet(?, Id) + UNION ALL + SELECT e.Id, e.ParentId FROM bis_Element e + INNER JOIN subtreeRoots s ON e.Id = s.parentId + WHERE InVirtualSet(?, s.parentId) + ), subtree(id) AS ( - SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) + SELECT id FROM subtreeRoots + WHERE parentId IS NULL OR NOT InVirtualSet(?, parentId) UNION ALL SELECT e.Id FROM bis_Element e INNER JOIN subtree s ON e.ParentId = s.id + WHERE InVirtualSet(?, e.Id) ) SELECT id FROM subtree )sql"; @@ -749,6 +759,9 @@ namespace return {}; elementSubtreeStmt.BindVirtualSet(1, violatingScopeIds); + elementSubtreeStmt.BindVirtualSet(2, expandedDeleteSet); + elementSubtreeStmt.BindVirtualSet(3, expandedDeleteSet); + elementSubtreeStmt.BindVirtualSet(4, expandedDeleteSet); DgnElementIdSet subtreeIds; while (elementSubtreeStmt.Step() == BE_SQLITE_ROW) @@ -767,16 +780,16 @@ namespace if (violatingScopeIds.empty()) return {}; - // For each violator, find its highest ancestor that is still inside the delete set - DgnElementIdSet subtreesToExclude = CollectSubtreesToExclude(db, violatingScopeIds); + // Walk up from each violating element to its highest ancestor still inside the delete set, + // then collect the full downward subtree from those roots. The entire subtree must be + // excluded from deletion so that code-scope integrity is preserved. + DgnElementIdSet subtreesToExclude = CollectSubtreesToExclude(db, violatingScopeIds, elementIds); // Remove excluded subtrees from the delete set and return them as the failed set. DgnElementIdSet failedToDeleteElements; + failedToDeleteElements.insert(subtreesToExclude.begin(), subtreesToExclude.end()); for (const auto& id : subtreesToExclude) - { - failedToDeleteElements.insert(id); elementIds.erase(id); - } return failedToDeleteElements; } @@ -798,54 +811,45 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c // because an external element uses one of them as a CodeScope (deleting would leave a dangling FK). DgnElementIdSet failedToDeleteElements = ExpandElementHierarchyAndValidateCodeScopes(m_dgndb, validatedElementIds); if (!failedToDeleteElements.empty()) - LOG.warningv("deleteElements: Skipping elements as them or their subtrees contain code scopes for elements outside the delete set: %s", failedToDeleteElements.ToString().c_str()); + LOG.warningv("deleteElements: Skipping elements as them or their subtrees contain code scopes for elements outside the delete Id set: %s", failedToDeleteElements.ToString().c_str()); // Prep the elements, handlers and the Db for a bulk deletion SetBulkOperation(true); + // Use a scope guard to ensure the DB state is always reset, even on early return. + auto resetDbState = [&]() + { + m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = false"); + SetBulkOperation(false); + }; + // Call the pre-delete handlers. Remove the elements that get veto'd off the delete list from the handlers. // We need to save these elements to avoid a re-load when calling the post-delete handlers std::vector elementsToDelete; if (!skipHandlerCallbacks) { - std::vector idsToRemove; for (const auto& elementId : validatedElementIds) { const auto element = GetElement(elementId); - if (!element.IsValid()) - { - idsToRemove.push_back(elementId); - continue; - } - // Call the pre-delete handler - if (element->_OnDelete() != DgnDbStatus::Success) - { - idsToRemove.push_back(elementId); + if (!element.IsValid() || element->_OnDelete() != DgnDbStatus::Success) continue; - } // Ask the parent if it's okay to delete the child. // Also, skip parent callback if the parent itself is also being deleted. auto parent = GetElement(element->m_parent.m_id); - if (parent.IsValid() && - validatedElementIds.find(element->m_parent.m_id) == validatedElementIds.end() && + if (parent.IsValid() && + validatedElementIds.find(element->m_parent.m_id) == validatedElementIds.end() && parent->_OnChildDelete(*element) != DgnDbStatus::Success) - { - idsToRemove.push_back(elementId); continue; - } elementsToDelete.push_back(element); } - std::for_each(idsToRemove.begin(), idsToRemove.end(), [&validatedElementIds](const DgnElementId elementId){ validatedElementIds.erase(elementId); }); - - elementsToDelete.erase( - std::remove_if( - elementsToDelete.begin(), - elementsToDelete.end(), - [&validatedElementIds](const DgnElementCPtr& el) { return validatedElementIds.find(el->GetElementId()) == validatedElementIds.end(); } - ), elementsToDelete.end()); + + // Rebuild validatedElementIds from only the elements that passed all pre-delete checks. + validatedElementIds.clear(); + for (const auto& element : elementsToDelete) + validatedElementIds.insert(element->GetElementId()); } // Since we have already handled all the external code scope violations, @@ -853,12 +857,24 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = true"); // Clear up the link-table relationships in bulk - DeleteLinkTableRelationships(m_dgndb, validatedElementIds); + if (!DeleteLinkTableRelationships(m_dgndb, validatedElementIds)) + { + LOG.errorv("deleteElements: Failed to delete link table relationships for element Ids: %s", validatedElementIds.ToString().c_str()); + failedToDeleteElements.insert(validatedElementIds.begin(), validatedElementIds.end()); + resetDbState(); + return failedToDeleteElements; + } // Delete the elements - CachedStatementPtr statement = GetStatement("DELETE FROM " BIS_TABLE(BIS_CLASS_Element) " WHERE InVirtualSet(?, Id)"); + auto statement = GetStatement("DELETE FROM " BIS_TABLE(BIS_CLASS_Element) " WHERE InVirtualSet(?, Id)"); statement->BindVirtualSet(1, validatedElementIds); - statement->Step(); + if (statement->Step() != BE_SQLITE_DONE) + { + LOG.errorv("deleteElements: Failed to delete element Ids from database: %s", validatedElementIds.ToString().c_str()); + failedToDeleteElements.insert(validatedElementIds.begin(), validatedElementIds.end()); + resetDbState(); + return failedToDeleteElements; + } // Evict all deleted elements from the MRU cache so stale pointers are not returned to callers for (const auto& elementId : validatedElementIds) @@ -879,28 +895,31 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c } }); } - - // Reset the db state - m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = false"); - SetBulkOperation(false); + resetDbState(); if (!failedToDeleteElements.empty()) - LOG.warningv("deleteElements: Failed to delete elements: %s", failedToDeleteElements.ToString().c_str()); + LOG.errorv("deleteElements: Failed to delete element Ids: %s", failedToDeleteElements.ToString().c_str()); return failedToDeleteElements; } /* static */ -void DgnElements::DeleteLinkTableRelationships(DgnDbR db, const DgnElementIdSet& elementIds) +bool DgnElements::DeleteLinkTableRelationships(DgnDbR db, const DgnElementIdSet& elementIds) { for (const auto& table : { BIS_TABLE(BIS_REL_ElementRefersToElements), BIS_TABLE(BIS_REL_ElementDrivesElement) }) { BeAssert(db.TableExists(table)); auto statement = db.GetCachedStatement(Utf8PrintfString("DELETE FROM %s WHERE InVirtualSet(?, SourceId) OR InVirtualSet(?, TargetId)", table).c_str()); + if (!statement.IsValid()) + return false; + statement->BindVirtualSet(1, elementIds); statement->BindVirtualSet(2, elementIds); - statement->Step(); + if (statement->Step() != BE_SQLITE_DONE) + return false; } + + return true; } /*---------------------------------------------------------------------------------**//** @@ -939,7 +958,7 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele } if (!nonDefinitionElementIds.empty()) - LOG.warningv("deleteDefinitionElements: Elements %s are not DefinitionElements and cannot be deleted with this API.", nonDefinitionElementIds.ToString().c_str()); + LOG.warningv("deleteDefinitionElements: Element Ids %s are not DefinitionElements and cannot be deleted with this API.", nonDefinitionElementIds.ToString().c_str()); if (definitionElementIds.empty()) return {}; @@ -965,7 +984,7 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele if (toBeDeleted.empty()) { if (!cannotBeDeleted.empty()) - LOG.warningv("deleteDefinitionElements: Skipping elements that are in use: %s", cannotBeDeleted.ToString().c_str()); + LOG.warningv("deleteDefinitionElements: Skipping element Ids that are in use: %s", cannotBeDeleted.ToString().c_str()); return cannotBeDeleted; } @@ -976,7 +995,7 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele cannotBeDeleted.insert(failedToDeleteIds.begin(), failedToDeleteIds.end()); if (!cannotBeDeleted.empty()) - LOG.warningv("deleteDefinitionElements: Skipping elements that are in use or blocked: %s", cannotBeDeleted.ToString().c_str()); + LOG.warningv("deleteDefinitionElements: Skipping element Ids that are in use or blocked: %s", cannotBeDeleted.ToString().c_str()); return cannotBeDeleted; } diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index 2d898bc058..0353d79961 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -3911,7 +3911,7 @@ struct DgnElements : DgnDbTable void SetBulkOperation(const bool isBulk) { m_isBulkOperation = isBulk; } bool IsBulkOperation() const { return m_isBulkOperation; } - static void DeleteLinkTableRelationships(DgnDbR db, const DgnElementIdSet& elementIds); + static bool DeleteLinkTableRelationships(DgnDbR db, const DgnElementIdSet& elementIds); public: DGNPLATFORM_EXPORT BeSQLite::SnappyFromMemory& GetSnappyFrom() {return m_snappyFrom;} // NB: Not to be used during loading of a GeometricElement or GeometryPart! diff --git a/iModelJsNodeAddon/IModelJsNative.cpp b/iModelJsNodeAddon/IModelJsNative.cpp index 2f959255a4..c4b7276825 100644 --- a/iModelJsNodeAddon/IModelJsNative.cpp +++ b/iModelJsNodeAddon/IModelJsNative.cpp @@ -1745,9 +1745,10 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps if (ARGUMENT_IS_NOT_PRESENT(0) || !info[0].IsArray()) { THROW_JS_TYPE_EXCEPTION("Invalid argument given to deleteElements"); } - OPTIONAL_ARGUMENT_BOOL(1, skipHandlerCallbacks, false); - auto elemIds = JsInterop::DeleteElements(db, info[0].As(), skipHandlerCallbacks); + const auto deleteOptions = ARGUMENT_IS_PRESENT(1) ? info[1].As() : Env().Undefined(); + + auto elemIds = JsInterop::DeleteElements(db, info[0].As(), deleteOptions); uint32_t index = 0; auto ret = Napi::Array::New(Env(), elemIds.size()); for (const auto& elemId : elemIds) @@ -1761,9 +1762,9 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps if (ARGUMENT_IS_NOT_PRESENT(0) || !info[0].IsArray()) { THROW_JS_TYPE_EXCEPTION("Invalid argument given to deleteDefinitionElements"); } - OPTIONAL_ARGUMENT_BOOL(1, skipHandlerCallbacks, false); + const auto deleteOptions = ARGUMENT_IS_PRESENT(1) ? info[1].As() : Env().Undefined(); - auto elemIds = JsInterop::DeleteDefinitionElements(db, info[0].As(), skipHandlerCallbacks); + auto elemIds = JsInterop::DeleteDefinitionElements(db, info[0].As(), deleteOptions); uint32_t index = 0; auto ret = Napi::Array::New(Env(), elemIds.size()); for (const auto& elemId : elemIds) diff --git a/iModelJsNodeAddon/IModelJsNative.h b/iModelJsNodeAddon/IModelJsNative.h index af3827b6ee..12ed75e5bf 100644 --- a/iModelJsNodeAddon/IModelJsNative.h +++ b/iModelJsNodeAddon/IModelJsNative.h @@ -488,6 +488,7 @@ struct JsInterop { BE_JSON_NAME(writeable) BE_JSON_NAME(yesNo) BE_JSON_NAME(uncompressedSize) + BE_JSON_NAME(skipHandlerCallbacks) #define JSON_NAME(__val__) JsInterop::json_##__val__() @@ -511,8 +512,8 @@ struct JsInterop { static Napi::String InsertElement(DgnDbR db, Napi::Object props, Napi::Value options); static void UpdateElement(DgnDbR db, Napi::Object); static void DeleteElement(DgnDbR db, Utf8StringCR eidStr); - static DgnElementIdSet DeleteElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipHandlerCallbacks = false); - static DgnElementIdSet DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipHandlerCallbacks = false); + static DgnElementIdSet DeleteElements(DgnDbR dgndb, Napi::Array elementIds, Napi::Value deleteOptionsObj); + static DgnElementIdSet DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds, Napi::Value deleteOptionsObj); static DgnDbStatus SimplifyElementGeometry(DgnDbR db, Napi::Object simplifyArgs); static InlineGeometryPartsResult InlineGeometryParts(DgnDbR db); static Napi::String InsertElementAspect(DgnDbR db, Napi::Object aspectProps); diff --git a/iModelJsNodeAddon/JsInteropDgnDb.cpp b/iModelJsNodeAddon/JsInteropDgnDb.cpp index 950c5a9d1d..754fad3555 100644 --- a/iModelJsNodeAddon/JsInteropDgnDb.cpp +++ b/iModelJsNodeAddon/JsInteropDgnDb.cpp @@ -684,7 +684,7 @@ void JsInterop::DeleteElement(DgnDbR dgndb, Utf8StringCR eidStr) { THROW_JS_DGN_DB_EXCEPTION(Env(), "error deleting element", stat); } -DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipHandlerCallbacks) { +DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds, Napi::Value deleteOptionsObj) { DgnElementIdSet elementIdSet; for (auto i = 0U; i < elementIds.Length(); ++i) { Napi::Value arrayItem = elementIds[i]; @@ -693,10 +693,12 @@ DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds, if (val.IsValid()) elementIdSet.insert(DgnElementId(val.GetValue())); } - return dgndb.Elements().DeleteElements(elementIdSet, skipHandlerCallbacks); + + BeJsConst deleteOptionsJson(deleteOptionsObj); + return dgndb.Elements().DeleteElements(elementIdSet, deleteOptionsJson.isObject() && deleteOptionsJson.Get(json_skipHandlerCallbacks()).asBool()); } -DgnElementIdSet JsInterop::DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds, const bool skipHandlerCallbacks) { +DgnElementIdSet JsInterop::DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds, Napi::Value deleteOptionsObj) { DgnElementIdSet elementIdSet; for (auto i = 0U; i < elementIds.Length(); ++i) { Napi::Value arrayItem = elementIds[i]; @@ -705,7 +707,9 @@ DgnElementIdSet JsInterop::DeleteDefinitionElements(DgnDbR dgndb, Napi::Array el if (val.IsValid()) elementIdSet.insert(DgnElementId(val.GetValue())); } - return dgndb.Elements().DeleteDefinitionElements(elementIdSet, skipHandlerCallbacks); + + BeJsConst deleteOptionsJson(deleteOptionsObj); + return dgndb.Elements().DeleteDefinitionElements(elementIdSet, deleteOptionsJson.isObject() && deleteOptionsJson.Get(json_skipHandlerCallbacks()).asBool()); } /*---------------------------------------------------------------------------------**//** diff --git a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts index c99829a79b..5ec1e5d0aa 100644 --- a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts +++ b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts @@ -624,8 +624,8 @@ export declare namespace IModelJsNative { public createIModel(fileName: string, props: CreateEmptyStandaloneIModelProps): void; public deleteAllTxns(): void; public deleteElement(elemIdJson: string): void; - public deleteElements(elementIds: Id64Array, skipHandlerCallbacks?: boolean): Id64Array; - public deleteDefinitionElements(elementIds: Id64Array, skipHandlerCallbacks?: boolean): Id64Array; + public deleteElements(elementIds: Id64Array, deleteOptions?: { skipHandlerCallbacks?: boolean }): Id64Array; + public deleteDefinitionElements(elementIds: Id64Array, deleteOptions?: { skipHandlerCallbacks?: boolean }): Id64Array; public deleteElementAspect(aspectIdJson: string): void; public deleteLinkTableRelationship(props: RelationshipProps): DbResult; public deleteLinkTableRelationships(props: ReadonlyArray): DbResult; From b0b3d30ed5edb1d4426a62c4957f0a6e407eab79 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:38:20 +0530 Subject: [PATCH 09/23] Removed experimental flag --- .../iModelPlatform/DgnCore/DgnElements.cpp | 66 +++++++++---------- .../PublicAPI/DgnPlatform/DgnElement.h | 6 +- iModelJsNodeAddon/IModelJsNative.cpp | 7 +- iModelJsNodeAddon/IModelJsNative.h | 5 +- iModelJsNodeAddon/JsInteropDgnDb.cpp | 10 ++- .../api_package/ts/src/NativeLibrary.ts | 4 +- 6 files changed, 42 insertions(+), 56 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index c98cdf56b9..1e4f88a5d4 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -798,7 +798,7 @@ namespace /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, const bool skipHandlerCallbacks) +DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds) { DgnDb::VerifyClientThread(); @@ -826,32 +826,29 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c // Call the pre-delete handlers. Remove the elements that get veto'd off the delete list from the handlers. // We need to save these elements to avoid a re-load when calling the post-delete handlers std::vector elementsToDelete; - if (!skipHandlerCallbacks) + for (const auto& elementId : validatedElementIds) { - for (const auto& elementId : validatedElementIds) - { - const auto element = GetElement(elementId); - // Call the pre-delete handler - if (!element.IsValid() || element->_OnDelete() != DgnDbStatus::Success) - continue; - - // Ask the parent if it's okay to delete the child. - // Also, skip parent callback if the parent itself is also being deleted. - auto parent = GetElement(element->m_parent.m_id); - if (parent.IsValid() && - validatedElementIds.find(element->m_parent.m_id) == validatedElementIds.end() && - parent->_OnChildDelete(*element) != DgnDbStatus::Success) - continue; + const auto element = GetElement(elementId); + // Call the pre-delete handler + if (!element.IsValid() || element->_OnDelete() != DgnDbStatus::Success) + continue; - elementsToDelete.push_back(element); - } + // Ask the parent if it's okay to delete the child. + // Also, skip parent callback if the parent itself is also being deleted. + auto parent = GetElement(element->m_parent.m_id); + if (parent.IsValid() && + validatedElementIds.find(element->m_parent.m_id) == validatedElementIds.end() && + parent->_OnChildDelete(*element) != DgnDbStatus::Success) + continue; - // Rebuild validatedElementIds from only the elements that passed all pre-delete checks. - validatedElementIds.clear(); - for (const auto& element : elementsToDelete) - validatedElementIds.insert(element->GetElementId()); + elementsToDelete.push_back(element); } + // Rebuild validatedElementIds from only the elements that passed all pre-delete checks. + validatedElementIds.clear(); + for (const auto& element : elementsToDelete) + validatedElementIds.insert(element->GetElementId()); + // Since we have already handled all the external code scope violations, // defer the FK integrity check for all the intra set violations as all of them are being deleted anyway m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = true"); @@ -881,20 +878,17 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c m_mruCache->DropElement(elementId); // Call the post delete handlers - if (!skipHandlerCallbacks) + std::for_each(elementsToDelete.begin(), elementsToDelete.end(), [&](const DgnElementCPtr& element) { - std::for_each(elementsToDelete.begin(), elementsToDelete.end(), [&](const DgnElementCPtr& element) + element->_OnDeleted(); + // Notify parent only if it is not itself being deleted + if (element->m_parent.m_id.IsValid() && validatedElementIds.find(element->m_parent.m_id) == validatedElementIds.end()) { - element->_OnDeleted(); - // Notify parent only if it is not itself being deleted - if (element->m_parent.m_id.IsValid() && validatedElementIds.find(element->m_parent.m_id) == validatedElementIds.end()) - { - auto parent = GetElement(element->m_parent.m_id); - if (parent.IsValid()) - parent->_OnChildDeleted(*element); - } - }); - } + auto parent = GetElement(element->m_parent.m_id); + if (parent.IsValid()) + parent->_OnChildDeleted(*element); + } + }); resetDbState(); if (!failedToDeleteElements.empty()) @@ -925,7 +919,7 @@ bool DgnElements::DeleteLinkTableRelationships(DgnDbR db, const DgnElementIdSet& /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& elementIds, bool skipHandlerCallbacks) +DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& elementIds) { DgnDb::VerifyClientThread(); @@ -989,7 +983,7 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele } m_dgndb.BeginPurgeOperation(); - const auto failedToDeleteIds = DeleteElements(toBeDeleted, skipHandlerCallbacks); + const auto failedToDeleteIds = DeleteElements(toBeDeleted); m_dgndb.EndPurgeOperation(); cannotBeDeleted.insert(failedToDeleteIds.begin(), failedToDeleteIds.end()); diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index 0353d79961..643ebe1e02 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -4038,17 +4038,15 @@ struct DgnElements : DgnDbTable //! Delete multiple DgnElements from this DgnDb. //! @param[in] elementIds The element set to delete. - //! @param[in] skipHandlerCallbacks Skip any domain handler callbacks before and after deletion. Defaults to false. //! @return A DgnElementIdSet of elements that failed to delete. //! @note This function can only be safely invoked from the client thread. - DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds, const bool skipHandlerCallbacks = false); + DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds); //! Delete multiple definition elements from this DgnDb. //! @param[in] elementIds The set of definition elements to delete. - //! @param[in] skipHandlerCallbacks Skip any domain handler callbacks before and after deletion. Defaults to false. //! @return A DgnElementIdSet of definition elements that failed to delete. //! @note This function can only be safely invoked from the client thread. - DGNPLATFORM_EXPORT DgnElementIdSet DeleteDefinitionElements(const DgnElementIdSet& elementIds, const bool skipHandlerCallbacks = false); + DGNPLATFORM_EXPORT DgnElementIdSet DeleteDefinitionElements(const DgnElementIdSet& elementIds); //! Delete a DgnElement from this DgnDb by DgnElementId. //! @return DgnDbStatus::Success if the element was deleted, error status otherwise. diff --git a/iModelJsNodeAddon/IModelJsNative.cpp b/iModelJsNodeAddon/IModelJsNative.cpp index c4b7276825..00f6e11acb 100644 --- a/iModelJsNodeAddon/IModelJsNative.cpp +++ b/iModelJsNodeAddon/IModelJsNative.cpp @@ -1746,9 +1746,7 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps THROW_JS_TYPE_EXCEPTION("Invalid argument given to deleteElements"); } - const auto deleteOptions = ARGUMENT_IS_PRESENT(1) ? info[1].As() : Env().Undefined(); - - auto elemIds = JsInterop::DeleteElements(db, info[0].As(), deleteOptions); + auto elemIds = JsInterop::DeleteElements(db, info[0].As()); uint32_t index = 0; auto ret = Napi::Array::New(Env(), elemIds.size()); for (const auto& elemId : elemIds) @@ -1762,9 +1760,8 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps if (ARGUMENT_IS_NOT_PRESENT(0) || !info[0].IsArray()) { THROW_JS_TYPE_EXCEPTION("Invalid argument given to deleteDefinitionElements"); } - const auto deleteOptions = ARGUMENT_IS_PRESENT(1) ? info[1].As() : Env().Undefined(); - auto elemIds = JsInterop::DeleteDefinitionElements(db, info[0].As(), deleteOptions); + auto elemIds = JsInterop::DeleteDefinitionElements(db, info[0].As()); uint32_t index = 0; auto ret = Napi::Array::New(Env(), elemIds.size()); for (const auto& elemId : elemIds) diff --git a/iModelJsNodeAddon/IModelJsNative.h b/iModelJsNodeAddon/IModelJsNative.h index 12ed75e5bf..e2fbbd9ab4 100644 --- a/iModelJsNodeAddon/IModelJsNative.h +++ b/iModelJsNodeAddon/IModelJsNative.h @@ -488,7 +488,6 @@ struct JsInterop { BE_JSON_NAME(writeable) BE_JSON_NAME(yesNo) BE_JSON_NAME(uncompressedSize) - BE_JSON_NAME(skipHandlerCallbacks) #define JSON_NAME(__val__) JsInterop::json_##__val__() @@ -512,8 +511,8 @@ struct JsInterop { static Napi::String InsertElement(DgnDbR db, Napi::Object props, Napi::Value options); static void UpdateElement(DgnDbR db, Napi::Object); static void DeleteElement(DgnDbR db, Utf8StringCR eidStr); - static DgnElementIdSet DeleteElements(DgnDbR dgndb, Napi::Array elementIds, Napi::Value deleteOptionsObj); - static DgnElementIdSet DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds, Napi::Value deleteOptionsObj); + static DgnElementIdSet DeleteElements(DgnDbR dgndb, Napi::Array elementIds); + static DgnElementIdSet DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds); static DgnDbStatus SimplifyElementGeometry(DgnDbR db, Napi::Object simplifyArgs); static InlineGeometryPartsResult InlineGeometryParts(DgnDbR db); static Napi::String InsertElementAspect(DgnDbR db, Napi::Object aspectProps); diff --git a/iModelJsNodeAddon/JsInteropDgnDb.cpp b/iModelJsNodeAddon/JsInteropDgnDb.cpp index 754fad3555..e7606e4bea 100644 --- a/iModelJsNodeAddon/JsInteropDgnDb.cpp +++ b/iModelJsNodeAddon/JsInteropDgnDb.cpp @@ -684,7 +684,7 @@ void JsInterop::DeleteElement(DgnDbR dgndb, Utf8StringCR eidStr) { THROW_JS_DGN_DB_EXCEPTION(Env(), "error deleting element", stat); } -DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds, Napi::Value deleteOptionsObj) { +DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds) { DgnElementIdSet elementIdSet; for (auto i = 0U; i < elementIds.Length(); ++i) { Napi::Value arrayItem = elementIds[i]; @@ -694,11 +694,10 @@ DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds, elementIdSet.insert(DgnElementId(val.GetValue())); } - BeJsConst deleteOptionsJson(deleteOptionsObj); - return dgndb.Elements().DeleteElements(elementIdSet, deleteOptionsJson.isObject() && deleteOptionsJson.Get(json_skipHandlerCallbacks()).asBool()); + return dgndb.Elements().DeleteElements(elementIdSet); } -DgnElementIdSet JsInterop::DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds, Napi::Value deleteOptionsObj) { +DgnElementIdSet JsInterop::DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds) { DgnElementIdSet elementIdSet; for (auto i = 0U; i < elementIds.Length(); ++i) { Napi::Value arrayItem = elementIds[i]; @@ -708,8 +707,7 @@ DgnElementIdSet JsInterop::DeleteDefinitionElements(DgnDbR dgndb, Napi::Array el elementIdSet.insert(DgnElementId(val.GetValue())); } - BeJsConst deleteOptionsJson(deleteOptionsObj); - return dgndb.Elements().DeleteDefinitionElements(elementIdSet, deleteOptionsJson.isObject() && deleteOptionsJson.Get(json_skipHandlerCallbacks()).asBool()); + return dgndb.Elements().DeleteDefinitionElements(elementIdSet); } /*---------------------------------------------------------------------------------**//** diff --git a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts index 5ec1e5d0aa..26e7f8fc4e 100644 --- a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts +++ b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts @@ -624,8 +624,8 @@ export declare namespace IModelJsNative { public createIModel(fileName: string, props: CreateEmptyStandaloneIModelProps): void; public deleteAllTxns(): void; public deleteElement(elemIdJson: string): void; - public deleteElements(elementIds: Id64Array, deleteOptions?: { skipHandlerCallbacks?: boolean }): Id64Array; - public deleteDefinitionElements(elementIds: Id64Array, deleteOptions?: { skipHandlerCallbacks?: boolean }): Id64Array; + public deleteElements(elementIds: Id64Array): Id64Array; + public deleteDefinitionElements(elementIds: Id64Array): Id64Array; public deleteElementAspect(aspectIdJson: string): void; public deleteLinkTableRelationship(props: RelationshipProps): DbResult; public deleteLinkTableRelationships(props: ReadonlyArray): DbResult; From c00783bc475922020bfd91c62806279276f57dbb Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:03:56 +0530 Subject: [PATCH 10/23] Cleaned up inconsistencies after removing the temporary flag --- .../iModelPlatform/DgnCore/DgnElements.cpp | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index 1e4f88a5d4..d0e9694818 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -853,15 +853,6 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds) // defer the FK integrity check for all the intra set violations as all of them are being deleted anyway m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = true"); - // Clear up the link-table relationships in bulk - if (!DeleteLinkTableRelationships(m_dgndb, validatedElementIds)) - { - LOG.errorv("deleteElements: Failed to delete link table relationships for element Ids: %s", validatedElementIds.ToString().c_str()); - failedToDeleteElements.insert(validatedElementIds.begin(), validatedElementIds.end()); - resetDbState(); - return failedToDeleteElements; - } - // Delete the elements auto statement = GetStatement("DELETE FROM " BIS_TABLE(BIS_CLASS_Element) " WHERE InVirtualSet(?, Id)"); statement->BindVirtualSet(1, validatedElementIds); @@ -873,9 +864,14 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds) return failedToDeleteElements; } - // Evict all deleted elements from the MRU cache so stale pointers are not returned to callers - for (const auto& elementId : validatedElementIds) - m_mruCache->DropElement(elementId); + // Clear up the link-table relationships in bulk + if (!DeleteLinkTableRelationships(m_dgndb, validatedElementIds)) + { + LOG.errorv("deleteElements: Failed to delete link table relationships for element Ids: %s", validatedElementIds.ToString().c_str()); + failedToDeleteElements.insert(validatedElementIds.begin(), validatedElementIds.end()); + resetDbState(); + return failedToDeleteElements; + } // Call the post delete handlers std::for_each(elementsToDelete.begin(), elementsToDelete.end(), [&](const DgnElementCPtr& element) From a96c3c0b7582423513c8d71c059284d986c28100 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:33:49 +0530 Subject: [PATCH 11/23] Fixed bug --- iModelCore/iModelPlatform/DgnCore/DgnElement.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElement.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElement.cpp index 2e4f648fba..a677b230d1 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElement.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElement.cpp @@ -1048,7 +1048,7 @@ void DgnElement::_OnDeleted() const GetDgnDb().Elements().DropFromPool(*this); // For a bulk delete operation, the relationship classes will be handled separately - if (GetDgnDb().Elements().IsBulkOperation()) + if (!GetDgnDb().Elements().IsBulkOperation()) deleteLinkTableRelationships(GetDgnDb(), GetElementId()); DgnModelPtr model = GetModel(); From 16042edb2454e0ee62efa61715ae219f42dddbd5 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:37:04 +0530 Subject: [PATCH 12/23] Addressed co-pilot review comments --- .../iModelPlatform/DgnCore/DgnElements.cpp | 2 +- .../PublicAPI/DgnPlatform/DgnElement.h | 2 +- iModelJsNodeAddon/JsInteropDgnDb.cpp | 16 ++++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index d0e9694818..a557cbf3b3 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -811,7 +811,7 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds) // because an external element uses one of them as a CodeScope (deleting would leave a dangling FK). DgnElementIdSet failedToDeleteElements = ExpandElementHierarchyAndValidateCodeScopes(m_dgndb, validatedElementIds); if (!failedToDeleteElements.empty()) - LOG.warningv("deleteElements: Skipping elements as them or their subtrees contain code scopes for elements outside the delete Id set: %s", failedToDeleteElements.ToString().c_str()); + LOG.warningv("deleteElements: Skipping elements as they (or their subtrees) contain code scopes for elements outside the delete Id set: %s", failedToDeleteElements.ToString().c_str()); // Prep the elements, handlers and the Db for a bulk deletion SetBulkOperation(true); diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index 643ebe1e02..ee5c3a2fee 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -319,7 +319,7 @@ struct EXPORT_VTABLE_ATTRIBUTE DefinitionElementUsageInfo : RefCountedBase //! Generate usage information for the specified set of DefinitionElementIds DGNPLATFORM_EXPORT static DefinitionElementUsageInfoPtr Create(DgnDbR db, BeSQLite::IdSet const& definitionElementIds, std::shared_ptr> excludeIds = nullptr); DGNPLATFORM_EXPORT void ToJson(BeJsValue) const; - DGNPLATFORM_EXPORT DgnElementIdSet GetUsedIds() const { return m_usedIds; } + DGNPLATFORM_EXPORT DgnElementIdSet const& GetUsedIds() const { return m_usedIds; } }; //======================================================================================= diff --git a/iModelJsNodeAddon/JsInteropDgnDb.cpp b/iModelJsNodeAddon/JsInteropDgnDb.cpp index e7606e4bea..572462d8f1 100644 --- a/iModelJsNodeAddon/JsInteropDgnDb.cpp +++ b/iModelJsNodeAddon/JsInteropDgnDb.cpp @@ -689,9 +689,11 @@ DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds) for (auto i = 0U; i < elementIds.Length(); ++i) { Napi::Value arrayItem = elementIds[i]; - auto val = BeInt64Id::FromString(arrayItem.As().Utf8Value().c_str()); - if (val.IsValid()) - elementIdSet.insert(DgnElementId(val.GetValue())); + if (arrayItem.IsString()) { + auto val = BeInt64Id::FromString(arrayItem.As().Utf8Value().c_str()); + if (val.IsValid()) + elementIdSet.insert(DgnElementId(val.GetValue())); + } } return dgndb.Elements().DeleteElements(elementIdSet); @@ -702,9 +704,11 @@ DgnElementIdSet JsInterop::DeleteDefinitionElements(DgnDbR dgndb, Napi::Array el for (auto i = 0U; i < elementIds.Length(); ++i) { Napi::Value arrayItem = elementIds[i]; - auto val = BeInt64Id::FromString(arrayItem.As().Utf8Value().c_str()); - if (val.IsValid()) - elementIdSet.insert(DgnElementId(val.GetValue())); + if (arrayItem.IsString()) { + auto val = BeInt64Id::FromString(arrayItem.As().Utf8Value().c_str()); + if (val.IsValid()) + elementIdSet.insert(DgnElementId(val.GetValue())); + } } return dgndb.Elements().DeleteDefinitionElements(elementIdSet); From e319f59c65b4ad76153ba2523bb0e50040a60d4d Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:40:32 +0530 Subject: [PATCH 13/23] Made the function comments clearer --- .../iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index ee5c3a2fee..0bfa4929cd 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -4037,14 +4037,14 @@ struct DgnElements : DgnDbTable DGNPLATFORM_EXPORT DgnDbStatus Delete(DgnElementCR element); //! Delete multiple DgnElements from this DgnDb. - //! @param[in] elementIds The element set to delete. - //! @return A DgnElementIdSet of elements that failed to delete. + //! @param[in] elementIds The element set to delete. Invalid Ids will be ignored. + //! @return A DgnElementIdSet of valid element Ids that failed to delete. //! @note This function can only be safely invoked from the client thread. DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds); //! Delete multiple definition elements from this DgnDb. - //! @param[in] elementIds The set of definition elements to delete. - //! @return A DgnElementIdSet of definition elements that failed to delete. + //! @param[in] elementIds The set of definition elements to delete. Invalid and non-definition element Ids will be ignored. + //! @return A DgnElementIdSet of valid definition element Ids that failed to delete. //! @note This function can only be safely invoked from the client thread. DGNPLATFORM_EXPORT DgnElementIdSet DeleteDefinitionElements(const DgnElementIdSet& elementIds); From c155d2e3ba4a0cabd805f6ea6a1ce7821b6f746a Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:28:37 +0530 Subject: [PATCH 14/23] Updated API documentation --- .../PublicAPI/DgnPlatform/DgnElement.h | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index 0bfa4929cd..c77a9fe8bd 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -4036,16 +4036,32 @@ struct DgnElements : DgnDbTable //! @note This function can only be safely invoked from the client thread. DGNPLATFORM_EXPORT DgnDbStatus Delete(DgnElementCR element); - //! Delete multiple DgnElements from this DgnDb. - //! @param[in] elementIds The element set to delete. Invalid Ids will be ignored. - //! @return A DgnElementIdSet of valid element Ids that failed to delete. - //! @note This function can only be safely invoked from the client thread. + /** + * Delete multiple DgnElements from this DgnDb, including their descendants. + * + * This method is intended for general non-definition elements. + * Definition elements need to be handled as per their usage which makes them a special case for element deletion. + * The handlers for definition elements veto deletion unless a purge operation is enabled. + * Hence, for bulk deletion of definition Elements, DeleteDefinitionElements API should be used instead. + * This method will fail to delete definition elements. + * + * @param[in] elementIds The element set to delete. Invalid Ids will be ignored. + * @return A DgnElementIdSet of valid element Ids that failed to delete (either vetoed or blocked by FK/code scope constraints). + * @note This function can only be safely invoked from the client thread. + */ DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds); - //! Delete multiple definition elements from this DgnDb. - //! @param[in] elementIds The set of definition elements to delete. Invalid and non-definition element Ids will be ignored. - //! @return A DgnElementIdSet of valid definition element Ids that failed to delete. - //! @note This function can only be safely invoked from the client thread. + /** + * Delete multiple definition elements from this DgnDb. + * + * Definition elements need to be handled as per their usage which makes them a special case for element deletion. + * The handlers for definition elements veto deletion unless a purge operation is enabled. + * Any non-definition element will be ignored and should use the general purpose DeleteElements API instead. + * + * @param[in] elementIds The set of definition elements to delete. Invalid and non-definition element Ids will be ignored. + * @return A DgnElementIdSet of valid definition element Ids that failed to delete (either not DefinitionElements or in use). + * @note This function can only be safely invoked from the client thread. + */ DGNPLATFORM_EXPORT DgnElementIdSet DeleteDefinitionElements(const DgnElementIdSet& elementIds); //! Delete a DgnElement from this DgnDb by DgnElementId. From 76b0f7fcc1d53666de7248dde1835d31f02ea496 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:58:52 +0530 Subject: [PATCH 15/23] Extended API to handle sub model hierarchies --- .../DgnCore/DefinitionElementUsageInfo.cpp | 7 +- .../iModelPlatform/DgnCore/DgnElements.cpp | 507 ++++++++++++------ .../iModelPlatform/DgnCore/DgnModel.cpp | 23 +- .../PublicAPI/DgnPlatform/DgnElement.h | 5 +- .../PublicAPI/DgnPlatform/DgnModel.h | 4 + 5 files changed, 376 insertions(+), 170 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DefinitionElementUsageInfo.cpp b/iModelCore/iModelPlatform/DgnCore/DefinitionElementUsageInfo.cpp index 46ff3d5988..1773da301e 100644 --- a/iModelCore/iModelPlatform/DgnCore/DefinitionElementUsageInfo.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DefinitionElementUsageInfo.cpp @@ -91,7 +91,12 @@ void DefinitionElementUsageInfo::Initialize(BeSQLite::IdSet const& DgnSubCategoryId subCategoryId = subCategory->GetSubCategoryId(); m_subCategoryIds.insert(subCategoryId); if (subCategory->IsDefaultSubCategory()) - m_usedIds.insert(subCategoryId); + { + // If a parent category is being excluded from the check then exclude the default sub category as well. + const bool parentCategoryAlsoBeingExcluded = (m_excludeIds != nullptr && m_excludeIds->find(subCategory->GetCategoryId()) != m_excludeIds->end()); + if (!parentCategoryAlsoBeingExcluded) + m_usedIds.insert(subCategoryId); + } continue; } diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index a557cbf3b3..056fa7d597 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -143,6 +143,16 @@ void DgnElements::DropFromPool(DgnElementCR element) const m_mruCache->DropElement(element.GetElementId()); } +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +void DgnElements::DropFromPool(DgnElementIdSet ids) const + { + BeMutexHolder _v_v(m_mutex); + for (const auto& id : ids) + m_mruCache->DropElement(id); + } + /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ @@ -687,229 +697,396 @@ DgnDbStatus DgnElements::Delete(DgnElementCR elementIn) namespace { - void ExpandElementHierarchy(DgnDbR db, DgnElementIdSet& elementIds) + std::unordered_map fullSetParents; + + DgnElementIdSet ExpandIdSet(DgnDbR db, const DgnElementIdSet& elementIds) { - constexpr auto expandHierarchySql = R"sql( - WITH RECURSIVE - fullDeleteSet(id) AS ( - SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) + constexpr auto expandSql = R"sql( + WITH RECURSIVE fullSet(id, logicalParentId) AS ( + SELECT Id, ParentId FROM bis_Element WHERE InVirtualSet(?, Id) UNION ALL - SELECT e.Id FROM bis_Element e - INNER JOIN fullDeleteSet p ON e.ParentId = p.id + -- Get all the child elements + SELECT e.Id, e.ParentId FROM bis_Element e INNER JOIN fullSet f ON e.ParentId = f.id + UNION ALL + -- Get all the elements in a possible sub model + SELECT e.Id, m.ModeledElementId FROM bis_Element e + INNER JOIN bis_Model m ON m.Id = e.ModelId + INNER JOIN fullSet f ON m.ModeledElementId = f.id ) - SELECT id FROM fullDeleteSet + SELECT id, logicalParentId FROM fullSet )sql"; - Statement expandHierarchyStmt; - if (BE_SQLITE_OK != expandHierarchyStmt.Prepare(db, expandHierarchySql)) - return; + Statement expandStmt; + if (BE_SQLITE_OK != expandStmt.Prepare(db, expandSql)) + return {}; + + expandStmt.BindVirtualSet(1, elementIds); - expandHierarchyStmt.BindVirtualSet(1, elementIds); - while (expandHierarchyStmt.Step() == BE_SQLITE_ROW) - elementIds.insert(expandHierarchyStmt.GetValueId(0)); + DgnElementIdSet expandedIds; + while (BE_SQLITE_ROW == expandStmt.Step()) + { + if (auto id = expandStmt.GetValueId(0); id.IsValid()) + { + expandedIds.insert(id); + fullSetParents[id.GetValueUnchecked()] = expandStmt.GetValueId(1).GetValueUnchecked(); + } + } + return expandedIds; } - DgnElementIdSet FindViolatingCodeScopeIds(DgnDbR db, const DgnElementIdSet& elementIds) + DgnElementIdSet FindViolators(DgnDbR db, const DgnElementIdSet& elementIds) { - constexpr auto violatingCodeScopesSql = R"sql( + constexpr auto violatorSql = R"sql( + -- Find elements that are in a code scope but not in the delete set SELECT DISTINCT CodeScopeId FROM bis_Element - WHERE InVirtualSet(?, CodeScopeId) - AND NOT InVirtualSet(?, Id) + WHERE CodeScopeId IN ( + SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) + ) + AND Id NOT IN ( + SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) + ) )sql"; - Statement violatingCodeScopesStmt; - if (BE_SQLITE_OK != violatingCodeScopesStmt.Prepare(db, violatingCodeScopesSql)) + Statement violatorStmt; + if (BE_SQLITE_OK != violatorStmt.Prepare(db, violatorSql)) return {}; - violatingCodeScopesStmt.BindVirtualSet(1, elementIds); - violatingCodeScopesStmt.BindVirtualSet(2, elementIds); + violatorStmt.BindVirtualSet(1, elementIds); + violatorStmt.BindVirtualSet(2, elementIds); - DgnElementIdSet violatingScopeIds; - while (violatingCodeScopesStmt.Step() == BE_SQLITE_ROW) - violatingScopeIds.insert(violatingCodeScopesStmt.GetValueId(0)); + DgnElementIdSet violators; + while (BE_SQLITE_ROW == violatorStmt.Step()) + { + auto id = violatorStmt.GetValueId(0); + if (id.IsValid()) + violators.insert(id); + } + + return violators; + } + + DgnElementIdSet GetRootsToPrune(DgnDbR db, const DgnElementIdSet& violators) + { + // Find all root elements of the violators that need to be pruned from the delete set + DgnElementIdSet pruneRoots; + for (const auto& violatorId : violators) + { + uint64_t current = violatorId.GetValueUnchecked(); + while (true) + { + auto it = fullSetParents.find(current); + if (it == fullSetParents.end()) + break; + + uint64_t parentVal = it->second; + if (parentVal == 0 || fullSetParents.find(parentVal) == fullSetParents.end()) + { + pruneRoots.insert(DgnElementId(current)); + break; + } + current = parentVal; + } + } - return violatingScopeIds; + return pruneRoots; } - DgnElementIdSet CollectSubtreesToExclude(DgnDbR db, const DgnElementIdSet& violatingScopeIds, const DgnElementIdSet& expandedDeleteSet) + void PruneViolators(DgnDbR db, const DgnElementIdSet& pruneRoots, DgnElementIdSet& elementsToDelete) { - constexpr auto elementSubtreeSql = R"sql( + constexpr auto pruneSql = R"sql( + WITH RECURSIVE pruned(id) AS ( + SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) + UNION ALL + -- Get all the child elements + SELECT e.Id FROM bis_Element e INNER JOIN pruned p ON e.ParentId = p.id + UNION ALL + -- Get all the modeled elements in a possible sub model + SELECT e.Id FROM bis_Element e + INNER JOIN bis_Model m ON m.Id = e.ModelId + INNER JOIN pruned p ON m.ModeledElementId = p.id + ) + SELECT id FROM pruned + )sql"; + + Statement pruneStmt; + if (BE_SQLITE_OK == pruneStmt.Prepare(db, pruneSql)) + { + pruneStmt.BindVirtualSet(1, pruneRoots); + while (BE_SQLITE_ROW == pruneStmt.Step()) + { + auto id = pruneStmt.GetValueId(0); + if (id.IsValid()) + elementsToDelete.erase(id); + } + } + } + + DgnElementIdSet ResolveElementsAfterPossibleVeto(DgnDbR db, const DgnElementIdSet& vetoedElementIds, DgnElementIdSet& failedToDeleteElements) + { + constexpr auto expandVetoedSql = R"sql( WITH RECURSIVE - subtreeRoots(id, parentId) AS ( - SELECT Id, ParentId FROM bis_Element - WHERE InVirtualSet(?, Id) + vetoedSubtree(id) AS ( + SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) UNION ALL - SELECT e.Id, e.ParentId FROM bis_Element e - INNER JOIN subtreeRoots s ON e.Id = s.parentId - WHERE InVirtualSet(?, s.parentId) - ), - subtree(id) AS ( - SELECT id FROM subtreeRoots - WHERE parentId IS NULL OR NOT InVirtualSet(?, parentId) + SELECT e.Id FROM bis_Element e INNER JOIN vetoedSubtree v ON e.ParentId = v.id UNION ALL SELECT e.Id FROM bis_Element e - INNER JOIN subtree s ON e.ParentId = s.id - WHERE InVirtualSet(?, e.Id) + INNER JOIN bis_Model m ON m.Id = e.ModelId + INNER JOIN vetoedSubtree v ON m.ModeledElementId = v.id ) - SELECT id FROM subtree + SELECT id FROM vetoedSubtree )sql"; - Statement elementSubtreeStmt; - if (BE_SQLITE_OK != elementSubtreeStmt.Prepare(db, elementSubtreeSql)) - return {}; + Statement vetoStmt; + DgnElementIdSet finalDeleteSet; + if (BE_SQLITE_OK == vetoStmt.Prepare(db, expandVetoedSql)) + { + vetoStmt.BindVirtualSet(1, vetoedElementIds); + while (BE_SQLITE_ROW == vetoStmt.Step()) + { + auto id = vetoStmt.GetValueId(0); + if (id.IsValid()) + finalDeleteSet.erase(id); + } + } + // Also record the vetoed input elements themselves as having failed. + failedToDeleteElements.insert(vetoedElementIds.begin(), vetoedElementIds.end()); + return finalDeleteSet; + } - elementSubtreeStmt.BindVirtualSet(1, violatingScopeIds); - elementSubtreeStmt.BindVirtualSet(2, expandedDeleteSet); - elementSubtreeStmt.BindVirtualSet(3, expandedDeleteSet); - elementSubtreeStmt.BindVirtualSet(4, expandedDeleteSet); + DgnElementIdSet ExpandAndPruneElementIds(DgnDbR db, const DgnElementIdSet& elementIds, DgnElementIdSet& failedToDeleteElements) + { + // Expand the element IDs to recursively include: + // 1. All children elements + // 2. If the delete set contains a modeled element, then all the elements in the sub model. + auto expandedIds = ExpandIdSet(db, elementIds); + const auto violators = FindViolators(db, expandedIds); + if (!violators.empty()) + { + const auto pruneRoots = GetRootsToPrune(db, violators); + PruneViolators(db, pruneRoots, expandedIds); - DgnElementIdSet subtreeIds; - while (elementSubtreeStmt.Step() == BE_SQLITE_ROW) - subtreeIds.insert(elementSubtreeStmt.GetValueId(0)); + for (const auto& elementId : elementIds) + { + if (expandedIds.find(elementId) == expandedIds.end()) + failedToDeleteElements.insert(elementId); + } + } - return subtreeIds; + return expandedIds; } - DgnElementIdSet ExpandElementHierarchyAndValidateCodeScopes(DgnDbR db, DgnElementIdSet& elementIds) + void SegregateDefinitionElements(DgnDbR db, const DgnElementIdSet& elementIds, DgnElementIdSet& definitionElementIds, DgnElementIdSet& nonDefinitionElementIds) { - // Expand input roots to their full descendant hierarchy to get all affected child elements - ExpandElementHierarchy(db, elementIds); + definitionElementIds.clear(); + nonDefinitionElementIds.clear(); + + // Remove all the non-definition elements from the original delete set. + // Any definition partition elements will get removed as they are not definition elements themselves. + constexpr auto classifySql = R"sql( + SELECT e.ECInstanceId, CASE WHEN d.ECInstanceId IS NULL THEN 0 ELSE 1 END + FROM bis.Element e + LEFT JOIN bis.DefinitionElement d ON d.ECInstanceId = e.ECInstanceId + WHERE InVirtualSet(?, e.ECInstanceId) + )sql"; - // Elements outside the delete set that use a delete-set element as their CodeScope will lead to FK violations. - DgnElementIdSet violatingScopeIds = FindViolatingCodeScopeIds(db, elementIds); - if (violatingScopeIds.empty()) - return {}; + ECSqlStatement classifyStmt; + if (ECSqlStatus::Success != classifyStmt.Prepare(db, classifySql)) + return; - // Walk up from each violating element to its highest ancestor still inside the delete set, - // then collect the full downward subtree from those roots. The entire subtree must be - // excluded from deletion so that code-scope integrity is preserved. - DgnElementIdSet subtreesToExclude = CollectSubtreesToExclude(db, violatingScopeIds, elementIds); + classifyStmt.BindVirtualSet(1, std::make_shared>(BeIdSet(elementIds.GetBeIdSet()))); - // Remove excluded subtrees from the delete set and return them as the failed set. - DgnElementIdSet failedToDeleteElements; - failedToDeleteElements.insert(subtreesToExclude.begin(), subtreesToExclude.end()); - for (const auto& id : subtreesToExclude) - elementIds.erase(id); + while (classifyStmt.Step() == BE_SQLITE_ROW) + { + const auto id = classifyStmt.GetValueId(0); + if (classifyStmt.GetValueInt(1) != 0) + definitionElementIds.insert(id); + else + nonDefinitionElementIds.insert(id); + } + } - return failedToDeleteElements; + bool DeleteLinkTableRelationships(DgnDbR db, const std::vector& tableNames, const DgnElementIdSet& sourceElementIds, const DgnElementIdSet& targetElementIds) + { + for (const auto& table : tableNames) + { + BeAssert(db.TableExists(table.c_str())); + + auto statement = db.GetCachedStatement(Utf8PrintfString("DELETE FROM %s WHERE InVirtualSet(?, SourceId) OR InVirtualSet(?, TargetId)", table.c_str()).c_str()); + if (!statement.IsValid()) + return false; + + statement->BindVirtualSet(1, sourceElementIds); + statement->BindVirtualSet(2, targetElementIds); + if (statement->Step() != BE_SQLITE_DONE) + return false; + } + + return true; } } /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds) +DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, const bool skipIdSetExpansion) { DgnDb::VerifyClientThread(); if (elementIds.empty()) return {}; - DgnElementIdSet validatedElementIds = elementIds; + DgnElementIdSet failedToDeleteElements; + DgnElementIdSet elementsToDelete = elementIds; + if (!skipIdSetExpansion) + elementsToDelete = ExpandAndPruneElementIds(m_dgndb, elementIds, failedToDeleteElements); - // Expand the input set to include all descendants, then detect any elements that must be excluded - // because an external element uses one of them as a CodeScope (deleting would leave a dangling FK). - DgnElementIdSet failedToDeleteElements = ExpandElementHierarchyAndValidateCodeScopes(m_dgndb, validatedElementIds); - if (!failedToDeleteElements.empty()) - LOG.warningv("deleteElements: Skipping elements as they (or their subtrees) contain code scopes for elements outside the delete Id set: %s", failedToDeleteElements.ToString().c_str()); + if (failedToDeleteElements.empty()) + LOG.infov("DeleteElements: All requested element Ids are being deleted as part of the bulk delete operation: %s", elementIds.ToString().c_str()); + else + LOG.warningv("DeleteElements: The following element Ids are not being deleted as part of the bulk delete operation due to external code scope violations: %s", failedToDeleteElements.ToString().c_str()); - // Prep the elements, handlers and the Db for a bulk deletion - SetBulkOperation(true); + std::vector elementsToDeletePtrs; + std::vector subModelsToDelete; + DgnElementIdSet subModelIds; // ModelId = Modeling element's id, so we're good with this + DgnElementIdSet vetoedElementIds; + bool subModelsExist = false; - // Use a scope guard to ensure the DB state is always reset, even on early return. - auto resetDbState = [&]() - { - m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = false"); - SetBulkOperation(false); - }; - - // Call the pre-delete handlers. Remove the elements that get veto'd off the delete list from the handlers. - // We need to save these elements to avoid a re-load when calling the post-delete handlers - std::vector elementsToDelete; - for (const auto& elementId : validatedElementIds) + // Call the pre-delete handlers for the elements from the original delete set and any sub-models that will be deleted. + for (const auto& elementId : elementsToDelete) { const auto element = GetElement(elementId); - // Call the pre-delete handler - if (!element.IsValid() || element->_OnDelete() != DgnDbStatus::Success) + if (!element.IsValid()) + { + vetoedElementIds.insert(elementId); + continue; + } + + // Sub-Model deletion should trigger callback even if they are being deleted implicitly + DgnModelPtr subModel = element->GetSubModel(); + if (subModel.IsValid()) + { + subModelsExist = true; + if (element->_OnSubModelDelete(*subModel) != DgnDbStatus::Success) + { + vetoedElementIds.insert(elementId); + continue; + } + + if (subModel->_OnDeleteNotify() != DgnDbStatus::Success) + { + vetoedElementIds.insert(elementId); + continue; + } + subModelsToDelete.emplace_back(subModel); + subModelIds.insert(DgnElementId(subModel->GetModelId().GetValueUnchecked())); + } + + if (elementIds.find(elementId) == elementIds.end()) + continue; + + // Call the element's own pre-delete handler. + if (element->_OnDelete() != DgnDbStatus::Success) + { + vetoedElementIds.insert(elementId); continue; + } - // Ask the parent if it's okay to delete the child. - // Also, skip parent callback if the parent itself is also being deleted. + // Ask the parent whether it is ok to delete this child. + // Skip the parent callback when the parent itself is also in the caller's input set auto parent = GetElement(element->m_parent.m_id); if (parent.IsValid() && - validatedElementIds.find(element->m_parent.m_id) == validatedElementIds.end() && + elementsToDelete.find(element->m_parent.m_id) == elementsToDelete.end() && parent->_OnChildDelete(*element) != DgnDbStatus::Success) + { + vetoedElementIds.insert(elementId); continue; + } - elementsToDelete.push_back(element); + elementsToDeletePtrs.push_back(element); } - // Rebuild validatedElementIds from only the elements that passed all pre-delete checks. - validatedElementIds.clear(); - for (const auto& element : elementsToDelete) - validatedElementIds.insert(element->GetElementId()); + if (!vetoedElementIds.empty()) + elementsToDelete = ResolveElementsAfterPossibleVeto(m_dgndb, vetoedElementIds, failedToDeleteElements); - // Since we have already handled all the external code scope violations, + // Prep the elements and handlers and the Db for a bulk deletion + SetBulkOperation(true); + + // Since we have already handled all the external code scope violations and sub model deletes, // defer the FK integrity check for all the intra set violations as all of them are being deleted anyway m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = true"); - // Delete the elements + auto resetDbState = [&]() + { + m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = false"); + SetBulkOperation(false); + }; + + // Delete the elements in a single delete sql statement auto statement = GetStatement("DELETE FROM " BIS_TABLE(BIS_CLASS_Element) " WHERE InVirtualSet(?, Id)"); - statement->BindVirtualSet(1, validatedElementIds); + statement->BindVirtualSet(1, elementsToDelete); if (statement->Step() != BE_SQLITE_DONE) { - LOG.errorv("deleteElements: Failed to delete element Ids from database: %s", validatedElementIds.ToString().c_str()); - failedToDeleteElements.insert(validatedElementIds.begin(), validatedElementIds.end()); + LOG.errorv("DeleteElements: Failed to delete element Ids from database: %s", elementsToDelete.ToString().c_str()); + failedToDeleteElements.insert(elementsToDelete.begin(), elementsToDelete.end()); resetDbState(); return failedToDeleteElements; } + // Clear out the elements from the cache + DropFromPool(elementsToDelete); + // Clear up the link-table relationships in bulk - if (!DeleteLinkTableRelationships(m_dgndb, validatedElementIds)) + if (!DeleteLinkTableRelationships(m_dgndb, { BIS_TABLE(BIS_REL_ElementRefersToElements), BIS_TABLE(BIS_REL_ElementDrivesElement) }, elementsToDelete, elementsToDelete)) { - LOG.errorv("deleteElements: Failed to delete link table relationships for element Ids: %s", validatedElementIds.ToString().c_str()); - failedToDeleteElements.insert(validatedElementIds.begin(), validatedElementIds.end()); + LOG.errorv("DeleteElements: Failed to delete link table relationships for element Ids: %s", elementsToDelete.ToString().c_str()); + failedToDeleteElements.insert(elementsToDelete.begin(), elementsToDelete.end()); resetDbState(); return failedToDeleteElements; } - // Call the post delete handlers - std::for_each(elementsToDelete.begin(), elementsToDelete.end(), [&](const DgnElementCPtr& element) + // If any modeling elements have been deleted, clear out the model table entries as well + if (!subModelsToDelete.empty()) + { + auto modelDeleteStmt = GetStatement("DELETE FROM " BIS_TABLE(BIS_CLASS_Model) " WHERE InVirtualSet(?, Id)"); + modelDeleteStmt->BindVirtualSet(1, subModelIds); + if (modelDeleteStmt->Step() != BE_SQLITE_DONE) + LOG.errorv("DeleteElements: Failed to delete sub-model rows for element Ids: %s", subModelIds.ToString().c_str()); + return failedToDeleteElements; + } + + // Call the post delete handlers for the deleted elements + for (const auto& element : elementsToDeletePtrs) { element->_OnDeleted(); - // Notify parent only if it is not itself being deleted - if (element->m_parent.m_id.IsValid() && validatedElementIds.find(element->m_parent.m_id) == validatedElementIds.end()) + + if (element->m_parent.m_id.IsValid() && elementIds.find(element->m_parent.m_id) == elementIds.end()) { auto parent = GetElement(element->m_parent.m_id); if (parent.IsValid()) parent->_OnChildDeleted(*element); } - }); - resetDbState(); + } - if (!failedToDeleteElements.empty()) - LOG.errorv("deleteElements: Failed to delete element Ids: %s", failedToDeleteElements.ToString().c_str()); - return failedToDeleteElements; - } + // Call the post delete handlers for the deleted sub-models + for (const auto& model : subModelsToDelete) + model->_OnDeleted(); -/* static */ -bool DgnElements::DeleteLinkTableRelationships(DgnDbR db, const DgnElementIdSet& elementIds) - { - for (const auto& table : { BIS_TABLE(BIS_REL_ElementRefersToElements), BIS_TABLE(BIS_REL_ElementDrivesElement) }) + // Clear up the model's link-table relationships in bulk + if (!DeleteLinkTableRelationships(m_dgndb, { BIS_TABLE(BIS_REL_ModelSelectorRefersToModels) }, DgnElementIdSet() /* all ModelSelectors */, subModelIds)) // replicate former foreign key behavior { - BeAssert(db.TableExists(table)); - - auto statement = db.GetCachedStatement(Utf8PrintfString("DELETE FROM %s WHERE InVirtualSet(?, SourceId) OR InVirtualSet(?, TargetId)", table).c_str()); - if (!statement.IsValid()) - return false; - - statement->BindVirtualSet(1, elementIds); - statement->BindVirtualSet(2, elementIds); - if (statement->Step() != BE_SQLITE_DONE) - return false; + LOG.errorv("DeleteElements: Failed to delete link table relationships for model Ids: %s", subModelIds.ToString().c_str()); + failedToDeleteElements.insert(subModelIds.begin(), subModelIds.end()); + resetDbState(); + return failedToDeleteElements; } - return true; + resetDbState(); + + if (!failedToDeleteElements.empty()) + LOG.errorv("DeleteElements: Failed to delete element Ids: %s", failedToDeleteElements.ToString().c_str()); + + return failedToDeleteElements; } /*---------------------------------------------------------------------------------**//** @@ -925,44 +1102,33 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele DgnElementIdSet definitionElementIds; DgnElementIdSet nonDefinitionElementIds; - constexpr auto classifySql = R"sql( - SELECT e.ECInstanceId, CASE WHEN d.ECInstanceId IS NULL THEN 0 ELSE 1 END - FROM bis.Element e - LEFT JOIN bis.DefinitionElement d ON d.ECInstanceId = e.ECInstanceId - WHERE InVirtualSet(?, e.ECInstanceId) - )sql"; + SegregateDefinitionElements(m_dgndb, elementIds, definitionElementIds, nonDefinitionElementIds); - ECSqlStatement classifyStmt; - if (ECSqlStatus::Success != classifyStmt.Prepare(m_dgndb, classifySql)) - return elementIds; + if (!nonDefinitionElementIds.empty()) + LOG.warningv("DeleteDefinitionElements: Element Ids %s are not DefinitionElements and cannot be deleted with this API.", nonDefinitionElementIds.ToString().c_str()); - classifyStmt.BindVirtualSet(1, std::make_shared>(BeIdSet(elementIds.GetBeIdSet()))); + if (definitionElementIds.empty()) + return nonDefinitionElementIds.empty() ? elementIds : DgnElementIdSet(); - while (classifyStmt.Step() == BE_SQLITE_ROW) - { - const auto id = classifyStmt.GetValueId(0); - if (classifyStmt.GetValueInt(1) != 0) - definitionElementIds.insert(id); - else - nonDefinitionElementIds.insert(id); - } + DgnElementIdSet failedToDeleteElements; + // Expand the set of definition element IDs to include all related elements so implicitly added element's usage can be verified. + const DgnElementIdSet expandedIds = ExpandAndPruneElementIds(m_dgndb, definitionElementIds, failedToDeleteElements); - if (!nonDefinitionElementIds.empty()) - LOG.warningv("deleteDefinitionElements: Element Ids %s are not DefinitionElements and cannot be deleted with this API.", nonDefinitionElementIds.ToString().c_str()); + if (!failedToDeleteElements.empty()) + LOG.errorv("DeleteDefinitionElements: Failed to delete definition element Ids: %s", failedToDeleteElements.ToString().c_str()); - if (definitionElementIds.empty()) - return {}; + DgnElementIdSet toBeDeleted; + // After expansion, we need to re-classify into definition vs non-definition elements. + // DefinitionElementUsageInfo::Create skips non-definition elements, so we must separate them out and mark them for deletion directly. + SegregateDefinitionElements(m_dgndb, expandedIds, definitionElementIds, toBeDeleted); - // Get the usage info for all the elements except for the ones in elementIds. - // This will give us all the dependencies that exist outside the user supplied set. - // For any intra set dependencies, since we are bulk deleting, they will all get deleted anyway. + // Get the usage info for all definition elements in the expanded set, excluding intra-set usages. + // For any intra-set dependencies, since we are bulk deleting, they will all get deleted anyway. auto usageInfo = DefinitionElementUsageInfo::Create(m_dgndb, definitionElementIds, std::make_shared>(BeIdSet(definitionElementIds.GetBeIdSet()))); if (!usageInfo.IsValid()) return definitionElementIds; - DgnElementIdSet toBeDeleted; DgnElementIdSet cannotBeDeleted; - for (const auto& elementId : definitionElementIds) { if (usageInfo->GetUsedIds().Contains(elementId)) @@ -971,21 +1137,38 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele toBeDeleted.insert(elementId); } + // It might happen that a parent element is in use, but the child element might not be. + // We need to scan the descendants of each blocked element and mark them as blocked too. + if (!cannotBeDeleted.empty()) + { + DgnElementIdSet blockedDescendants = toBeDeleted; + PruneViolators(m_dgndb, cannotBeDeleted, blockedDescendants); + // blockedDescendants now contains only the elements NOT affected by the cannotBeDeleted roots. + for (const auto& elementId : toBeDeleted) + { + // The element was pruned from the list, mark it as cannot be deleted. + if (blockedDescendants.find(elementId) == blockedDescendants.end()) + cannotBeDeleted.insert(elementId); + } + // Now we have a fresh list of elements of which they themselves and their descendants are not in use. + toBeDeleted = std::move(blockedDescendants); + } + if (toBeDeleted.empty()) { if (!cannotBeDeleted.empty()) - LOG.warningv("deleteDefinitionElements: Skipping element Ids that are in use: %s", cannotBeDeleted.ToString().c_str()); + LOG.warningv("DeleteDefinitionElements: Skipping element Ids that are in use: %s", cannotBeDeleted.ToString().c_str()); return cannotBeDeleted; } m_dgndb.BeginPurgeOperation(); - const auto failedToDeleteIds = DeleteElements(toBeDeleted); + const auto failedToDeleteIds = DeleteElements(toBeDeleted, true); m_dgndb.EndPurgeOperation(); cannotBeDeleted.insert(failedToDeleteIds.begin(), failedToDeleteIds.end()); if (!cannotBeDeleted.empty()) - LOG.warningv("deleteDefinitionElements: Skipping element Ids that are in use or blocked: %s", cannotBeDeleted.ToString().c_str()); + LOG.warningv("DeleteDefinitionElements: Skipping element Ids that are in use or blocked: %s", cannotBeDeleted.ToString().c_str()); return cannotBeDeleted; } diff --git a/iModelCore/iModelPlatform/DgnCore/DgnModel.cpp b/iModelCore/iModelPlatform/DgnCore/DgnModel.cpp index 16b039266e..5a30607f65 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnModel.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnModel.cpp @@ -1235,13 +1235,23 @@ DgnDbStatus DgnModel::_OnUpdateElement(DgnElementCR modified, DgnElementCR origi /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -DgnDbStatus DgnModel::_OnDelete() { +DgnDbStatus DgnModel::_OnDeleteNotify() { ModelHandlerR modelHandler = GetModelHandler(); if (modelHandler.GetDomain().IsReadonly()) return DgnDbStatus::ReadOnlyDomain; CallJsPostHandler("onDelete"); NotifyAppData([](AppData& handler, DgnModelR model) { handler._OnDelete(model); }); + return DgnDbStatus::Success; +} + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +DgnDbStatus DgnModel::_OnDelete() { + DgnDbStatus stat = _OnDeleteNotify(); + if (DgnDbStatus::Success != stat) + return stat; // before we can delete a model, we must delete all of its elements. If that fails, we cannot continue. Statement stmt(m_dgndb, "SELECT Id FROM " BIS_TABLE(BIS_CLASS_Element) " WHERE ModelId=?"); @@ -1256,13 +1266,11 @@ DgnDbStatus DgnModel::_OnDelete() { } // Note: this may look dangerous (deleting an element in the model we're iterating), but is is actually safe in SQLite. - auto stat = el->Delete(); + stat = el->Delete(); if (DgnDbStatus::Success != stat) return stat; } - BeAssert(GetRefCount() > 1); - m_dgndb.Models().DropLoadedModel(*this); return DgnDbStatus::Success; } @@ -1276,7 +1284,12 @@ struct DeletedCaller { void DgnModel::_OnDeleted() { CallJsPostHandler("onDeleted"); CallAppData(DeletedCaller()); - GetDgnDb().DeleteLinkTableRelationships(BIS_SCHEMA(BIS_REL_ModelSelectorRefersToModels), DgnElementId() /* all ModelSelectors */, GetModeledElementId()); // replicate former foreign key behavior + + if (!GetDgnDb().Elements().IsBulkOperation()) + GetDgnDb().DeleteLinkTableRelationships(BIS_SCHEMA(BIS_REL_ModelSelectorRefersToModels), DgnElementId() /* all ModelSelectors */, GetModeledElementId()); // replicate former foreign key behavior + + BeAssert(GetRefCount() > 1); + m_dgndb.Models().DropLoadedModel(*this); } /*---------------------------------------------------------------------------------**//** diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index c77a9fe8bd..dd1f6df97f 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -3911,7 +3911,6 @@ struct DgnElements : DgnDbTable void SetBulkOperation(const bool isBulk) { m_isBulkOperation = isBulk; } bool IsBulkOperation() const { return m_isBulkOperation; } - static bool DeleteLinkTableRelationships(DgnDbR db, const DgnElementIdSet& elementIds); public: DGNPLATFORM_EXPORT BeSQLite::SnappyFromMemory& GetSnappyFrom() {return m_snappyFrom;} // NB: Not to be used during loading of a GeometricElement or GeometryPart! @@ -3931,6 +3930,7 @@ struct DgnElements : DgnDbTable DGNPLATFORM_EXPORT BeSQLite::CachedStatementPtr GetStatement(Utf8CP sql) const; //!< Get a statement from the element-specific statement cache for this DgnDb @private DGNPLATFORM_EXPORT void DropFromPool(DgnElementCR) const; //!< @private + DGNPLATFORM_EXPORT void DropFromPool(DgnElementIdSet) const; //!< @private DGNPLATFORM_EXPORT DgnDbStatus LoadGeometryStream(GeometryStreamR geom, void const* blob, int blobSize); //!< @private DGNPLATFORM_EXPORT bool ElementExists(DgnElementId); @@ -4046,10 +4046,11 @@ struct DgnElements : DgnDbTable * This method will fail to delete definition elements. * * @param[in] elementIds The element set to delete. Invalid Ids will be ignored. + * @param[in] skipIdSetExpansion The elementIds set will not be expanded to include all descendants or sub-models. Defaults to false. * @return A DgnElementIdSet of valid element Ids that failed to delete (either vetoed or blocked by FK/code scope constraints). * @note This function can only be safely invoked from the client thread. */ - DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds); + DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds, const bool skipIdSetExpansion = false); /** * Delete multiple definition elements from this DgnDb. diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnModel.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnModel.h index 29c28dae9e..79289131bf 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnModel.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnModel.h @@ -371,6 +371,10 @@ struct EXPORT_VTABLE_ATTRIBUTE DgnModel : RefCountedBase //! @note If you override this method, you @em must call the T_Super implementation, forwarding its status. DGNPLATFORM_EXPORT virtual DgnDbStatus _OnDelete(); + //! Handles on deletion notifications only. + //! @note Do not override this method. Override _OnDelete instead. + DGNPLATFORM_EXPORT DgnDbStatus _OnDeleteNotify(); + //! Called after this DgnModel was loaded from the DgnDb. //! @note If you override this method, you @em must call the T_Super implementation. DGNPLATFORM_EXPORT virtual void _OnLoaded(); From b52aeb94b0d51333b18c0a915d2eab1b25e48364 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:41:51 +0530 Subject: [PATCH 16/23] Cleaned up API --- .../iModelPlatform/DgnCore/DgnElements.cpp | 111 ++++++++++++------ .../PublicAPI/DgnPlatform/DgnElement.h | 7 +- 2 files changed, 78 insertions(+), 40 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index 056fa7d597..22b879ef00 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -697,9 +697,13 @@ DgnDbStatus DgnElements::Delete(DgnElementCR elementIn) namespace { - std::unordered_map fullSetParents; + struct ExpandedElementSet + { + DgnElementIdSet ids; + std::unordered_map logicalParents; // might be a parent element or a modeled element + }; - DgnElementIdSet ExpandIdSet(DgnDbR db, const DgnElementIdSet& elementIds) + ExpandedElementSet ExpandIdSet(DgnDbR db, const DgnElementIdSet& elementIds) { constexpr auto expandSql = R"sql( WITH RECURSIVE fullSet(id, logicalParentId) AS ( @@ -722,16 +726,16 @@ namespace expandStmt.BindVirtualSet(1, elementIds); - DgnElementIdSet expandedIds; + ExpandedElementSet result; while (BE_SQLITE_ROW == expandStmt.Step()) { if (auto id = expandStmt.GetValueId(0); id.IsValid()) { - expandedIds.insert(id); - fullSetParents[id.GetValueUnchecked()] = expandStmt.GetValueId(1).GetValueUnchecked(); + result.ids.insert(id); + result.logicalParents[id.GetValueUnchecked()] = expandStmt.GetValueId(1).GetValueUnchecked(); } } - return expandedIds; + return result; } DgnElementIdSet FindViolators(DgnDbR db, const DgnElementIdSet& elementIds) @@ -765,7 +769,7 @@ namespace return violators; } - DgnElementIdSet GetRootsToPrune(DgnDbR db, const DgnElementIdSet& violators) + DgnElementIdSet GetRootsToPrune(DgnDbR db, const DgnElementIdSet& violators, const std::unordered_map& logicalParents) { // Find all root elements of the violators that need to be pruned from the delete set DgnElementIdSet pruneRoots; @@ -774,12 +778,12 @@ namespace uint64_t current = violatorId.GetValueUnchecked(); while (true) { - auto it = fullSetParents.find(current); - if (it == fullSetParents.end()) + auto it = logicalParents.find(current); + if (it == logicalParents.end()) break; uint64_t parentVal = it->second; - if (parentVal == 0 || fullSetParents.find(parentVal) == fullSetParents.end()) + if (parentVal == 0 || logicalParents.find(parentVal) == logicalParents.end()) { pruneRoots.insert(DgnElementId(current)); break; @@ -859,21 +863,21 @@ namespace // Expand the element IDs to recursively include: // 1. All children elements // 2. If the delete set contains a modeled element, then all the elements in the sub model. - auto expandedIds = ExpandIdSet(db, elementIds); - const auto violators = FindViolators(db, expandedIds); + auto expanded = ExpandIdSet(db, elementIds); + const auto violators = FindViolators(db, expanded.ids); if (!violators.empty()) { - const auto pruneRoots = GetRootsToPrune(db, violators); - PruneViolators(db, pruneRoots, expandedIds); + const auto pruneRoots = GetRootsToPrune(db, violators, expanded.logicalParents); + PruneViolators(db, pruneRoots, expanded.ids); for (const auto& elementId : elementIds) { - if (expandedIds.find(elementId) == expandedIds.end()) + if (expanded.ids.find(elementId) == expanded.ids.end()) failedToDeleteElements.insert(elementId); } } - return expandedIds; + return expanded.ids; } void SegregateDefinitionElements(DgnDbR db, const DgnElementIdSet& elementIds, DgnElementIdSet& definitionElementIds, DgnElementIdSet& nonDefinitionElementIds) @@ -906,6 +910,37 @@ namespace } } + // Expand a subtree rooted at a definition element that is in use and cannot be deleted. + void ExpandBlockedSubtrees(DgnDbR db, const DgnElementIdSet& blockedRoots, DgnElementIdSet& candidateSet, DgnElementIdSet& blockedSet) + { + constexpr auto expandBlockedSql = R"sql( + WITH RECURSIVE blockedSubtree(id) AS ( + SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) + UNION ALL + SELECT e.Id FROM bis_Element e INNER JOIN blockedSubtree b ON e.ParentId = b.id + UNION ALL + SELECT e.Id FROM bis_Element e + INNER JOIN bis_Model m ON m.Id = e.ModelId + INNER JOIN blockedSubtree b ON m.ModeledElementId = b.id + ) + SELECT id FROM blockedSubtree + )sql"; + + Statement stmt; + if (BE_SQLITE_OK != stmt.Prepare(db, expandBlockedSql)) + return; + + stmt.BindVirtualSet(1, blockedRoots); + while (BE_SQLITE_ROW == stmt.Step()) + { + auto id = stmt.GetValueId(0); + if (!id.IsValid()) + continue; + if (candidateSet.erase(id) > 0) + blockedSet.insert(id); + } + } + bool DeleteLinkTableRelationships(DgnDbR db, const std::vector& tableNames, const DgnElementIdSet& sourceElementIds, const DgnElementIdSet& targetElementIds) { for (const auto& table : tableNames) @@ -929,7 +964,7 @@ namespace /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, const bool skipIdSetExpansion) +DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds) { DgnDb::VerifyClientThread(); @@ -937,15 +972,26 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c return {}; DgnElementIdSet failedToDeleteElements; - DgnElementIdSet elementsToDelete = elementIds; - if (!skipIdSetExpansion) - elementsToDelete = ExpandAndPruneElementIds(m_dgndb, elementIds, failedToDeleteElements); + const DgnElementIdSet elementsToDelete = ExpandAndPruneElementIds(m_dgndb, elementIds, failedToDeleteElements); if (failedToDeleteElements.empty()) LOG.infov("DeleteElements: All requested element Ids are being deleted as part of the bulk delete operation: %s", elementIds.ToString().c_str()); else LOG.warningv("DeleteElements: The following element Ids are not being deleted as part of the bulk delete operation due to external code scope violations: %s", failedToDeleteElements.ToString().c_str()); + const auto additionalFailures = DeleteElementsPreExpanded(elementsToDelete, elementIds); + failedToDeleteElements.insert(additionalFailures.begin(), additionalFailures.end()); + return failedToDeleteElements; + } + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +DgnElementIdSet DgnElements::DeleteElementsPreExpanded(const DgnElementIdSet& expandedElementIdSet, const DgnElementIdSet& originalElementIdSet) + { + // Work on a mutable copy so veto resolution can shrink the set without modifying the caller's data. + DgnElementIdSet elementsToDelete = expandedElementIdSet; + std::vector elementsToDeletePtrs; std::vector subModelsToDelete; DgnElementIdSet subModelIds; // ModelId = Modeling element's id, so we're good with this @@ -982,7 +1028,7 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c subModelIds.insert(DgnElementId(subModel->GetModelId().GetValueUnchecked())); } - if (elementIds.find(elementId) == elementIds.end()) + if (originalElementIdSet.find(elementId) == originalElementIdSet.end()) continue; // Call the element's own pre-delete handler. @@ -1006,6 +1052,7 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c elementsToDeletePtrs.push_back(element); } + DgnElementIdSet failedToDeleteElements; if (!vetoedElementIds.empty()) elementsToDelete = ResolveElementsAfterPossibleVeto(m_dgndb, vetoedElementIds, failedToDeleteElements); @@ -1060,7 +1107,7 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, c { element->_OnDeleted(); - if (element->m_parent.m_id.IsValid() && elementIds.find(element->m_parent.m_id) == elementIds.end()) + if (element->m_parent.m_id.IsValid() && originalElementIdSet.find(element->m_parent.m_id) == originalElementIdSet.end()) { auto parent = GetElement(element->m_parent.m_id); if (parent.IsValid()) @@ -1137,22 +1184,10 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele toBeDeleted.insert(elementId); } - // It might happen that a parent element is in use, but the child element might not be. - // We need to scan the descendants of each blocked element and mark them as blocked too. + // It might happen that a parent element is in use, but its structural descendants might not be. + // We need to ensure that all descendants are marked for deletion as well. if (!cannotBeDeleted.empty()) - { - DgnElementIdSet blockedDescendants = toBeDeleted; - PruneViolators(m_dgndb, cannotBeDeleted, blockedDescendants); - // blockedDescendants now contains only the elements NOT affected by the cannotBeDeleted roots. - for (const auto& elementId : toBeDeleted) - { - // The element was pruned from the list, mark it as cannot be deleted. - if (blockedDescendants.find(elementId) == blockedDescendants.end()) - cannotBeDeleted.insert(elementId); - } - // Now we have a fresh list of elements of which they themselves and their descendants are not in use. - toBeDeleted = std::move(blockedDescendants); - } + ExpandBlockedSubtrees(m_dgndb, cannotBeDeleted, toBeDeleted, cannotBeDeleted); if (toBeDeleted.empty()) { @@ -1162,7 +1197,7 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele } m_dgndb.BeginPurgeOperation(); - const auto failedToDeleteIds = DeleteElements(toBeDeleted, true); + const auto failedToDeleteIds = DeleteElementsPreExpanded(toBeDeleted, toBeDeleted); m_dgndb.EndPurgeOperation(); cannotBeDeleted.insert(failedToDeleteIds.begin(), failedToDeleteIds.end()); diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index dd1f6df97f..2680b4111b 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -4046,11 +4046,10 @@ struct DgnElements : DgnDbTable * This method will fail to delete definition elements. * * @param[in] elementIds The element set to delete. Invalid Ids will be ignored. - * @param[in] skipIdSetExpansion The elementIds set will not be expanded to include all descendants or sub-models. Defaults to false. * @return A DgnElementIdSet of valid element Ids that failed to delete (either vetoed or blocked by FK/code scope constraints). * @note This function can only be safely invoked from the client thread. */ - DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds, const bool skipIdSetExpansion = false); + DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds); /** * Delete multiple definition elements from this DgnDb. @@ -4065,6 +4064,10 @@ struct DgnElements : DgnDbTable */ DGNPLATFORM_EXPORT DgnElementIdSet DeleteDefinitionElements(const DgnElementIdSet& elementIds); +private: + DgnElementIdSet DeleteElementsPreExpanded(const DgnElementIdSet& expandedElementIdSet, const DgnElementIdSet& originalElementIdSet); +public: + //! Delete a DgnElement from this DgnDb by DgnElementId. //! @return DgnDbStatus::Success if the element was deleted, error status otherwise. //! @note This method is merely a shortcut to #GetElement and then #Delete From 407f7af7d0f45e3cd39b7aa9b554a9f3c8022ef7 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:10:15 +0530 Subject: [PATCH 17/23] Fixed a bug --- iModelCore/iModelPlatform/DgnCore/DgnElements.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index 22b879ef00..38d22e38d6 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -1098,8 +1098,10 @@ DgnElementIdSet DgnElements::DeleteElementsPreExpanded(const DgnElementIdSet& ex auto modelDeleteStmt = GetStatement("DELETE FROM " BIS_TABLE(BIS_CLASS_Model) " WHERE InVirtualSet(?, Id)"); modelDeleteStmt->BindVirtualSet(1, subModelIds); if (modelDeleteStmt->Step() != BE_SQLITE_DONE) + { LOG.errorv("DeleteElements: Failed to delete sub-model rows for element Ids: %s", subModelIds.ToString().c_str()); - return failedToDeleteElements; + return failedToDeleteElements; + } } // Call the post delete handlers for the deleted elements From 47cce7203ecd4366136e4df777bb827406b280bf Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:24:27 +0530 Subject: [PATCH 18/23] Some bug fixes --- .../iModelPlatform/DgnCore/DgnElements.cpp | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index 38d22e38d6..f7f0918b5f 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -858,7 +858,7 @@ namespace return finalDeleteSet; } - DgnElementIdSet ExpandAndPruneElementIds(DgnDbR db, const DgnElementIdSet& elementIds, DgnElementIdSet& failedToDeleteElements) + ExpandedElementSet ExpandAndPruneElementIds(DgnDbR db, const DgnElementIdSet& elementIds, DgnElementIdSet& failedToDeleteElements) { // Expand the element IDs to recursively include: // 1. All children elements @@ -877,7 +877,7 @@ namespace } } - return expanded.ids; + return expanded; } void SegregateDefinitionElements(DgnDbR db, const DgnElementIdSet& elementIds, DgnElementIdSet& definitionElementIds, DgnElementIdSet& nonDefinitionElementIds) @@ -972,14 +972,14 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds) return {}; DgnElementIdSet failedToDeleteElements; - const DgnElementIdSet elementsToDelete = ExpandAndPruneElementIds(m_dgndb, elementIds, failedToDeleteElements); + const auto expandedIds = ExpandAndPruneElementIds(m_dgndb, elementIds, failedToDeleteElements); if (failedToDeleteElements.empty()) LOG.infov("DeleteElements: All requested element Ids are being deleted as part of the bulk delete operation: %s", elementIds.ToString().c_str()); else LOG.warningv("DeleteElements: The following element Ids are not being deleted as part of the bulk delete operation due to external code scope violations: %s", failedToDeleteElements.ToString().c_str()); - const auto additionalFailures = DeleteElementsPreExpanded(elementsToDelete, elementIds); + const auto additionalFailures = DeleteElementsPreExpanded(expandedIds.ids, elementIds); failedToDeleteElements.insert(additionalFailures.begin(), additionalFailures.end()); return failedToDeleteElements; } @@ -996,7 +996,6 @@ DgnElementIdSet DgnElements::DeleteElementsPreExpanded(const DgnElementIdSet& ex std::vector subModelsToDelete; DgnElementIdSet subModelIds; // ModelId = Modeling element's id, so we're good with this DgnElementIdSet vetoedElementIds; - bool subModelsExist = false; // Call the pre-delete handlers for the elements from the original delete set and any sub-models that will be deleted. for (const auto& elementId : elementsToDelete) @@ -1012,7 +1011,6 @@ DgnElementIdSet DgnElements::DeleteElementsPreExpanded(const DgnElementIdSet& ex DgnModelPtr subModel = element->GetSubModel(); if (subModel.IsValid()) { - subModelsExist = true; if (element->_OnSubModelDelete(*subModel) != DgnDbStatus::Success) { vetoedElementIds.insert(elementId); @@ -1161,7 +1159,7 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele DgnElementIdSet failedToDeleteElements; // Expand the set of definition element IDs to include all related elements so implicitly added element's usage can be verified. - const DgnElementIdSet expandedIds = ExpandAndPruneElementIds(m_dgndb, definitionElementIds, failedToDeleteElements); + const ExpandedElementSet expanded = ExpandAndPruneElementIds(m_dgndb, definitionElementIds, failedToDeleteElements); if (!failedToDeleteElements.empty()) LOG.errorv("DeleteDefinitionElements: Failed to delete definition element Ids: %s", failedToDeleteElements.ToString().c_str()); @@ -1169,7 +1167,7 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele DgnElementIdSet toBeDeleted; // After expansion, we need to re-classify into definition vs non-definition elements. // DefinitionElementUsageInfo::Create skips non-definition elements, so we must separate them out and mark them for deletion directly. - SegregateDefinitionElements(m_dgndb, expandedIds, definitionElementIds, toBeDeleted); + SegregateDefinitionElements(m_dgndb, expanded.ids, definitionElementIds, toBeDeleted); // Get the usage info for all definition elements in the expanded set, excluding intra-set usages. // For any intra-set dependencies, since we are bulk deleting, they will all get deleted anyway. @@ -1186,28 +1184,33 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele toBeDeleted.insert(elementId); } - // It might happen that a parent element is in use, but its structural descendants might not be. - // We need to ensure that all descendants are marked for deletion as well. + // It might happen that a parent element is in use, but its structural descendants might not be and vice versa. + // We need to ensure that all affected are marked for deletion as well. if (!cannotBeDeleted.empty()) - ExpandBlockedSubtrees(m_dgndb, cannotBeDeleted, toBeDeleted, cannotBeDeleted); + { + const auto blockedRoots = GetRootsToPrune(m_dgndb, cannotBeDeleted, expanded.logicalParents); + ExpandBlockedSubtrees(m_dgndb, blockedRoots, toBeDeleted, cannotBeDeleted); + } if (toBeDeleted.empty()) { if (!cannotBeDeleted.empty()) LOG.warningv("DeleteDefinitionElements: Skipping element Ids that are in use: %s", cannotBeDeleted.ToString().c_str()); - return cannotBeDeleted; + failedToDeleteElements.insert(cannotBeDeleted.begin(), cannotBeDeleted.end()); + return failedToDeleteElements; } m_dgndb.BeginPurgeOperation(); - const auto failedToDeleteIds = DeleteElementsPreExpanded(toBeDeleted, toBeDeleted); + const auto failedToDelete = DeleteElementsPreExpanded(toBeDeleted, toBeDeleted); m_dgndb.EndPurgeOperation(); - cannotBeDeleted.insert(failedToDeleteIds.begin(), failedToDeleteIds.end()); + failedToDeleteElements.insert(cannotBeDeleted.begin(), cannotBeDeleted.end()); + failedToDeleteElements.insert(failedToDelete.begin(), failedToDelete.end()); - if (!cannotBeDeleted.empty()) - LOG.warningv("DeleteDefinitionElements: Skipping element Ids that are in use or blocked: %s", cannotBeDeleted.ToString().c_str()); + if (!failedToDeleteElements.empty()) + LOG.warningv("DeleteDefinitionElements: Skipping element Ids that are in use or blocked: %s", failedToDeleteElements.ToString().c_str()); - return cannotBeDeleted; + return failedToDeleteElements; } //--------------------------------------------------------------------------------------- From a25f38833cfad33bb646055679331424324dff2c Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:57:56 +0530 Subject: [PATCH 19/23] Renamed functions to be clear of intent --- .../iModelPlatform/DgnCore/DgnElements.cpp | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index f7f0918b5f..5ef4cc1b78 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -769,10 +769,10 @@ namespace return violators; } - DgnElementIdSet GetRootsToPrune(DgnDbR db, const DgnElementIdSet& violators, const std::unordered_map& logicalParents) + DgnElementIdSet GetRootsToIgnore(DgnDbR db, const DgnElementIdSet& violators, const std::unordered_map& logicalParents) { - // Find all root elements of the violators that need to be pruned from the delete set - DgnElementIdSet pruneRoots; + // Find all root elements of the violators that need to be ignored from the delete set + DgnElementIdSet ignoreRoots; for (const auto& violatorId : violators) { uint64_t current = violatorId.GetValueUnchecked(); @@ -785,40 +785,40 @@ namespace uint64_t parentVal = it->second; if (parentVal == 0 || logicalParents.find(parentVal) == logicalParents.end()) { - pruneRoots.insert(DgnElementId(current)); + ignoreRoots.insert(DgnElementId(current)); break; } current = parentVal; } } - return pruneRoots; + return ignoreRoots; } - void PruneViolators(DgnDbR db, const DgnElementIdSet& pruneRoots, DgnElementIdSet& elementsToDelete) + void IgnoreViolators(DgnDbR db, const DgnElementIdSet& ignoreRoots, DgnElementIdSet& elementsToDelete) { - constexpr auto pruneSql = R"sql( - WITH RECURSIVE pruned(id) AS ( + constexpr auto ignoreSql = R"sql( + WITH RECURSIVE ignored(id) AS ( SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) UNION ALL -- Get all the child elements - SELECT e.Id FROM bis_Element e INNER JOIN pruned p ON e.ParentId = p.id + SELECT e.Id FROM bis_Element e INNER JOIN ignored p ON e.ParentId = p.id UNION ALL -- Get all the modeled elements in a possible sub model SELECT e.Id FROM bis_Element e INNER JOIN bis_Model m ON m.Id = e.ModelId - INNER JOIN pruned p ON m.ModeledElementId = p.id + INNER JOIN ignored p ON m.ModeledElementId = p.id ) - SELECT id FROM pruned + SELECT id FROM ignored )sql"; - Statement pruneStmt; - if (BE_SQLITE_OK == pruneStmt.Prepare(db, pruneSql)) + Statement ignoreStmt; + if (BE_SQLITE_OK == ignoreStmt.Prepare(db, ignoreSql)) { - pruneStmt.BindVirtualSet(1, pruneRoots); - while (BE_SQLITE_ROW == pruneStmt.Step()) + ignoreStmt.BindVirtualSet(1, ignoreRoots); + while (BE_SQLITE_ROW == ignoreStmt.Step()) { - auto id = pruneStmt.GetValueId(0); + auto id = ignoreStmt.GetValueId(0); if (id.IsValid()) elementsToDelete.erase(id); } @@ -858,7 +858,7 @@ namespace return finalDeleteSet; } - ExpandedElementSet ExpandAndPruneElementIds(DgnDbR db, const DgnElementIdSet& elementIds, DgnElementIdSet& failedToDeleteElements) + ExpandedElementSet ExpandAndIgnoreElementIds(DgnDbR db, const DgnElementIdSet& elementIds, DgnElementIdSet& failedToDeleteElements) { // Expand the element IDs to recursively include: // 1. All children elements @@ -867,8 +867,8 @@ namespace const auto violators = FindViolators(db, expanded.ids); if (!violators.empty()) { - const auto pruneRoots = GetRootsToPrune(db, violators, expanded.logicalParents); - PruneViolators(db, pruneRoots, expanded.ids); + const auto ignoreRoots = GetRootsToIgnore(db, violators, expanded.logicalParents); + IgnoreViolators(db, ignoreRoots, expanded.ids); for (const auto& elementId : elementIds) { @@ -972,7 +972,7 @@ DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds) return {}; DgnElementIdSet failedToDeleteElements; - const auto expandedIds = ExpandAndPruneElementIds(m_dgndb, elementIds, failedToDeleteElements); + const auto expandedIds = ExpandAndIgnoreElementIds(m_dgndb, elementIds, failedToDeleteElements); if (failedToDeleteElements.empty()) LOG.infov("DeleteElements: All requested element Ids are being deleted as part of the bulk delete operation: %s", elementIds.ToString().c_str()); @@ -1159,7 +1159,7 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele DgnElementIdSet failedToDeleteElements; // Expand the set of definition element IDs to include all related elements so implicitly added element's usage can be verified. - const ExpandedElementSet expanded = ExpandAndPruneElementIds(m_dgndb, definitionElementIds, failedToDeleteElements); + const ExpandedElementSet expanded = ExpandAndIgnoreElementIds(m_dgndb, definitionElementIds, failedToDeleteElements); if (!failedToDeleteElements.empty()) LOG.errorv("DeleteDefinitionElements: Failed to delete definition element Ids: %s", failedToDeleteElements.ToString().c_str()); @@ -1188,7 +1188,7 @@ DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& ele // We need to ensure that all affected are marked for deletion as well. if (!cannotBeDeleted.empty()) { - const auto blockedRoots = GetRootsToPrune(m_dgndb, cannotBeDeleted, expanded.logicalParents); + const auto blockedRoots = GetRootsToIgnore(m_dgndb, cannotBeDeleted, expanded.logicalParents); ExpandBlockedSubtrees(m_dgndb, blockedRoots, toBeDeleted, cannotBeDeleted); } From b0b67498e36cda7e0f54aa491ffdbd8ce5f7da3e Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:39:54 +0530 Subject: [PATCH 20/23] Updated the code to use direct sqls instead of element id sets --- .../iModelPlatform/DgnCore/DgnElements.cpp | 1042 +++++++++-------- .../PublicAPI/DgnPlatform/DgnElement.h | 51 +- .../PublicAPI/DgnPlatform/DgnModel.h | 1 + iModelJsNodeAddon/IModelJsNative.cpp | 19 +- iModelJsNodeAddon/IModelJsNative.h | 4 +- iModelJsNodeAddon/JsInteropDgnDb.cpp | 20 +- .../api_package/ts/src/NativeLibrary.ts | 3 +- 7 files changed, 576 insertions(+), 564 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index 5ef4cc1b78..513b6465b4 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -467,6 +467,538 @@ ECInstanceId ElementAspectIteratorEntry::GetECInstanceId() const {return m_state DgnClassId ElementAspectIteratorEntry::GetClassId() const {return m_statement->GetValueId(1);} DgnElementId ElementAspectIteratorEntry::GetElementId() const {return m_statement->GetValueId(2);} +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +bool BulkElementDeletion::CreateTempTables() const + { + // Create a temp table to hold the entries for the elements to be deleted + auto stat = m_dgndb.CreateTableIfNotExists(TEMP_TABLE(TEMP_ELEMENT_DELETION), + R"sql( + ElementId INTEGER PRIMARY KEY, + LogicalParentId INTEGER, + IsSubModelRoot INTEGER NOT NULL DEFAULT 0, + Depth INTEGER NOT NULL DEFAULT 0, + IsViolator INTEGER NOT NULL DEFAULT 0 + )sql"); + if (stat != BE_SQLITE_OK) + { + LOG.errorv("Error creating temp table %s: %s", TEMP_TABLE(TEMP_ELEMENT_DELETION), BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } + + if (BE_SQLITE_OK != m_dgndb.TryExecuteSql("CREATE INDEX IF NOT EXISTS idx_etd_logicalParent ON " TEMP_ELEMENT_DELETION " (LogicalParentId)")) + return false; + + // Partial index to accelerate IsViolator=1 scans (PruneViolators, deleteAllViolators). + if (BE_SQLITE_OK != m_dgndb.TryExecuteSql("CREATE INDEX IF NOT EXISTS idx_etd_violator ON " TEMP_ELEMENT_DELETION " (IsViolator) WHERE IsViolator = 1")) + return false; + + // Partial index to accelerate IsSubModelRoot=1 scans (DeleteLinkTableRelationships, ExecuteDeletion). + if (BE_SQLITE_OK != m_dgndb.TryExecuteSql("CREATE INDEX IF NOT EXISTS idx_etd_submodelroot ON " TEMP_ELEMENT_DELETION " (IsSubModelRoot) WHERE IsSubModelRoot = 1")) + return false; + + // Clear out table to avoid stale entries + if (BE_SQLITE_OK != m_dgndb.TryExecuteSql("DELETE FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION))) + return false; + + return true; + } + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +bool BulkElementDeletion::ExpandElementIdList() const + { + constexpr auto expandSql = + "WITH RECURSIVE fullSet(id, logicalParentId, isSubModelRoot, depth) AS (" + "SELECT e.Id, " + "e.ParentId, " + "CASE WHEN m.Id IS NOT NULL THEN 1 ELSE 0 END, " + "0 " + "FROM bis_Element e " + "LEFT JOIN bis_Model m ON m.ModeledElementId = e.Id " + "WHERE InVirtualSet(?, e.Id) " + "UNION ALL " + "SELECT e.Id," + "e.ParentId, " + "CASE WHEN m.Id IS NOT NULL THEN 1 ELSE 0 END, " + "f.depth + 1 " + "FROM bis_Element e " + "INNER JOIN fullSet f ON e.ParentId = f.id " + "LEFT JOIN bis_Model m ON m.ModeledElementId = e.Id " + "UNION ALL " + "SELECT e.Id," + "sm.ModeledElementId, " + "CASE WHEN m.Id IS NOT NULL THEN 1 ELSE 0 END, " + "f.depth + 1 " + "FROM bis_Element e " + "INNER JOIN bis_Model sm ON sm.Id = e.ModelId " + "INNER JOIN fullSet f ON sm.ModeledElementId = f.id " + "LEFT JOIN bis_Model m ON m.ModeledElementId = e.Id " + ") " + "INSERT OR REPLACE INTO " TEMP_TABLE(TEMP_ELEMENT_DELETION) " (ElementId, LogicalParentId, IsSubModelRoot, Depth) " + "SELECT id, logicalParentId, isSubModelRoot, depth " + "FROM fullSet"; + + const auto expandStmt = m_dgndb.GetCachedStatement(expandSql); + if (!expandStmt.IsValid()) + { + LOG.error("BulkElementDeletion: Failed to add dependent elements"); + return false; + } + expandStmt->BindVirtualSet(1, m_originalElementIds); + if (const auto stat = expandStmt->Step(); stat != BE_SQLITE_DONE) + { + LOG.errorv("BulkElementDeletion: Failed to add dependent elements: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } + + m_dgndb.TryExecuteSql("ANALYZE " TEMP_TABLE(TEMP_ELEMENT_DELETION)); + + return true; + } + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +bool BulkElementDeletion::PruneViolators() + { + // This is purely for error reporting. + // While this is a hinderance for the performance, it is important to import the caller exactly which elements failed deletion. + constexpr auto collectViolatorsSql = + "SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " WHERE IsViolator = 1"; + + auto pruneStmt = m_dgndb.GetCachedStatement(collectViolatorsSql); + if (!pruneStmt.IsValid()) + { + LOG.error("BulkElementDeletion: Failed to find constraint violators"); + return false; + } + + while (BE_SQLITE_ROW == pruneStmt->Step()) + { + auto id = pruneStmt->GetValueId(0); + if (id.IsValid() && m_originalElementIds.Contains(id)) + m_failedToDelete.insert(id); + } + + constexpr auto deleteAllViolators = "DELETE FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " WHERE IsViolator = 1"; + if (const auto stat = m_dgndb.TryExecuteSql(deleteAllViolators); stat != BE_SQLITE_OK) + { + LOG.errorv("BulkElementDeletion: Failed to delete all constraint violators: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } + + return true; + } + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +bool BulkElementDeletion::FindAndPruneConstraintViolators() + { + // Optimization guard: Don't run the expensive search queries if no violators exist + constexpr auto guardSql = + "SELECT 1 FROM bis_Element e " + "WHERE e.CodeScopeId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " + "AND e.Id NOT IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " + "UNION ALL " + "SELECT 1 FROM bis_GeometricElement3d g " + "WHERE g.CategoryId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " + "AND g.ElementId NOT IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " + "UNION ALL " + "SELECT 1 FROM bis_GeometricElement2d g " + "WHERE g.CategoryId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " + "AND g.ElementId NOT IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " + "LIMIT 1"; + auto guardStmt = m_dgndb.GetCachedStatement(guardSql); + if (guardStmt.IsNull()) + { + LOG.error("BulkElementDeletion: Constraint violator guard check failed"); + return false; + } + + if (BE_SQLITE_ROW != guardStmt->Step()) + return true; + + constexpr auto findViolators = + "WITH " + // Get the direct constraint violators from the delete set + "directViolators AS (" + "SELECT DISTINCT t.ElementId " + "FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t " + "WHERE EXISTS (" + "SELECT 1 FROM bis_Element e " + "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " td ON td.ElementId = e.Id " + "WHERE e.CodeScopeId = t.ElementId AND td.ElementId IS NULL" + ") " + "OR EXISTS (" + "SELECT 1 FROM bis_GeometricElement3d g3 " + "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " td ON td.ElementId = g3.ElementId " + "WHERE g3.CategoryId = t.ElementId AND td.ElementId IS NULL" + ") " + "OR EXISTS (" + "SELECT 1 FROM bis_GeometricElement2d g2 " + "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " td ON td.ElementId = g2.ElementId " + "WHERE g2.CategoryId = t.ElementId AND td.ElementId IS NULL" + ")" + "), " + // Walk UP from each direct violator to find its highest ancestor in the delete set + "ancestors(id, logicalParentId) AS (" + "SELECT t.ElementId, t.LogicalParentId " + "FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t " + "WHERE t.ElementId IN (SELECT ElementId FROM directViolators) " + "UNION ALL " + "SELECT t.ElementId, t.LogicalParentId " + "FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t " + "INNER JOIN ancestors a ON t.ElementId = a.logicalParentId" + "), " + "subtreeRoots AS (" + "SELECT DISTINCT a.id FROM ancestors a " + "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " p ON p.ElementId = a.logicalParentId " + "WHERE a.logicalParentId IS NULL OR p.ElementId IS NULL" + "), " + // Walk DOWN from each root to collect all descendants + "subtree(id) AS (" + "SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " " + "WHERE ElementId IN (SELECT id FROM subtreeRoots) " + "UNION ALL " + "SELECT t.ElementId " + "FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t " + "INNER JOIN subtree s ON t.LogicalParentId = s.id" + ") " + "UPDATE " TEMP_TABLE(TEMP_ELEMENT_DELETION) " SET IsViolator = 1 WHERE ElementId IN (SELECT id FROM subtree)"; + + if (const auto stat = m_dgndb.TryExecuteSql(findViolators); stat != BE_SQLITE_OK) + { + LOG.errorv("BulkElementDeletion: Failed to find constraint violators: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } + + return PruneViolators(); + } + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +bool BulkElementDeletion::FindAndPruneInUseDefinitionElements() + { + // Collect only the DefinitionElement rows from the temp table. + // Non-definition elements in the set don't need usage checking. + constexpr auto defIdsSql = + "SELECT t.ElementId " + "FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t " + "INNER JOIN bis_DefinitionElement d ON d.ElementId = t.ElementId"; + + auto defStmt = m_dgndb.GetCachedStatement(defIdsSql); + if (!defStmt.IsValid()) + { + LOG.error("BulkElementDeletion: Failed to query for definition elements"); + return false; + } + + DgnElementIdSet definitionIds; + while (BE_SQLITE_ROW == defStmt->Step()) + { + if (auto id = defStmt->GetValueId(0); id.IsValid()) + definitionIds.insert(id); + } + + if (definitionIds.empty()) + return true; + + m_definitionElementsExist = true; + + // Exclude intra-set usages from the usage check — elements referencing + // each other inside the delete set are fine since all will be deleted. + auto usageInfo = DefinitionElementUsageInfo::Create(m_dgndb, definitionIds, std::make_shared>(BeIdSet(definitionIds.GetBeIdSet()))); + if (!usageInfo.IsValid()) + return true; // no usage info means nothing blocks deletion + + DgnElementIdSet inUse; + for (const auto& id : definitionIds) + { + if (usageInfo->GetUsedIds().Contains(id)) + inUse.insert(id); + } + + if (inUse.empty()) + return true; + + constexpr auto findDependents = + "WITH RECURSIVE ancestry(id, logicalParentId) AS (" + // Get all in-use elements in the delete list + "SELECT ElementId, LogicalParentId " + "FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " " + "WHERE InVirtualSet(?, ElementId) " + + "UNION ALL " + + // Get all descendants of the current element + "SELECT t.ElementId, t.LogicalParentId " + "FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t " + "INNER JOIN ancestry a ON t.ElementId = a.logicalParentId" + ") " + "Update " TEMP_TABLE(TEMP_ELEMENT_DELETION) " SET IsViolator = 1 WHERE ElementId IN (SELECT id FROM ancestry)"; + + const auto findDependentsStmt = m_dgndb.GetCachedStatement(findDependents); + if (!findDependentsStmt.IsValid()) + { + LOG.error("BulkElementDeletion: Failed to query for dependent elements"); + return false; + } + findDependentsStmt->BindVirtualSet(1, inUse); + if (const auto stat = findDependentsStmt->Step(); stat != BE_SQLITE_OK) + { + LOG.errorv("BulkElementDeletion: Failed to prune in-use definition elements: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } + + return PruneViolators(); + } + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +bool BulkElementDeletion::FindAndNullTypeDefinitionReferences() const + { + if (!m_definitionElementsExist) + return true; + + // Optimization guard against unnecessary scans + auto guard = m_dgndb.GetCachedStatement( + "SELECT 1 " + "FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t " + "WHERE EXISTS (" + "SELECT 1 FROM bis_GeometricElement3d g " + "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " td ON td.ElementId = g.ElementId " + "WHERE g.TypeDefinitionId = t.ElementId AND td.ElementId IS NULL" + ") " + "OR EXISTS (" + "SELECT 1 FROM bis_GeometricElement2d g " + "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " td ON td.ElementId = g.ElementId " + "WHERE g.TypeDefinitionId = t.ElementId AND td.ElementId IS NULL" + ") " + "LIMIT 1"); + + if (!guard.IsValid()) + { + LOG.error("BulkElementDeletion: Failed to query for TypeDefinition references"); + return false; + } + + if (BE_SQLITE_ROW != guard->Step()) + return true; + + // NULL out TypeDefinitionId on all surviving GeometricElements that point to an element we are about to delete. + constexpr auto null3dSql = + "UPDATE bis_GeometricElement3d SET TypeDefinitionId = NULL " + "WHERE TypeDefinitionId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " + "AND EXISTS (" + "SELECT 1 FROM bis_GeometricElement3d g2 " + "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " td ON td.ElementId = g2.ElementId " + "WHERE g2.ElementId = bis_GeometricElement3d.ElementId AND td.ElementId IS NULL" + ")"; + + if (const auto stat = m_dgndb.TryExecuteSql(null3dSql); stat != BE_SQLITE_OK) + { + LOG.errorv("BulkElementDeletion: TypeDefinitionId NULL (3d) failed: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } + + constexpr auto null2dSql = + "UPDATE bis_GeometricElement2d SET TypeDefinitionId = NULL " + "WHERE TypeDefinitionId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " + "AND EXISTS (" + "SELECT 1 FROM bis_GeometricElement2d g2 " + "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " td ON td.ElementId = g2.ElementId " + "WHERE g2.ElementId = bis_GeometricElement2d.ElementId AND td.ElementId IS NULL" + ")"; + + if (const auto stat = m_dgndb.TryExecuteSql(null2dSql); stat != BE_SQLITE_OK) + { + LOG.errorv("BulkElementDeletion: TypeDefinitionId NULL (2d) failed: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } + + return true; + } + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +bool BulkElementDeletion::FireAllCallbacks() + { + constexpr auto getAllElements = + "SELECT t.ElementId, t.IsSubModelRoot, t.Depth, " + "CASE WHEN InVirtualSet(?, t.ElementId) THEN 1 ELSE 0 END AS IsOriginal " + "FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t " + "ORDER BY t.Depth DESC"; + + const auto stmt = m_dgndb.GetCachedStatement(getAllElements); + if (!stmt.IsValid()) + { + LOG.error("BulkElementDeletion: Failed to fire element callbacks"); + return false; + } + stmt->BindVirtualSet(1, m_originalElementIds); + + while (BE_SQLITE_ROW == stmt->Step()) + { + const auto elementId = stmt->GetValueId(0); + if (!elementId.IsValid()) + return false; + + const auto element = m_dgndb.Elements().GetElement(elementId); + if (!element.IsValid()) + return false; + + if (const auto isSubModelRoot = stmt->GetValueInt(1); isSubModelRoot != 0) + { + if (auto subModel = element->GetSubModel(); subModel.IsValid()) + { + element->_OnSubModelDelete(*subModel); + subModel->_OnDeleteNotify(); + subModel->_OnDeleted(); + element->_OnSubModelDeleted(*subModel); + } + m_subModelRootExists = true; + } + + if (stmt->GetValueInt(3) != 0) + { + element->_OnDelete(); + + if (const auto parentId = element->m_parent.m_id; parentId.IsValid()) + { + if (const auto parentElement = m_dgndb.Elements().GetElement(parentId); parentElement.IsValid()) + { + parentElement->_OnChildDelete(*element); + parentElement->_OnChildDeleted(*element); + } + } + } + + element->_OnDeleted(); + } + return true; + } + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +bool BulkElementDeletion::ExecuteDeletion() + { + // Prep the db and handlers for a bulk delete + if (m_definitionElementsExist) + m_dgndb.BeginPurgeOperation(); + m_dgndb.Elements().SetBulkOperation(true); + m_dgndb.TryExecuteSql("PRAGMA defer_foreign_keys = true"); + + auto reset = [&]() { + m_dgndb.Elements().SetBulkOperation(false); + if (m_definitionElementsExist) + m_dgndb.EndPurgeOperation(); + m_dgndb.TryExecuteSql("PRAGMA defer_foreign_keys = false"); + }; + + const auto stat = m_dgndb.TryExecuteSql("DELETE FROM " BIS_TABLE(BIS_CLASS_Element) " WHERE Id IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ")"); + if (stat != BE_SQLITE_OK) + { + LOG.errorv("BulkElementDeletion: Element deletion failed: %s", BeSQLiteLib::GetLogError(stat).c_str()); + reset(); + return false; + } + + if (m_subModelRootExists) + { + const auto modelStat = m_dgndb.TryExecuteSql("DELETE FROM " BIS_TABLE(BIS_CLASS_Model) " WHERE Id IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " WHERE IsSubModelRoot = 1)"); + if (modelStat != BE_SQLITE_OK) + { + LOG.errorv("BulkElementDeletion: Sub-model root deletion failed: %s", BeSQLiteLib::GetLogError(modelStat).c_str()); + reset(); + return false; + } + } + + reset(); + return true; + } + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +bool BulkElementDeletion::DeleteLinkTableRelationships() + { + for (const auto& tableName : { BIS_TABLE(BIS_REL_ElementRefersToElements), BIS_TABLE(BIS_REL_ElementDrivesElement) }) + { + auto stat = m_dgndb.TryExecuteSql(Utf8PrintfString("DELETE FROM %s WHERE SourceId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ")", tableName).c_str()); + if (stat != BE_SQLITE_OK) + { + LOG.errorv("BulkElementDeletion: Link table SourceId deletion failed: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } + + stat = m_dgndb.TryExecuteSql(Utf8PrintfString("DELETE FROM %s WHERE TargetId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ")", tableName).c_str()); + if (stat != BE_SQLITE_OK) + { + LOG.errorv("BulkElementDeletion: Link table TargetId deletion failed: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } + } + + if (m_subModelRootExists) + { + const auto stat = m_dgndb.TryExecuteSql("DELETE FROM " BIS_TABLE(BIS_REL_ModelSelectorRefersToModels) " WHERE TargetId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " WHERE IsSubModelRoot = 1)"); + if (stat != BE_SQLITE_OK) + { + LOG.errorv("BulkElementDeletion: Sub-model root deletion failed: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } + } + + return true; + } + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +DgnElementIdSet BulkElementDeletion::Execute() + { + DgnDb::VerifyClientThread(); + + if (!CreateTempTables()) + return m_originalElementIds; + + if (!ExpandElementIdList()) + return m_originalElementIds; + + if (!m_skipFkValidation && !FindAndPruneConstraintViolators()) + return m_originalElementIds; + + // If definition elements exist in the delete set, check usage and null out any externally references type definitions + if (!FindAndPruneInUseDefinitionElements()) + return m_originalElementIds; + + if (!m_skipFkValidation && !FindAndNullTypeDefinitionReferences()) + return m_originalElementIds; + + // Fire the pre and post delete callbacks at once + if (!FireAllCallbacks()) + return m_originalElementIds; + + // Bulk delete the elements + if (!ExecuteDeletion() && m_failedToDelete.empty()) + return m_originalElementIds; + + // Clean up link-table relationships + if (!DeleteLinkTableRelationships()) + return m_originalElementIds; + + return m_failedToDelete; + } + /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ @@ -695,522 +1227,16 @@ DgnDbStatus DgnElements::Delete(DgnElementCR elementIn) return DgnDbStatus::Success; } -namespace - { - struct ExpandedElementSet - { - DgnElementIdSet ids; - std::unordered_map logicalParents; // might be a parent element or a modeled element - }; - - ExpandedElementSet ExpandIdSet(DgnDbR db, const DgnElementIdSet& elementIds) - { - constexpr auto expandSql = R"sql( - WITH RECURSIVE fullSet(id, logicalParentId) AS ( - SELECT Id, ParentId FROM bis_Element WHERE InVirtualSet(?, Id) - UNION ALL - -- Get all the child elements - SELECT e.Id, e.ParentId FROM bis_Element e INNER JOIN fullSet f ON e.ParentId = f.id - UNION ALL - -- Get all the elements in a possible sub model - SELECT e.Id, m.ModeledElementId FROM bis_Element e - INNER JOIN bis_Model m ON m.Id = e.ModelId - INNER JOIN fullSet f ON m.ModeledElementId = f.id - ) - SELECT id, logicalParentId FROM fullSet - )sql"; - - Statement expandStmt; - if (BE_SQLITE_OK != expandStmt.Prepare(db, expandSql)) - return {}; - - expandStmt.BindVirtualSet(1, elementIds); - - ExpandedElementSet result; - while (BE_SQLITE_ROW == expandStmt.Step()) - { - if (auto id = expandStmt.GetValueId(0); id.IsValid()) - { - result.ids.insert(id); - result.logicalParents[id.GetValueUnchecked()] = expandStmt.GetValueId(1).GetValueUnchecked(); - } - } - return result; - } - - DgnElementIdSet FindViolators(DgnDbR db, const DgnElementIdSet& elementIds) - { - constexpr auto violatorSql = R"sql( - -- Find elements that are in a code scope but not in the delete set - SELECT DISTINCT CodeScopeId FROM bis_Element - WHERE CodeScopeId IN ( - SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) - ) - AND Id NOT IN ( - SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) - ) - )sql"; - - Statement violatorStmt; - if (BE_SQLITE_OK != violatorStmt.Prepare(db, violatorSql)) - return {}; - - violatorStmt.BindVirtualSet(1, elementIds); - violatorStmt.BindVirtualSet(2, elementIds); - - DgnElementIdSet violators; - while (BE_SQLITE_ROW == violatorStmt.Step()) - { - auto id = violatorStmt.GetValueId(0); - if (id.IsValid()) - violators.insert(id); - } - - return violators; - } - - DgnElementIdSet GetRootsToIgnore(DgnDbR db, const DgnElementIdSet& violators, const std::unordered_map& logicalParents) - { - // Find all root elements of the violators that need to be ignored from the delete set - DgnElementIdSet ignoreRoots; - for (const auto& violatorId : violators) - { - uint64_t current = violatorId.GetValueUnchecked(); - while (true) - { - auto it = logicalParents.find(current); - if (it == logicalParents.end()) - break; - - uint64_t parentVal = it->second; - if (parentVal == 0 || logicalParents.find(parentVal) == logicalParents.end()) - { - ignoreRoots.insert(DgnElementId(current)); - break; - } - current = parentVal; - } - } - - return ignoreRoots; - } - - void IgnoreViolators(DgnDbR db, const DgnElementIdSet& ignoreRoots, DgnElementIdSet& elementsToDelete) - { - constexpr auto ignoreSql = R"sql( - WITH RECURSIVE ignored(id) AS ( - SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) - UNION ALL - -- Get all the child elements - SELECT e.Id FROM bis_Element e INNER JOIN ignored p ON e.ParentId = p.id - UNION ALL - -- Get all the modeled elements in a possible sub model - SELECT e.Id FROM bis_Element e - INNER JOIN bis_Model m ON m.Id = e.ModelId - INNER JOIN ignored p ON m.ModeledElementId = p.id - ) - SELECT id FROM ignored - )sql"; - - Statement ignoreStmt; - if (BE_SQLITE_OK == ignoreStmt.Prepare(db, ignoreSql)) - { - ignoreStmt.BindVirtualSet(1, ignoreRoots); - while (BE_SQLITE_ROW == ignoreStmt.Step()) - { - auto id = ignoreStmt.GetValueId(0); - if (id.IsValid()) - elementsToDelete.erase(id); - } - } - } - - DgnElementIdSet ResolveElementsAfterPossibleVeto(DgnDbR db, const DgnElementIdSet& vetoedElementIds, DgnElementIdSet& failedToDeleteElements) - { - constexpr auto expandVetoedSql = R"sql( - WITH RECURSIVE - vetoedSubtree(id) AS ( - SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) - UNION ALL - SELECT e.Id FROM bis_Element e INNER JOIN vetoedSubtree v ON e.ParentId = v.id - UNION ALL - SELECT e.Id FROM bis_Element e - INNER JOIN bis_Model m ON m.Id = e.ModelId - INNER JOIN vetoedSubtree v ON m.ModeledElementId = v.id - ) - SELECT id FROM vetoedSubtree - )sql"; - - Statement vetoStmt; - DgnElementIdSet finalDeleteSet; - if (BE_SQLITE_OK == vetoStmt.Prepare(db, expandVetoedSql)) - { - vetoStmt.BindVirtualSet(1, vetoedElementIds); - while (BE_SQLITE_ROW == vetoStmt.Step()) - { - auto id = vetoStmt.GetValueId(0); - if (id.IsValid()) - finalDeleteSet.erase(id); - } - } - // Also record the vetoed input elements themselves as having failed. - failedToDeleteElements.insert(vetoedElementIds.begin(), vetoedElementIds.end()); - return finalDeleteSet; - } - - ExpandedElementSet ExpandAndIgnoreElementIds(DgnDbR db, const DgnElementIdSet& elementIds, DgnElementIdSet& failedToDeleteElements) - { - // Expand the element IDs to recursively include: - // 1. All children elements - // 2. If the delete set contains a modeled element, then all the elements in the sub model. - auto expanded = ExpandIdSet(db, elementIds); - const auto violators = FindViolators(db, expanded.ids); - if (!violators.empty()) - { - const auto ignoreRoots = GetRootsToIgnore(db, violators, expanded.logicalParents); - IgnoreViolators(db, ignoreRoots, expanded.ids); - - for (const auto& elementId : elementIds) - { - if (expanded.ids.find(elementId) == expanded.ids.end()) - failedToDeleteElements.insert(elementId); - } - } - - return expanded; - } - - void SegregateDefinitionElements(DgnDbR db, const DgnElementIdSet& elementIds, DgnElementIdSet& definitionElementIds, DgnElementIdSet& nonDefinitionElementIds) - { - definitionElementIds.clear(); - nonDefinitionElementIds.clear(); - - // Remove all the non-definition elements from the original delete set. - // Any definition partition elements will get removed as they are not definition elements themselves. - constexpr auto classifySql = R"sql( - SELECT e.ECInstanceId, CASE WHEN d.ECInstanceId IS NULL THEN 0 ELSE 1 END - FROM bis.Element e - LEFT JOIN bis.DefinitionElement d ON d.ECInstanceId = e.ECInstanceId - WHERE InVirtualSet(?, e.ECInstanceId) - )sql"; - - ECSqlStatement classifyStmt; - if (ECSqlStatus::Success != classifyStmt.Prepare(db, classifySql)) - return; - - classifyStmt.BindVirtualSet(1, std::make_shared>(BeIdSet(elementIds.GetBeIdSet()))); - - while (classifyStmt.Step() == BE_SQLITE_ROW) - { - const auto id = classifyStmt.GetValueId(0); - if (classifyStmt.GetValueInt(1) != 0) - definitionElementIds.insert(id); - else - nonDefinitionElementIds.insert(id); - } - } - - // Expand a subtree rooted at a definition element that is in use and cannot be deleted. - void ExpandBlockedSubtrees(DgnDbR db, const DgnElementIdSet& blockedRoots, DgnElementIdSet& candidateSet, DgnElementIdSet& blockedSet) - { - constexpr auto expandBlockedSql = R"sql( - WITH RECURSIVE blockedSubtree(id) AS ( - SELECT Id FROM bis_Element WHERE InVirtualSet(?, Id) - UNION ALL - SELECT e.Id FROM bis_Element e INNER JOIN blockedSubtree b ON e.ParentId = b.id - UNION ALL - SELECT e.Id FROM bis_Element e - INNER JOIN bis_Model m ON m.Id = e.ModelId - INNER JOIN blockedSubtree b ON m.ModeledElementId = b.id - ) - SELECT id FROM blockedSubtree - )sql"; - - Statement stmt; - if (BE_SQLITE_OK != stmt.Prepare(db, expandBlockedSql)) - return; - - stmt.BindVirtualSet(1, blockedRoots); - while (BE_SQLITE_ROW == stmt.Step()) - { - auto id = stmt.GetValueId(0); - if (!id.IsValid()) - continue; - if (candidateSet.erase(id) > 0) - blockedSet.insert(id); - } - } - - bool DeleteLinkTableRelationships(DgnDbR db, const std::vector& tableNames, const DgnElementIdSet& sourceElementIds, const DgnElementIdSet& targetElementIds) - { - for (const auto& table : tableNames) - { - BeAssert(db.TableExists(table.c_str())); - - auto statement = db.GetCachedStatement(Utf8PrintfString("DELETE FROM %s WHERE InVirtualSet(?, SourceId) OR InVirtualSet(?, TargetId)", table.c_str()).c_str()); - if (!statement.IsValid()) - return false; - - statement->BindVirtualSet(1, sourceElementIds); - statement->BindVirtualSet(2, targetElementIds); - if (statement->Step() != BE_SQLITE_DONE) - return false; - } - - return true; - } - } - /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds) +DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, const bool skipFkValidation) { DgnDb::VerifyClientThread(); - if (elementIds.empty()) return {}; - DgnElementIdSet failedToDeleteElements; - const auto expandedIds = ExpandAndIgnoreElementIds(m_dgndb, elementIds, failedToDeleteElements); - - if (failedToDeleteElements.empty()) - LOG.infov("DeleteElements: All requested element Ids are being deleted as part of the bulk delete operation: %s", elementIds.ToString().c_str()); - else - LOG.warningv("DeleteElements: The following element Ids are not being deleted as part of the bulk delete operation due to external code scope violations: %s", failedToDeleteElements.ToString().c_str()); - - const auto additionalFailures = DeleteElementsPreExpanded(expandedIds.ids, elementIds); - failedToDeleteElements.insert(additionalFailures.begin(), additionalFailures.end()); - return failedToDeleteElements; - } - -/*---------------------------------------------------------------------------------**//** -* @bsimethod -+---------------+---------------+---------------+---------------+---------------+------*/ -DgnElementIdSet DgnElements::DeleteElementsPreExpanded(const DgnElementIdSet& expandedElementIdSet, const DgnElementIdSet& originalElementIdSet) - { - // Work on a mutable copy so veto resolution can shrink the set without modifying the caller's data. - DgnElementIdSet elementsToDelete = expandedElementIdSet; - - std::vector elementsToDeletePtrs; - std::vector subModelsToDelete; - DgnElementIdSet subModelIds; // ModelId = Modeling element's id, so we're good with this - DgnElementIdSet vetoedElementIds; - - // Call the pre-delete handlers for the elements from the original delete set and any sub-models that will be deleted. - for (const auto& elementId : elementsToDelete) - { - const auto element = GetElement(elementId); - if (!element.IsValid()) - { - vetoedElementIds.insert(elementId); - continue; - } - - // Sub-Model deletion should trigger callback even if they are being deleted implicitly - DgnModelPtr subModel = element->GetSubModel(); - if (subModel.IsValid()) - { - if (element->_OnSubModelDelete(*subModel) != DgnDbStatus::Success) - { - vetoedElementIds.insert(elementId); - continue; - } - - if (subModel->_OnDeleteNotify() != DgnDbStatus::Success) - { - vetoedElementIds.insert(elementId); - continue; - } - subModelsToDelete.emplace_back(subModel); - subModelIds.insert(DgnElementId(subModel->GetModelId().GetValueUnchecked())); - } - - if (originalElementIdSet.find(elementId) == originalElementIdSet.end()) - continue; - - // Call the element's own pre-delete handler. - if (element->_OnDelete() != DgnDbStatus::Success) - { - vetoedElementIds.insert(elementId); - continue; - } - - // Ask the parent whether it is ok to delete this child. - // Skip the parent callback when the parent itself is also in the caller's input set - auto parent = GetElement(element->m_parent.m_id); - if (parent.IsValid() && - elementsToDelete.find(element->m_parent.m_id) == elementsToDelete.end() && - parent->_OnChildDelete(*element) != DgnDbStatus::Success) - { - vetoedElementIds.insert(elementId); - continue; - } - - elementsToDeletePtrs.push_back(element); - } - - DgnElementIdSet failedToDeleteElements; - if (!vetoedElementIds.empty()) - elementsToDelete = ResolveElementsAfterPossibleVeto(m_dgndb, vetoedElementIds, failedToDeleteElements); - - // Prep the elements and handlers and the Db for a bulk deletion - SetBulkOperation(true); - - // Since we have already handled all the external code scope violations and sub model deletes, - // defer the FK integrity check for all the intra set violations as all of them are being deleted anyway - m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = true"); - - auto resetDbState = [&]() - { - m_dgndb.ExecuteSql("PRAGMA defer_foreign_keys = false"); - SetBulkOperation(false); - }; - - // Delete the elements in a single delete sql statement - auto statement = GetStatement("DELETE FROM " BIS_TABLE(BIS_CLASS_Element) " WHERE InVirtualSet(?, Id)"); - statement->BindVirtualSet(1, elementsToDelete); - if (statement->Step() != BE_SQLITE_DONE) - { - LOG.errorv("DeleteElements: Failed to delete element Ids from database: %s", elementsToDelete.ToString().c_str()); - failedToDeleteElements.insert(elementsToDelete.begin(), elementsToDelete.end()); - resetDbState(); - return failedToDeleteElements; - } - - // Clear out the elements from the cache - DropFromPool(elementsToDelete); - - // Clear up the link-table relationships in bulk - if (!DeleteLinkTableRelationships(m_dgndb, { BIS_TABLE(BIS_REL_ElementRefersToElements), BIS_TABLE(BIS_REL_ElementDrivesElement) }, elementsToDelete, elementsToDelete)) - { - LOG.errorv("DeleteElements: Failed to delete link table relationships for element Ids: %s", elementsToDelete.ToString().c_str()); - failedToDeleteElements.insert(elementsToDelete.begin(), elementsToDelete.end()); - resetDbState(); - return failedToDeleteElements; - } - - // If any modeling elements have been deleted, clear out the model table entries as well - if (!subModelsToDelete.empty()) - { - auto modelDeleteStmt = GetStatement("DELETE FROM " BIS_TABLE(BIS_CLASS_Model) " WHERE InVirtualSet(?, Id)"); - modelDeleteStmt->BindVirtualSet(1, subModelIds); - if (modelDeleteStmt->Step() != BE_SQLITE_DONE) - { - LOG.errorv("DeleteElements: Failed to delete sub-model rows for element Ids: %s", subModelIds.ToString().c_str()); - return failedToDeleteElements; - } - } - - // Call the post delete handlers for the deleted elements - for (const auto& element : elementsToDeletePtrs) - { - element->_OnDeleted(); - - if (element->m_parent.m_id.IsValid() && originalElementIdSet.find(element->m_parent.m_id) == originalElementIdSet.end()) - { - auto parent = GetElement(element->m_parent.m_id); - if (parent.IsValid()) - parent->_OnChildDeleted(*element); - } - } - - // Call the post delete handlers for the deleted sub-models - for (const auto& model : subModelsToDelete) - model->_OnDeleted(); - - // Clear up the model's link-table relationships in bulk - if (!DeleteLinkTableRelationships(m_dgndb, { BIS_TABLE(BIS_REL_ModelSelectorRefersToModels) }, DgnElementIdSet() /* all ModelSelectors */, subModelIds)) // replicate former foreign key behavior - { - LOG.errorv("DeleteElements: Failed to delete link table relationships for model Ids: %s", subModelIds.ToString().c_str()); - failedToDeleteElements.insert(subModelIds.begin(), subModelIds.end()); - resetDbState(); - return failedToDeleteElements; - } - - resetDbState(); - - if (!failedToDeleteElements.empty()) - LOG.errorv("DeleteElements: Failed to delete element Ids: %s", failedToDeleteElements.ToString().c_str()); - - return failedToDeleteElements; - } - -/*---------------------------------------------------------------------------------**//** -* @bsimethod -+---------------+---------------+---------------+---------------+---------------+------*/ -DgnElementIdSet DgnElements::DeleteDefinitionElements(const DgnElementIdSet& elementIds) - { - DgnDb::VerifyClientThread(); - - if (elementIds.empty()) - return {}; - - DgnElementIdSet definitionElementIds; - DgnElementIdSet nonDefinitionElementIds; - - SegregateDefinitionElements(m_dgndb, elementIds, definitionElementIds, nonDefinitionElementIds); - - if (!nonDefinitionElementIds.empty()) - LOG.warningv("DeleteDefinitionElements: Element Ids %s are not DefinitionElements and cannot be deleted with this API.", nonDefinitionElementIds.ToString().c_str()); - - if (definitionElementIds.empty()) - return nonDefinitionElementIds.empty() ? elementIds : DgnElementIdSet(); - - DgnElementIdSet failedToDeleteElements; - // Expand the set of definition element IDs to include all related elements so implicitly added element's usage can be verified. - const ExpandedElementSet expanded = ExpandAndIgnoreElementIds(m_dgndb, definitionElementIds, failedToDeleteElements); - - if (!failedToDeleteElements.empty()) - LOG.errorv("DeleteDefinitionElements: Failed to delete definition element Ids: %s", failedToDeleteElements.ToString().c_str()); - - DgnElementIdSet toBeDeleted; - // After expansion, we need to re-classify into definition vs non-definition elements. - // DefinitionElementUsageInfo::Create skips non-definition elements, so we must separate them out and mark them for deletion directly. - SegregateDefinitionElements(m_dgndb, expanded.ids, definitionElementIds, toBeDeleted); - - // Get the usage info for all definition elements in the expanded set, excluding intra-set usages. - // For any intra-set dependencies, since we are bulk deleting, they will all get deleted anyway. - auto usageInfo = DefinitionElementUsageInfo::Create(m_dgndb, definitionElementIds, std::make_shared>(BeIdSet(definitionElementIds.GetBeIdSet()))); - if (!usageInfo.IsValid()) - return definitionElementIds; - - DgnElementIdSet cannotBeDeleted; - for (const auto& elementId : definitionElementIds) - { - if (usageInfo->GetUsedIds().Contains(elementId)) - cannotBeDeleted.insert(elementId); - else - toBeDeleted.insert(elementId); - } - - // It might happen that a parent element is in use, but its structural descendants might not be and vice versa. - // We need to ensure that all affected are marked for deletion as well. - if (!cannotBeDeleted.empty()) - { - const auto blockedRoots = GetRootsToIgnore(m_dgndb, cannotBeDeleted, expanded.logicalParents); - ExpandBlockedSubtrees(m_dgndb, blockedRoots, toBeDeleted, cannotBeDeleted); - } - - if (toBeDeleted.empty()) - { - if (!cannotBeDeleted.empty()) - LOG.warningv("DeleteDefinitionElements: Skipping element Ids that are in use: %s", cannotBeDeleted.ToString().c_str()); - failedToDeleteElements.insert(cannotBeDeleted.begin(), cannotBeDeleted.end()); - return failedToDeleteElements; - } - - m_dgndb.BeginPurgeOperation(); - const auto failedToDelete = DeleteElementsPreExpanded(toBeDeleted, toBeDeleted); - m_dgndb.EndPurgeOperation(); - - failedToDeleteElements.insert(cannotBeDeleted.begin(), cannotBeDeleted.end()); - failedToDeleteElements.insert(failedToDelete.begin(), failedToDelete.end()); - - if (!failedToDeleteElements.empty()) - LOG.warningv("DeleteDefinitionElements: Skipping element Ids that are in use or blocked: %s", failedToDeleteElements.ToString().c_str()); - - return failedToDeleteElements; + return BulkElementDeletion(m_dgndb, elementIds, skipFkValidation).Execute(); } //--------------------------------------------------------------------------------------- diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index 2680b4111b..e33a12150b 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -41,6 +41,7 @@ namespace ElementDependency { struct Graph; struct Edge;}; struct ElementAutoHandledPropertiesECInstanceAdapter; struct LsComponent; struct ExternalSourceAttachment; +class BulkElementDeletion; //======================================================================================= //! Holds Id remapping tables @@ -546,6 +547,35 @@ struct ElementECPropertyAccessor DGNPLATFORM_EXPORT DgnDbStatus GetPropertyValue(ECN::ECValueR value, PropertyArrayIndex const& arrayIndex) const; }; +#define TEMP_ELEMENT_DELETION "ElementsToDelete" +class BulkElementDeletion + { + DgnDbR m_dgndb; + DgnElementIdSet m_originalElementIds; + DgnElementIdSet m_failedToDelete; + bool m_skipFkValidation = false; + bool m_definitionElementsExist = false; + bool m_subModelRootExists = false; + + // Create temporary tables for bulk deletion + bool CreateTempTables() const; + bool ExpandElementIdList() const; + + // Find and prune constraint violators + bool FindAndPruneConstraintViolators(); + bool FindAndPruneInUseDefinitionElements(); + bool FindAndNullTypeDefinitionReferences() const; + bool PruneViolators(); + + bool FireAllCallbacks(); + bool DeleteLinkTableRelationships(); + bool ExecuteDeletion(); + +public: + BulkElementDeletion(DgnDbR dgndb, const DgnElementIdSet& originalElementIds, const bool skipFkValidation) : m_dgndb(dgndb), m_originalElementIds(originalElementIds), m_skipFkValidation(skipFkValidation) {} + DgnElementIdSet Execute(); + }; + #define DGNELEMENT_DECLARE_MEMBERS(__ECClassName__,__superclass__) \ private: typedef __superclass__ T_Super;\ public: static Utf8CP MyHandlerECClassName() {return __ECClassName__;}\ @@ -712,6 +742,7 @@ struct EXPORT_VTABLE_ATTRIBUTE DgnElement : RefCountedBase, NonCopyableClass { friend struct GeometrySource; friend struct ElementECPropertyAccessor; friend struct ElementAutoHandledPropertiesECInstanceAdapter; + friend class BulkElementDeletion; enum class ColumnNumbers : int32_t { ElementId = 0, @@ -3857,6 +3888,7 @@ struct DgnElements : DgnDbTable friend struct dgn_TxnTable::Element; friend struct GeometricElement; friend struct ElementAutoHandledPropertiesECInstanceAdapter; + friend class BulkElementDeletion; private: // THIS MUST NOT BE EXPORTED, AS IT BYPASSES THE ECCRUDWRITETOKEN @@ -4049,24 +4081,7 @@ struct DgnElements : DgnDbTable * @return A DgnElementIdSet of valid element Ids that failed to delete (either vetoed or blocked by FK/code scope constraints). * @note This function can only be safely invoked from the client thread. */ - DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds); - - /** - * Delete multiple definition elements from this DgnDb. - * - * Definition elements need to be handled as per their usage which makes them a special case for element deletion. - * The handlers for definition elements veto deletion unless a purge operation is enabled. - * Any non-definition element will be ignored and should use the general purpose DeleteElements API instead. - * - * @param[in] elementIds The set of definition elements to delete. Invalid and non-definition element Ids will be ignored. - * @return A DgnElementIdSet of valid definition element Ids that failed to delete (either not DefinitionElements or in use). - * @note This function can only be safely invoked from the client thread. - */ - DGNPLATFORM_EXPORT DgnElementIdSet DeleteDefinitionElements(const DgnElementIdSet& elementIds); - -private: - DgnElementIdSet DeleteElementsPreExpanded(const DgnElementIdSet& expandedElementIdSet, const DgnElementIdSet& originalElementIdSet); -public: + DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds, const bool skipFkValidation = false); //! Delete a DgnElement from this DgnDb by DgnElementId. //! @return DgnDbStatus::Success if the element was deleted, error status otherwise. diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnModel.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnModel.h index 79289131bf..b68d101b33 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnModel.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnModel.h @@ -115,6 +115,7 @@ struct EXPORT_VTABLE_ATTRIBUTE DgnModel : RefCountedBase friend struct dgn_TxnTable::Model; friend struct dgn_TxnTable::Element; friend struct dgn_ModelHandler::Model; + friend class BulkElementDeletion; enum class ColumnNumbers : int32_t { ECClassId = 1 diff --git a/iModelJsNodeAddon/IModelJsNative.cpp b/iModelJsNodeAddon/IModelJsNative.cpp index b244d50cdc..93e81d1eab 100644 --- a/iModelJsNodeAddon/IModelJsNative.cpp +++ b/iModelJsNodeAddon/IModelJsNative.cpp @@ -1756,22 +1756,8 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps THROW_JS_TYPE_EXCEPTION("Invalid argument given to deleteElements"); } - auto elemIds = JsInterop::DeleteElements(db, info[0].As()); - uint32_t index = 0; - auto ret = Napi::Array::New(Env(), elemIds.size()); - for (const auto& elemId : elemIds) - ret.Set(index++, Napi::String::New(Env(), elemId.ToHexStr().c_str())); - - return ret; - } - - Napi::Value DeleteDefinitionElements(NapiInfoCR info) { - auto& db = GetOpenedDb(info); - if (ARGUMENT_IS_NOT_PRESENT(0) || !info[0].IsArray()) { - THROW_JS_TYPE_EXCEPTION("Invalid argument given to deleteDefinitionElements"); - } - - auto elemIds = JsInterop::DeleteDefinitionElements(db, info[0].As()); + const auto deleteOptions = ARGUMENT_IS_PRESENT(1) ? info[1].As() : Env().Undefined(); + auto elemIds = JsInterop::DeleteElements(db, info[0].As(), deleteOptions); uint32_t index = 0; auto ret = Napi::Array::New(Env(), elemIds.size()); for (const auto& elemId : elemIds) @@ -3159,7 +3145,6 @@ struct NativeDgnDb : BeObjectWrap, SQLiteOps InstanceMethod("deleteAllTxns", &NativeDgnDb::DeleteAllTxns), InstanceMethod("deleteElement", &NativeDgnDb::DeleteElement), InstanceMethod("deleteElements", &NativeDgnDb::DeleteElements), - InstanceMethod("deleteDefinitionElements", &NativeDgnDb::DeleteDefinitionElements), InstanceMethod("deleteElementAspect", &NativeDgnDb::DeleteElementAspect), InstanceMethod("deleteLinkTableRelationship", &NativeDgnDb::DeleteLinkTableRelationship), InstanceMethod("deleteLinkTableRelationships", &NativeDgnDb::DeleteLinkTableRelationships), diff --git a/iModelJsNodeAddon/IModelJsNative.h b/iModelJsNodeAddon/IModelJsNative.h index e2fbbd9ab4..d83c3ca19f 100644 --- a/iModelJsNodeAddon/IModelJsNative.h +++ b/iModelJsNodeAddon/IModelJsNative.h @@ -488,6 +488,7 @@ struct JsInterop { BE_JSON_NAME(writeable) BE_JSON_NAME(yesNo) BE_JSON_NAME(uncompressedSize) + BE_JSON_NAME(skipFkValidation) #define JSON_NAME(__val__) JsInterop::json_##__val__() @@ -511,8 +512,7 @@ struct JsInterop { static Napi::String InsertElement(DgnDbR db, Napi::Object props, Napi::Value options); static void UpdateElement(DgnDbR db, Napi::Object); static void DeleteElement(DgnDbR db, Utf8StringCR eidStr); - static DgnElementIdSet DeleteElements(DgnDbR dgndb, Napi::Array elementIds); - static DgnElementIdSet DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds); + static DgnElementIdSet DeleteElements(DgnDbR dgndb, Napi::Array elementIds, Napi::Value deleteOptionsObj); static DgnDbStatus SimplifyElementGeometry(DgnDbR db, Napi::Object simplifyArgs); static InlineGeometryPartsResult InlineGeometryParts(DgnDbR db); static Napi::String InsertElementAspect(DgnDbR db, Napi::Object aspectProps); diff --git a/iModelJsNodeAddon/JsInteropDgnDb.cpp b/iModelJsNodeAddon/JsInteropDgnDb.cpp index 572462d8f1..c71319ea29 100644 --- a/iModelJsNodeAddon/JsInteropDgnDb.cpp +++ b/iModelJsNodeAddon/JsInteropDgnDb.cpp @@ -684,7 +684,7 @@ void JsInterop::DeleteElement(DgnDbR dgndb, Utf8StringCR eidStr) { THROW_JS_DGN_DB_EXCEPTION(Env(), "error deleting element", stat); } -DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds) { +DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds, Napi::Value deleteOptionsObj) { DgnElementIdSet elementIdSet; for (auto i = 0U; i < elementIds.Length(); ++i) { Napi::Value arrayItem = elementIds[i]; @@ -696,22 +696,8 @@ DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds) } } - return dgndb.Elements().DeleteElements(elementIdSet); -} - -DgnElementIdSet JsInterop::DeleteDefinitionElements(DgnDbR dgndb, Napi::Array elementIds) { - DgnElementIdSet elementIdSet; - for (auto i = 0U; i < elementIds.Length(); ++i) { - Napi::Value arrayItem = elementIds[i]; - - if (arrayItem.IsString()) { - auto val = BeInt64Id::FromString(arrayItem.As().Utf8Value().c_str()); - if (val.IsValid()) - elementIdSet.insert(DgnElementId(val.GetValue())); - } - } - - return dgndb.Elements().DeleteDefinitionElements(elementIdSet); + BeJsConst deleteOptionsJson(deleteOptionsObj); + return dgndb.Elements().DeleteElements(elementIdSet, deleteOptionsJson.isObject() && deleteOptionsJson.Get(json_skipFkValidation()).asBool()); } /*---------------------------------------------------------------------------------**//** diff --git a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts index fc910883c3..d7c5ae4857 100644 --- a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts +++ b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts @@ -624,8 +624,7 @@ export declare namespace IModelJsNative { public createIModel(fileName: string, props: CreateEmptyStandaloneIModelProps): void; public deleteAllTxns(): void; public deleteElement(elemIdJson: string): void; - public deleteElements(elementIds: Id64Array): Id64Array; - public deleteDefinitionElements(elementIds: Id64Array): Id64Array; + public deleteElements(elementIds: Id64Array, deleteOptions?: { skipFkValidation?: boolean }): Id64Array; public deleteElementAspect(aspectIdJson: string): void; public deleteLinkTableRelationship(props: RelationshipProps): DbResult; public deleteLinkTableRelationships(props: ReadonlyArray): DbResult; From 3c0b66b5fe5f9b2be64072a8be3d1c196ce42aa5 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:21:14 +0530 Subject: [PATCH 21/23] Final minor changes --- .../iModelPlatform/DgnCore/DgnElements.cpp | 10 +++--- .../PublicAPI/DgnPlatform/DgnElement.h | 33 ++++++++++--------- iModelJsNodeAddon/IModelJsNative.h | 1 - iModelJsNodeAddon/JsInteropDgnDb.cpp | 3 +- .../api_package/ts/src/NativeLibrary.ts | 2 +- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index 513b6465b4..cbd06d9663 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -749,7 +749,7 @@ bool BulkElementDeletion::FindAndPruneInUseDefinitionElements() return false; } findDependentsStmt->BindVirtualSet(1, inUse); - if (const auto stat = findDependentsStmt->Step(); stat != BE_SQLITE_OK) + if (const auto stat = findDependentsStmt->Step(); stat != BE_SQLITE_DONE) { LOG.errorv("BulkElementDeletion: Failed to prune in-use definition elements: %s", BeSQLiteLib::GetLogError(stat).c_str()); return false; @@ -974,14 +974,14 @@ DgnElementIdSet BulkElementDeletion::Execute() if (!ExpandElementIdList()) return m_originalElementIds; - if (!m_skipFkValidation && !FindAndPruneConstraintViolators()) + if (!FindAndPruneConstraintViolators()) return m_originalElementIds; // If definition elements exist in the delete set, check usage and null out any externally references type definitions if (!FindAndPruneInUseDefinitionElements()) return m_originalElementIds; - if (!m_skipFkValidation && !FindAndNullTypeDefinitionReferences()) + if (!FindAndNullTypeDefinitionReferences()) return m_originalElementIds; // Fire the pre and post delete callbacks at once @@ -1230,13 +1230,13 @@ DgnDbStatus DgnElements::Delete(DgnElementCR elementIn) /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds, const bool skipFkValidation) +DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds) { DgnDb::VerifyClientThread(); if (elementIds.empty()) return {}; - return BulkElementDeletion(m_dgndb, elementIds, skipFkValidation).Execute(); + return BulkElementDeletion(m_dgndb, elementIds).Execute(); } //--------------------------------------------------------------------------------------- diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index e33a12150b..56e1a51fdc 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -553,7 +553,7 @@ class BulkElementDeletion DgnDbR m_dgndb; DgnElementIdSet m_originalElementIds; DgnElementIdSet m_failedToDelete; - bool m_skipFkValidation = false; + bool m_definitionElementsExist = false; bool m_subModelRootExists = false; @@ -572,7 +572,7 @@ class BulkElementDeletion bool ExecuteDeletion(); public: - BulkElementDeletion(DgnDbR dgndb, const DgnElementIdSet& originalElementIds, const bool skipFkValidation) : m_dgndb(dgndb), m_originalElementIds(originalElementIds), m_skipFkValidation(skipFkValidation) {} + BulkElementDeletion(DgnDbR dgndb, const DgnElementIdSet& originalElementIds) : m_dgndb(dgndb), m_originalElementIds(originalElementIds) {} DgnElementIdSet Execute(); }; @@ -4068,20 +4068,21 @@ struct DgnElements : DgnDbTable //! @note This function can only be safely invoked from the client thread. DGNPLATFORM_EXPORT DgnDbStatus Delete(DgnElementCR element); - /** - * Delete multiple DgnElements from this DgnDb, including their descendants. - * - * This method is intended for general non-definition elements. - * Definition elements need to be handled as per their usage which makes them a special case for element deletion. - * The handlers for definition elements veto deletion unless a purge operation is enabled. - * Hence, for bulk deletion of definition Elements, DeleteDefinitionElements API should be used instead. - * This method will fail to delete definition elements. - * - * @param[in] elementIds The element set to delete. Invalid Ids will be ignored. - * @return A DgnElementIdSet of valid element Ids that failed to delete (either vetoed or blocked by FK/code scope constraints). - * @note This function can only be safely invoked from the client thread. - */ - DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds, const bool skipFkValidation = false); + + //! Bulk-delete a set of elements from this DgnDb. + //! + //! This method resolves intra-set dependencies (parent-child hierarchies, code-scope relationships) before + //! attempting deletion, so callers may pass an entire sub-tree or a group of mutually-scoped elements + //! and expect them all to be removed in one call. + //! + //! @param[in] elementIds The set of element IDs to delete. May contain IDs of any element type. + //! Invalid IDs are ignored. The set may include parent elements whose children are not + //! explicitly listed; those children will be pulled in automatically. + //! @return The subset of elementIds whose elements could not be deleted because they + //! are still referenced or are in use (definition elements) from outside the set. An empty set means every requested element was + //! deleted successfully. + //! @note This function can only be safely invoked from the client thread. + DGNPLATFORM_EXPORT DgnElementIdSet DeleteElements(const DgnElementIdSet& elementIds); //! Delete a DgnElement from this DgnDb by DgnElementId. //! @return DgnDbStatus::Success if the element was deleted, error status otherwise. diff --git a/iModelJsNodeAddon/IModelJsNative.h b/iModelJsNodeAddon/IModelJsNative.h index d83c3ca19f..d0064c3f60 100644 --- a/iModelJsNodeAddon/IModelJsNative.h +++ b/iModelJsNodeAddon/IModelJsNative.h @@ -488,7 +488,6 @@ struct JsInterop { BE_JSON_NAME(writeable) BE_JSON_NAME(yesNo) BE_JSON_NAME(uncompressedSize) - BE_JSON_NAME(skipFkValidation) #define JSON_NAME(__val__) JsInterop::json_##__val__() diff --git a/iModelJsNodeAddon/JsInteropDgnDb.cpp b/iModelJsNodeAddon/JsInteropDgnDb.cpp index c71319ea29..d651928f07 100644 --- a/iModelJsNodeAddon/JsInteropDgnDb.cpp +++ b/iModelJsNodeAddon/JsInteropDgnDb.cpp @@ -696,8 +696,7 @@ DgnElementIdSet JsInterop::DeleteElements(DgnDbR dgndb, Napi::Array elementIds, } } - BeJsConst deleteOptionsJson(deleteOptionsObj); - return dgndb.Elements().DeleteElements(elementIdSet, deleteOptionsJson.isObject() && deleteOptionsJson.Get(json_skipFkValidation()).asBool()); + return dgndb.Elements().DeleteElements(elementIdSet); } /*---------------------------------------------------------------------------------**//** diff --git a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts index d7c5ae4857..1c8aef13d6 100644 --- a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts +++ b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts @@ -624,7 +624,7 @@ export declare namespace IModelJsNative { public createIModel(fileName: string, props: CreateEmptyStandaloneIModelProps): void; public deleteAllTxns(): void; public deleteElement(elemIdJson: string): void; - public deleteElements(elementIds: Id64Array, deleteOptions?: { skipFkValidation?: boolean }): Id64Array; + public deleteElements(elementIds: Id64Array): Id64Array; public deleteElementAspect(aspectIdJson: string): void; public deleteLinkTableRelationship(props: RelationshipProps): DbResult; public deleteLinkTableRelationships(props: ReadonlyArray): DbResult; From 8b53689456bfe7e47e8747d129379ce8c0fc9105 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:24:05 +0530 Subject: [PATCH 22/23] Added log messages --- .../iModelPlatform/DgnCore/DgnElements.cpp | 94 ++++++++++++------- .../PublicAPI/DgnPlatform/DgnElement.h | 3 +- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index cbd06d9663..6fefd4c938 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -470,37 +470,54 @@ DgnElementId ElementAspectIteratorEntry::GetElementId() const {return m_statemen /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -bool BulkElementDeletion::CreateTempTables() const +bool BulkElementDeletion::CreateTempTables() { // Create a temp table to hold the entries for the elements to be deleted - auto stat = m_dgndb.CreateTableIfNotExists(TEMP_TABLE(TEMP_ELEMENT_DELETION), - R"sql( - ElementId INTEGER PRIMARY KEY, - LogicalParentId INTEGER, - IsSubModelRoot INTEGER NOT NULL DEFAULT 0, - Depth INTEGER NOT NULL DEFAULT 0, - IsViolator INTEGER NOT NULL DEFAULT 0 - )sql"); - if (stat != BE_SQLITE_OK) + if (!m_tempTableExists) { - LOG.errorv("Error creating temp table %s: %s", TEMP_TABLE(TEMP_ELEMENT_DELETION), BeSQLiteLib::GetLogError(stat).c_str()); - return false; - } + auto stat = m_dgndb.CreateTableIfNotExists(TEMP_TABLE(TEMP_ELEMENT_DELETION), + R"sql( + ElementId INTEGER PRIMARY KEY, + LogicalParentId INTEGER, + IsSubModelRoot INTEGER NOT NULL DEFAULT 0, + Depth INTEGER NOT NULL DEFAULT 0, + IsViolator INTEGER NOT NULL DEFAULT 0 + )sql"); + if (stat != BE_SQLITE_OK) + { + LOG.errorv("Error prepping elements for bulk deletion: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } - if (BE_SQLITE_OK != m_dgndb.TryExecuteSql("CREATE INDEX IF NOT EXISTS idx_etd_logicalParent ON " TEMP_ELEMENT_DELETION " (LogicalParentId)")) - return false; + if (stat = m_dgndb.TryExecuteSql("CREATE INDEX IF NOT EXISTS idx_etd_logicalParent ON " TEMP_ELEMENT_DELETION " (LogicalParentId)"); stat != BE_SQLITE_OK) + { + LOG.errorv("Error prepping elements for bulk deletion: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } - // Partial index to accelerate IsViolator=1 scans (PruneViolators, deleteAllViolators). - if (BE_SQLITE_OK != m_dgndb.TryExecuteSql("CREATE INDEX IF NOT EXISTS idx_etd_violator ON " TEMP_ELEMENT_DELETION " (IsViolator) WHERE IsViolator = 1")) - return false; + // Partial index to accelerate IsViolator=1 scans (PruneViolators, deleteAllViolators). + if (stat = m_dgndb.TryExecuteSql("CREATE INDEX IF NOT EXISTS idx_etd_violator ON " TEMP_ELEMENT_DELETION " (IsViolator) WHERE IsViolator = 1"); stat != BE_SQLITE_OK) + { + LOG.errorv("Error prepping elements for bulk deletion: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } - // Partial index to accelerate IsSubModelRoot=1 scans (DeleteLinkTableRelationships, ExecuteDeletion). - if (BE_SQLITE_OK != m_dgndb.TryExecuteSql("CREATE INDEX IF NOT EXISTS idx_etd_submodelroot ON " TEMP_ELEMENT_DELETION " (IsSubModelRoot) WHERE IsSubModelRoot = 1")) - return false; + // Partial index to accelerate IsSubModelRoot=1 scans (DeleteLinkTableRelationships, ExecuteDeletion). + if (stat = m_dgndb.TryExecuteSql("CREATE INDEX IF NOT EXISTS idx_etd_submodelroot ON " TEMP_ELEMENT_DELETION " (IsSubModelRoot) WHERE IsSubModelRoot = 1"); stat != BE_SQLITE_OK) + { + LOG.errorv("Error prepping elements for bulk deletion: %s", BeSQLiteLib::GetLogError(stat).c_str()); + return false; + } + + m_tempTableExists = true; + } // Clear out table to avoid stale entries - if (BE_SQLITE_OK != m_dgndb.TryExecuteSql("DELETE FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION))) + if (const auto stat = m_dgndb.TryExecuteSql("DELETE FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION)); stat != BE_SQLITE_OK) + { + LOG.errorv("Error prepping elements for bulk deletion: %s", BeSQLiteLib::GetLogError(stat).c_str()); return false; + } return true; } @@ -554,7 +571,8 @@ bool BulkElementDeletion::ExpandElementIdList() const return false; } - m_dgndb.TryExecuteSql("ANALYZE " TEMP_TABLE(TEMP_ELEMENT_DELETION)); + if (m_originalElementIds.size() >= 100) + m_dgndb.TryExecuteSql("ANALYZE " TEMP_TABLE(TEMP_ELEMENT_DELETION)); return true; } @@ -600,17 +618,24 @@ bool BulkElementDeletion::FindAndPruneConstraintViolators() { // Optimization guard: Don't run the expensive search queries if no violators exist constexpr auto guardSql = - "SELECT 1 FROM bis_Element e " - "WHERE e.CodeScopeId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " - "AND e.Id NOT IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " + "SELECT 1 FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t " + "INNER JOIN bis_Element e ON e.CodeScopeId = t.ElementId " + "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " td ON td.ElementId = e.Id " + "WHERE td.ElementId IS NULL " + "UNION ALL " - "SELECT 1 FROM bis_GeometricElement3d g " - "WHERE g.CategoryId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " - "AND g.ElementId NOT IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " + + "SELECT 1 FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t " + "INNER JOIN bis_GeometricElement3d g ON g.CategoryId = t.ElementId " + "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " td ON td.ElementId = g.ElementId " + "WHERE td.ElementId IS NULL " + "UNION ALL " - "SELECT 1 FROM bis_GeometricElement2d g " - "WHERE g.CategoryId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " - "AND g.ElementId NOT IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ") " + + "SELECT 1 FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t " + "INNER JOIN bis_GeometricElement2d g ON g.CategoryId = t.ElementId " + "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " td ON td.ElementId = g.ElementId " + "WHERE td.ElementId IS NULL " "LIMIT 1"; auto guardStmt = m_dgndb.GetCachedStatement(guardSql); if (guardStmt.IsNull()) @@ -870,9 +895,10 @@ bool BulkElementDeletion::FireAllCallbacks() { element->_OnDelete(); - if (const auto parentId = element->m_parent.m_id; parentId.IsValid()) + if (const auto parentId = element->m_parent.m_id; parentId.IsValid() && !m_originalElementIds.Contains(parentId)) { - if (const auto parentElement = m_dgndb.Elements().GetElement(parentId); parentElement.IsValid()) + const auto parentElement = m_dgndb.Elements().GetElement(parentId); + if (parentElement.IsValid()) { parentElement->_OnChildDelete(*element); parentElement->_OnChildDeleted(*element); diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index 56e1a51fdc..a814d85dcf 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -556,9 +556,10 @@ class BulkElementDeletion bool m_definitionElementsExist = false; bool m_subModelRootExists = false; + bool m_tempTableExists = false; // Create temporary tables for bulk deletion - bool CreateTempTables() const; + bool CreateTempTables(); bool ExpandElementIdList() const; // Find and prune constraint violators From 1fa1bf00e518bffff619362798d5a510308dbcb6 Mon Sep 17 00:00:00 2001 From: RohitPtnkr1996 <111407262+RohitPtnkr1996@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:27:25 +0530 Subject: [PATCH 23/23] Minor optimization for geometric elements --- .../iModelPlatform/DgnCore/DgnElements.cpp | 25 ++++++++++++++++++- .../PublicAPI/DgnPlatform/DgnElement.h | 3 ++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp index 6fefd4c938..cfb5bb3070 100644 --- a/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp +++ b/iModelCore/iModelPlatform/DgnCore/DgnElements.cpp @@ -525,7 +525,7 @@ bool BulkElementDeletion::CreateTempTables() /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ -bool BulkElementDeletion::ExpandElementIdList() const +bool BulkElementDeletion::ExpandElementIdList() { constexpr auto expandSql = "WITH RECURSIVE fullSet(id, logicalParentId, isSubModelRoot, depth) AS (" @@ -574,6 +574,14 @@ bool BulkElementDeletion::ExpandElementIdList() const if (m_originalElementIds.size() >= 100) m_dgndb.TryExecuteSql("ANALYZE " TEMP_TABLE(TEMP_ELEMENT_DELETION)); + // Check if any geometric elements are in the delete set to conditionally optimize the spatial index cleanup. + const auto geomCheckStmt = m_dgndb.GetCachedStatement( + "SELECT 1 FROM " BIS_TABLE(BIS_CLASS_GeometricElement3d) " g " + "WHERE EXISTS (SELECT 1 FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " t WHERE t.ElementId = g.ElementId) " + "LIMIT 1"); + if (geomCheckStmt.IsValid() && geomCheckStmt->Step() == BE_SQLITE_ROW) + m_geometricElementsExist = true; + return true; } @@ -637,6 +645,7 @@ bool BulkElementDeletion::FindAndPruneConstraintViolators() "LEFT JOIN " TEMP_TABLE(TEMP_ELEMENT_DELETION) " td ON td.ElementId = g.ElementId " "WHERE td.ElementId IS NULL " "LIMIT 1"; + auto guardStmt = m_dgndb.GetCachedStatement(guardSql); if (guardStmt.IsNull()) { @@ -929,14 +938,28 @@ bool BulkElementDeletion::ExecuteDeletion() m_dgndb.TryExecuteSql("PRAGMA defer_foreign_keys = false"); }; + // For geometric elements, bulk delete the affected spatial indexes to avoid the deletion trigger + if (m_geometricElementsExist) + { + m_dgndb.TryExecuteSql("DROP TRIGGER IF EXISTS dgn_prjrange_del"); + m_dgndb.TryExecuteSql("DELETE FROM " DGN_VTABLE_SpatialIndex " WHERE ElementId IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ")"); + } + const auto stat = m_dgndb.TryExecuteSql("DELETE FROM " BIS_TABLE(BIS_CLASS_Element) " WHERE Id IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) ")"); if (stat != BE_SQLITE_OK) { + // Recreate the trigger even on failure so we don't leave the db in a bad state + if (m_geometricElementsExist) + m_dgndb.TryExecuteSql("CREATE TRIGGER IF NOT EXISTS dgn_prjrange_del AFTER DELETE ON " BIS_TABLE(BIS_CLASS_GeometricElement3d) " BEGIN DELETE FROM " DGN_VTABLE_SpatialIndex " WHERE ElementId=old.ElementId;END"); LOG.errorv("BulkElementDeletion: Element deletion failed: %s", BeSQLiteLib::GetLogError(stat).c_str()); reset(); return false; } + // Recreate the trigger now that the bulk delete is done + if (m_geometricElementsExist) + m_dgndb.TryExecuteSql("CREATE TRIGGER IF NOT EXISTS dgn_prjrange_del AFTER DELETE ON " BIS_TABLE(BIS_CLASS_GeometricElement3d) " BEGIN DELETE FROM " DGN_VTABLE_SpatialIndex " WHERE ElementId=old.ElementId;END"); + if (m_subModelRootExists) { const auto modelStat = m_dgndb.TryExecuteSql("DELETE FROM " BIS_TABLE(BIS_CLASS_Model) " WHERE Id IN (SELECT ElementId FROM " TEMP_TABLE(TEMP_ELEMENT_DELETION) " WHERE IsSubModelRoot = 1)"); diff --git a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h index a814d85dcf..e626956b33 100644 --- a/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h +++ b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnElement.h @@ -555,12 +555,13 @@ class BulkElementDeletion DgnElementIdSet m_failedToDelete; bool m_definitionElementsExist = false; + bool m_geometricElementsExist = false; bool m_subModelRootExists = false; bool m_tempTableExists = false; // Create temporary tables for bulk deletion bool CreateTempTables(); - bool ExpandElementIdList() const; + bool ExpandElementIdList(); // Find and prune constraint violators bool FindAndPruneConstraintViolators();