diff --git a/openmetadata-service/pom.xml b/openmetadata-service/pom.xml
index 45fc311e15c..cfbf337038d 100644
--- a/openmetadata-service/pom.xml
+++ b/openmetadata-service/pom.xml
@@ -335,6 +335,12 @@
2.40
test
+
+ org.assertj
+ assertj-core
+ 3.25.3
+ test
+
javax.json
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java
index ae0d2fdcaa4..11cb5f60741 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java
@@ -409,13 +409,15 @@ public class TestCaseRepository extends EntityRepository {
updateResultSummaries(testCase, isDeleted, resultSummaries, resultSummary);
// Update test case result summary attribute for the test suite
+ TestSuiteRepository testSuiteRepository =
+ (TestSuiteRepository) Entity.getEntityRepository(Entity.TEST_SUITE);
+ TestSuite original =
+ TestSuiteRepository.copyTestSuite(
+ testSuite); // we'll need the original state to update the test suite
testSuite.setTestCaseResultSummary(resultSummaries);
- daoCollection
- .testSuiteDAO()
- .update(
- testSuite.getId(),
- testSuite.getFullyQualifiedName(),
- JsonUtils.pojoToJson(testSuite));
+ EntityRepository.EntityUpdater testSuiteUpdater =
+ testSuiteRepository.getUpdater(original, testSuite, Operation.PUT);
+ testSuiteUpdater.update();
}
}
@@ -652,11 +654,16 @@ public class TestCaseRepository extends EntityRepository {
testSuite.setSummary(null); // we don't want to store the summary in the database
List resultSummaries = testSuite.getTestCaseResultSummary();
resultSummaries.removeIf(summary -> summary.getTestCaseName().equals(testCaseFqn));
+
+ TestSuiteRepository testSuiteRepository =
+ (TestSuiteRepository) Entity.getEntityRepository(Entity.TEST_SUITE);
+ TestSuite original =
+ TestSuiteRepository.copyTestSuite(
+ testSuite); // we'll need the original state to update the test suite
testSuite.setTestCaseResultSummary(resultSummaries);
- daoCollection
- .testSuiteDAO()
- .update(
- testSuite.getId(), testSuite.getFullyQualifiedName(), JsonUtils.pojoToJson(testSuite));
+ EntityRepository.EntityUpdater testSuiteUpdater =
+ testSuiteRepository.getUpdater(original, testSuite, Operation.PUT);
+ testSuiteUpdater.update();
}
@Override
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestSuiteRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestSuiteRepository.java
index bbb50770542..c85e618a405 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestSuiteRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestSuiteRepository.java
@@ -9,10 +9,14 @@ import static org.openmetadata.service.Entity.TEST_CASE;
import static org.openmetadata.service.Entity.TEST_SUITE;
import static org.openmetadata.service.util.FullyQualifiedName.quoteName;
+import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+import javax.json.JsonValue;
import javax.ws.rs.core.SecurityContext;
import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.sqlobject.transaction.Transaction;
@@ -23,7 +27,6 @@ import org.openmetadata.schema.tests.type.TestCaseStatus;
import org.openmetadata.schema.tests.type.TestSummary;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.EventType;
-import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.dqtests.TestSuiteResource;
@@ -55,7 +58,7 @@ public class TestSuiteRepository extends EntityRepository {
fields.contains("pipelines") ? getIngestionPipelines(entity) : entity.getPipelines());
entity.setSummary(
fields.contains("summary") ? getTestCasesExecutionSummary(entity) : entity.getSummary());
- entity.withTests(fields.contains("tests") ? getTestCases(entity) : entity.getTests());
+ entity.withTests(fields.contains(UPDATE_FIELDS) ? getTestCases(entity) : entity.getTests());
}
@Override
@@ -71,7 +74,7 @@ public class TestSuiteRepository extends EntityRepository {
public void clearFields(TestSuite entity, EntityUtil.Fields fields) {
entity.setPipelines(fields.contains("pipelines") ? entity.getPipelines() : null);
entity.setSummary(fields.contains("summary") ? entity.getSummary() : null);
- entity.withTests(fields.contains("tests") ? entity.getTests() : null);
+ entity.withTests(fields.contains(UPDATE_FIELDS) ? entity.getTests() : null);
}
private TestSummary buildTestSummary(Map testCaseSummary) {
@@ -117,31 +120,81 @@ public class TestSuiteRepository extends EntityRepository {
return buildTestSummary(testCaseSummary);
}
- private TestSummary getTestCasesExecutionSummary(List entities) {
- if (entities.isEmpty()) return new TestSummary();
- Map testsSummary = new HashMap<>();
- for (TestSuite testSuite : entities) {
- Map testSummary = getResultSummary(testSuite);
- for (Map.Entry entry : testSummary.entrySet()) {
- testsSummary.put(
- entry.getKey(), testsSummary.getOrDefault(entry.getKey(), 0) + entry.getValue());
+ private TestSummary getTestCasesExecutionSummary(JsonObject aggregation) {
+ // Initialize the test summary with 0 values
+ TestSummary testSummary =
+ new TestSummary().withAborted(0).withFailed(0).withSuccess(0).withQueued(0).withTotal(0);
+ JsonObject summary = aggregation.getJsonObject("nested#testCaseResultSummary");
+ testSummary.setTotal(summary.getJsonNumber("doc_count").intValue());
+
+ JsonObject statusCount = summary.getJsonObject("sterms#status_counts");
+ JsonArray buckets = statusCount.getJsonArray("buckets");
+
+ for (JsonValue bucket : buckets) {
+ String key = ((JsonObject) bucket).getString("key");
+ Integer count = ((JsonObject) bucket).getJsonNumber("doc_count").intValue();
+ switch (key) {
+ case "Success":
+ testSummary.setSuccess(count);
+ break;
+ case "Failed":
+ testSummary.setFailed(count);
+ break;
+ case "Aborted":
+ testSummary.setAborted(count);
+ break;
+ case "Queued":
+ testSummary.setQueued(count);
+ break;
}
- testSuite.getTestCaseResultSummary().size();
}
- return buildTestSummary(testsSummary);
+ return testSummary;
}
- public TestSummary getTestSummary(UUID testSuiteId) {
+ public TestSummary getTestSummary(UUID testSuiteId) throws IOException {
+ String aggregationQuery =
+ """
+ {
+ "aggregations": {
+ "test_case_results": {
+ "nested": {
+ "path": "testCaseResultSummary"
+ },
+ "aggs": {
+ "status_counts": {
+ "terms": {
+ "field": "testCaseResultSummary.status"
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+ JsonObject aggregationJson = JsonUtils.readJson(aggregationQuery).asJsonObject();
TestSummary testSummary;
if (testSuiteId == null) {
- ListFilter filter = new ListFilter();
- filter.addQueryParam("testSuiteType", "executable");
- List testSuites = listAll(EntityUtil.Fields.EMPTY_FIELDS, filter);
- testSummary = getTestCasesExecutionSummary(testSuites);
+ JsonObject testCaseResultSummary =
+ searchRepository.aggregate(null, TEST_SUITE, aggregationJson);
+ testSummary = getTestCasesExecutionSummary(testCaseResultSummary);
} else {
+ String query =
+ """
+ {
+ "query": {
+ "bool": {
+ "must": {
+ "term": {"id": "%s"}
+ }
+ }
+ }
+ }
+ """
+ .formatted(testSuiteId);
// don't want to get it from the cache as test results summary may be stale
- TestSuite testSuite = Entity.getEntity(TEST_SUITE, testSuiteId, "", Include.ALL, false);
- testSummary = getTestCasesExecutionSummary(testSuite);
+ JsonObject testCaseResultSummary =
+ searchRepository.aggregate(query, TEST_SUITE, aggregationJson);
+ testSummary = getTestCasesExecutionSummary(testCaseResultSummary);
}
return testSummary;
}
@@ -211,6 +264,26 @@ public class TestSuiteRepository extends EntityRepository {
return new RestUtil.DeleteResponse<>(updated, changeType);
}
+ public static TestSuite copyTestSuite(TestSuite testSuite) {
+ return new TestSuite()
+ .withConnection(testSuite.getConnection())
+ .withDescription(testSuite.getDescription())
+ .withChangeDescription(testSuite.getChangeDescription())
+ .withDeleted(testSuite.getDeleted())
+ .withDisplayName(testSuite.getDisplayName())
+ .withFullyQualifiedName(testSuite.getFullyQualifiedName())
+ .withHref(testSuite.getHref())
+ .withId(testSuite.getId())
+ .withName(testSuite.getName())
+ .withExecutable(testSuite.getExecutable())
+ .withExecutableEntityReference(testSuite.getExecutableEntityReference())
+ .withServiceType(testSuite.getServiceType())
+ .withOwner(testSuite.getOwner())
+ .withUpdatedBy(testSuite.getUpdatedBy())
+ .withUpdatedAt(testSuite.getUpdatedAt())
+ .withVersion(testSuite.getVersion());
+ }
+
public class TestSuiteUpdater extends EntityUpdater {
public TestSuiteUpdater(TestSuite original, TestSuite updated, Operation operation) {
super(original, updated, operation);
@@ -221,7 +294,13 @@ public class TestSuiteRepository extends EntityRepository {
public void entitySpecificUpdate() {
List origTests = listOrEmpty(original.getTests());
List updatedTests = listOrEmpty(updated.getTests());
- recordChange("tests", origTests, updatedTests);
+ List origTestCaseResultSummary =
+ listOrEmpty(original.getTestCaseResultSummary());
+ List updatedTestCaseResultSummary =
+ listOrEmpty(updated.getTestCaseResultSummary());
+ recordChange(UPDATE_FIELDS, origTests, updatedTests);
+ recordChange(
+ "testCaseResultSummary", origTestCaseResultSummary, updatedTestCaseResultSummary);
}
}
}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java
index 7bf403ae1fe..553577e9b4c 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java
@@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
+import java.io.IOException;
import java.util.List;
import java.util.UUID;
import javax.json.JsonPatch;
@@ -321,7 +322,8 @@ public class TestSuiteResource extends EntityResource resourceContext = getResourceContext();
OperationContext operationContext =
new OperationContext(Entity.TABLE, MetadataOperation.VIEW_TESTS);
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClient.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClient.java
index d67843c3f5a..b6ffee7c7ad 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClient.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClient.java
@@ -8,6 +8,7 @@ import java.text.ParseException;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
+import javax.json.JsonObject;
import javax.net.ssl.SSLContext;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.tuple.Pair;
@@ -87,6 +88,8 @@ public interface SearchClient {
Response aggregate(String index, String fieldName, String value, String query) throws IOException;
+ JsonObject aggregate(String query, String index, JsonObject aggregationJson) throws IOException;
+
Response suggest(SearchRequest request) throws IOException;
void createEntity(String indexName, String docId, String doc);
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
index 36d22d87787..353d93f5133 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
@@ -669,6 +669,11 @@ public class SearchRepository {
return searchClient.aggregate(index, fieldName, value, query);
}
+ public JsonObject aggregate(String query, String index, JsonObject aggregationJson)
+ throws IOException {
+ return searchClient.aggregate(query, index, aggregationJson);
+ }
+
public Response suggest(SearchRequest request) throws IOException {
return searchClient.suggest(request);
}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchClient.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchClient.java
index 4d7bc8cb68c..e8bd90066bb 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchClient.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchClient.java
@@ -105,6 +105,7 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
+import javax.json.JsonObject;
import javax.net.ssl.SSLContext;
import javax.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
@@ -704,6 +705,82 @@ public class ElasticSearchClient implements SearchClient {
return Response.status(OK).entity(response).build();
}
+ /*
+ Build dynamic aggregation from elasticsearch JSON like aggregation query.
+ See TestSuiteResourceTest for example usage (ln. 506) for tested aggregation query.
+
+ @param aggregations - JsonObject containing the aggregation query
+ */
+ public static List buildAggregation(JsonObject aggregations) {
+ List aggregationBuilders = new ArrayList<>();
+ for (String key : aggregations.keySet()) {
+ JsonObject aggregation = aggregations.getJsonObject(key);
+ for (String aggregationType : aggregation.keySet()) {
+ switch (aggregationType) {
+ case "terms":
+ JsonObject termAggregation = aggregation.getJsonObject(aggregationType);
+ TermsAggregationBuilder termsAggregationBuilder =
+ AggregationBuilders.terms(key).field(termAggregation.getString("field"));
+ aggregationBuilders.add(termsAggregationBuilder);
+ break;
+ case "nested":
+ JsonObject nestedAggregation = aggregation.getJsonObject("nested");
+ AggregationBuilder nestedAggregationBuilder =
+ AggregationBuilders.nested(
+ nestedAggregation.getString("path"), nestedAggregation.getString("path"));
+ JsonObject nestedAggregations = aggregation.getJsonObject("aggs");
+
+ List nestedAggregationBuilders =
+ buildAggregation(nestedAggregations);
+ for (AggregationBuilder nestedAggregationBuilder1 : nestedAggregationBuilders) {
+ nestedAggregationBuilder.subAggregation(nestedAggregationBuilder1);
+ }
+ aggregationBuilders.add(nestedAggregationBuilder);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ return aggregationBuilders;
+ }
+
+ @Override
+ public JsonObject aggregate(String query, String index, JsonObject aggregationJson)
+ throws IOException {
+ JsonObject aggregations = aggregationJson.getJsonObject("aggregations");
+ if (aggregations == null) {
+ return null;
+ }
+
+ List aggregationBuilder = buildAggregation(aggregations);
+ es.org.elasticsearch.action.search.SearchRequest searchRequest =
+ new es.org.elasticsearch.action.search.SearchRequest(
+ Entity.getSearchRepository().getIndexOrAliasName(index));
+ SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
+ if (query != null) {
+ XContentParser queryParser =
+ XContentType.JSON
+ .xContent()
+ .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, query);
+ QueryBuilder parsedQuery = SearchSourceBuilder.fromXContent(queryParser).query();
+ BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(parsedQuery);
+ searchSourceBuilder.query(boolQueryBuilder);
+ }
+
+ searchSourceBuilder.size(0).timeout(new TimeValue(30, TimeUnit.SECONDS));
+
+ for (AggregationBuilder aggregation : aggregationBuilder) {
+ searchSourceBuilder.aggregation(aggregation);
+ }
+
+ searchRequest.source(searchSourceBuilder);
+
+ String response = client.search(searchRequest, RequestOptions.DEFAULT).toString();
+ JsonObject jsonResponse = JsonUtils.readJson(response).asJsonObject();
+ return jsonResponse.getJsonObject("aggregations");
+ }
+
private static ScriptScoreFunctionBuilder boostScore() {
return ScoreFunctionBuilders.scriptFunction(
"double score = _score;"
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java
index 6cb2fa66a0b..36ed01b4d73 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchClient.java
@@ -37,6 +37,7 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
+import javax.json.JsonObject;
import javax.net.ssl.SSLContext;
import javax.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
@@ -724,6 +725,82 @@ public class OpenSearchClient implements SearchClient {
return Response.status(OK).entity(response).build();
}
+ /*
+ Build dynamic aggregation from elasticsearch JSON like aggregation query.
+ See TestSuiteResourceTest for example usage (ln. 506) for tested aggregation query.
+
+ @param aggregations - JsonObject containing the aggregation query
+ */
+ public static List buildAggregation(JsonObject aggregations) {
+ List aggregationBuilders = new ArrayList<>();
+ for (String key : aggregations.keySet()) {
+ JsonObject aggregation = aggregations.getJsonObject(key);
+ for (String aggregationType : aggregation.keySet()) {
+ switch (aggregationType) {
+ case "terms":
+ JsonObject termAggregation = aggregation.getJsonObject(aggregationType);
+ TermsAggregationBuilder termsAggregationBuilder =
+ AggregationBuilders.terms(key).field(termAggregation.getString("field"));
+ aggregationBuilders.add(termsAggregationBuilder);
+ break;
+ case "nested":
+ JsonObject nestedAggregation = aggregation.getJsonObject("nested");
+ AggregationBuilder nestedAggregationBuilder =
+ AggregationBuilders.nested(
+ nestedAggregation.getString("path"), nestedAggregation.getString("path"));
+ JsonObject nestedAggregations = aggregation.getJsonObject("aggs");
+
+ List nestedAggregationBuilders =
+ buildAggregation(nestedAggregations);
+ for (AggregationBuilder nestedAggregationBuilder1 : nestedAggregationBuilders) {
+ nestedAggregationBuilder.subAggregation(nestedAggregationBuilder1);
+ }
+ aggregationBuilders.add(nestedAggregationBuilder);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ return aggregationBuilders;
+ }
+
+ @Override
+ public JsonObject aggregate(String query, String index, JsonObject aggregationJson)
+ throws IOException {
+ JsonObject aggregations = aggregationJson.getJsonObject("aggregations");
+ if (aggregations == null) {
+ return null;
+ }
+
+ List aggregationBuilder = buildAggregation(aggregations);
+ os.org.opensearch.action.search.SearchRequest searchRequest =
+ new os.org.opensearch.action.search.SearchRequest(
+ Entity.getSearchRepository().getIndexOrAliasName(index));
+ SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
+ if (query != null) {
+ XContentParser queryParser =
+ XContentType.JSON
+ .xContent()
+ .createParser(X_CONTENT_REGISTRY, LoggingDeprecationHandler.INSTANCE, query);
+ QueryBuilder parsedQuery = SearchSourceBuilder.fromXContent(queryParser).query();
+ BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(parsedQuery);
+ searchSourceBuilder.query(boolQueryBuilder);
+ }
+
+ searchSourceBuilder.size(0).timeout(new TimeValue(30, TimeUnit.SECONDS));
+
+ for (AggregationBuilder aggregation : aggregationBuilder) {
+ searchSourceBuilder.aggregation(aggregation);
+ }
+
+ searchRequest.source(searchSourceBuilder);
+
+ String response = client.search(searchRequest, RequestOptions.DEFAULT).toString();
+ JsonObject jsonResponse = JsonUtils.readJson(response).asJsonObject();
+ return jsonResponse.getJsonObject("aggregations");
+ }
+
public void updateSearch(UpdateRequest updateRequest) {
if (updateRequest != null) {
updateRequest.docAsUpsert(true);
diff --git a/openmetadata-service/src/main/resources/elasticsearch/en/test_suite_index_mapping.json b/openmetadata-service/src/main/resources/elasticsearch/en/test_suite_index_mapping.json
index 353405394c1..6b58935c6a2 100644
--- a/openmetadata-service/src/main/resources/elasticsearch/en/test_suite_index_mapping.json
+++ b/openmetadata-service/src/main/resources/elasticsearch/en/test_suite_index_mapping.json
@@ -84,6 +84,30 @@
"entityType": {
"type": "keyword"
},
+ "testCaseResultSummary": {
+ "type": "nested",
+ "properties": {
+ "status": {
+ "type": "keyword"
+ },
+ "testCaseName": {
+ "type": "text",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ },
+ "ngram": {
+ "type": "text",
+ "analyzer": "om_ngram"
+ }
+ }
+ },
+ "timestamp": {
+ "type": "long"
+ }
+ }
+ },
"suggest": {
"type": "completion",
"contexts": [
diff --git a/openmetadata-service/src/main/resources/elasticsearch/jp/test_suite_index_mapping.json b/openmetadata-service/src/main/resources/elasticsearch/jp/test_suite_index_mapping.json
index 6c7ccb3130d..6062d4074c9 100644
--- a/openmetadata-service/src/main/resources/elasticsearch/jp/test_suite_index_mapping.json
+++ b/openmetadata-service/src/main/resources/elasticsearch/jp/test_suite_index_mapping.json
@@ -82,6 +82,27 @@
"entityType": {
"type": "keyword"
},
+ "testCaseResultSummary": {
+ "type": "nested",
+ "properties": {
+ "status": {
+ "type": "keyword"
+ },
+ "testCaseName": {
+ "type": "text",
+ "analyzer": "om_analyzer_jp",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ },
+ "timestamp": {
+ "type": "long"
+ }
+ }
+ },
"suggest": {
"type": "completion",
"contexts": [
diff --git a/openmetadata-service/src/main/resources/elasticsearch/zh/test_suite_index_mapping.json b/openmetadata-service/src/main/resources/elasticsearch/zh/test_suite_index_mapping.json
index 8860dd5c4ee..c1c45b7b82b 100644
--- a/openmetadata-service/src/main/resources/elasticsearch/zh/test_suite_index_mapping.json
+++ b/openmetadata-service/src/main/resources/elasticsearch/zh/test_suite_index_mapping.json
@@ -67,6 +67,28 @@
"entityType": {
"type": "keyword"
},
+ "testCaseResultSummary": {
+ "type": "nested",
+ "properties": {
+ "status": {
+ "type": "keyword"
+ },
+ "testCaseName": {
+ "type": "text",
+ "analyzer": "ik_max_word",
+ "search_analyzer": "ik_smart",
+ "fields": {
+ "keyword": {
+ "type": "keyword",
+ "ignore_above": 256
+ }
+ }
+ },
+ "timestamp": {
+ "type": "long"
+ }
+ }
+ },
"fqnParts": {
"type": "keyword"
},
diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java
index f1e8d30e259..1fc78d6e3e2 100644
--- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java
+++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/dqtests/TestCaseResourceTest.java
@@ -381,17 +381,14 @@ public class TestCaseResourceTest extends EntityResourceTest getTestSummary(ADMIN_AUTH_HEADERS, randomUUID),
- NOT_FOUND,
- "testSuite instance for " + randomUUID + " not found");
+ TestSummary testSummary;
+ if (supportsSearchIndex && RUN_ELASTIC_SEARCH_TESTCASES) {
+ testSummary = getTestSummary(ADMIN_AUTH_HEADERS, null);
+ assertNotEquals(0, testSummary.getFailed());
+ assertNotEquals(0, testSummary.getSuccess());
+ assertNotEquals(0, testSummary.getTotal());
+ assertEquals(0, testSummary.getAborted());
+ }
// Test that we can get the test summary for a logical test suite and that
// adding a logical test suite does not change the total number of tests
@@ -403,25 +400,30 @@ public class TestCaseResourceTest extends EntityResourceTest actual;
+ List expected = new ArrayList<>();
+ String aggregationQuery;
+
+ // Test aggregation with nested aggregation
+ aggregationQuery =
+ """
+ {
+ "aggregations": {
+ "test_case_results": {
+ "nested": {
+ "path": "testCaseResultSummary"
+ },
+ "aggs": {
+ "status_counts": {
+ "terms": {
+ "field": "testCaseResultSummary.status"
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ expected.add(
+ AggregationBuilders.nested("testCaseResultSummary", "testCaseResultSummary")
+ .subAggregation(
+ AggregationBuilders.terms("status_counts").field("testCaseResultSummary.status")));
+
+ aggregationJson = JsonUtils.readJson(aggregationQuery).asJsonObject();
+ actual = ElasticSearchClient.buildAggregation(aggregationJson.getJsonObject("aggregations"));
+ assertThat(actual).hasSameElementsAs(expected);
+
+ // Test aggregation with multiple aggregations
+ aggregationQuery =
+ """
+ {
+ "aggregations": {
+ "my-first-agg-name": {
+ "terms": {
+ "field": "my-field"
+ }
+ },
+ "my-second-agg-name": {
+ "terms": {
+ "field": "my-other-field"
+ }
+ }
+ }
+ }
+ """;
+ aggregationJson = JsonUtils.readJson(aggregationQuery).asJsonObject();
+
+ expected.clear();
+ expected.addAll(
+ List.of(
+ AggregationBuilders.terms("my-second-agg-name").field("my-other-field"),
+ AggregationBuilders.terms("my-first-agg-name").field("my-field")));
+
+ actual = ElasticSearchClient.buildAggregation(aggregationJson.getJsonObject("aggregations"));
+ assertThat(actual).hasSameElementsAs(expected);
+
+ // Test aggregation with multiple aggregations including a nested one which has itself multiple
+ // aggregations
+ aggregationQuery =
+ """
+ {
+ "aggregations": {
+ "my-first-agg-name": {
+ "terms": {
+ "field": "my-field"
+ }
+ },
+ "test_case_results": {
+ "nested": {
+ "path": "testCaseResultSummary"
+ },
+ "aggs": {
+ "status_counts": {
+ "terms": {
+ "field": "testCaseResultSummary.status"
+ }
+ },
+ "other_status_counts": {
+ "terms": {
+ "field": "testCaseResultSummary.status"
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+ aggregationJson = JsonUtils.readJson(aggregationQuery).asJsonObject();
+
+ expected.clear();
+ expected.addAll(
+ List.of(
+ AggregationBuilders.nested("testCaseResultSummary", "testCaseResultSummary")
+ .subAggregation(
+ AggregationBuilders.terms("status_counts")
+ .field("testCaseResultSummary.status"))
+ .subAggregation(
+ AggregationBuilders.terms("other_status_counts")
+ .field("testCaseResultSummary.status")),
+ AggregationBuilders.terms("my-first-agg-name").field("my-field")));
+
+ actual = ElasticSearchClient.buildAggregation(aggregationJson.getJsonObject("aggregations"));
+ assertThat(actual).hasSameElementsAs(expected);
+ }
+
@Test
void delete_LogicalTestSuite_200(TestInfo test) throws IOException {
TestCaseResourceTest testCaseResourceTest = new TestCaseResourceTest();