Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.openmetadata.schema.api.configuration.rdf.RdfConfiguration;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.EntityRelationship;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.service.Entity;
import org.openmetadata.service.monitoring.RequestLatencyContext;
import org.openmetadata.service.util.AsyncService;

Expand Down Expand Up @@ -72,6 +74,15 @@ public static void addRelationship(EntityRelationship relationship) {
if (rdfRepository == null || !rdfRepository.isEnabled()) {
return;
}
if (isGlossaryTermRelatedTo(relationship)) {
// Glossary term ⇔ glossary term RELATED_TO is owned by the typed path
// (addGlossaryTermRelation), which writes the precise predicate —
// skos:exactMatch for synonym, skos:broader for broader, om:relatedTo
// for relatedTo, etc. The generic addRelationship would unconditionally
// write om:relatedTo on top of that, so every type change would leak a
// residual om:relatedTo triple that nothing later cleans up.
return;
}
submitAsync(
"addRelationship",
() -> {
Expand All @@ -90,6 +101,11 @@ public static void removeRelationship(EntityRelationship relationship) {
if (rdfRepository == null || !rdfRepository.isEnabled()) {
return;
}
if (isGlossaryTermRelatedTo(relationship)) {
// See addRelationship — the typed removal path
// (removeGlossaryTermRelation) owns these deletions.
return;
}
submitAsync(
"removeRelationship",
() -> {
Expand All @@ -104,6 +120,12 @@ public static void removeRelationship(EntityRelationship relationship) {
});
}

private static boolean isGlossaryTermRelatedTo(EntityRelationship relationship) {
return Entity.GLOSSARY_TERM.equals(relationship.getFromEntity())
&& Entity.GLOSSARY_TERM.equals(relationship.getToEntity())
&& relationship.getRelationshipType() == Relationship.RELATED_TO;
}

public static boolean isEnabled() {
return rdfRepository != null && rdfRepository.isEnabled();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1128,7 +1128,17 @@ private void addTypedProperty(
XSDDatatype datatype = getXSDDatatype(xsdType);

if (datatype != null && !value.isNull()) {
resource.addProperty(property, model.createTypedLiteral(value.asText(), datatype));
String literal = value.asText();
// Skip blank xsd:string triples. An empty literal carries no real
// information and downstream readers had to special-case it — most
// visibly skos:prefLabel="" winning over rdfs:label in the glossary
// term graph SPARQL. By not writing the triple at all, OPTIONAL
// patterns and COALESCE on the read side behave correctly with no
// extra logic.
if (XSDDatatype.XSDstring.equals(datatype) && literal.isBlank()) {
return;
}
resource.addProperty(property, model.createTypedLiteral(literal, datatype));
}
} else {
addSimpleProperty(resource, propertyId, value, model);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,12 @@ export function convertRdfGraphToOntologyGraph(
});

const nodes: OntologyNode[] = rdfData.nodes.map((node) => {
let glossaryId: string | undefined;
if (node.group) {
// Prefer the explicit glossaryId from the RDF endpoint — it survives
// glossary rename / display-name drift better than the FQN-prefix
// heuristic. Fall back to looking up the glossary by `group` (display
// name) or the FQN's first segment for backwards-compatible payloads.
let glossaryId: string | undefined = node.glossaryId;
if (!glossaryId && node.group) {
glossaryId = glossaryNameToId.get(node.group.toLowerCase());
}
if (!glossaryId && node.fullyQualifiedName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,15 @@ export function buildHierarchyGraphs({
}

const comboId = `hierarchy-combo-${glossaryId}`;
const comboLabel = glossaryNames[glossaryId] ?? glossaryId;
// Prefer the glossary's name from the caller's visible glossary list, but
// fall back to a `group` value carried on any term node (the RDF endpoint
// populates this from the glossary's om:name) so callers who can see the
// term but not the parent glossary still see a human label instead of the
// raw UUID.
const groupFallback = comboNodes
.map((n) => n.group)
.find((g): g is string => typeof g === 'string' && g.length > 0);
const comboLabel = glossaryNames[glossaryId] ?? groupFallback ?? glossaryId;
combos.push({ id: comboId, label: comboLabel, glossaryId });
nodesToShow.forEach((n) => nodes.push(n));
keptEdges.forEach((e) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ export interface GraphNode {
id: string;
label: string;
type: string;
// Human label of the parent glossary (set by the RDF endpoint so the UI
// hierarchy view can show a group name even when the parent Glossary is
// not in the caller's accessible glossary list).
group?: string;
// UUID of the parent glossary; supplied by the RDF endpoint when the
// term-to-glossary membership triple is available.
glossaryId?: string;
title?: string;
fullyQualifiedName?: string;
description?: string;
Expand Down
Loading