diff --git a/iModelCore/iModelPlatform/DgnCore/DefinitionElementUsageInfo.cpp b/iModelCore/iModelPlatform/DgnCore/DefinitionElementUsageInfo.cpp index 6038fb75fd..1773da301e 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; @@ -90,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; } @@ -215,9 +221,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 +237,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 +253,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 +269,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 +285,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 +303,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 +361,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 +386,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 +419,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 +439,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 +520,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/DgnElement.cpp b/iModelCore/iModelPlatform/DgnCore/DgnElement.cpp index 2426c76fcd..a677b230d1 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..cfb5bb3070 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 +---------------+---------------+---------------+---------------+---------------+------*/ @@ -457,6 +467,587 @@ 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() + { + // Create a temp table to hold the entries for the elements to be deleted + if (!m_tempTableExists) + { + 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 (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 (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 (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 (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; + } + +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +bool BulkElementDeletion::ExpandElementIdList() + { + 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; + } + + 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; + } + +/*---------------------------------------------------------------------------------**//** +* @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 " 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 " 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 " 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()) + { + 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_DONE) + { + 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() && !m_originalElementIds.Contains(parentId)) + { + const auto parentElement = m_dgndb.Elements().GetElement(parentId); + if (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"); + }; + + // 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)"); + 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 (!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 (!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 +---------------+---------------+---------------+---------------+---------------+------*/ @@ -685,6 +1276,18 @@ DgnDbStatus DgnElements::Delete(DgnElementCR elementIn) return DgnDbStatus::Success; } +/*---------------------------------------------------------------------------------**//** +* @bsimethod ++---------------+---------------+---------------+---------------+---------------+------*/ +DgnElementIdSet DgnElements::DeleteElements(const DgnElementIdSet& elementIds) + { + DgnDb::VerifyClientThread(); + if (elementIds.empty()) + return {}; + + return BulkElementDeletion(m_dgndb, elementIds).Execute(); + } + //--------------------------------------------------------------------------------------- // @bsimethod //--------------------------------------------------------------------------------------- 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 71f40fcdc3..e626956b33 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 @@ -278,6 +279,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 +318,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 const& GetUsedIds() const { return m_usedIds; } }; //======================================================================================= @@ -544,6 +547,37 @@ 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_definitionElementsExist = false; + bool m_geometricElementsExist = false; + bool m_subModelRootExists = false; + bool m_tempTableExists = false; + + // Create temporary tables for bulk deletion + bool CreateTempTables(); + bool ExpandElementIdList(); + + // 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) : m_dgndb(dgndb), m_originalElementIds(originalElementIds) {} + DgnElementIdSet Execute(); + }; + #define DGNELEMENT_DECLARE_MEMBERS(__ECClassName__,__superclass__) \ private: typedef __superclass__ T_Super;\ public: static Utf8CP MyHandlerECClassName() {return __ECClassName__;}\ @@ -710,6 +744,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, @@ -3855,6 +3890,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 @@ -3882,6 +3918,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; @@ -3905,6 +3942,9 @@ 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; } public: DGNPLATFORM_EXPORT BeSQLite::SnappyFromMemory& GetSnappyFrom() {return m_snappyFrom;} // NB: Not to be used during loading of a GeometricElement or GeometryPart! @@ -3924,6 +3964,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); @@ -4028,6 +4069,22 @@ 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); + + + //! 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/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnModel.h b/iModelCore/iModelPlatform/PublicAPI/DgnPlatform/DgnModel.h index 29c28dae9e..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 @@ -371,6 +372,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(); diff --git a/iModelJsNodeAddon/IModelJsNative.cpp b/iModelJsNodeAddon/IModelJsNative.cpp index 76fa134759..93e81d1eab 100644 --- a/iModelJsNodeAddon/IModelJsNative.cpp +++ b/iModelJsNodeAddon/IModelJsNative.cpp @@ -1750,6 +1750,22 @@ 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"); + } + + 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) + ret.Set(index++, Napi::String::New(Env(), elemId.ToHexStr().c_str())); + + return ret; + } + Napi::Value QueryDefinitionElementUsage(NapiInfoCR info) { auto& db = GetOpenedDb(info); @@ -3128,6 +3144,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..d0064c3f60 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, 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 93020de173..d651928f07 100644 --- a/iModelJsNodeAddon/JsInteropDgnDb.cpp +++ b/iModelJsNodeAddon/JsInteropDgnDb.cpp @@ -684,6 +684,21 @@ 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 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().DeleteElements(elementIdSet); +} + /*---------------------------------------------------------------------------------**//** * @bsimethod +---------------+---------------+---------------+---------------+---------------+------*/ diff --git a/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts b/iModelJsNodeAddon/api_package/ts/src/NativeLibrary.ts index 4ac9857dbf..1c8aef13d6 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;