mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-01 05:36:58 +00:00
Fix Entity Relationship Cleanup to not deleted entities without repo… (#22400)
* - Fix Entity Relationship Cleanup to not delted entities without repositories * - Add Migration from Test Case to Test Case Resolution * - Typo should be resolution id * Add Tests and Fix Condition * Update BatchSize
This commit is contained in:
parent
4f3385bf74
commit
060df56aaf
@ -6026,6 +6026,13 @@ public interface CollectionDAO {
|
||||
+ "WHERE stateId = :stateId ORDER BY timestamp DESC")
|
||||
List<String> listTestCaseResolutionStatusesForStateId(@Bind("stateId") String stateId);
|
||||
|
||||
@SqlQuery(
|
||||
value =
|
||||
"SELECT json FROM test_case_resolution_status_time_series "
|
||||
+ "WHERE entityFQNHash = :entityFQNHash ORDER BY timestamp DESC")
|
||||
List<String> listTestCaseResolutionForEntityFQNHash(
|
||||
@BindFQN("entityFQNHash") String entityFqnHas);
|
||||
|
||||
@SqlQuery(
|
||||
value =
|
||||
"SELECT json FROM test_case_resolution_status_time_series "
|
||||
|
@ -0,0 +1,20 @@
|
||||
package org.openmetadata.service.migration.mysql.v185;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.openmetadata.service.migration.api.MigrationProcessImpl;
|
||||
import org.openmetadata.service.migration.utils.MigrationFile;
|
||||
import org.openmetadata.service.migration.utils.v185.MigrationUtil;
|
||||
|
||||
public class Migration extends MigrationProcessImpl {
|
||||
|
||||
public Migration(MigrationFile migrationFile) {
|
||||
super(migrationFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void runDataMigration() {
|
||||
MigrationUtil migrationUtil = new MigrationUtil(collectionDAO);
|
||||
migrationUtil.createTestCaseToTestCaseResolutionRelation();
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package org.openmetadata.service.migration.postgres.v185;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.openmetadata.service.migration.api.MigrationProcessImpl;
|
||||
import org.openmetadata.service.migration.utils.MigrationFile;
|
||||
import org.openmetadata.service.migration.utils.v185.MigrationUtil;
|
||||
|
||||
public class Migration extends MigrationProcessImpl {
|
||||
|
||||
public Migration(MigrationFile migrationFile) {
|
||||
super(migrationFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void runDataMigration() {
|
||||
MigrationUtil migrationUtil = new MigrationUtil(collectionDAO);
|
||||
migrationUtil.createTestCaseToTestCaseResolutionRelation();
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
package org.openmetadata.service.migration.utils.v185;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.openmetadata.schema.tests.TestCase;
|
||||
import org.openmetadata.schema.tests.type.TestCaseResolutionStatus;
|
||||
import org.openmetadata.schema.type.Include;
|
||||
import org.openmetadata.schema.type.Relationship;
|
||||
import org.openmetadata.schema.utils.JsonUtils;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.jdbi3.CollectionDAO;
|
||||
import org.openmetadata.service.jdbi3.EntityDAO;
|
||||
import org.openmetadata.service.jdbi3.ListFilter;
|
||||
import org.openmetadata.service.jdbi3.TestCaseRepository;
|
||||
import org.openmetadata.service.util.EntityUtil;
|
||||
import org.openmetadata.service.util.RestUtil;
|
||||
import org.openmetadata.service.util.ResultList;
|
||||
|
||||
@Slf4j
|
||||
public class MigrationUtil {
|
||||
|
||||
private final CollectionDAO collectionDAO;
|
||||
|
||||
public MigrationUtil(CollectionDAO collectionDAO) {
|
||||
this.collectionDAO = collectionDAO;
|
||||
}
|
||||
|
||||
public void createTestCaseToTestCaseResolutionRelation() {
|
||||
LOG.info(
|
||||
"Starting migration to recreate test case to test case resolution status relationships");
|
||||
|
||||
try {
|
||||
TestCaseRepository testCaseRepository =
|
||||
(TestCaseRepository) Entity.getEntityRepository(Entity.TEST_CASE);
|
||||
EntityDAO<TestCase> testCaseDAO = testCaseRepository.getDao();
|
||||
// Process test cases in batches
|
||||
int batchSize = 100;
|
||||
int relationshipsCreated = 0;
|
||||
int processedCount = 0;
|
||||
int offset = 0;
|
||||
String cursor = RestUtil.encodeCursor("0");
|
||||
int total = testCaseDAO.listCount(new ListFilter(Include.ALL));
|
||||
LOG.info("Processing TestCases: total={}", total);
|
||||
|
||||
while (offset < total) {
|
||||
ResultList<TestCase> testCases =
|
||||
testCaseRepository.listWithOffset(
|
||||
testCaseDAO::listAfter,
|
||||
testCaseDAO::listCount,
|
||||
new ListFilter(Include.ALL),
|
||||
batchSize,
|
||||
cursor,
|
||||
true,
|
||||
EntityUtil.Fields.EMPTY_FIELDS,
|
||||
null);
|
||||
|
||||
LOG.info(
|
||||
"Processing batch: offset={}, size={}",
|
||||
RestUtil.decodeCursor(cursor),
|
||||
testCases.getData().size());
|
||||
|
||||
for (TestCase testCase : testCases.getData()) {
|
||||
processedCount++;
|
||||
try {
|
||||
// Check if this test case has any test case resolution status records
|
||||
List<TestCaseResolutionStatus> resolutionStatuses =
|
||||
JsonUtils.readObjects(
|
||||
collectionDAO
|
||||
.testCaseResolutionStatusTimeSeriesDao()
|
||||
.listTestCaseResolutionForEntityFQNHash(testCase.getFullyQualifiedName()),
|
||||
TestCaseResolutionStatus.class);
|
||||
|
||||
if (!resolutionStatuses.isEmpty()) {
|
||||
// Group by stateId to get unique resolution status records
|
||||
Set<UUID> uniqueResolutionId =
|
||||
resolutionStatuses.stream()
|
||||
.map(TestCaseResolutionStatus::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
LOG.debug(
|
||||
"Test case {} has {} unique resolution status states",
|
||||
testCase.getFullyQualifiedName(),
|
||||
uniqueResolutionId.size());
|
||||
|
||||
// Create parent-child relationship for each unique state
|
||||
for (UUID resolutionId : uniqueResolutionId) {
|
||||
try {
|
||||
collectionDAO
|
||||
.relationshipDAO()
|
||||
.insert(
|
||||
testCase.getId(),
|
||||
resolutionId,
|
||||
Entity.TEST_CASE,
|
||||
Entity.TEST_CASE_RESOLUTION_STATUS,
|
||||
Relationship.PARENT_OF.ordinal(),
|
||||
null);
|
||||
} catch (Exception e) {
|
||||
LOG.error(
|
||||
"Failed to create relationship for test case {} and state {}: {}",
|
||||
testCase.getFullyQualifiedName(),
|
||||
resolutionId,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.error(
|
||||
"Error processing test case {}: {}",
|
||||
testCase.getFullyQualifiedName(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
LOG.info(
|
||||
"Processed {} test cases so far, created {} relationships",
|
||||
processedCount,
|
||||
relationshipsCreated);
|
||||
|
||||
offset += testCases.getData().size();
|
||||
cursor = RestUtil.encodeCursor(String.valueOf(offset));
|
||||
}
|
||||
|
||||
LOG.info(
|
||||
"Migration completed. Processed {} test cases, created {} new relationships",
|
||||
processedCount,
|
||||
relationshipsCreated);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to complete test case to test case resolution status migration", e);
|
||||
throw new RuntimeException("Migration failed", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -31,19 +31,22 @@ import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.exception.EntityNotFoundException;
|
||||
import org.openmetadata.service.jdbi3.CollectionDAO;
|
||||
import org.openmetadata.service.jdbi3.EntityRepository;
|
||||
import org.openmetadata.service.jdbi3.EntityTimeSeriesRepository;
|
||||
|
||||
@Slf4j
|
||||
public class EntityRelationshipCleanup {
|
||||
|
||||
private final CollectionDAO collectionDAO;
|
||||
private final Map<String, EntityRepository<?>> entityRepositories;
|
||||
private final Map<String, EntityRepository<?>> entityRepositories = new HashMap<>();
|
||||
private final Map<String, EntityTimeSeriesRepository<?>> entityTimeSeriesRepositoy =
|
||||
new HashMap<>();
|
||||
private final boolean dryRun;
|
||||
|
||||
public EntityRelationshipCleanup(CollectionDAO collectionDAO, boolean dryRun) {
|
||||
this.collectionDAO = collectionDAO;
|
||||
this.dryRun = dryRun;
|
||||
this.entityRepositories = new HashMap<>();
|
||||
initializeEntityRepositories();
|
||||
initializeTimeSeriesRepositories();
|
||||
}
|
||||
|
||||
@Data
|
||||
@ -84,6 +87,17 @@ public class EntityRelationshipCleanup {
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeTimeSeriesRepositories() {
|
||||
for (String entityType : Entity.getEntityList()) {
|
||||
try {
|
||||
EntityTimeSeriesRepository<?> repository = Entity.getEntityTimeSeriesRepository(entityType);
|
||||
entityTimeSeriesRepositoy.put(entityType, repository);
|
||||
} catch (EntityNotFoundException e) {
|
||||
LOG.error("No repository found for entity type: {}", entityType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public EntityCleanupResult performCleanup(int batchSize) {
|
||||
LOG.info(
|
||||
"Starting entity relationship cleanup. Dry run: {}, Batch size: {}", dryRun, batchSize);
|
||||
@ -178,6 +192,23 @@ public class EntityRelationshipCleanup {
|
||||
UUID toId = UUID.fromString(relationship.getToId());
|
||||
String fromEntity = relationship.getFromEntity();
|
||||
String toEntity = relationship.getToEntity();
|
||||
|
||||
// Check if fromEntity has any repository
|
||||
boolean fromEntityHasNoRepository = doEntityHaveAnyRepository(fromEntity);
|
||||
if (!fromEntityHasNoRepository) {
|
||||
LOG.error(
|
||||
"No repository found for from entity type: {}, the entity will not be cleaned",
|
||||
fromEntity);
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean toEntityHasNoRepository = doEntityHaveAnyRepository(toEntity);
|
||||
if (!toEntityHasNoRepository) {
|
||||
LOG.error(
|
||||
"No repository found for to entity type: {}, the entity will not be cleaned", toEntity);
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean fromExists = entityExists(fromId, fromEntity);
|
||||
boolean toExists = entityExists(toId, toEntity);
|
||||
|
||||
@ -223,23 +254,35 @@ public class EntityRelationshipCleanup {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doEntityHaveAnyRepository(String entityType) {
|
||||
return entityRepositories.containsKey(entityType)
|
||||
|| entityTimeSeriesRepositoy.containsKey(entityType);
|
||||
}
|
||||
|
||||
private boolean entityExists(UUID entityId, String entityType) {
|
||||
boolean existsInEntityRepo = checkInEntityRepository(entityId, entityType);
|
||||
boolean existsInTimeSeriesRepo = checkInEntityTimeSeriesRepository(entityId, entityType);
|
||||
return existsInEntityRepo || existsInTimeSeriesRepo;
|
||||
}
|
||||
|
||||
private boolean checkInEntityRepository(UUID entityId, String entityType) {
|
||||
try {
|
||||
EntityRepository<?> repository = entityRepositories.get(entityType);
|
||||
if (repository == null) {
|
||||
LOG.debug("No repository found for entity type: {}", entityType);
|
||||
return false;
|
||||
}
|
||||
repository.get(null, entityId, EntityUtil.Fields.EMPTY_FIELDS, ALL, false);
|
||||
return true;
|
||||
} catch (EntityNotFoundException e) {
|
||||
LOG.debug("Entity {}:{} not found", entityType, entityId);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
LOG.debug(
|
||||
"Error checking existence of entity {}:{}: {}", entityType, entityId, e.getMessage());
|
||||
LOG.debug("Entity {}:{} not found in repository: {}", entityType, entityId, e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkInEntityTimeSeriesRepository(UUID entityId, String entityType) {
|
||||
EntityTimeSeriesRepository<?> repository = entityTimeSeriesRepositoy.get(entityType);
|
||||
if (repository == null) {
|
||||
LOG.debug("No repository found for entity type: {}", entityType);
|
||||
return false;
|
||||
}
|
||||
return repository.getById(entityId) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -479,4 +479,191 @@ class EntityRelationshipCleanupTest extends OpenMetadataApplicationTest {
|
||||
deleteResult.getRelationshipsDeleted() >= 0,
|
||||
"Delete mode should delete 0 or more relationships");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
void test_entityWithTimeSeriesRepository_shouldNotBeCleanedWhenExists() {
|
||||
UUID testCaseId = UUID.randomUUID();
|
||||
UUID testCaseResolutionId = UUID.randomUUID();
|
||||
UUID tableId = testTables.get(0).getId();
|
||||
|
||||
collectionDAO
|
||||
.relationshipDAO()
|
||||
.insert(
|
||||
testCaseId,
|
||||
tableId,
|
||||
Entity.TABLE,
|
||||
Entity.TEST_CASE,
|
||||
Relationship.CONTAINS.ordinal(),
|
||||
null);
|
||||
|
||||
collectionDAO
|
||||
.relationshipDAO()
|
||||
.insert(
|
||||
testCaseId,
|
||||
testCaseResolutionId,
|
||||
Entity.TEST_CASE,
|
||||
Entity.TEST_CASE_RESULT,
|
||||
Relationship.PARENT_OF.ordinal(),
|
||||
null);
|
||||
|
||||
EntityRelationshipCleanup cleanup = new EntityRelationshipCleanup(collectionDAO, true);
|
||||
EntityRelationshipCleanup.EntityCleanupResult result = cleanup.performCleanup(100);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(
|
||||
result.getOrphanedRelationshipsFound() > 0,
|
||||
"Should find orphaned relationships for non-existent time series entities");
|
||||
|
||||
boolean foundTestCaseOrphan =
|
||||
result.getOrphanedRelationships().stream()
|
||||
.anyMatch(
|
||||
orphan ->
|
||||
testCaseId.toString().equals(orphan.getFromId())
|
||||
|| testCaseId.toString().equals(orphan.getToId()));
|
||||
|
||||
boolean foundTestCaseResultOrphan =
|
||||
result.getOrphanedRelationships().stream()
|
||||
.anyMatch(
|
||||
orphan ->
|
||||
testCaseResolutionId.toString().equals(orphan.getFromId())
|
||||
|| testCaseResolutionId.toString().equals(orphan.getToId()));
|
||||
|
||||
assertTrue(foundTestCaseOrphan, "Should find orphaned relationship for non-existent testCase");
|
||||
assertTrue(
|
||||
foundTestCaseResultOrphan,
|
||||
"Should find orphaned relationship for non-existent testCaseResult");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
void test_entityWithoutAnyRepository_shouldNotBeCleanedUpEvenIfRelationshipExists() {
|
||||
UUID nonExistentId1 = UUID.randomUUID();
|
||||
UUID nonExistentId2 = UUID.randomUUID();
|
||||
UUID tableId = testTables.get(0).getId();
|
||||
|
||||
collectionDAO
|
||||
.relationshipDAO()
|
||||
.insert(
|
||||
nonExistentId1,
|
||||
tableId,
|
||||
"nonExistentEntityType",
|
||||
Entity.TABLE,
|
||||
Relationship.CONTAINS.ordinal(),
|
||||
null);
|
||||
|
||||
collectionDAO
|
||||
.relationshipDAO()
|
||||
.insert(
|
||||
tableId,
|
||||
nonExistentId2,
|
||||
Entity.TABLE,
|
||||
"anotherNonExistentEntityType",
|
||||
Relationship.CONTAINS.ordinal(),
|
||||
null);
|
||||
|
||||
EntityRelationshipCleanup cleanup = new EntityRelationshipCleanup(collectionDAO, true);
|
||||
EntityRelationshipCleanup.EntityCleanupResult result = cleanup.performCleanup(100);
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
boolean foundNonExistentFromEntity =
|
||||
result.getOrphanedRelationships().stream()
|
||||
.anyMatch(orphan -> nonExistentId1.toString().equals(orphan.getFromId()));
|
||||
|
||||
boolean foundNonExistentToEntity =
|
||||
result.getOrphanedRelationships().stream()
|
||||
.anyMatch(orphan -> nonExistentId2.toString().equals(orphan.getToId()));
|
||||
|
||||
assertFalse(
|
||||
foundNonExistentFromEntity,
|
||||
"Should NOT find orphaned relationship for entity without repository (from)");
|
||||
assertFalse(
|
||||
foundNonExistentToEntity,
|
||||
"Should NOT find orphaned relationship for entity without repository (to)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
void test_mixedEntityTypes_onlyValidRepositoryEntitiesAreProcessed() {
|
||||
UUID testCaseId = UUID.randomUUID();
|
||||
UUID queryCostId = UUID.randomUUID();
|
||||
UUID workflowInstanceId = UUID.randomUUID();
|
||||
UUID invalidEntityId = UUID.randomUUID();
|
||||
UUID tableId = testTables.get(0).getId();
|
||||
|
||||
collectionDAO
|
||||
.relationshipDAO()
|
||||
.insert(
|
||||
testCaseId,
|
||||
tableId,
|
||||
Entity.TEST_CASE,
|
||||
Entity.TABLE,
|
||||
Relationship.TESTED_BY.ordinal(),
|
||||
null);
|
||||
|
||||
collectionDAO
|
||||
.relationshipDAO()
|
||||
.insert(
|
||||
queryCostId,
|
||||
tableId,
|
||||
Entity.QUERY_COST_RECORD,
|
||||
Entity.TABLE,
|
||||
Relationship.RELATED_TO.ordinal(),
|
||||
null);
|
||||
|
||||
collectionDAO
|
||||
.relationshipDAO()
|
||||
.insert(
|
||||
workflowInstanceId,
|
||||
tableId,
|
||||
Entity.WORKFLOW_INSTANCE,
|
||||
Entity.TABLE,
|
||||
Relationship.HAS.ordinal(),
|
||||
null);
|
||||
|
||||
collectionDAO
|
||||
.relationshipDAO()
|
||||
.insert(
|
||||
invalidEntityId,
|
||||
tableId,
|
||||
"invalidEntityType",
|
||||
Entity.TABLE,
|
||||
Relationship.CONTAINS.ordinal(),
|
||||
null);
|
||||
|
||||
EntityRelationshipCleanup cleanup = new EntityRelationshipCleanup(collectionDAO, true);
|
||||
EntityRelationshipCleanup.EntityCleanupResult result = cleanup.performCleanup(100);
|
||||
|
||||
assertNotNull(result);
|
||||
|
||||
boolean foundTestCaseOrphan =
|
||||
result.getOrphanedRelationships().stream()
|
||||
.anyMatch(orphan -> testCaseId.toString().equals(orphan.getFromId()));
|
||||
|
||||
boolean foundQueryCostOrphan =
|
||||
result.getOrphanedRelationships().stream()
|
||||
.anyMatch(orphan -> queryCostId.toString().equals(orphan.getFromId()));
|
||||
|
||||
boolean foundWorkflowInstanceOrphan =
|
||||
result.getOrphanedRelationships().stream()
|
||||
.anyMatch(orphan -> workflowInstanceId.toString().equals(orphan.getFromId()));
|
||||
|
||||
boolean foundInvalidEntityOrphan =
|
||||
result.getOrphanedRelationships().stream()
|
||||
.anyMatch(orphan -> invalidEntityId.toString().equals(orphan.getFromId()));
|
||||
|
||||
assertTrue(
|
||||
foundTestCaseOrphan,
|
||||
"Should find orphaned relationship for non-existent testCase (time series entity)");
|
||||
assertTrue(
|
||||
foundQueryCostOrphan,
|
||||
"Should find orphaned relationship for non-existent queryCostRecord (time series entity)");
|
||||
assertTrue(
|
||||
foundWorkflowInstanceOrphan,
|
||||
"Should find orphaned relationship for non-existent workflowInstance (time series entity)");
|
||||
assertFalse(
|
||||
foundInvalidEntityOrphan,
|
||||
"Should NOT find orphaned relationship for invalid entity type without repository");
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user