Affected module
Backend — openmetadata-service
Describe the bug
TestCaseRepository.deleteChildren() (line 896) has a nested loop where both the outer and inner loops iterate over the same children list. This causes:
- O(N²) delete calls — for N children,
deleteById() is called N×N times instead of N
- Each child is deleted N times instead of once
TestCaseResolutionStatusRepository is re-instantiated on every outer iteration instead of once
- The outer loop variable
entityRelationshipRecord is only used for logging — never for deletion
- The log message uses
hardDelete ? "hard" : "soft" ternary, but the enclosing if (hardDelete) guarantees it always logs "hard"
Additionally, in the same file, deleteAllTestCaseResults() (line 921) calls testCaseResultRepository.deleteAllTestCaseResults(fqn) synchronously and then submits the exact same call asynchronously via asyncExecutor, causing every test case result to be deleted twice.
Current code (deleteChildren):
@Transaction
@Override
protected void deleteChildren(
List<CollectionDAO.EntityRelationshipRecord> children, boolean hardDelete, String updatedBy) {
if (hardDelete) {
for (CollectionDAO.EntityRelationshipRecord entityRelationshipRecord : children) { // outer loop
LOG.info(
"Recursively {} deleting {} {}",
hardDelete ? "hard" : "soft", // always "hard"
entityRelationshipRecord.getType(),
entityRelationshipRecord.getId());
TestCaseResolutionStatusRepository testCaseResolutionStatusRepository =
(TestCaseResolutionStatusRepository)
Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESOLUTION_STATUS);
for (CollectionDAO.EntityRelationshipRecord child : children) { // inner loop — SAME LIST
testCaseResolutionStatusRepository.deleteById(child.getId(), hardDelete);
}
}
}
}
Current code (deleteAllTestCaseResults):
private void deleteAllTestCaseResults(String fqn) {
TestCaseResultRepository testCaseResultRepository =
(TestCaseResultRepository) Entity.getEntityTimeSeriesRepository(TEST_CASE_RESULT);
testCaseResultRepository.deleteAllTestCaseResults(fqn); // sync call
asyncExecutor.submit(
() -> {
try {
testCaseResultRepository.deleteAllTestCaseResults(fqn); // duplicate async call
} catch (Exception e) {
LOG.error("Error deleting test case results for test case {}", fqn, e);
}
});
}
To Reproduce
- Create a
TestCase entity
- Create N
TestCaseResolutionStatus children for it
- Hard-delete the
TestCase
- Observe that
deleteById() is called N² times (N iterations × N children per iteration) instead of N
For deleteAllTestCaseResults:
- Create a
TestCase with test results
- Hard-delete the
TestCase
- Observe two
DELETE queries against data_quality_data_time_series — one sync, one async
Expected behavior
deleteChildren() should iterate children once and call deleteById() exactly N times (O(N))
TestCaseResolutionStatusRepository should be instantiated once, outside the loop
deleteAllTestCaseResults() should delete results exactly once (synchronously, since it runs inside a transaction)
Version:
- OpenMetadata version: main (1.7.0-SNAPSHOT)
Additional context
The base class EntityRepository.deleteChildren() has the correct single-loop pattern. Only the TestCaseRepository override introduces this N² bug.
The deleteById() method in EntityTimeSeriesRepository is idempotent (returns early if entity not found), so the redundant calls don't crash — they just waste N² database queries, which becomes a significant performance issue when deleting test cases with many resolution statuses.
Affected module
Backend —
openmetadata-serviceDescribe the bug
TestCaseRepository.deleteChildren()(line 896) has a nested loop where both the outer and inner loops iterate over the samechildrenlist. This causes:deleteById()is called N×N times instead of NTestCaseResolutionStatusRepositoryis re-instantiated on every outer iteration instead of onceentityRelationshipRecordis only used for logging — never for deletionhardDelete ? "hard" : "soft"ternary, but the enclosingif (hardDelete)guarantees it always logs"hard"Additionally, in the same file,
deleteAllTestCaseResults()(line 921) callstestCaseResultRepository.deleteAllTestCaseResults(fqn)synchronously and then submits the exact same call asynchronously viaasyncExecutor, causing every test case result to be deleted twice.Current code (
deleteChildren):Current code (
deleteAllTestCaseResults):To Reproduce
TestCaseentityTestCaseResolutionStatuschildren for itTestCasedeleteById()is called N² times (N iterations × N children per iteration) instead of NFor
deleteAllTestCaseResults:TestCasewith test resultsTestCaseDELETEqueries againstdata_quality_data_time_series— one sync, one asyncExpected behavior
deleteChildren()should iteratechildrenonce and calldeleteById()exactly N times (O(N))TestCaseResolutionStatusRepositoryshould be instantiated once, outside the loopdeleteAllTestCaseResults()should delete results exactly once (synchronously, since it runs inside a transaction)Version:
Additional context
The base class
EntityRepository.deleteChildren()has the correct single-loop pattern. Only theTestCaseRepositoryoverride introduces this N² bug.The
deleteById()method inEntityTimeSeriesRepositoryis idempotent (returns early if entity not found), so the redundant calls don't crash — they just waste N² database queries, which becomes a significant performance issue when deleting test cases with many resolution statuses.