Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a07ad5d
feat(search): add fetchParentsAliases / fetchChildAliases flags to sc…
mohityadav766 Apr 27, 2026
1abfea9
Merge branch 'main' into fix-cluster-aliasing
mohityadav766 Apr 27, 2026
e9155b5
Update generated TypeScript types
github-actions[bot] Apr 27, 2026
f426da2
fix(search): default fetchChildAliases to false
mohityadav766 Apr 27, 2026
5062bc5
Update generated TypeScript types
github-actions[bot] Apr 27, 2026
2ec748e
Merge remote-tracking branch 'origin/main' into fix-cluster-aliasing
mohityadav766 Apr 27, 2026
b08ef0f
Merge branch 'fix-cluster-aliasing' of github.com:open-metadata/OpenM…
mohityadav766 Apr 27, 2026
96ff8b6
fix(search): make getIndexOrAliasName idempotent and tighten alias-fl…
mohityadav766 Apr 28, 2026
d3d3d8f
fix(it): make alias-flag tests use a unique token visible to both ind…
mohityadav766 Apr 28, 2026
a48ac1e
fix(search): eliminate double cluster-alias prefix at the manager layer
mohityadav766 Apr 28, 2026
6819193
Update generated TypeScript types
github-actions[bot] Apr 28, 2026
fa8f8fe
Merge branch 'main' into fix-cluster-aliasing
mohityadav766 Apr 28, 2026
1a4631d
fix(it): assert against the actual entityType ('tableColumn') and unb…
mohityadav766 Apr 28, 2026
cd49228
Merge branch 'fix-cluster-aliasing' of github.com:open-metadata/OpenM…
mohityadav766 Apr 28, 2026
c56be4d
chore(search): restore cluster-alias idempotency in getIndexOrAliasNa…
mohityadav766 Apr 28, 2026
9038e90
fix(search): drop empty tokens when prefixing comma-separated index l…
mohityadav766 Apr 28, 2026
299d3bf
fix(search): preserve original input in getIndexOrAliasName when all …
mohityadav766 Apr 28, 2026
a649f6d
Merge branch 'main' into fix-cluster-aliasing
mohityadav766 Apr 28, 2026
bffc943
feat(search): convert fetchParentsAliases / fetchChildAliases to sele…
mohityadav766 Apr 29, 2026
f062bfb
Merge branch 'fix-cluster-aliasing' of github.com:open-metadata/OpenM…
mohityadav766 Apr 29, 2026
41d8421
Merge branch 'main' into fix-cluster-aliasing
mohityadav766 Apr 29, 2026
601699f
chore(search): drop unused legacy boolean-token aliasing in AliasFilter
mohityadav766 Apr 29, 2026
2ff5718
Merge branch 'fix-cluster-aliasing' of github.com:open-metadata/OpenM…
mohityadav766 Apr 29, 2026
971d678
Update generated TypeScript types
github-actions[bot] Apr 29, 2026
b2ec7ab
Merge remote-tracking branch 'origin/main' into fix-cluster-aliasing
mohityadav766 Apr 29, 2026
b3def1e
Merge branch 'fix-cluster-aliasing' of github.com:open-metadata/OpenM…
mohityadav766 Apr 29, 2026
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 @@ -1926,4 +1926,261 @@
String[] lines = response.body().split("\n");
assertEquals(1, lines.length, "Export beyond results should only contain header");
}

// ===================================================================
// FETCH PARENT/CHILD ALIASES TESTS
// ===================================================================

private HttpResponse<String> httpGetJson(String path) throws Exception {
String baseUrl = SdkClients.getServerUrl();
String token = SdkClients.getAdminToken();

HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(baseUrl + path))
.header("Authorization", "Bearer " + token)
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(30))
.GET()
.build();

return HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
}

/**
* Wait until the column document is visible in {@code column_search_index}. The test fixtures
* deliberately put {@code unique} only into the column name (not the table name), which is
* exactly the shape the original bug report describes: a UI search for {@code index=table}
* accidentally returns column docs because ES alias expansion routes the {@code table} alias
* to {@code column_search_index} too.
*/
private void awaitColumnIndexed(String unique) {
Awaitility.await()
.atMost(90, TimeUnit.SECONDS)
.pollInterval(500, TimeUnit.MILLISECONDS)
.until(
() -> {
HttpResponse<String> r =
httpGetJson("/v1/search/query?q=" + unique + "&index=tableColumn&size=20");
return r.statusCode() == 200
&& OBJECT_MAPPER.readTree(r.body()).path("hits").path("hits").size() > 0;
});
}

private static boolean anyHitOfType(JsonNode hits, String entityType) {
for (JsonNode hit : hits) {
if (entityType.equalsIgnoreCase(hit.path("_source").path("entityType").asText())) {
return true;
}
}
return false;
}

/**
* Bug regression: the UI now passes the alias {@code "table"} (instead of the legacy
* {@code table_search_index}). ES alias expansion bleeds {@code column_search_index} into the
* results because tableColumn declares {@code "table"} as a parent alias. Asserts the bleed
* reproduces when child expansion is explicitly enabled via {@code fetchChildAliases=*} and
* disappears under the new default ({@code fetchChildAliases=none}) — a comparison test, not
* a vacuous "no columns" check that would pass on an empty response.
*/
@Test
void testDefaultChildScopeExcludesColumns(TestNamespace ns) throws Exception {
String unique = "fetchchild_excl_" + ns.shortPrefix();
Column uniqueColumn =
new Column()
.withName(unique + "_col")
.withDataType(ColumnDataType.VARCHAR)
.withDataLength(64);
createTestTableWithColumns(ns, ns.prefix("fetchchild_excl_t"), List.of(uniqueColumn));

awaitColumnIndexed(unique);

// Opt-in legacy expansion (fetchChildAliases=*): tableColumn hits MUST appear so the rest
// of the assertion isn't vacuous.
HttpResponse<String> withChildren =
httpGetJson("/v1/search/query?q=" + unique + "&index=table&fetchChildAliases=*&size=20");
assertEquals(200, withChildren.statusCode());
JsonNode withChildrenHits =
OBJECT_MAPPER.readTree(withChildren.body()).path("hits").path("hits");
assertTrue(
anyHitOfType(withChildrenHits, "tableColumn"),
"fetchChildAliases=* on index=table must surface tableColumn hits — otherwise the "
+ "fixture is broken and the no-columns assertion would pass vacuously. body="
+ withChildren.body());

// Default flags (fetchChildAliases=none) — the bug fix path. tableColumn hits must NOT leak.
HttpResponse<String> withoutChildren =
httpGetJson("/v1/search/query?q=" + unique + "&index=table&size=20");
assertEquals(200, withoutChildren.statusCode());
JsonNode withoutChildrenHits =
OBJECT_MAPPER.readTree(withoutChildren.body()).path("hits").path("hits");
assertFalse(
anyHitOfType(withoutChildrenHits, "tableColumn"),
"Default flags on index=table must drop tableColumn hits, got: " + withoutChildren.body());
}

/**
* Confirms the named-filter syntax: passing only a specific child entity type expands that one
* but no others. Uses {@code fetchChildAliases=tableColumn} so the response includes
* {@code tableColumn} hits but not, say, {@code testCase} hits (testCase isn't a child of
* 'table' in indexMapping.json so it's already implicitly excluded — this test pins the
* positive direction).
*/
@Test
void testFetchChildAliasesNamedFilterIncludesOnlySpecified(TestNamespace ns) throws Exception {
String unique = "fetchchild_named_" + ns.shortPrefix();
Column uniqueColumn =
new Column()
.withName(unique + "_col")
.withDataType(ColumnDataType.VARCHAR)
.withDataLength(64);
createTestTableWithColumns(ns, ns.prefix("fetchchild_named_t"), List.of(uniqueColumn));

awaitColumnIndexed(unique);

HttpResponse<String> response =
httpGetJson(
"/v1/search/query?q=" + unique + "&index=table&fetchChildAliases=tableColumn&size=20");
assertEquals(200, response.statusCode());
JsonNode hits = OBJECT_MAPPER.readTree(response.body()).path("hits").path("hits");
assertTrue(
anyHitOfType(hits, "tableColumn"),
"fetchChildAliases=tableColumn must include tableColumn docs: " + response.body());

// A filter that names a non-child must not introduce that index — passing 'topic' (not a
// child of 'table') should yield zero tableColumn hits.
HttpResponse<String> nonChild =
httpGetJson(
"/v1/search/query?q=" + unique + "&index=table&fetchChildAliases=topic&size=20");
assertEquals(200, nonChild.statusCode());
JsonNode nonChildHits = OBJECT_MAPPER.readTree(nonChild.body()).path("hits").path("hits");
assertFalse(
anyHitOfType(nonChildHits, "tableColumn"),
"fetchChildAliases=topic must not introduce tableColumn hits: " + nonChild.body());
}

@Test
void testFetchParentsAliasesWildcardIncludesParents(TestNamespace ns) throws Exception {
String unique = "fetchparents_" + ns.shortPrefix();
Column uniqueColumn =
new Column()
.withName(unique + "_col")
.withDataType(ColumnDataType.VARCHAR)
.withDataLength(64);
Table parentTable =
createTestTableWithColumns(ns, ns.prefix("fetchparents_" + unique), List.of(uniqueColumn));
String tableName = parentTable.getName();

awaitColumnIndexed(unique);

Awaitility.await()
.atMost(90, TimeUnit.SECONDS)
.pollInterval(500, TimeUnit.MILLISECONDS)
.until(

Check failure on line 2080 in openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/SearchResourceIT.java

View workflow job for this annotation

GitHub Actions / Test Report

SearchResourceIT.testFetchParentsAliasesWildcardIncludesParents(TestNamespace)

Condition org.openmetadata.it.tests.SearchResourceIT$$Lambda/0x00007fa825f40228 was not fulfilled within 1 minutes 30 seconds.
Raw output
org.awaitility.core.ConditionTimeoutException: Condition org.openmetadata.it.tests.SearchResourceIT$$Lambda/0x00007fa825f40228 was not fulfilled within 1 minutes  30 seconds.
	at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
	at org.awaitility.core.CallableCondition.await(CallableCondition.java:78)
	at org.awaitility.core.CallableCondition.await(CallableCondition.java:26)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:985)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:954)
	at org.openmetadata.it.tests.SearchResourceIT.testFetchParentsAliasesWildcardIncludesParents(SearchResourceIT.java:2080)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
	at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)
	at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)
	at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)
	at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)
() -> {
HttpResponse<String> r =
httpGetJson(
"/v1/search/query?q="
+ tableName
+ "&index=tableColumn&fetchParentsAliases=*&fetchChildAliases=none&size=20");
return r.statusCode() == 200
&& anyHitOfType(
OBJECT_MAPPER.readTree(r.body()).path("hits").path("hits"), "table");
});
}

/**
* The flag must also be honored on the streaming export endpoint. Comparison test: default
* export carries column rows; explicit fetchChildAliases=false drops them.
Comment on lines +2094 to +2095
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Javadoc still refers to fetchChildAliases=false, but the implementation and tests use the new string-based syntax (none / * / comma-separated entity types). Update the comment to match the actual supported values to avoid confusing future maintainers and reviewers.

Suggested change
* The flag must also be honored on the streaming export endpoint. Comparison test: default
* export carries column rows; explicit fetchChildAliases=false drops them.
* The child-alias filter must also be honored on the streaming export endpoint. Comparison
* test: default export drops child rows, while explicit string-based filters such as
* fetchChildAliases=* include them. Supported values use the current syntax: none, *, or a
* comma-separated list of entity types.

Copilot uses AI. Check for mistakes.
*/
@Test
void testDefaultChildScopeOnExportEndpoint(TestNamespace ns) throws Exception {
String unique = "fetchchild_exp_" + ns.shortPrefix();
Column uniqueColumn =
new Column()
.withName(unique + "_col")
.withDataType(ColumnDataType.VARCHAR)
.withDataLength(64);
createTestTableWithColumns(ns, ns.prefix("fetchchild_exp_t"), List.of(uniqueColumn));

awaitColumnIndexed(unique);

HttpResponse<String> wildcardExport =
httpGetExport("/v1/search/export?q=" + unique + "&index=table&fetchChildAliases=*&size=50");
assertEquals(200, wildcardExport.statusCode());
String wildcardBody = wildcardExport.body();
boolean wildcardHasColumn =
java.util.Arrays.stream(wildcardBody.split("\n"))
.skip(1)
.anyMatch(row -> row.toLowerCase().startsWith("tablecolumn,"));
assertTrue(
wildcardHasColumn,
"fetchChildAliases=* export on index=table must include tableColumn rows; "
+ "otherwise the no-columns assertion below is vacuous. body="
+ wildcardBody);

HttpResponse<String> defaultExport =
httpGetExport("/v1/search/export?q=" + unique + "&index=table&size=50");
assertEquals(200, defaultExport.statusCode());
String defaultBody = defaultExport.body();
for (String row : defaultBody.split("\n")) {
assertFalse(
row.toLowerCase().startsWith("tablecolumn,"),
"Default export on /export must drop tableColumn rows, got: " + row);
}
}

/**
* The flag must propagate to /aggregate. Comparison test: default aggregation has a column
* bucket; explicit fetchChildAliases=false drops it.
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Javadoc mentions fetchChildAliases=false, but the endpoint uses the new string-based filter syntax (e.g. fetchChildAliases=none). Please update the comment so the documented behavior matches the actual API contract.

Suggested change
* bucket; explicit fetchChildAliases=false drops it.
* bucket; explicit fetchChildAliases=none drops it.

Copilot uses AI. Check for mistakes.
*/
@Test
void testDefaultChildScopeOnAggregate(TestNamespace ns) throws Exception {
String unique = "fetchchild_agg_" + ns.shortPrefix();
Column uniqueColumn =
new Column()
.withName(unique + "_col")
.withDataType(ColumnDataType.VARCHAR)
.withDataLength(64);
createTestTableWithColumns(ns, ns.prefix("fetchchild_agg_t"), List.of(uniqueColumn));

awaitColumnIndexed(unique);

// value=.* — the aggregate endpoint always wraps the terms agg in
// .include(regexp(fieldValue.toLowerCase())), so an empty fieldValue compiles to an empty
// regex that matches no bucket keys. ".*" sidesteps that filter.
HttpResponse<String> wildcardAggregate =
httpGetJson(
"/v1/search/aggregate?index=table&field=entityType.keyword&value=.*&q="
+ unique
+ "&fetchChildAliases=*&size=20");
assertEquals(200, wildcardAggregate.statusCode());
JsonNode wildcardBuckets = OBJECT_MAPPER.readTree(wildcardAggregate.body()).findPath("buckets");
boolean wildcardHasColumn = false;
for (JsonNode bucket : wildcardBuckets) {
if ("tablecolumn".equalsIgnoreCase(bucket.path("key").asText())) {
wildcardHasColumn = true;
break;
}
}
assertTrue(
wildcardHasColumn,
"fetchChildAliases=* aggregate on index=table must include a 'tablecolumn' bucket; "
+ "otherwise the no-columns assertion below is vacuous. body="
+ wildcardAggregate.body());

HttpResponse<String> defaultAggregate =
httpGetJson(
"/v1/search/aggregate?index=table&field=entityType.keyword&value=.*&q="
+ unique
+ "&size=20");
assertEquals(200, defaultAggregate.statusCode());
JsonNode defaultBuckets = OBJECT_MAPPER.readTree(defaultAggregate.body()).findPath("buckets");
for (JsonNode bucket : defaultBuckets) {
assertFalse(
"tablecolumn".equalsIgnoreCase(bucket.path("key").asText()),
"Default aggregate on /aggregate must drop 'tablecolumn' bucket: " + bucket);
}
}
}
Loading
Loading