Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -39,6 +39,7 @@
import org.openmetadata.schema.type.ApiStatus;
import org.openmetadata.schema.type.EntityHistory;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.EntityStatus;
import org.openmetadata.schema.type.FieldChange;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.schema.type.csv.CsvImportResult;
Expand Down Expand Up @@ -1425,6 +1426,123 @@ void test_importCsv_withApprovedRelatedTerm_succeeds(TestNamespace ns)
}
}

/**
* Regression test for issue #29510: glossaryStatus column must be parsed case-insensitively.
* Users supplying valid status values in any casing (draft, APPROVED, drAFT, deprecated, etc.)
* should produce a successful import with the canonical {@link EntityStatus} stored on the term.
* Invalid values must still surface as a clear validation error.
*/
@Test
void test_importCsv_glossaryStatusIsCaseInsensitive(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();

Glossary glossary = createEntity(createMinimalRequest(ns));
String header =
client.glossaries().exportCsv(glossary.getName()).lines().findFirst().orElse("");
assertTrue(header.contains("glossaryStatus"), "Exported header must include glossaryStatus");

String draftTermName = ns.prefix("statusDraft");
String approvedTermName = ns.prefix("statusApproved");
String deprecatedTermName = ns.prefix("statusDeprecated");
String mixedCaseTermName = ns.prefix("statusMixed");

String csv =
header
+ "\n"
+ buildStatusRow(header, draftTermName, "Lower Draft", "draft (lower)", "draft")
+ buildStatusRow(
header, approvedTermName, "Upper Approved", "approved (upper)", "APPROVED")
+ buildStatusRow(
header, deprecatedTermName, "Lower Deprecated", "deprecated (lower)", "deprecated")
+ buildStatusRow(header, mixedCaseTermName, "Mixed Draft", "drAFT (mixed)", "drAFT");

String resultJson = client.glossaries().importCsv(glossary.getName(), csv, false);
CsvImportResult importResult = JsonUtils.readValue(resultJson, CsvImportResult.class);
assertEquals(
ApiStatus.SUCCESS,
importResult.getStatus(),
"Case-insensitive glossaryStatus import should succeed. Result: " + resultJson);
assertEquals(4, importResult.getNumberOfRowsPassed());
assertEquals(0, importResult.getNumberOfRowsFailed());

for (String termName :
List.of(draftTermName, approvedTermName, deprecatedTermName, mixedCaseTermName)) {
GlossaryTerm term =
client.glossaryTerms().getByName(glossary.getFullyQualifiedName() + "." + termName);
assertNotNull(
term.getEntityStatus(),
"Imported term '" + termName + "' must have an entityStatus assigned");
}
Comment thread
yan-3005 marked this conversation as resolved.
Outdated
}

/** Invalid glossaryStatus values must still surface as a clear validation failure. */
@Test
void test_importCsv_glossaryStatusInvalidValueFails(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();

Glossary glossary = createEntity(createMinimalRequest(ns));
String header =
client.glossaries().exportCsv(glossary.getName()).lines().findFirst().orElse("");

String badTermName = ns.prefix("statusBad");
String csv =
header
+ "\n"
+ buildStatusRow(header, badTermName, "Bad Status", "invalid status", "not-a-status");

String resultJson = client.glossaries().importCsv(glossary.getName(), csv, false);
assertNotNull(resultJson);
CsvImportResult importResult = JsonUtils.readValue(resultJson, CsvImportResult.class);
assertTrue(
importResult.getNumberOfRowsFailed() != null && importResult.getNumberOfRowsFailed() >= 1,
"Import should report at least one failed row. Result: " + resultJson);
assertTrue(
importResult.getImportResultsCsv() != null
&& importResult.getImportResultsCsv().contains("not-a-status"),
"Failure message should reference the invalid status value. Result: " + resultJson);
assertTrue(
importResult.getImportResultsCsv().contains("is invalid"),
"Failure message should mark the value as invalid. Result: " + resultJson);
}

private static String buildStatusRow(
String header, String name, String displayName, String description, String status) {
String[] columns = header.split(",");
int statusIdx = -1;
int nameIdx = -1;
int displayIdx = -1;
int descIdx = -1;
for (int i = 0; i < columns.length; i++) {
String col = columns[i].trim();
if (col.equals("glossaryStatus")) {
statusIdx = i;
} else if (col.equals("name*") || col.equals("name")) {
nameIdx = i;
} else if (col.equals("displayName")) {
displayIdx = i;
} else if (col.equals("description")) {
descIdx = i;
}
}
String[] row = new String[columns.length];
for (int i = 0; i < row.length; i++) {
row[i] = "";
}
if (nameIdx >= 0) {
row[nameIdx] = "\"" + name + "\"";
}
if (displayIdx >= 0) {
row[displayIdx] = "\"" + displayName + "\"";
}
if (descIdx >= 0) {
row[descIdx] = "\"" + description + "\"";
}
if (statusIdx >= 0) {
row[statusIdx] = status;
}
return String.join(",", row) + "\n";
}

/**
* Regression test: glossary terms must keep their assigned domains through a CSV export/import
* round-trip (the UI "bulk edit" flow). Domains were silently dropped because the glossary CSV
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
import org.openmetadata.service.resources.glossary.GlossaryResource;
import org.openmetadata.service.resources.settings.SettingsCache;
import org.openmetadata.service.security.policyevaluator.PolicyConditionUpdater;
import org.openmetadata.service.util.EntityFieldUtils;
import org.openmetadata.service.util.EntityUtil.Fields;
import org.openmetadata.service.util.EntityUtil.RelationIncludes;
import org.openmetadata.service.util.FullyQualifiedName;
Expand Down Expand Up @@ -485,7 +486,9 @@ private EntityStatus getTermStatus(CSVPrinter printer, CSVRecord csvRecord) thro
}
String termStatus = csvRecord.get(10);
try {
return nullOrEmpty(termStatus) ? EntityStatus.DRAFT : EntityStatus.fromValue(termStatus);
return nullOrEmpty(termStatus)
? EntityStatus.DRAFT
: EntityFieldUtils.parseEntityStatus(termStatus);
} catch (Exception ex) {
// List should have even numbered terms - termName and endPoint
importFailure(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
import org.openmetadata.service.jdbi3.FeedRepository.ThreadContext;
import org.openmetadata.service.resources.metrics.MetricResource;
import org.openmetadata.service.security.AuthorizationException;
import org.openmetadata.service.util.EntityFieldUtils;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.EntityUtil.RelationIncludes;
import org.openmetadata.service.util.MemoryOwnership;
Expand Down Expand Up @@ -421,7 +422,7 @@ private EntityStatus getEntityStatus(CSVPrinter printer, CSVRecord csvRecord, in
return null;
}
try {
return EntityStatus.fromValue(csvRecord.get(fieldNumber));
return EntityFieldUtils.parseEntityStatus(csvRecord.get(fieldNumber));
} catch (Exception ex) {
importFailure(
printer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,14 +488,35 @@ public static void setReviewers(EntityInterface entity, String reviewerNames) {
}
}

/**
* Parses an {@link EntityStatus} from a string in a case-insensitive way so that
* CSV imports and other user inputs accept values like {@code draft} / {@code DRAFT}
* in addition to the canonical {@code Draft}. Multi-word values such as
* {@code "In Review"} are matched ignoring case as well.
*
* @throws IllegalArgumentException when the value does not match any enum constant
*/
public static EntityStatus parseEntityStatus(String value) {
try {
return EntityStatus.fromValue(value);
} catch (IllegalArgumentException ex) {
for (EntityStatus status : EntityStatus.values()) {
if (status.value().equalsIgnoreCase(value)) {
return status;
}
}
throw ex;
}
}
Comment thread
yan-3005 marked this conversation as resolved.

/**
* Sets the status field on various entity types using appropriate enum values.
* Handles both legacy 'status' field and new 'entityStatus' field.
*/
public static void setEntityStatus(EntityInterface entity, String statusValue) {
try {
try {
EntityStatus status = EntityStatus.fromValue(statusValue);
EntityStatus status = parseEntityStatus(statusValue);
entity.setEntityStatus(status);
LOG.debug(
"Successfully set entityStatus to '{}' on entity type: {}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
Expand Down Expand Up @@ -413,6 +414,32 @@ void setReviewersResolvesUsersAndTeamsAndClearsWhenEmpty() {
}
}

@Test
void parseEntityStatusAcceptsCanonicalAndMixedCaseValues() {
assertEquals(EntityStatus.DRAFT, EntityFieldUtils.parseEntityStatus("Draft"));
assertEquals(EntityStatus.DRAFT, EntityFieldUtils.parseEntityStatus("draft"));
assertEquals(EntityStatus.DRAFT, EntityFieldUtils.parseEntityStatus("DRAFT"));
assertEquals(EntityStatus.DRAFT, EntityFieldUtils.parseEntityStatus("drAFT"));
assertEquals(EntityStatus.DRAFT, EntityFieldUtils.parseEntityStatus("DrAfT"));
assertEquals(EntityStatus.APPROVED, EntityFieldUtils.parseEntityStatus("approved"));
assertEquals(EntityStatus.APPROVED, EntityFieldUtils.parseEntityStatus("APPROVED"));
assertEquals(EntityStatus.APPROVED, EntityFieldUtils.parseEntityStatus("ApPrOvEd"));
assertEquals(EntityStatus.DEPRECATED, EntityFieldUtils.parseEntityStatus("Deprecated"));
assertEquals(EntityStatus.DEPRECATED, EntityFieldUtils.parseEntityStatus("deprecated"));
assertEquals(EntityStatus.DEPRECATED, EntityFieldUtils.parseEntityStatus("DEPRECATED"));
assertEquals(EntityStatus.IN_REVIEW, EntityFieldUtils.parseEntityStatus("in review"));
assertEquals(EntityStatus.IN_REVIEW, EntityFieldUtils.parseEntityStatus("IN REVIEW"));
assertEquals(EntityStatus.IN_REVIEW, EntityFieldUtils.parseEntityStatus("In Review"));
assertEquals(EntityStatus.REJECTED, EntityFieldUtils.parseEntityStatus("rejected"));
assertEquals(EntityStatus.ARCHIVED, EntityFieldUtils.parseEntityStatus("ARCHIVED"));
}

@Test
void parseEntityStatusThrowsForUnknownValues() {
assertThrows(
IllegalArgumentException.class, () -> EntityFieldUtils.parseEntityStatus("not-a-status"));
}

@Test
void setEntityStatusSupportsEnumAndLegacyFallbacks() {
TestEntity entity = entity();
Expand Down
Loading