Skip to content

Commit 4dfedb1

Browse files
Merge branch 'main' into fix/airflow-n1-task-instances-bulk-query
2 parents 64cd089 + 62362d7 commit 4dfedb1

37 files changed

+620
-116
lines changed

openmetadata-service/src/main/java/org/openmetadata/service/TypeRegistry.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,11 @@ public static String getCustomPropertyFQN(String entityType, String propertyName
154154
}
155155

156156
public static String getPropertyName(String propertyFQN) {
157-
return FullyQualifiedName.split(propertyFQN)[2];
157+
CustomProperty property = CUSTOM_PROPERTIES.get(propertyFQN);
158+
if (property != null) {
159+
return property.getName();
160+
}
161+
return FullyQualifiedName.unquoteName(FullyQualifiedName.split(propertyFQN)[2]);
158162
}
159163

160164
public static String getCustomPropertyType(String entityType, String propertyName) {

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4438,7 +4438,7 @@ public final Object getExtension(T entity) {
44384438
}
44394439
ObjectNode objectNode = JsonUtils.getObjectNode();
44404440
for (ExtensionRecord extensionRecord : records) {
4441-
String fieldName = extensionRecord.extensionName().substring(fieldFQNPrefix.length() + 1);
4441+
String fieldName = TypeRegistry.getPropertyName(extensionRecord.extensionName());
44424442
JsonNode fieldValue = JsonUtils.readTree(extensionRecord.extensionJson());
44434443
String customPropertyType = TypeRegistry.getCustomPropertyType(entityType, fieldName);
44444444
if ("enum".equals(customPropertyType) && fieldValue.isArray() && fieldValue.size() > 1) {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2021 Collate
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
package org.openmetadata.service;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
18+
import org.junit.jupiter.api.AfterEach;
19+
import org.junit.jupiter.api.Test;
20+
import org.openmetadata.schema.entity.type.CustomProperty;
21+
22+
/**
23+
* Tests {@link TypeRegistry#getPropertyName(String)}.
24+
*
25+
* <p>The OpenMetadata FQN system normalises quotes — a property named {@code "/random/"} (with
26+
* literal quote characters) and a property named {@code custom.test} (with dots) end up with FQN
27+
* segments that look the same after normalisation. To return the original name to API consumers,
28+
* {@code getPropertyName} prefers the registered {@link CustomProperty#getName()} over re-deriving
29+
* from the FQN. These tests pin that behaviour.
30+
*/
31+
class TypeRegistryTest {
32+
33+
private static final String ENTITY_TYPE = "table";
34+
private static final String FQN_PREFIX_TABLE = "table.customProperties";
35+
36+
@AfterEach
37+
void cleanRegistry() {
38+
TypeRegistry.CUSTOM_PROPERTIES.clear();
39+
}
40+
41+
@Test
42+
void getPropertyName_returnsRegisteredNameWithLiteralQuotesPreserved() {
43+
// Property name has literal quote characters that the FQN system would
44+
// otherwise strip during quoteName() normalisation.
45+
String propertyName = "\"/random/\"";
46+
String fqn = TypeRegistry.getCustomPropertyFQN(ENTITY_TYPE, propertyName);
47+
TypeRegistry.CUSTOM_PROPERTIES.put(fqn, customPropertyNamed(propertyName));
48+
49+
// Sanity: FQN-level normalisation strips the literal quotes — there is no
50+
// way to recover them by parsing the FQN segment alone.
51+
assertEquals(FQN_PREFIX_TABLE + "./random/", fqn);
52+
53+
assertEquals(propertyName, TypeRegistry.getPropertyName(fqn));
54+
}
55+
56+
@Test
57+
void getPropertyName_returnsRegisteredNameForPropertyWithDots() {
58+
String propertyName = "custom.test";
59+
String fqn = TypeRegistry.getCustomPropertyFQN(ENTITY_TYPE, propertyName);
60+
TypeRegistry.CUSTOM_PROPERTIES.put(fqn, customPropertyNamed(propertyName));
61+
62+
// FQN-quoting wraps names containing dots so the FQN parser can split them.
63+
assertEquals(FQN_PREFIX_TABLE + ".\"custom.test\"", fqn);
64+
65+
// The returned name is the original (unquoted) form, not the FQN-quoted one.
66+
assertEquals(propertyName, TypeRegistry.getPropertyName(fqn));
67+
}
68+
69+
@Test
70+
void getPropertyName_returnsRegisteredNameForSimpleProperty() {
71+
String propertyName = "demo";
72+
String fqn = TypeRegistry.getCustomPropertyFQN(ENTITY_TYPE, propertyName);
73+
TypeRegistry.CUSTOM_PROPERTIES.put(fqn, customPropertyNamed(propertyName));
74+
75+
assertEquals(FQN_PREFIX_TABLE + ".demo", fqn);
76+
assertEquals(propertyName, TypeRegistry.getPropertyName(fqn));
77+
}
78+
79+
@Test
80+
void getPropertyName_fallsBackToFqnParsingWhenNotRegistered() {
81+
// Unregistered property with FQN-quoted segment (i.e., name had dots).
82+
// Without a registry hit, we must fall back to FQN parsing + unquoteName.
83+
String fqn = FQN_PREFIX_TABLE + ".\"custom.test\"";
84+
85+
assertEquals("custom.test", TypeRegistry.getPropertyName(fqn));
86+
}
87+
88+
@Test
89+
void getPropertyName_fallsBackToFqnParsingForUnquotedSegment() {
90+
String fqn = FQN_PREFIX_TABLE + ".demo";
91+
92+
assertEquals("demo", TypeRegistry.getPropertyName(fqn));
93+
}
94+
95+
@Test
96+
void getPropertyName_registeredNameWinsOverFqnFallback() {
97+
// The FQN segment is "/random/" (no quotes — they were stripped during
98+
// FQN building). The registered name has literal quotes. Without the
99+
// registry lookup, the fallback would return "/random/" and we'd lose
100+
// the original quotes — which is exactly the bug this fix addresses.
101+
String registeredName = "\"/random/\"";
102+
String fqn = FQN_PREFIX_TABLE + "./random/";
103+
TypeRegistry.CUSTOM_PROPERTIES.put(fqn, customPropertyNamed(registeredName));
104+
105+
assertEquals(registeredName, TypeRegistry.getPropertyName(fqn));
106+
}
107+
108+
private static CustomProperty customPropertyNamed(String name) {
109+
return new CustomProperty().withName(name);
110+
}
111+
}

openmetadata-ui/src/main/resources/ui/playwright/constant/customProperty.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -434,12 +434,5 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
434434
},
435435
};
436436

437-
export const CUSTOM_PROPERTY_INVALID_NAMES = {
438-
CAPITAL_CASE: 'CapitalCase',
439-
WITH_UNDERSCORE: 'with_underscore',
440-
WITH_DOTS: 'with.',
441-
WITH_SPACE: 'with ',
442-
};
443-
444437
export const CUSTOM_PROPERTY_NAME_VALIDATION_ERROR =
445-
'Name must start with lower case with no space, underscore, or dots.';
438+
"Name must not contain '::'.";

openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { UserClass } from '../../support/user/UserClass';
1919
import { REACTION_EMOJIS, reactOnFeed } from '../../utils/activityFeed';
2020
import { performAdminLogin } from '../../utils/admin';
2121
import {
22+
getAuthContext,
23+
getToken,
2224
redirectToHomePage,
2325
removeLandingBanner,
2426
uuid,
@@ -411,7 +413,6 @@ test.describe('Mention notifications in Notification Box', () => {
411413

412414
await apiContext.post('/api/v1/feed', {
413415
data: {
414-
from: adminUser.responseData.name,
415416
message: 'Initial conversation thread for mention test',
416417
about: `<#E::table::${entity.entityResponseData.fullyQualifiedName}>`,
417418
type: 'Conversation',
@@ -430,6 +431,30 @@ test.describe('Mention notifications in Notification Box', () => {
430431
await test.step('User1 mentions admin user in a reply', async () => {
431432
await entity.visitEntityPage(user1Page);
432433

434+
const token = await getToken(user1Page);
435+
const apiContext = await getAuthContext(token);
436+
const feedUrl = `/api/v1/feed?entityLink=${encodeURIComponent(
437+
`<#E::table::${entity.entityResponseData.fullyQualifiedName}>`
438+
)}&type=Conversation&limit=25`;
439+
440+
await expect
441+
.poll(
442+
async () => {
443+
const response = await apiContext.get(feedUrl);
444+
const data = await response.json();
445+
446+
return (data.data ?? []).some((thread: { message?: string }) =>
447+
thread.message?.includes(
448+
'Initial conversation thread for mention test'
449+
)
450+
);
451+
},
452+
{ timeout: 60_000, intervals: [2_000] }
453+
)
454+
.toBe(true);
455+
456+
await apiContext.dispose();
457+
433458
await user1Page.getByTestId('activity_feed').click();
434459

435460
await waitForAllLoadersToDisappear(user1Page);

openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ChangeSummaryBadge.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import { waitForAllLoadersToDisappear } from '../../utils/entity';
2020
import { navigateToExploreAndSelectEntity } from '../../utils/explore';
2121
import { test } from '../fixtures/pages';
2222

23-
const table = new TableClass();
24-
2523
test.describe(
2624
'ChangeSummary DescriptionSourceBadge',
2725
{ tag: [DOMAIN_TAGS.DISCOVERY] },
2826
() => {
27+
let table: TableClass;
28+
2929
test.beforeAll('Setup test entities', async ({ browser }) => {
30+
table = new TableClass();
3031
const { apiContext, afterAction } = await performAdminLogin(browser);
3132

3233
await table.create(apiContext);

openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/DataObservabilityGovernanceTab.spec.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ import { waitForAllLoadersToDisappear } from '../../../utils/entity';
3030

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

33-
const classification = new ClassificationClass();
34-
const tag = new TagClass({ classification: classification.data.name });
35-
const glossary = new Glossary();
36-
const glossaryTerm = new GlossaryTerm(glossary);
37-
const domain = new Domain();
38-
const table = new TableClass();
33+
let classification: ClassificationClass;
34+
let tag: TagClass;
35+
let glossary: Glossary;
36+
let glossaryTerm: GlossaryTerm;
37+
let domain: Domain;
38+
let table: TableClass;
3939

4040
const testCaseResult = {
4141
result: 'Found value outside expected range.',
@@ -58,6 +58,13 @@ const watchDashboardResponse = (page: Page, filterKey: string) =>
5858
test.beforeAll('setup', async ({ browser }) => {
5959
const { apiContext, afterAction } = await createNewPage(browser);
6060

61+
classification = new ClassificationClass();
62+
tag = new TagClass({ classification: classification.data.name });
63+
glossary = new Glossary();
64+
glossaryTerm = new GlossaryTerm(glossary);
65+
domain = new Domain();
66+
table = new TableClass();
67+
6168
await classification.create(apiContext);
6269
await tag.create(apiContext);
6370
await glossary.create(apiContext);

openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/AdvanceSearchFilter/CustomPropertyAdvanceSeach.spec.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,122 @@ test.describe('Custom Property Advanced Search Filter for Dashboard', () => {
8282
});
8383

8484
test.describe('Text Field Custom Properties', () => {
85+
test('String CP with numeric-like string value', async ({
86+
browser,
87+
page,
88+
}) => {
89+
test.slow();
90+
const numericStringDashboard = new DashboardClass();
91+
92+
await test.step('Setup dashboard with numeric-like string value', async () => {
93+
const { apiContext, afterAction } = await createNewPage(browser);
94+
95+
await numericStringDashboard.create(apiContext);
96+
97+
await apiContext.patch(
98+
`/api/v1/dashboards/${numericStringDashboard.entityResponseData.id}`,
99+
{
100+
data: [
101+
{
102+
op: 'add',
103+
path: '/extension',
104+
value: { [propertyNames['string']]: '100' },
105+
},
106+
],
107+
headers: {
108+
'Content-Type': 'application/json-patch+json',
109+
},
110+
}
111+
);
112+
113+
await afterAction();
114+
});
115+
116+
await test.step('Equal operator finds dashboard with string value "100"', async () => {
117+
await showAdvancedSearchDialog(page);
118+
await applyCustomPropertyFilter(
119+
page,
120+
propertyNames['string'],
121+
'equal',
122+
'100'
123+
);
124+
await verifySearchResults(
125+
page,
126+
numericStringDashboard.entityResponseData.fullyQualifiedName,
127+
true,
128+
'100'
129+
);
130+
await clearAdvancedSearchFilters(page);
131+
});
132+
133+
await test.step('Not_equal operator excludes dashboard with string value "100"', async () => {
134+
await showAdvancedSearchDialog(page);
135+
await applyCustomPropertyFilter(
136+
page,
137+
propertyNames['string'],
138+
'not_equal',
139+
'100'
140+
);
141+
await verifySearchResults(
142+
page,
143+
numericStringDashboard.entityResponseData.fullyQualifiedName,
144+
false,
145+
'100'
146+
);
147+
await clearAdvancedSearchFilters(page);
148+
});
149+
150+
await test.step('Contains operator finds dashboard with partial numeric-like string "10"', async () => {
151+
await showAdvancedSearchDialog(page);
152+
await applyCustomPropertyFilter(
153+
page,
154+
propertyNames['string'],
155+
'like',
156+
'10'
157+
);
158+
await verifySearchResults(
159+
page,
160+
numericStringDashboard.entityResponseData.fullyQualifiedName,
161+
true,
162+
'10'
163+
);
164+
await clearAdvancedSearchFilters(page);
165+
});
166+
167+
await test.step('Not contains operator excludes dashboard with partial numeric-like string "10"', async () => {
168+
await showAdvancedSearchDialog(page);
169+
await applyCustomPropertyFilter(
170+
page,
171+
propertyNames['string'],
172+
'not_like',
173+
'10'
174+
);
175+
await verifySearchResults(
176+
page,
177+
numericStringDashboard.entityResponseData.fullyQualifiedName,
178+
false,
179+
'10'
180+
);
181+
await clearAdvancedSearchFilters(page);
182+
});
183+
184+
await test.step('Is not null operator finds dashboard with numeric-like string value', async () => {
185+
await showAdvancedSearchDialog(page);
186+
await applyCustomPropertyFilter(
187+
page,
188+
propertyNames['string'],
189+
'is_not_null',
190+
''
191+
);
192+
await verifySearchResults(
193+
page,
194+
numericStringDashboard.entityResponseData.fullyQualifiedName,
195+
true
196+
);
197+
await clearAdvancedSearchFilters(page);
198+
});
199+
});
200+
85201
test('String CP with all operators', async ({ page }) => {
86202
test.slow();
87203
const propertyName = propertyNames['string'];

0 commit comments

Comments
 (0)