Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,8 @@ public ResultList<T> listFromSearchWithOffset(
searchListFilter, limit, offset, entityType, searchSortFilter, q, queryString);
total = results.getTotal();
for (Map<String, Object> json : results.getResults()) {
T entity = setFieldsInternal(JsonUtils.readOrConvertValue(json, entityClass), fields);
T entity =
setFieldsInternal(JsonUtils.readOrConvertValueLenient(json, entityClass), fields);
try {
Comment thread
mohityadav766 marked this conversation as resolved.
Comment thread
mohityadav766 marked this conversation as resolved.
setInheritedFields(entity);
} catch (RuntimeException e) {
Expand Down Expand Up @@ -526,7 +527,7 @@ public ResultList<T> listLatestFromSearch(
Map<String, Object> source = extractAndFilterSource(hit);
T entity =
setFieldsInternal(
JsonUtils.readOrConvertValue(source, entityClass), fields);
JsonUtils.readOrConvertValueLenient(source, entityClass), fields);
if (entity != null) {
try {
setInheritedFields(entity);
Expand Down Expand Up @@ -684,7 +685,7 @@ public T latestFromSearch(EntityUtil.Fields fields, SearchListFilter searchListF
SearchResultListMapper results =
searchRepository.listWithOffset(searchListFilter, 1, 0, entityType, searchSortFilter, q);
for (Map<String, Object> json : results.getResults()) {
T entity = setFieldsInternal(JsonUtils.readOrConvertValue(json, entityClass), fields);
T entity = setFieldsInternal(JsonUtils.readOrConvertValueLenient(json, entityClass), fields);
setInheritedFields(entity);
clearFieldsInternal(entity, fields);
return entity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ default void applyTagFields(Map<String, Object> doc) {
if (!(entity instanceof EntityInterface ei)) {
return;
}
ParseTags parseTags = new ParseTags(Entity.getEntityTags(getEntityTypeName(), ei));
doc.put("tags", parseTags.getTags());
List<TagLabel> entityTags = Entity.getEntityTags(getEntityTypeName(), ei);
ParseTags parseTags = new ParseTags(entityTags);
doc.put("tags", entityTags);
doc.put("tier", parseTags.getTierTag());
doc.put("classificationTags", parseTags.getClassificationTags());
doc.put("glossaryTags", parseTags.getGlossaryTags());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public Set<String> getRequiredReindexFields() {
fields.add("testSuite");
fields.add("testSuites");
fields.add("testDefinition");
fields.add(Entity.TEST_CASE_RESULT);
fields.add("incidentId");
return java.util.Collections.unmodifiableSet(fields);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.openmetadata.service.search.indexes;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -29,6 +31,13 @@ public Set<String> getExcludedFields() {
return excludeFields;
}

@Override
public Set<String> getRequiredReindexFields() {
Set<String> fields = new HashSet<>(TaggableIndex.super.getRequiredReindexFields());
fields.add("summary");
return Collections.unmodifiableSet(fields);
}

public Map<String, Object> buildSearchIndexDocInternal(Map<String, Object> doc) {
setParentRelationships(doc, testSuite);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,17 @@ void reindexFieldsIncludeKnownOverrides() {
assertTrue(testCaseFields.contains("testSuite"));
assertTrue(testCaseFields.contains("testSuites"));
assertTrue(testCaseFields.contains("testDefinition"));
// Regression: testCaseResult/incidentId are stripped from storage JSON and
// only fetched by setFieldsInBulk when explicitly requested. Reindex without
// them produces docs missing testCaseStatus, blanking statuses in the UI.
assertTrue(testCaseFields.contains(Entity.TEST_CASE_RESULT));
assertTrue(testCaseFields.contains("incidentId"));
// TestSuiteRepository registers a fetcher for "summary" that populates
// testCaseResultSummary. The DQ TestSuites list page sorts by the
// top-level lastResultTimestamp field (computed in TestSuiteIndex from
// that summary) and renders a success-% column per row. Without
// "summary" the fetcher never runs and the ES doc has neither field.
assertTrue(factory.getReindexFieldsFor(Entity.TEST_SUITE).contains("summary"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
Expand Down Expand Up @@ -144,6 +145,28 @@ void testBuildSearchIndexDoc_endToEnd_hasCommonAndTagFields() {
assertNotNull(result.get("originEntityFQN"));
}

@Test
void testRequiredReindexFields_includesTestCaseResultAndIncidentId() {
// Regression test for the 1.12.7 reindex bug: testCaseResult and incidentId
// are stripped from the storage JSON and only loaded by
// TestCaseRepository.setFieldsInBulk when present in the requested field
// set. If they are not in getRequiredReindexFields(), the reindexer writes
// a doc with no testCaseStatus and the UI/search shows test cases with no
// status until a per-case write re-populates them.
TestCase tc = new TestCase().withId(UUID.randomUUID()).withName("tc");
Set<String> required = new TestCaseIndex(tc).getRequiredReindexFields();

assertTrue(
required.contains(Entity.TEST_CASE_RESULT),
"TestCaseIndex.getRequiredReindexFields() must include 'testCaseResult'");
assertTrue(
required.contains("incidentId"),
"TestCaseIndex.getRequiredReindexFields() must include 'incidentId'");
assertTrue(required.contains("testSuite"));
assertTrue(required.contains("testSuites"));
assertTrue(required.contains("testDefinition"));
}

@Test
void testBuildSearchIndexDocInternal_testDefinitionNotFound() {
UUID testDefId = UUID.randomUUID();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright 2026 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Regression for the 1.12.7 selective-reindex bug
* (https://github.com/open-metadata/OpenMetadata/pull/27723):
* TestCaseIndex.getRequiredReindexFields() omitted `testCaseResult` and
* `incidentId`, both of which are stripped from the storage JSON. On reindex,
* TestCaseRepository.setFieldsInBulk skipped fetching them and the resulting
* ES doc had no `testCaseStatus` — wiping status from search/UI until a
* per-case write re-populated it.
*
* This test creates a test case, writes a result, forces an entity reindex
* with `recreate=true` (delete + re-add), and asserts the status survives.
*/

import test, { expect } from '@playwright/test';
import { TableClass } from '../../../support/entity/TableClass';
import { createNewPage } from '../../../utils/common';

test.use({ storageState: 'playwright/.auth/admin.json' });

const TEST_CASE_STATUS = 'Failed' as const;

test('Test case status survives a full entity reindex', async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);

const table = new TableClass();

try {
await table.create(apiContext);

const testCase = await table.createTestCase(apiContext);
const testCaseFqn = testCase.fullyQualifiedName as string;
const testCaseId = testCase.id as string;

await table.addTestCaseResult(apiContext, testCaseFqn, {
result: 'Reindex regression check',
testCaseStatus: TEST_CASE_STATUS,
timestamp: Date.now(),
});

// Wait for the search doc to settle and assert the status is indexed.
await expect
.poll(
async () => {
const res = await apiContext.get(
`/api/v1/search/query?q=fullyQualifiedName:%22${encodeURIComponent(
testCaseFqn
)}%22&index=test_case_search_index`
);
if (res.status() !== 200) {
return undefined;
}
const body = await res.json();
const hits = body?.hits?.hits ?? [];
return hits[0]?._source?.testCaseResult?.testCaseStatus;
},
{
message:
'pre-reindex: test case search doc must include testCaseResult.testCaseStatus',
timeout: 30_000,
}
Comment thread
mohityadav766 marked this conversation as resolved.
)
.toBe(TEST_CASE_STATUS);

// Force a recreate-style reindex of the test case — this is the exact
// path that drops the status before the fix.
const reindexRes = await apiContext.post(
'/api/v1/search/reindexEntities?recreate=true',
{
data: [
{
id: testCaseId,
type: 'testCase',
fullyQualifiedName: testCaseFqn,
},
],
}
);

expect(reindexRes.status()).toBeLessThan(400);
Comment on lines +79 to +92

Comment thread
mohityadav766 marked this conversation as resolved.
// Assert the status is still there after reindex. Before the fix, the
// recreated doc had no testCaseResult and this poll would time out.
await expect
.poll(
async () => {
const res = await apiContext.get(
`/api/v1/search/query?q=fullyQualifiedName:%22${encodeURIComponent(
testCaseFqn
)}%22&index=test_case_search_index`
);
if (res.status() !== 200) {
return undefined;
}
const body = await res.json();
const hits = body?.hits?.hits ?? [];
return hits[0]?._source?.testCaseResult?.testCaseStatus;
},
{
message:
'post-reindex: test case search doc must still include testCaseResult.testCaseStatus',
Comment thread
mohityadav766 marked this conversation as resolved.
timeout: 30_000,
}
)
.toBe(TEST_CASE_STATUS);
} finally {
await table.delete(apiContext);
await afterAction();
}
Comment thread
gitar-bot[bot] marked this conversation as resolved.
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2026 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Regression for the selective-reindex refactor (PR 27723):
*
* TestSuiteIndex.buildSearchIndexDocInternal computes `lastResultTimestamp`
* from `testSuite.getTestCaseResultSummary()`. TestSuiteRepository registers
* a fetcher for that under the field name `"summary"`. The reindex path only
* runs fetchers whose field is in `getRequiredReindexFields()`. Without
* `"summary"` declared, the fetcher does not run, the Index falls through to
* `doc.put("lastResultTimestamp", 0L)` (TestSuiteIndex.java:41), and the DQ
* `/data-quality/test-suites` list page — which sorts by that exact field
* (`TestSuites.component.tsx:175`) — collapses every reindexed suite to the
* 1970 epoch, breaking "most recently run first" ordering.
*
* The test creates a basic suite, writes a result, asserts the field is
* non-zero on the live-write path, forces a `recreate=true` reindex of the
* testSuite, and asserts the field is still non-zero. Before the fix this
* dropped back to 0 — provably the data the UI sort depends on.
*/
Comment thread
mohityadav766 marked this conversation as resolved.

import test, { expect } from '@playwright/test';
import { TableClass } from '../../../support/entity/TableClass';
import { createNewPage } from '../../../utils/common';

test.use({ storageState: 'playwright/.auth/admin.json' });

test('Test suite lastResultTimestamp survives a full entity reindex', async ({
browser,
}) => {
const { apiContext, afterAction } = await createNewPage(browser);

const table = new TableClass();

try {
await table.create(apiContext);
const testCase = await table.createTestCase(apiContext);

const resultTimestamp = Date.now();
await table.addTestCaseResult(apiContext, testCase.fullyQualifiedName, {
result: 'Reindex regression check',
testCaseStatus: 'Success',
timestamp: resultTimestamp,
});

const suite = testCase.testSuite as {
id: string;
fullyQualifiedName: string;
};

const reindexRes = await apiContext.post(
'/api/v1/search/reindexEntities?recreate=true',
{
data: [
{
fullyQualifiedName: suite.fullyQualifiedName,
id: suite.id,
type: 'testSuite',
},
],
}
);

expect(reindexRes.status()).toBeLessThan(400);
Comment thread
mohityadav766 marked this conversation as resolved.
Comment on lines +62 to +75

// Before the fix, the recreated doc has lastResultTimestamp=0 because the
// "summary" fetcher never ran and the Index hit its 0L fallback branch.
// After the fix it is the millisecond timestamp of the most recent result.
await expect
.poll(
async () => {
const res = await apiContext.get(
`/api/v1/search/query?q=fullyQualifiedName:%22${encodeURIComponent(
suite.fullyQualifiedName
)}%22&index=test_suite_search_index`
);
if (res.status() !== 200) {
return 0;
}
const body = await res.json();

return body?.hits?.hits?.[0]?._source?.lastResultTimestamp ?? 0;
},
{
message:
'post-reindex: test suite doc must still include a non-zero lastResultTimestamp',
timeout: 30_000,
}
)
.toBeGreaterThan(0);
} finally {
await table.delete(apiContext);
await afterAction();
}
});
Loading