Skip to content

[Bug] TestCaseRepository.deleteChildren() has O(N²) nested loop bug causing redundant deletes #27060

@RajdeepKushwaha5

Description

@RajdeepKushwaha5

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:

  1. O(N²) delete calls — for N children, deleteById() is called N×N times instead of N
  2. Each child is deleted N times instead of once
  3. TestCaseResolutionStatusRepository is re-instantiated on every outer iteration instead of once
  4. The outer loop variable entityRelationshipRecord is only used for logging — never for deletion
  5. 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

  1. Create a TestCase entity
  2. Create N TestCaseResolutionStatus children for it
  3. Hard-delete the TestCase
  4. Observe that deleteById() is called N² times (N iterations × N children per iteration) instead of N

For deleteAllTestCaseResults:

  1. Create a TestCase with test results
  2. Hard-delete the TestCase
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions