Skip to content

Commit 68c50bb

Browse files
committed
refactor fqn parsing
1 parent 5a40d0d commit 68c50bb

File tree

4 files changed

+237
-22
lines changed

4 files changed

+237
-22
lines changed

openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/SearchIndex.java

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
import java.util.ArrayList;
2020
import java.util.Collections;
2121
import java.util.HashMap;
22-
import java.util.HashSet;
2322
import java.util.List;
2423
import java.util.Map;
2524
import java.util.Optional;
2625
import java.util.Set;
26+
import java.util.stream.Collectors;
2727
import org.apache.commons.lang3.tuple.ImmutablePair;
2828
import org.apache.commons.lang3.tuple.Pair;
2929
import org.openmetadata.schema.EntityInterface;
@@ -128,28 +128,12 @@ default Map<String, Object> getCommonAttributesMap(EntityInterface entity, Strin
128128
}
129129

130130
default Set<String> getFQNParts(String fqn) {
131-
Set<String> fqnParts = new HashSet<>();
132-
String[] parts = FullyQualifiedName.split(fqn);
131+
var parts = FullyQualifiedName.split(fqn);
132+
var entityName = parts[parts.length - 1];
133133

134-
int n = parts.length;
135-
136-
// 1. Full top-down hierarchy: service.database.schema.table -> service
137-
StringBuilder sb = new StringBuilder();
138-
for (int i = 0; i < n; i++) {
139-
if (i > 0) sb.append(".");
140-
sb.append(parts[i]);
141-
fqnParts.add(sb.toString());
142-
}
143-
Collections.addAll(fqnParts, parts);
144-
for (int i = 1; i < n; i++) {
145-
StringBuilder btm = new StringBuilder(parts[i]);
146-
for (int j = i + 1; j < n; j++) {
147-
btm.append(".").append(parts[j]);
148-
fqnParts.add(btm.toString());
149-
}
150-
}
151-
152-
return fqnParts;
134+
return FullyQualifiedName.getAllParts(fqn).stream()
135+
.filter(part -> !part.equals(entityName))
136+
.collect(Collectors.toSet());
153137
}
154138

155139
default List<EntityReference> getEntitiesWithDisplayName(List<EntityReference> entities) {

openmetadata-service/src/main/java/org/openmetadata/service/util/FullyQualifiedName.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
package org.openmetadata.service.util;
1515

1616
import java.util.ArrayList;
17+
import java.util.HashSet;
1718
import java.util.List;
19+
import java.util.Set;
1820
import java.util.regex.Matcher;
1921
import java.util.regex.Pattern;
2022
import org.antlr.v4.runtime.BailErrorStrategy;
@@ -206,4 +208,63 @@ public static String getTableFQN(String columnFQN) {
206208
public static String getColumnName(String columnFQN) {
207209
return FullyQualifiedName.split(columnFQN)[4]; // Get from column name from FQN
208210
}
211+
212+
/**
213+
* Generates all possible FQN parts for search and matching purposes.
214+
* For example, given FQN "service.database.schema.table", this method generates:
215+
* - Full hierarchy: "service", "service.database", "service.database.schema", "service.database.schema.table"
216+
* - Individual parts: "service", "database", "schema", "table"
217+
* - Bottom-up combinations: "database.schema.table", "schema.table", "table"
218+
*
219+
* @param fqn The fully qualified name to generate parts from
220+
* @return Set of all possible FQN parts
221+
*/
222+
public static Set<String> getAllParts(String fqn) {
223+
var parts = split(fqn);
224+
var fqnParts = new HashSet<String>();
225+
226+
// Generate all possible sub-paths
227+
for (int start = 0; start < parts.length; start++) {
228+
for (int end = start + 1; end <= parts.length; end++) {
229+
var subPath =
230+
String.join(Entity.SEPARATOR, java.util.Arrays.copyOfRange(parts, start, end));
231+
fqnParts.add(subPath);
232+
}
233+
}
234+
235+
return fqnParts;
236+
}
237+
238+
/**
239+
* Generates hierarchical FQN parts from root to the full FQN.
240+
* For example, given FQN "service.database.schema.table", this method generates:
241+
* ["service", "service.database", "service.database.schema", "service.database.schema.table"]
242+
*
243+
* @param fqn The fully qualified name to generate hierarchy from
244+
* @return List of hierarchical FQN parts from root to full FQN
245+
*/
246+
public static List<String> getHierarchicalParts(String fqn) {
247+
var parts = split(fqn);
248+
return java.util.stream.IntStream.rangeClosed(1, parts.length)
249+
.mapToObj(i -> String.join(Entity.SEPARATOR, java.util.Arrays.copyOfRange(parts, 0, i)))
250+
.toList();
251+
}
252+
253+
/**
254+
* Gets all ancestor FQNs for a given FQN.
255+
* For example, given FQN "service.database.schema.table", this method returns:
256+
* ["service.database.schema", "service.database", "service"]
257+
*
258+
* @param fqn The fully qualified name to get ancestors from
259+
* @return List of ancestor FQNs (excluding the input FQN itself)
260+
*/
261+
public static List<String> getAncestors(String fqn) {
262+
var parts = split(fqn);
263+
return java.util.stream.IntStream.range(1, parts.length)
264+
.mapToObj(
265+
i ->
266+
String.join(
267+
Entity.SEPARATOR, java.util.Arrays.copyOfRange(parts, 0, parts.length - i)))
268+
.toList();
269+
}
209270
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package org.openmetadata.service.search.indexes;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.util.Set;
6+
import java.util.stream.Collectors;
7+
import org.junit.jupiter.api.Test;
8+
import org.openmetadata.service.util.FullyQualifiedName;
9+
10+
class SearchIndexTest {
11+
12+
// Test the getFQNParts logic directly without instantiating SearchIndex
13+
private Set<String> getFQNParts(String fqn) {
14+
var parts = FullyQualifiedName.split(fqn);
15+
var entityName = parts[parts.length - 1];
16+
17+
return FullyQualifiedName.getAllParts(fqn).stream()
18+
.filter(part -> !part.equals(entityName))
19+
.collect(Collectors.toSet());
20+
}
21+
22+
@Test
23+
void testGetFQNParts_excludesEntityName() {
24+
String tableFqn = "service.database.schema.table";
25+
Set<String> parts = getFQNParts(tableFqn);
26+
assertFalse(parts.contains("table"), "Entity name 'table' should not be included in FQN parts");
27+
28+
assertTrue(parts.contains("service"));
29+
assertTrue(parts.contains("database"));
30+
assertTrue(parts.contains("schema"));
31+
assertTrue(parts.contains("service.database"));
32+
assertTrue(parts.contains("service.database.schema"));
33+
assertTrue(parts.contains("service.database.schema.table"));
34+
assertTrue(parts.contains("database.schema"));
35+
assertTrue(parts.contains("schema.table"));
36+
assertTrue(parts.contains("database.schema.table"));
37+
assertEquals(9, parts.size());
38+
}
39+
40+
@Test
41+
void testGetFQNParts_withDifferentPatterns() {
42+
// Test pipeline pattern: service.pipeline
43+
String pipelineFqn = "airflow.my_pipeline";
44+
Set<String> pipelineParts = getFQNParts(pipelineFqn);
45+
assertFalse(pipelineParts.contains("my_pipeline"), "Entity name should not be included");
46+
assertTrue(pipelineParts.contains("airflow"));
47+
assertEquals(2, pipelineParts.size());
48+
49+
// Test dashboard pattern: service.dashboard
50+
String dashboardFqn = "looker.sales_dashboard";
51+
Set<String> dashboardParts = getFQNParts(dashboardFqn);
52+
assertFalse(dashboardParts.contains("sales_dashboard"), "Entity name should not be included");
53+
assertTrue(dashboardParts.contains("looker"));
54+
assertEquals(2, dashboardParts.size());
55+
56+
// Test dashboard chart pattern: service.dashboard.chart
57+
String chartFqn = "tableau.analytics.revenue_chart";
58+
Set<String> chartParts = getFQNParts(chartFqn);
59+
assertFalse(chartParts.contains("revenue_chart"), "Entity name should not be included");
60+
assertTrue(chartParts.contains("tableau"));
61+
assertTrue(chartParts.contains("analytics"));
62+
assertTrue(chartParts.contains("tableau.analytics"));
63+
assertEquals(5, chartParts.size());
64+
}
65+
66+
@Test
67+
void testGetFQNParts_withQuotedNames() {
68+
// Test with quoted names that contain dots
69+
String quotedFqn = "\"service.v1\".database.\"schema.prod\".\"table.users\"";
70+
Set<String> parts = getFQNParts(quotedFqn);
71+
72+
// Verify that the entity name is not included
73+
assertFalse(parts.contains("\"table.users\""), "Entity name should not be included");
74+
assertFalse(parts.contains("table.users"), "Entity name should not be included");
75+
76+
// Verify other parts are included
77+
assertTrue(parts.contains("\"service.v1\""));
78+
assertTrue(parts.contains("database"));
79+
assertTrue(parts.contains("\"schema.prod\""));
80+
}
81+
82+
@Test
83+
void testGetFQNParts_withSinglePart() {
84+
// Test with a single part FQN (edge case)
85+
String singlePartFqn = "standalone_entity";
86+
Set<String> parts = getFQNParts(singlePartFqn);
87+
88+
// Should return empty set since we exclude the entity name
89+
assertTrue(parts.isEmpty(), "Single part FQN should return empty set");
90+
}
91+
}

openmetadata-service/src/test/java/org/openmetadata/service/util/FullyQualifiedNameTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import static org.junit.jupiter.api.Assertions.assertTrue;
88

99
import java.util.List;
10+
import java.util.Set;
1011
import org.antlr.v4.runtime.misc.ParseCancellationException;
1112
import org.junit.jupiter.api.Test;
1213

@@ -96,4 +97,82 @@ void test_isParent() {
9697
assertFalse(FullyQualifiedName.isParent("a.b.c", "a.b.c"));
9798
assertFalse(FullyQualifiedName.isParent("a.b c", "a.b"));
9899
}
100+
101+
@Test
102+
void test_getAllParts() {
103+
Set<String> parts = FullyQualifiedName.getAllParts("a.b.c.d");
104+
assertTrue(parts.contains("a"));
105+
assertTrue(parts.contains("b"));
106+
assertTrue(parts.contains("c"));
107+
assertTrue(parts.contains("d"));
108+
// Should contain top-down hierarchy
109+
assertTrue(parts.contains("a"));
110+
assertTrue(parts.contains("a.b"));
111+
assertTrue(parts.contains("a.b.c"));
112+
assertTrue(parts.contains("a.b.c.d"));
113+
// Should contain bottom-up combinations
114+
assertTrue(parts.contains("b.c.d"));
115+
assertTrue(parts.contains("c.d"));
116+
assertEquals(10, parts.size()); // 4 individual + 4 top-down + 2 bottom-up
117+
118+
// Test with quoted names
119+
Set<String> quotedParts = FullyQualifiedName.getAllParts("\"a.1\".\"b.2\".c.d");
120+
assertTrue(quotedParts.contains("\"a.1\""));
121+
assertTrue(quotedParts.contains("\"b.2\""));
122+
assertTrue(quotedParts.contains("c"));
123+
assertTrue(quotedParts.contains("d"));
124+
assertTrue(quotedParts.contains("\"a.1\".\"b.2\".c.d"));
125+
assertTrue(quotedParts.contains("\"b.2\".c.d"));
126+
127+
// Test with single part
128+
Set<String> singlePart = FullyQualifiedName.getAllParts("service");
129+
assertEquals(1, singlePart.size());
130+
assertTrue(singlePart.contains("service"));
131+
}
132+
133+
@Test
134+
void test_getHierarchicalParts() {
135+
List<String> hierarchy = FullyQualifiedName.getHierarchicalParts("a.b.c.d");
136+
assertEquals(4, hierarchy.size());
137+
assertEquals("a", hierarchy.get(0));
138+
assertEquals("a.b", hierarchy.get(1));
139+
assertEquals("a.b.c", hierarchy.get(2));
140+
assertEquals("a.b.c.d", hierarchy.get(3));
141+
142+
// Test with quoted names
143+
List<String> quotedHierarchy = FullyQualifiedName.getHierarchicalParts("\"a.1\".b.\"c.3\"");
144+
assertEquals(3, quotedHierarchy.size());
145+
assertEquals("\"a.1\"", quotedHierarchy.get(0));
146+
assertEquals("\"a.1\".b", quotedHierarchy.get(1));
147+
assertEquals("\"a.1\".b.\"c.3\"", quotedHierarchy.get(2));
148+
149+
// Test with single part
150+
List<String> singleHierarchy = FullyQualifiedName.getHierarchicalParts("service");
151+
assertEquals(1, singleHierarchy.size());
152+
assertEquals("service", singleHierarchy.getFirst());
153+
}
154+
155+
@Test
156+
void test_getAncestors() {
157+
List<String> ancestors = FullyQualifiedName.getAncestors("a.b.c.d");
158+
assertEquals(3, ancestors.size());
159+
assertEquals("a.b.c", ancestors.get(0));
160+
assertEquals("a.b", ancestors.get(1));
161+
assertEquals("a", ancestors.get(2));
162+
163+
List<String> twoPartAncestors = FullyQualifiedName.getAncestors("a.b");
164+
assertEquals(1, twoPartAncestors.size());
165+
assertEquals("a", twoPartAncestors.getFirst());
166+
167+
// Test with single part (no ancestors)
168+
List<String> noAncestors = FullyQualifiedName.getAncestors("service");
169+
assertEquals(0, noAncestors.size());
170+
171+
// Test with quoted names
172+
List<String> quotedAncestors = FullyQualifiedName.getAncestors("\"a.1\".b.\"c.3\".d");
173+
assertEquals(3, quotedAncestors.size());
174+
assertEquals("\"a.1\".b.\"c.3\"", quotedAncestors.get(0));
175+
assertEquals("\"a.1\".b", quotedAncestors.get(1));
176+
assertEquals("\"a.1\"", quotedAncestors.get(2));
177+
}
99178
}

0 commit comments

Comments
 (0)