From 56a14edee007ec3d1307cf9d3ab9715d2f02e503 Mon Sep 17 00:00:00 2001 From: "sanket.bhor" Date: Wed, 29 Apr 2026 12:42:11 +0530 Subject: [PATCH 1/6] ATLAS-5279:changes for handling rename propagation --- .../001-rename_propagation_typedef_patch.json | 43 ++ .../apache/atlas/repository/Constants.java | 4 + .../atlas/model/typedef/AtlasEntityDef.java | 32 +- .../typedef/AtlasRelationshipEndDef.java | 48 +- .../atlas/model/typedef/AtlasStructDef.java | 16 +- .../apache/atlas/type/AtlasEntityType.java | 281 ++++++++++- .../org/apache/atlas/type/AtlasTypeUtil.java | 1 + .../atlas/type/RenamePropagationTarget.java | 58 +++ .../graph/GraphBackedSearchIndexer.java | 2 + .../patches/AtlasPatchRegistry.java | 7 + .../AtlasTypeDefStoreInitializer.java | 101 ++++ .../store/graph/v2/AtlasEntityDefStoreV2.java | 12 + .../store/graph/v2/AtlasEntityStoreV2.java | 45 ++ .../graph/v2/AtlasTypeDefGraphStoreV2.java | 20 + .../store/graph/v2/EntityRenameHandler.java | 475 ++++++++++++++++++ .../AtlasTypeDefStoreInitializerTest.java | 196 ++++++++ 16 files changed, 1330 insertions(+), 11 deletions(-) create mode 100644 addons/models/6000-Trino/patches/001-rename_propagation_typedef_patch.json create mode 100644 intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java create mode 100644 repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java diff --git a/addons/models/6000-Trino/patches/001-rename_propagation_typedef_patch.json b/addons/models/6000-Trino/patches/001-rename_propagation_typedef_patch.json new file mode 100644 index 0000000000..e8f61837ab --- /dev/null +++ b/addons/models/6000-Trino/patches/001-rename_propagation_typedef_patch.json @@ -0,0 +1,43 @@ +{ + "patches": [ + { + "id": "TYPEDEF_PATCH_6000_001_A", + "description": "Persist attributeDefOverrides for trino_table.qualifiedName (upgrade DBs at typeVersion 1.0)", + "action": "SET_ATTRIBUTE_DEF_OVERRIDES", + "typeName": "trino_table", + "applyToVersion": "1.0", + "updateToVersion": "1.1", + "attributeDefs": [ + { + "name": "qualifiedName", + "autoComputeFormat": "{trinoschema.catalog.name}.{trinoschema.name}.{name}@{trinoschema.catalog.instance.name}" + } + ] + }, + { + "id": "TYPEDEF_PATCH_6000_001_B", + "description": "Persist attributeDefOverrides for trino_column.qualifiedName (upgrade DBs at typeVersion 1.0)", + "action": "SET_ATTRIBUTE_DEF_OVERRIDES", + "typeName": "trino_column", + "applyToVersion": "1.0", + "updateToVersion": "1.1", + "attributeDefs": [ + { + "name": "qualifiedName", + "autoComputeFormat": "{table.trinoschema.catalog.name}.{table.trinoschema.name}.{table.name}.{name}@{table.trinoschema.catalog.instance.name}" + } + ] + }, + { + "id": "TYPEDEF_PATCH_6000_001_C", + "description": "Set propagateRename=true on trino_table_columns endDef1 (upgrade DBs at typeVersion 1.0)", + "action": "SET_PROPAGATE_RENAME", + "typeName": "trino_table_columns", + "applyToVersion": "1.0", + "updateToVersion": "1.1", + "params": { + "endDefNumber": "endDef1" + } + } + ] +} diff --git a/common/src/main/java/org/apache/atlas/repository/Constants.java b/common/src/main/java/org/apache/atlas/repository/Constants.java index dcc3f123e9..7b62f88b86 100644 --- a/common/src/main/java/org/apache/atlas/repository/Constants.java +++ b/common/src/main/java/org/apache/atlas/repository/Constants.java @@ -69,6 +69,10 @@ public final class Constants { public static final String TYPEDESCRIPTION_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "type.description"); public static final String TYPEVERSION_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "type.version"); public static final String TYPEOPTIONS_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "type.options"); + /** + * JSON-serialized {@code List} for entity typedef attribute overrides (e.g. qualifiedName format). + */ + public static final String ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "entity.attrDefOverrides"); public static final String TYPESERVICETYPE_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "type.servicetype"); // relationship def constants diff --git a/intg/src/main/java/org/apache/atlas/model/typedef/AtlasEntityDef.java b/intg/src/main/java/org/apache/atlas/model/typedef/AtlasEntityDef.java index 76282d18f1..87d9b9f5fd 100644 --- a/intg/src/main/java/org/apache/atlas/model/typedef/AtlasEntityDef.java +++ b/intg/src/main/java/org/apache/atlas/model/typedef/AtlasEntityDef.java @@ -68,6 +68,10 @@ public class AtlasEntityDef extends AtlasStructDef implements java.io.Serializab // the value of this field is derived from all the businessMetadataDefs this entityType is referenced in private Map> businessAttributeDefs; + // this field allows derived entity types to override specific properties (like autoComputeFormat) + // of attributes inherited from their superTypes, without needing to redefine the entire attribute. + private List attributeDefOverrides; + public AtlasEntityDef() { this(null, null, null, null, null, null, null); } @@ -124,6 +128,7 @@ public AtlasEntityDef(AtlasEntityDef other) { setSubTypes(other.getSubTypes()); setRelationshipAttributeDefs(other.getRelationshipAttributeDefs()); setBusinessAttributeDefs(other.getBusinessAttributeDefs()); + setAttributeDefOverrides(other.getAttributeDefOverrides()); } } @@ -167,6 +172,14 @@ public void setBusinessAttributeDefs(Map> busine this.businessAttributeDefs = businessAttributeDefs; } + public List getAttributeDefOverrides() { + return attributeDefOverrides; + } + + public void setAttributeDefOverrides(List attributeDefOverrides) { + this.attributeDefOverrides = attributeDefOverrides; + } + public boolean hasSuperType(String typeName) { return hasSuperType(superTypes, typeName); } @@ -250,6 +263,20 @@ public StringBuilder toString(StringBuilder sb) { } } sb.append('}'); + + if (CollectionUtils.isNotEmpty(attributeDefOverrides)) { + sb.append(", attributeDefOverrides=["); + int i = 0; + for (AtlasAttributeDef override : attributeDefOverrides) { + if (i > 0) { + sb.append(", "); + } + override.toString(sb); + i++; + } + sb.append(']'); + } + sb.append('}'); return sb; @@ -267,12 +294,13 @@ public boolean equals(Object o) { AtlasEntityDef that = (AtlasEntityDef) o; - return Objects.equals(superTypes, that.superTypes); + return Objects.equals(superTypes, that.superTypes) && + Objects.equals(attributeDefOverrides, that.attributeDefOverrides); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), superTypes); + return Objects.hash(super.hashCode(), superTypes, attributeDefOverrides); } @Override diff --git a/intg/src/main/java/org/apache/atlas/model/typedef/AtlasRelationshipEndDef.java b/intg/src/main/java/org/apache/atlas/model/typedef/AtlasRelationshipEndDef.java index a6f3f44cf4..8067eb0426 100644 --- a/intg/src/main/java/org/apache/atlas/model/typedef/AtlasRelationshipEndDef.java +++ b/intg/src/main/java/org/apache/atlas/model/typedef/AtlasRelationshipEndDef.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.Cardinality; import javax.xml.bind.annotation.XmlAccessType; @@ -27,6 +28,9 @@ import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Objects; import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; @@ -69,6 +73,22 @@ public class AtlasRelationshipEndDef implements Serializable { * Description of the end */ private String description; + /** + * When set this end acts as a trigger for rename propagation. + */ + private boolean propagateRename; + + /** + * For association relationships, defines which source attributes are mapped to which target + * attributes when a rename is propagated. Each entry is a {@code {"source": "...", "target": "..."}} + * pair. When absent, only the entity's own {@code name} attribute is synchronized. + * + *

Example — Hive DB rename propagated to Trino schema: + *

+     *   [{"source": "name", "target": "name"}, {"source": "clusterName", "target": "sourceCluster"}]
+     * 
+ */ + private List> propagateAttributes; /** * Base constructor @@ -158,6 +178,8 @@ public AtlasRelationshipEndDef(AtlasRelationshipEndDef other) { setCardinality(other.getCardinality()); setIsLegacyAttribute(other.isLegacyAttribute); setDescription(other.description); + setIsPropagateRename(other.propagateRename); + setPropagateAttributes(other.propagateAttributes); } } @@ -233,6 +255,8 @@ public StringBuilder toString(StringBuilder sb) { sb.append(", isContainer==>'").append(isContainer).append('\''); sb.append(", cardinality==>'").append(cardinality).append('\''); sb.append(", isLegacyAttribute==>'").append(isLegacyAttribute).append('\''); + sb.append(", propagateRename==>'").append(propagateRename).append('\''); + sb.append(", propagateAttributes==>'").append(propagateAttributes).append('\''); sb.append('}'); return sb; @@ -240,7 +264,7 @@ public StringBuilder toString(StringBuilder sb) { @Override public int hashCode() { - return Objects.hash(type, getName(), description, isContainer, cardinality, isLegacyAttribute); + return Objects.hash(type, getName(), description, isContainer, cardinality, isLegacyAttribute, propagateRename, propagateAttributes); } @Override @@ -258,7 +282,27 @@ public boolean equals(Object o) { Objects.equals(description, that.description) && isContainer == that.isContainer && cardinality == that.cardinality && - isLegacyAttribute == that.isLegacyAttribute; + isLegacyAttribute == that.isLegacyAttribute && + propagateRename == that.propagateRename && + Objects.equals(propagateAttributes, that.propagateAttributes); + } + + @JsonProperty("propagateRename") + public boolean getIsPropagateRename() { + return propagateRename; + } + + @JsonProperty("propagateRename") + public void setIsPropagateRename(boolean propagateRename) { + this.propagateRename = propagateRename; + } + + public List> getPropagateAttributes() { + return propagateAttributes; + } + + public void setPropagateAttributes(List> propagateAttributes) { + this.propagateAttributes = (propagateAttributes != null) ? Collections.unmodifiableList(propagateAttributes) : null; } @Override diff --git a/intg/src/main/java/org/apache/atlas/model/typedef/AtlasStructDef.java b/intg/src/main/java/org/apache/atlas/model/typedef/AtlasStructDef.java index 1d17bffe9b..b8983f6d43 100644 --- a/intg/src/main/java/org/apache/atlas/model/typedef/AtlasStructDef.java +++ b/intg/src/main/java/org/apache/atlas/model/typedef/AtlasStructDef.java @@ -289,6 +289,7 @@ public static class AtlasAttributeDef implements Serializable { private List constraints; private Map options; private String displayName; + private String autoComputeFormat; public AtlasAttributeDef() { this(null, null); @@ -372,6 +373,7 @@ public AtlasAttributeDef(AtlasAttributeDef other) { setSearchWeight(other.getSearchWeight()); setIndexType(other.getIndexType()); setDisplayName(other.getDisplayName()); + setAutoComputeFormat(other.getAutoComputeFormat()); } } @@ -557,6 +559,14 @@ public void setDescription(String description) { this.description = description; } + public String getAutoComputeFormat() { + return autoComputeFormat; + } + + public void setAutoComputeFormat(String autoComputeFormat) { + this.autoComputeFormat = autoComputeFormat; + } + public StringBuilder toString(StringBuilder sb) { if (sb == null) { sb = new StringBuilder(); @@ -578,6 +588,7 @@ public StringBuilder toString(StringBuilder sb) { sb.append(", searchWeight='").append(searchWeight).append('\''); sb.append(", indexType='").append(indexType).append('\''); sb.append(", displayName='").append(displayName).append('\''); + sb.append(", autoComputeFormat='").append(autoComputeFormat).append('\''); sb.append(", constraints=["); if (CollectionUtils.isNotEmpty(constraints)) { int i = 0; @@ -597,7 +608,7 @@ public StringBuilder toString(StringBuilder sb) { @Override public int hashCode() { - return Objects.hash(name, typeName, isOptional, cardinality, valuesMinCount, valuesMaxCount, isUnique, isIndexable, includeInNotification, defaultValue, constraints, options, description, searchWeight, indexType, displayName); + return Objects.hash(name, typeName, isOptional, cardinality, valuesMinCount, valuesMaxCount, isUnique, isIndexable, includeInNotification, defaultValue, constraints, options, description, searchWeight, indexType, displayName, autoComputeFormat); } @Override @@ -625,7 +636,8 @@ public boolean equals(Object o) { Objects.equals(options, that.options) && Objects.equals(searchWeight, that.searchWeight) && Objects.equals(indexType, that.indexType) && - Objects.equals(displayName, that.displayName); + Objects.equals(displayName, that.displayName) && + Objects.equals(autoComputeFormat, that.autoComputeFormat); } @Override diff --git a/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java b/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java index e4838cf94f..a0c8d3c93f 100644 --- a/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java +++ b/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java @@ -24,6 +24,8 @@ import org.apache.atlas.model.typedef.AtlasEntityDef; import org.apache.atlas.model.typedef.AtlasEntityDef.AtlasRelationshipAttributeDef; import org.apache.atlas.model.typedef.AtlasRelationshipDef.PropagateTags; +import org.apache.atlas.model.typedef.AtlasRelationshipDef.RelationshipCategory; +import org.apache.atlas.model.typedef.AtlasRelationshipEndDef; import org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef; import org.apache.atlas.type.AtlasBuiltInTypes.AtlasObjectIdType; import org.apache.atlas.type.AtlasBusinessMetadataType.AtlasBusinessAttribute; @@ -107,7 +109,14 @@ public class AtlasEntityType extends AtlasStructType { private List dynAttributes = Collections.emptyList(); private List dynEvalTriggerAttributes = Collections.emptyList(); private Map> parsedTemplates = Collections.emptyMap(); - private Set tagPropagationEdges = Collections.emptySet(); + private Set tagPropagationEdges = Collections.emptySet(); + private List renamePropagationTargets = Collections.emptyList(); + /** + * Referenced entity type name → dotted slot in {@code qualifiedName} {@code autoComputeFormat} + * for that type's {@code name} (used when that entity is renamed). Built in + * {@link #computeRenamePropagationTemplateMap(AtlasTypeRegistry)}. + */ + private Map renamePropagationTemplateMap = Collections.emptyMap(); public AtlasEntityType(AtlasEntityDef entityDef) { super(entityDef); @@ -248,6 +257,14 @@ public Set getTagPropagationEdges() { return this.tagPropagationEdges; } + public List getRenamePropagationTargets() { + return renamePropagationTargets; + } + + public Map getRenamePropagationTemplateMap() { + return renamePropagationTemplateMap; + } + public String[] getTagPropagationEdgesArray() { return CollectionUtils.isNotEmpty(tagPropagationEdges) ? tagPropagationEdges.toArray(new String[tagPropagationEdges.size()]) : null; } @@ -379,6 +396,7 @@ void resolveReferencesPhase3(AtlasTypeRegistry typeRegistry) throws AtlasBaseExc } tagPropagationEdges.addAll(superType.tagPropagationEdges); + renamePropagationTargets.addAll(superType.renamePropagationTargets); } ownedRefAttributes = new ArrayList<>(); @@ -405,6 +423,7 @@ void resolveReferencesPhase3(AtlasTypeRegistry typeRegistry) throws AtlasBaseExc businessAttributes = Collections.unmodifiableMap(businessAttributes); ownedRefAttributes = Collections.unmodifiableList(ownedRefAttributes); tagPropagationEdges = Collections.unmodifiableSet(tagPropagationEdges); + renamePropagationTargets = Collections.unmodifiableList(renamePropagationTargets); entityDef.setSubTypes(subTypes); @@ -459,6 +478,8 @@ void resolveReferencesPhase3(AtlasTypeRegistry typeRegistry) throws AtlasBaseExc populateDynFlagsInfo(); + computeRenamePropagationTemplateMap(typeRegistry); + LOG.debug("resolveReferencesPhase3({}): tagPropagationEdges={}", getTypeName(), tagPropagationEdges); } @@ -552,6 +573,8 @@ void resolveReferences(AtlasTypeRegistry typeRegistry) throws AtlasBaseException this.relationshipAttributes = new HashMap<>(); // this will be populated in resolveReferencesPhase3() this.businessAttributes = new HashMap<>(); // this will be populated in resolveReferences(), from AtlasBusinessMetadataType this.tagPropagationEdges = new HashSet<>(); // this will be populated in resolveReferencesPhase2() + this.renamePropagationTargets = new ArrayList<>(); // this will be populated in resolveReferencesPhase2() + this.renamePropagationTemplateMap = new HashMap<>(); // this will be populated in resolveReferencesPhase3() this.typeAndAllSubTypes.add(this.getTypeName()); @@ -860,6 +883,222 @@ void addRelationshipAttribute(String attributeName, AtlasAttribute attribute, At if (propagatesTags) { tagPropagationEdges.add(relationshipType.getRelationshipLabel()); } + + addRenamePropagationTargetIfTriggered(relationshipType, attribute); + } + + /** + * Fills {@link #renamePropagationTemplateMap}: parse {@code qualifiedName} {@code autoComputeFormat}, + * for each {@code ….*.name} slot walk relationships to the referenced type, map type → slot + * ({@code putIfAbsent} per type). No-op if no template. + */ + private void computeRenamePropagationTemplateMap(AtlasTypeRegistry typeRegistry) { + AtlasAttribute qnAttr = getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME); + + if (qnAttr == null || qnAttr.getAttributeDef() == null) { + return; + } + + String template = qnAttr.getAttributeDef().getAutoComputeFormat(); + + if (StringUtils.isBlank(template)) { + return; + } + + List slots = extractTemplateSlots(template); + Map map = new HashMap<>(); + + for (String slot : slots) { + String[] segments = slot.split("\\."); + + // Single-segment slots (e.g. "name", "clusterName") are own attributes — not rename-relevant. + // Only process multi-segment slots that end in ".name". + if (segments.length < 2 || !NAME.equals(segments[segments.length - 1])) { + continue; + } + + AtlasEntityType resolved = resolveSlotPath(slot, segments, typeRegistry); + + if (resolved != null) { + // putIfAbsent: first matching slot per referenced type wins. + map.putIfAbsent(resolved.getTypeName(), slot); + } + } + + if (!map.isEmpty()) { + this.renamePropagationTemplateMap = Collections.unmodifiableMap(map); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("computeRenamePropagationTemplateMap({}): renamePropagationTemplateMap={}", getTypeName(), renamePropagationTemplateMap); + } + } + + /** + * Walks each relationship hop in {@code segments[0..n-2]} starting from {@code this} type. + * Returns the entity type reached at the end of the path, or {@code null} with a WARN log + * on any invalid step (blank segment, missing/non-relationship attribute, unresolvable type, + * non-entity attribute type, or degenerate self-resolution). + */ + private AtlasEntityType resolveSlotPath(String slot, String[] segments, AtlasTypeRegistry typeRegistry) { + // Walk segments[0..n-2] as relationship hops; segments[n-1] is "name" (already validated by caller). + // Blank-segment check is done inline to avoid a separate loop over segments. + AtlasEntityType currentType = this; + + for (int i = 0; i < segments.length - 1; i++) { + String hop = segments[i]; + + // Guard: blank segment means a malformed template (e.g. "a..b.name"). + if (StringUtils.isBlank(hop)) { + LOG.warn("computeRenamePropagationTemplateMap({}): slot '{}' has blank segment at hop {} — skipping", + getTypeName(), slot, i); + return null; + } + + // Resolve the relationship attribute for this hop. + AtlasAttribute relAttr = getFirstRelAttrByName(currentType, hop); + + if (relAttr == null) { + // Give a more specific message: attribute exists but is a plain (non-relationship) attribute vs truly missing. + String reason = currentType.getAttribute(hop) != null + ? "exists but is not a relationship attribute" + : "not found on type '" + currentType.getTypeName() + "'"; + LOG.warn("computeRenamePropagationTemplateMap({}): hop {} of slot '{}': relationship attribute '{}' {} — skipping", + getTypeName(), i, slot, hop, reason); + return null; + } + + // Resolve the entity type that the relationship attribute points to. + AtlasEntityType nextType; + + try { + nextType = getReferencedEntityType(typeRegistry.getType(relAttr.getTypeName())); + } catch (AtlasBaseException e) { + LOG.warn("computeRenamePropagationTemplateMap({}): hop {} of slot '{}': cannot resolve type '{}' for attribute '{}' on '{}' — skipping", + getTypeName(), i, slot, relAttr.getTypeName(), hop, currentType.getTypeName(), e); + return null; + } + + // Guard: relationship attribute must point to an entity type (not a primitive or struct). + if (nextType == null) { + LOG.warn("computeRenamePropagationTemplateMap({}): hop {} of slot '{}': attribute '{}' on '{}' has type '{}' which is not an entity type — skipping", + getTypeName(), i, slot, hop, currentType.getTypeName(), relAttr.getTypeName()); + return null; + } + + currentType = nextType; + } + + // Guard: a valid path must reach a different type; resolving back to self means the template + // only references own attributes and carries no rename-propagation signal. + if (currentType == this) { + LOG.warn("computeRenamePropagationTemplateMap({}): slot '{}' path resolved back to '{}' — skipping", + getTypeName(), slot, getTypeName()); + return null; + } + + return currentType; + } + + /** Ordered list of strings inside each brace-delimited placeholder in the template. */ + private static List extractTemplateSlots(String template) { + List slots = new ArrayList<>(); + int pos = 0; + + while (pos < template.length()) { + int open = template.indexOf(DYN_ATTRIBUTE_OPEN_DELIM, pos); + + if (open == -1) { + break; + } + + int close = template.indexOf(DYN_ATTRIBUTE_CLOSE_DELIM, open + 1); + + if (close == -1) { + break; + } + + String slot = template.substring(open + 1, close).trim(); + + if (!slot.isEmpty()) { + slots.add(slot); + } + + pos = close + 1; + } + + return slots; + } + + /** First relationship attribute named {@code attributeName} on {@code entityType}, or {@code null}. */ + private static AtlasAttribute getFirstRelAttrByName(AtlasEntityType entityType, String attributeName) { + Map> relAttrs = entityType.getRelationshipAttributes(); + + if (MapUtils.isEmpty(relAttrs)) { + return null; + } + + Map byRelType = relAttrs.get(attributeName); + + return MapUtils.isNotEmpty(byRelType) ? byRelType.values().iterator().next() : null; + } + + /** + * Adds one direct rename-propagation target when this entity type is the trigger end + * of the supplied relationship (that is, the matching endDef has propagateRename=true). + * + * Invoked during relationship attribute wiring in resolveReferencesPhase2 so runtime + * propagation can use precomputed targets instead of scanning typedefs per event. + */ + private void addRenamePropagationTargetIfTriggered(AtlasRelationshipType relationshipType, AtlasAttribute relationshipAttribute) { + AtlasRelationshipEndDef endDef1 = relationshipType.getRelationshipDef().getEndDef1(); + AtlasRelationshipEndDef endDef2 = relationshipType.getRelationshipDef().getEndDef2(); + + if (endDef1 == null || endDef2 == null) { + return; + } + + String thisTypeName = getTypeName(); + boolean triggerOnEnd1 = StringUtils.equals(relationshipType.getEnd1Type().getTypeName(), thisTypeName) && endDef1.getIsPropagateRename(); + boolean triggerOnEnd2 = StringUtils.equals(relationshipType.getEnd2Type().getTypeName(), thisTypeName) && endDef2.getIsPropagateRename(); + + if (!triggerOnEnd1 && !triggerOnEnd2) { + return; + } + + String targetTypeName = triggerOnEnd1 + ? relationshipType.getEnd2Type().getTypeName() + : relationshipType.getEnd1Type().getTypeName(); + + List> propagateAttributes = triggerOnEnd1 + ? endDef1.getPropagateAttributes() + : endDef2.getPropagateAttributes(); + + RelationshipCategory category = relationshipType.getRelationshipDef().getRelationshipCategory(); + RenamePropagationTarget target = new RenamePropagationTarget(targetTypeName, category, relationshipAttribute, propagateAttributes != null ? propagateAttributes : Collections.emptyList()); + + if (!containsRenamePropagationTarget(target)) { + renamePropagationTargets.add(target); + } + } + + /** + * Returns true when an equivalent direct target is already present. + */ + private boolean containsRenamePropagationTarget(RenamePropagationTarget candidate) { + if (candidate == null) { + return false; + } + + for (RenamePropagationTarget existing : renamePropagationTargets) { + if (StringUtils.equals(existing.getTargetTypeName(), candidate.getTargetTypeName()) && + existing.getCategory() == candidate.getCategory() && + existing.getRelAttr() == candidate.getRelAttr()) { + return true; + } + } + + return false; } boolean isAssignableFrom(AtlasObjectId objId) { @@ -897,10 +1136,13 @@ private void getTypeHierarchyInfo(AtlasTypeRegistry typeRegistry, Set al List visitedTypes = new ArrayList<>(); Map attributeToEntityNameMap = new HashMap<>(); - collectTypeHierarchyInfo(typeRegistry, allSuperTypeNames, allAttributes, attributeToEntityNameMap, visitedTypes); + // Pass this entity's attributeDefOverrides so that collectTypeHierarchyInfo can apply + // them inline when it encounters the inherited qualifiedName attribute, avoiding a + // separate post-construction mutation pass in resolveReferences(). + collectTypeHierarchyInfo(typeRegistry, allSuperTypeNames, allAttributes, attributeToEntityNameMap, visitedTypes, entityDef.getAttributeDefOverrides()); } - private void collectTypeHierarchyInfo(AtlasTypeRegistry typeRegistry, Set allSuperTypeNames, Map allAttributes, Map attributeToEntityNameMap, List visitedTypes) throws AtlasBaseException { + private void collectTypeHierarchyInfo(AtlasTypeRegistry typeRegistry, Set allSuperTypeNames, Map allAttributes, Map attributeToEntityNameMap, List visitedTypes, List derivedEntityOverrides) throws AtlasBaseException { if (visitedTypes.contains(entityDef.getName())) { throw new AtlasBaseException(AtlasErrorCode.CIRCULAR_REFERENCE, entityDef.getName(), visitedTypes.toString()); } @@ -912,7 +1154,7 @@ private void collectTypeHierarchyInfo(AtlasTypeRegistry typeRegistry, Set derivedEntityOverrides) { + if (!AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME.equals(attributeName) || CollectionUtils.isEmpty(derivedEntityOverrides)) { + return attributeDef; + } + + for (AtlasAttributeDef override : derivedEntityOverrides) { + if (AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME.equals(override.getName()) && override.getAutoComputeFormat() != null) { + AtlasAttributeDef copy = new AtlasAttributeDef(attributeDef); + + copy.setAutoComputeFormat(override.getAutoComputeFormat()); + + return copy; + } + } + + return attributeDef; + } + private void populateDynFlagsInfo() { dynAttributes = new ArrayList<>(); dynEvalTriggerAttributes = new ArrayList<>(); diff --git a/intg/src/main/java/org/apache/atlas/type/AtlasTypeUtil.java b/intg/src/main/java/org/apache/atlas/type/AtlasTypeUtil.java index 6a58377bcf..6ea4cd0ab8 100644 --- a/intg/src/main/java/org/apache/atlas/type/AtlasTypeUtil.java +++ b/intg/src/main/java/org/apache/atlas/type/AtlasTypeUtil.java @@ -73,6 +73,7 @@ */ public class AtlasTypeUtil { public static final String ATTRIBUTE_QUALIFIED_NAME = "qualifiedName"; + public static final String ATTRIBUTE_NAME = "name"; private static final Set ATLAS_BUILTIN_TYPENAMES = new HashSet<>(); private static final String NAME_REGEX = "[a-zA-Z][a-zA-Z0-9_ ]*"; diff --git a/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java b/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java new file mode 100644 index 0000000000..1dec9343cb --- /dev/null +++ b/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java @@ -0,0 +1,58 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.atlas.type; + +import org.apache.atlas.model.typedef.AtlasRelationshipDef.RelationshipCategory; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Typedef-time representation of a rename propagation target for an entity type. + */ +public class RenamePropagationTarget { + private final String targetTypeName; + private final RelationshipCategory category; + private final AtlasStructType.AtlasAttribute relAttr; + private final List> propagateAttributes; + + public RenamePropagationTarget(String targetTypeName, RelationshipCategory category, + AtlasStructType.AtlasAttribute relAttr, List> propagateAttributes) { + this.targetTypeName = targetTypeName; + this.category = category; + this.relAttr = relAttr; + this.propagateAttributes = propagateAttributes != null ? Collections.unmodifiableList(propagateAttributes) : Collections.emptyList(); + } + + public String getTargetTypeName() { + return targetTypeName; + } + + public RelationshipCategory getCategory() { + return category; + } + + public AtlasStructType.AtlasAttribute getRelAttr() { + return relAttr; + } + + public List> getPropagateAttributes() { + return propagateAttributes; + } +} diff --git a/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java b/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java index 721548d8d9..3d7c45f5c3 100755 --- a/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java +++ b/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java @@ -101,6 +101,7 @@ import static org.apache.atlas.repository.Constants.CREATED_BY_KEY; import static org.apache.atlas.repository.Constants.CUSTOM_ATTRIBUTES_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.EDGE_INDEX; +import static org.apache.atlas.repository.Constants.ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.ENTITY_DELETED_TIMESTAMP_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.ENTITY_TEXT_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.ENTITY_TYPE_PROPERTY_KEY; @@ -607,6 +608,7 @@ private void initialize(AtlasGraph graph) throws RepositoryException, IndexExcep createPropertyKey(management, TYPEVERSION_PROPERTY_KEY, String.class, SINGLE); createPropertyKey(management, VERSION_PROPERTY_KEY, Long.class, SINGLE); createPropertyKey(management, TYPEOPTIONS_PROPERTY_KEY, String.class, SINGLE); + createPropertyKey(management, ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY, String.class, SINGLE); createPropertyKey(management, IS_PROXY_KEY, Boolean.class, SINGLE); createPropertyKey(management, PROVENANCE_TYPE_KEY, Integer.class, SINGLE); createPropertyKey(management, HOME_ID_KEY, String.class, SINGLE); diff --git a/repository/src/main/java/org/apache/atlas/repository/patches/AtlasPatchRegistry.java b/repository/src/main/java/org/apache/atlas/repository/patches/AtlasPatchRegistry.java index e4f82425b2..8d0fdadaae 100644 --- a/repository/src/main/java/org/apache/atlas/repository/patches/AtlasPatchRegistry.java +++ b/repository/src/main/java/org/apache/atlas/repository/patches/AtlasPatchRegistry.java @@ -81,6 +81,13 @@ public AtlasPatchRegistry(AtlasGraph graph) { public boolean isApplicable(String incomingId, String patchFile, int index) { String patchId = getId(incomingId, patchFile, index); + // TODO: remove after testing - force typedef patches 020 (rename propagation) to always re-apply + if ("TYPEDEF_PATCH_1000_020_A".equals(patchId) + || "TYPEDEF_PATCH_1000_020_B".equals(patchId) + || "TYPEDEF_PATCH_1000_020_C".equals(patchId)) { + return true; + } + if (MapUtils.isEmpty(patchNameStatusMap) || !patchNameStatusMap.containsKey(patchId)) { return true; } diff --git a/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java b/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java index a7c78b1356..efb7031985 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java @@ -397,8 +397,14 @@ private void loadModelsInFolder(File typesDir, AtlasPatchRegistry patchRegistry) private void startInternal() { try { + // TODO: SANKET:INFO : This is the first step to load the types from the database. + LOG.info("SANKET:INFO : This is the first step to load the types from the database."); typeDefStore.init(); + // TODO: SANKET:INFO : This is the second step to load the types from the models. + LOG.info("SANKET:INFO : This is the second step to load the types from the models."); loadBootstrapTypeDefs(); + // TODO: SANKET:INFO : This is the third step to notify the load completion. + LOG.info("SANKET:INFO : This is the third step to notify the load completion."); typeDefStore.notifyLoadCompletion(); try { AtlasAuthorizerFactory.getAtlasAuthorizer(); @@ -462,6 +468,7 @@ private void applyTypePatches(String typesDirName, AtlasPatchRegistry patchRegis new RemoveLegacyRefAttributesPatchHandler(typeDefStore, typeRegistry), new UpdateTypeDefOptionsPatchHandler(typeDefStore, typeRegistry), new SetServiceTypePatchHandler(typeDefStore, typeRegistry), + new SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry), new UpdateAttributeMetadataHandler(typeDefStore, typeRegistry, graph), new AddSuperTypePatchHandler(typeDefStore, typeRegistry), new AddMandatoryAttributePatchHandler(typeDefStore, typeRegistry) @@ -1179,6 +1186,100 @@ public PatchStatus applyPatch(TypeDefPatch patch) throws AtlasBaseException { } } + /** + * Applies {@code SET_ATTRIBUTE_DEF_OVERRIDES} on entity typedefs and {@code SET_PROPAGATE_RENAME} on relationship typedefs. + */ + static class SetAttributeDefOverridesPatchHandler extends PatchHandler { + private static final String ACTION_SET_ATTRIBUTE_DEF_OVERRIDES = "SET_ATTRIBUTE_DEF_OVERRIDES"; + private static final String ACTION_SET_PROPAGATE_RENAME = "SET_PROPAGATE_RENAME"; + + SetAttributeDefOverridesPatchHandler(AtlasTypeDefStore typeDefStore, AtlasTypeRegistry typeRegistry) { + super(typeDefStore, typeRegistry, new String[] {ACTION_SET_ATTRIBUTE_DEF_OVERRIDES, ACTION_SET_PROPAGATE_RENAME}); + } + + @Override + public PatchStatus applyPatch(TypeDefPatch patch) throws AtlasBaseException { + String typeName = patch.getTypeName(); + AtlasBaseTypeDef typeDef = typeRegistry.getTypeDefByName(typeName); + + if (typeDef == null) { + throw new AtlasBaseException(AtlasErrorCode.PATCH_FOR_UNKNOWN_TYPE, patch.getAction(), typeName); + } + + if (!isPatchApplicable(patch, typeDef)) { + LOG.info("patch skipped: typeName={}; applyToVersion={}; updateToVersion={}", + patch.getTypeName(), patch.getApplyToVersion(), patch.getUpdateToVersion()); + + return SKIPPED; + } + + String action = patch.getAction(); + + if (ACTION_SET_ATTRIBUTE_DEF_OVERRIDES.equals(action)) { + if (!(typeDef instanceof AtlasEntityDef)) { + throw new AtlasBaseException(AtlasErrorCode.PATCH_NOT_APPLICABLE_FOR_TYPE, patch.getAction(), + typeDef.getClass().getSimpleName()); + } + + return applyEntityDefOverrides(patch, (AtlasEntityDef) typeDef); + } else if (ACTION_SET_PROPAGATE_RENAME.equals(action)) { + if (!(typeDef instanceof AtlasRelationshipDef)) { + throw new AtlasBaseException(AtlasErrorCode.PATCH_NOT_APPLICABLE_FOR_TYPE, patch.getAction(), + typeDef.getClass().getSimpleName()); + } + + return applyPropagateRename(patch, (AtlasRelationshipDef) typeDef); + } + + throw new AtlasBaseException(AtlasErrorCode.PATCH_INVALID_DATA, patch.getAction(), typeName); + } + + private PatchStatus applyEntityDefOverrides(TypeDefPatch patch, AtlasEntityDef typeDef) throws AtlasBaseException { + AtlasEntityDef updatedDef = new AtlasEntityDef(typeDef); + + updatedDef.setAttributeDefOverrides(patch.getAttributeDefs()); + updatedDef.setTypeVersion(patch.getUpdateToVersion()); + + typeDefStore.updateEntityDefByName(typeDef.getName(), updatedDef); + + return APPLIED; + } + + private PatchStatus applyPropagateRename(TypeDefPatch patch, AtlasRelationshipDef typeDef) throws AtlasBaseException { + Object endDefObj = patch.getParams() != null ? patch.getParams().get("endDefNumber") : null; + String endDefNumber = endDefObj != null ? String.valueOf(endDefObj).trim() : null; + + boolean useEndDef2 = "endDef2".equalsIgnoreCase(endDefNumber); + boolean useEndDef1 = "endDef1".equalsIgnoreCase(endDefNumber); + + if (endDefNumber == null || (!useEndDef1 && !useEndDef2)) { + throw new AtlasBaseException(AtlasErrorCode.INVALID_PARAMETERS, + "SET_PROPAGATE_RENAME patch for '" + typeDef.getName() + + "' must set params.endDefNumber to \"endDef1\" or \"endDef2\", got: " + + endDefNumber); + } + + AtlasRelationshipDef updatedDef = new AtlasRelationshipDef(typeDef); + + if (useEndDef2) { + AtlasRelationshipEndDef end2 = new AtlasRelationshipEndDef(updatedDef.getEndDef2()); + + end2.setIsPropagateRename(true); + updatedDef.setEndDef2(end2); + } else { + AtlasRelationshipEndDef end1 = new AtlasRelationshipEndDef(updatedDef.getEndDef1()); + + end1.setIsPropagateRename(true); + updatedDef.setEndDef1(end1); + } + + updatedDef.setTypeVersion(patch.getUpdateToVersion()); + typeDefStore.updateRelationshipDefByName(typeDef.getName(), updatedDef); + + return APPLIED; + } + } + static class UpdateAttributeMetadataHandler extends PatchHandler { public final AtlasGraph graph; diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityDefStoreV2.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityDefStoreV2.java index 9b21f3ffce..9b21c553e0 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityDefStoreV2.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityDefStoreV2.java @@ -17,18 +17,21 @@ */ package org.apache.atlas.repository.store.graph.v2; +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.atlas.AtlasErrorCode; import org.apache.atlas.authorize.AtlasAuthorizationUtils; import org.apache.atlas.authorize.AtlasPrivilege; import org.apache.atlas.authorize.AtlasTypeAccessRequest; import org.apache.atlas.exception.AtlasBaseException; import org.apache.atlas.model.typedef.AtlasEntityDef; +import org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef; import org.apache.atlas.repository.Constants; import org.apache.atlas.repository.graphdb.AtlasVertex; import org.apache.atlas.type.AtlasEntityType; import org.apache.atlas.type.AtlasType; import org.apache.atlas.type.AtlasTypeRegistry; import org.apache.atlas.typesystem.types.DataTypes.TypeCategory; +import org.apache.atlas.utils.AtlasJson; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -314,6 +317,15 @@ private AtlasEntityDef toEntityDef(AtlasVertex vertex) throws AtlasBaseException AtlasStructDefStoreV2.toStructDef(vertex, ret, typeDefStore); ret.setSuperTypes(typeDefStore.getSuperTypeNames(vertex)); + + String overridesJson = vertex.getProperty(Constants.ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY, String.class); + + if (StringUtils.isNotBlank(overridesJson)) { + List overrides = AtlasJson.fromJson(overridesJson, + new TypeReference>() { }); + + ret.setAttributeDefOverrides(overrides); + } } return ret; diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java index f9b8c36ae7..29998ed797 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java @@ -108,6 +108,7 @@ public class AtlasEntityStoreV2 implements AtlasEntityStore { private final IAtlasEntityChangeNotifier entityChangeNotifier; private final EntityGraphMapper entityGraphMapper; private final EntityGraphRetriever entityRetriever; + private EntityRenameHandler entityRenameHandler; private boolean storeDifferentialAudits; @Inject @@ -121,6 +122,11 @@ public AtlasEntityStoreV2(AtlasGraph graph, DeleteHandlerDelegate deleteDelegate this.storeDifferentialAudits = STORE_DIFFERENTIAL_AUDITS.getBoolean(); } + @Inject + public void setEntityRenameHandler(EntityRenameHandler entityRenameHandler) { + this.entityRenameHandler = entityRenameHandler; + } + @VisibleForTesting public void setStoreDifferentialAudits(boolean val) { this.storeDifferentialAudits = val; @@ -1202,6 +1208,43 @@ private EntityMutationResponse createOrUpdate(EntityStream entityStream, boolean } } + private void handleRenamePropagation(boolean isPartialUpdate, AtlasEntity entity, AtlasEntityType entityType, + AtlasVertex vertex, EntityMutationContext context) throws org.apache.atlas.exception.AtlasBaseException { + try { + if (!isPartialUpdate || entityRenameHandler == null) { + return; + } + + if (CollectionUtils.isEmpty(entityType.getRenamePropagationTargets())) { + return; + } + + String oldUniqueAttrValue = AtlasGraphUtilsV2.getProperty(vertex, entityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME), String.class); + String newUniqueAttrValue = (String) entity.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME); + + if (StringUtils.isBlank(oldUniqueAttrValue) || StringUtils.isBlank(newUniqueAttrValue)) { + return; + } + + if (StringUtils.equals(oldUniqueAttrValue, newUniqueAttrValue)) { + return; + } + + // TOTDO: + if (!newUniqueAttrValue.toLowerCase().contains("sb_latest")) { + LOG.info("SANKET:DEBUG:HOOK: Skipping rename propagation for entity "); + return; + } + + // Rename detected: populate dependent entities into the mutation context + entityRenameHandler.addDependentsToContext(context, entityType, vertex, entity); + //context.getUpdatedEntities().clear(); + } catch (Exception e) { + // context.getUpdatedEntities().clear(); + throw new RuntimeException(e); + } + } + private EntityMutationContext preCreateOrUpdate(EntityStream entityStream, EntityGraphMapper entityGraphMapper, boolean isPartialUpdate) throws AtlasBaseException { MetricRecorder metric = RequestContext.get().startMetricRecord("preCreateOrUpdate"); @@ -1251,6 +1294,8 @@ private EntityMutationContext preCreateOrUpdate(EntityStream entityStream, Entit } if (!isEntityIncomplete(vertex)) { // In case of an import shell entities, skip updating to entitiesCreated, to avoid mapAttributesAndClassification // In case of hook shell entities, it will not reach to this case context.addUpdated(guid, entity, entityType, vertex); + + handleRenamePropagation(isPartialUpdate, entity, entityType, vertex, context); } } else { graphDiscoverer.validateAndNormalize(entity); diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasTypeDefGraphStoreV2.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasTypeDefGraphStoreV2.java index 57146521ab..3391c324cf 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasTypeDefGraphStoreV2.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasTypeDefGraphStoreV2.java @@ -271,6 +271,15 @@ AtlasVertex createTypeVertex(AtlasBaseTypeDef typeDef) { ret.setProperty(Constants.VERSION_PROPERTY_KEY, typeDef.getVersion()); ret.setProperty(Constants.TYPEOPTIONS_PROPERTY_KEY, AtlasType.toJson(typeDef.getOptions())); + if (typeDef instanceof AtlasEntityDef) { + AtlasEntityDef entityDef = (AtlasEntityDef) typeDef; + + if (CollectionUtils.isNotEmpty(entityDef.getAttributeDefOverrides())) { + ret.setProperty(Constants.ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY, + AtlasType.toJson(entityDef.getAttributeDefOverrides())); + } + } + return ret; } @@ -292,6 +301,17 @@ void updateTypeVertex(AtlasBaseTypeDef typeDef, AtlasVertex vertex) { updateVertexProperty(vertex, Constants.TYPEVERSION_PROPERTY_KEY, typeDef.getTypeVersion()); updateVertexProperty(vertex, Constants.TYPEOPTIONS_PROPERTY_KEY, AtlasType.toJson(typeDef.getOptions())); + if (typeDef instanceof AtlasEntityDef) { + AtlasEntityDef entityDef = (AtlasEntityDef) typeDef; + + if (CollectionUtils.isNotEmpty(entityDef.getAttributeDefOverrides())) { + updateVertexProperty(vertex, Constants.ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY, + AtlasType.toJson(entityDef.getAttributeDefOverrides())); + } else { + vertex.removeProperty(Constants.ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY); + } + } + if (StringUtils.isNotEmpty(typeDef.getServiceType())) { updateVertexProperty(vertex, Constants.TYPESERVICETYPE_PROPERTY_KEY, typeDef.getServiceType()); } diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java new file mode 100644 index 0000000000..28d869ea46 --- /dev/null +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java @@ -0,0 +1,475 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.atlas.repository.store.graph.v2; + +import org.apache.atlas.exception.AtlasBaseException; +import org.apache.atlas.model.instance.AtlasEntity; +import org.apache.atlas.repository.graph.GraphHelper; +import org.apache.atlas.repository.graphdb.AtlasEdge; +import org.apache.atlas.repository.graphdb.AtlasVertex; +import org.apache.atlas.type.AtlasEntityType; +import org.apache.atlas.type.AtlasStructType.AtlasAttribute; +import org.apache.atlas.type.AtlasTypeRegistry; +import org.apache.atlas.type.AtlasTypeUtil; +import org.apache.atlas.type.RenamePropagationTarget; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Component +public class EntityRenameHandler { + private static final Logger LOG = LoggerFactory.getLogger(EntityRenameHandler.class); + + private final AtlasTypeRegistry typeRegistry; + + @Inject + public EntityRenameHandler(AtlasTypeRegistry typeRegistry) { + this.typeRegistry = typeRegistry; + } + + /** + * Discovers all entities whose qualifiedName is impacted by the given rename and injects + * them into the mutation context so they are persisted in the same transaction. + * + * Uses typedef-time metadata on each {@link AtlasEntityType} for O(1) slot-key lookup where + * applicable, without deriving template paths from the graph at runtime. + */ + public void addDependentsToContext(EntityMutationContext context, + AtlasEntityType sourceEntityType, + AtlasVertex sourceVertex, + AtlasEntity sourceEntity) throws AtlasBaseException { + String renamedTypeName = sourceEntityType.getTypeName(); + String newName = (String) sourceEntity.getAttribute(AtlasTypeUtil.ATTRIBUTE_NAME); + + LOG.debug("addDependentsToContext(): start — renamedType={}, newName={}", renamedTypeName, newName); + + Map dependents = new LinkedHashMap<>(); + Set visitedGuids = new HashSet<>(); + + collectDependents(sourceVertex, sourceEntityType, renamedTypeName, newName, visitedGuids, dependents); + + LOG.info("SANKET:DEBUG:HOOK: dependents: {}", dependents); + + LOG.debug("addDependentsToContext(): done — renamedType={}, dependentCount={}", renamedTypeName, dependents.size()); + + injectDependentsIntoContext(context, dependents); + LOG.info("SANKET:DEBUG:HOOK: context.getUpdatedEntities(): {}", context.getUpdatedEntities()); + } + + // ─── context injection ────────────────────────────────────────────────────── + + private void injectDependentsIntoContext(EntityMutationContext context, Map dependents) { + for (DependentUpdate dep : dependents.values()) { + AtlasEntity stub = new AtlasEntity(dep.getTypeName()); + + stub.setGuid(dep.getGuid()); + stub.setAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME, dep.getNewUniqueAttribute()); + + if (MapUtils.isNotEmpty(dep.getMappedAttrs())) { + for (Map.Entry entry : dep.getMappedAttrs().entrySet()) { + stub.setAttribute(entry.getKey(), entry.getValue()); + } + } + + AtlasEntityType dependentEntityType = typeRegistry.getEntityTypeByName(dep.getTypeName()); + + context.addUpdated(dep.getGuid(), stub, dependentEntityType, dep.getVertex()); + } + } + + // ─── core traversal ───────────────────────────────────────────────────────── + + /** + * Single recursive traversal over all propagation targets. + * + *

The override strategy is decided purely by whether the target carries + * explicit {@code propagateAttributes} mappings: + * + *

    + *
  • propagateAttributes present: the slot patched in the dependent's {@code qualifiedName} + * template is the {@code target} of the mapping whose {@code source} is {@code "name"} + * (see {@link #findQualifiedNameOverrideKey}); {@code newName} is written into that slot. + * Extra stub attributes come from {@link #buildMappedAttrs}. The updated dependent becomes + * the new rename root for any further cascade.
  • + *
  • propagateAttributes absent: the override slot is resolved via O(1) lookup + * in the dependent type's precomputed {@code renamePropagationTemplateMap} using + * {@code renamedTypeName}. The same renamed type and new name are propagated + * unchanged through the cascade.
  • + *
+ */ + private void collectDependents(AtlasVertex sourceVertex, AtlasEntityType sourceEntityType, + String renamedTypeName, String newName, + Set visitedGuids, Map out) throws AtlasBaseException { + for (RenamePropagationTarget propagationTarget : sourceEntityType.getRenamePropagationTargets()) { + AtlasAttribute relAttr = propagationTarget.getRelAttr(); + + if (relAttr == null) { + continue; + } + + for (AtlasEdge edge : toList(GraphHelper.getEdgesForLabel(sourceVertex, relAttr.getRelationshipEdgeLabel(), relAttr.getRelationshipEdgeDirection()))) { + AtlasVertex dependentVertex = getOtherVertex(edge, sourceVertex); + + if (dependentVertex == null) { + continue; + } + + String dependentGuid = AtlasGraphUtilsV2.getIdFromVertex(dependentVertex); + + if (StringUtils.isBlank(dependentGuid) || !visitedGuids.add(dependentGuid)) { + continue; + } + + String dependentTypeName = GraphHelper.getTypeName(dependentVertex); + AtlasEntityType dependentEntityType = typeRegistry.getEntityTypeByName(dependentTypeName); + + if (dependentEntityType == null) { + continue; + } + + String template = getUniqueAttributeTemplate(dependentEntityType); + String oldQN = AtlasGraphUtilsV2.getProperty(dependentVertex, + dependentEntityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME), String.class); + + LOG.info("SANKET:DEBUG:HOOK: oldQN: {}", oldQN); + LOG.info("SANKET:DEBUG:HOOK: template: {}", template); + LOG.info("SANKET:DEBUG:HOOK: dependentTypeName: {}", dependentTypeName); + + if (StringUtils.isBlank(template) || StringUtils.isBlank(oldQN)) { + continue; + } + + String overrideKey; + Map mappedAttrs; + + if (CollectionUtils.isNotEmpty(propagationTarget.getPropagateAttributes())) { + // QualifiedName slot key comes from the propagateAttributes entry whose source is "name". + overrideKey = findQualifiedNameOverrideKey(propagationTarget.getPropagateAttributes()); + if (overrideKey == null) { + overrideKey = AtlasTypeUtil.ATTRIBUTE_NAME; + } + mappedAttrs = buildMappedAttrs(propagationTarget, newName); + } else { + // No propagateAttributes: O(1) lookup of the slot key from the typedef-time + // precomputed renamePropagationTemplateMap on the dependent entity type. + overrideKey = dependentEntityType.getRenamePropagationTemplateMap().get(renamedTypeName); + + if (overrideKey == null) { + LOG.debug("collectDependents(): no override key for renamedType={} on dependentType={} — skipping", + renamedTypeName, dependentTypeName); + continue; + } + + mappedAttrs = new HashMap<>(); + } + + String newQN = recomputeUniqueAttribute(oldQN, template, overrideKey, newName); + + if (StringUtils.isBlank(newQN) || StringUtils.equals(oldQN, newQN)) { + continue; + } + + LOG.debug("collectDependents(): updating {} [{}] {} -> {}", dependentTypeName, dependentGuid, oldQN, newQN); + + out.put(dependentGuid, new DependentUpdate(dependentGuid, dependentTypeName, dependentVertex, newQN, mappedAttrs)); + + if (CollectionUtils.isNotEmpty(dependentEntityType.getRenamePropagationTargets())) { + if (CollectionUtils.isNotEmpty(propagationTarget.getPropagateAttributes())) { + // The updated dependent becomes the new rename root for its own downstream cascade. + // Read the segment we patched (same key as overrideKey) for the next level's newName. + String dependentNewName = parseUniqueAttribute(newQN, template).get(overrideKey); + + if (StringUtils.isBlank(dependentNewName)) { + dependentNewName = newName; + } + + collectDependents(dependentVertex, dependentEntityType, dependentTypeName, dependentNewName, visitedGuids, out); + } else { + // Template-slot cascade: propagate the same renamed type and new name unchanged. + collectDependents(dependentVertex, dependentEntityType, renamedTypeName, newName, visitedGuids, out); + } + } + } + } + } + + // ─── association attribute mapping ────────────────────────────────────────── + + /** + * Builds the mapped-attribute payload for an association-linked dependent. + * Reads {@link RenamePropagationTarget#getPropagateAttributes()} so renamed values can be written + * to differently named attributes on the dependent stub when the mapping source is {@code name}. + * Falls back to {@code {name → newSourceEntityName}} when the list is empty. + */ + private Map buildMappedAttrs(RenamePropagationTarget target, String newSourceEntityName) { + List> propagateAttributes = target.getPropagateAttributes(); + + if (CollectionUtils.isEmpty(propagateAttributes)) { + Map fallback = new HashMap<>(); + + fallback.put(AtlasTypeUtil.ATTRIBUTE_NAME, newSourceEntityName); + + return fallback; + } + + Map mappedAttrs = new HashMap<>(); + + for (Map mapping : propagateAttributes) { + String sourceAttr = mapping.get("source"); + String targetAttr = mapping.get("target"); + + // Only "name" is available at this point; other source attributes would need the full sourceEntity. + if (AtlasTypeUtil.ATTRIBUTE_NAME.equals(sourceAttr) && StringUtils.isNotBlank(targetAttr)) { + mappedAttrs.put(targetAttr, newSourceEntityName); + } + } + + return mappedAttrs; + } + + /** + * Returns the {@code target} attribute name of the first {@code propagateAttributes} entry whose + * {@code source} is {@link AtlasTypeUtil#ATTRIBUTE_NAME "name"}. The returned {@code target} + * identifies which {@code {slot}} in the dependent's {@code qualifiedName} template is patched + * with {@code newName} in {@link #recomputeUniqueAttribute}. + * + * @return {@code null} when no mapping has {@code source=name} and a non-blank {@code target} + */ + private static String findQualifiedNameOverrideKey(List> propagateAttributes) { + if (CollectionUtils.isEmpty(propagateAttributes)) { + return null; + } + + for (Map mapping : propagateAttributes) { + String sourceAttr = mapping.get("source"); + String targetAttr = mapping.get("target"); + + if (AtlasTypeUtil.ATTRIBUTE_NAME.equals(sourceAttr) && StringUtils.isNotBlank(targetAttr)) { + return targetAttr; + } + } + + return null; + } + + // ─── qualifiedName recomputation ──────────────────────────────────────────── + + /** + * Recomputes a dependent entity's qualifiedName after a rename using a 3-step in-memory approach: + *
    + *
  1. Parse — extract each template slot's value from the stored qualifiedName string.
  2. + *
  3. Override — replace the single slot that changed (the renamed entity's name key).
  4. + *
  5. Rebuild — reconstruct the qualifiedName from the updated slots.
  6. + *
+ * No extra graph reads are required; all values come from the existing qualifiedName string. + */ + private String recomputeUniqueAttribute(String currentUniqueAttr, String template, String overrideKey, String newValue) { + Map slots = parseUniqueAttribute(currentUniqueAttr, template); + + if (StringUtils.isNotBlank(overrideKey) && newValue != null) { + slots.put(overrideKey, newValue); + } + + return buildUniqueAttribute(template, slots); + } + + /** + * Parses a qualifiedName string into a {@code { templateKey → value }} map by walking + * the template and value strings in parallel with two cursors. + * + *

Literal characters (such as {@code '.'} or {@code '@'}) are delimiters; the character immediately + * after {@code '}'} in the template is used as the value's end delimiter for each slot. + * The last slot captures everything remaining in the qualifiedName. + * + *

+     *   template   = "{db.name}.{name}@{clusterName}"
+     *   uniqueAttr = "mydb.mytable@cluster1"
+     *   result     = { "db.name" → "mydb", "name" → "mytable", "clusterName" → "cluster1" }
+     * 
+ */ + private Map parseUniqueAttribute(String uniqueAttr, String template) { + Map slots = new LinkedHashMap<>(); + int tPos = 0; // cursor into template + int uPos = 0; // cursor into uniqueAttr + + while (tPos < template.length() && uPos <= uniqueAttr.length()) { + char tc = template.charAt(tPos); + + if (tc != '{') { + // Literal delimiter — advance both cursors past it. + tPos++; + uPos = Math.min(uPos + 1, uniqueAttr.length()); + continue; + } + + int closeBrace = template.indexOf('}', tPos); + + if (closeBrace < 0) { + break; // malformed template + } + + String key = template.substring(tPos + 1, closeBrace); + + tPos = closeBrace + 1; // advance past '}' + + // Last slot: nothing follows in the template, so capture the rest of uniqueAttr. + if (tPos >= template.length()) { + slots.put(key, uniqueAttr.substring(uPos)); + break; + } + + // The character immediately after '}' in the template is the value's end delimiter. + char delim = template.charAt(tPos); + int delimPos = uniqueAttr.indexOf(delim, uPos); + + if (delimPos >= 0) { + slots.put(key, uniqueAttr.substring(uPos, delimPos)); + uPos = delimPos + 1; // skip past the delimiter in uniqueAttr + tPos++; // skip past the delimiter in template + } else { + // Delimiter not found — capture the rest (handles malformed / truncated values). + slots.put(key, uniqueAttr.substring(uPos)); + uPos = uniqueAttr.length(); + } + } + + return slots; + } + + /** + * Reconstructs a qualifiedName string from a template and a populated slot map. + * Each {@code {key}} in the template is replaced with its value; unknown keys become {@code ""}. + * + *
+     *   template = "{db.name}.{name}@{clusterName}"
+     *   slots    = { "db.name" → "mydb", "name" → "new_table", "clusterName" → "cluster1" }
+     *   result   = "mydb.new_table@cluster1"
+     * 
+ */ + private String buildUniqueAttribute(String template, Map slots) { + StringBuilder sb = new StringBuilder(template.length() + 32); + int tPos = 0; + + while (tPos < template.length()) { + if (template.charAt(tPos) != '{') { + sb.append(template.charAt(tPos++)); + continue; + } + + int closeBrace = template.indexOf('}', tPos); + + if (closeBrace < 0) { + break; // malformed template + } + + String key = template.substring(tPos + 1, closeBrace); + + sb.append(slots.getOrDefault(key, "")); + + tPos = closeBrace + 1; + } + + return sb.toString(); + } + + // ─── small helpers ────────────────────────────────────────────────────────── + + /** Returns the {@code autoComputeFormat} for the entity's {@code qualifiedName} attribute, trimmed. */ + private String getUniqueAttributeTemplate(AtlasEntityType entityType) { + AtlasAttribute uniqueAttr = entityType.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME); + + if (uniqueAttr == null || uniqueAttr.getAttributeDef() == null) { + return null; + } + + String fmt = uniqueAttr.getAttributeDef().getAutoComputeFormat(); + + return fmt != null ? fmt.trim() : null; + } + + private AtlasVertex getOtherVertex(AtlasEdge edge, AtlasVertex anchor) { + AtlasVertex out = edge.getOutVertex(); + AtlasVertex in = edge.getInVertex(); + + return out != null && out.equals(anchor) ? in : out; + } + + private List toList(Iterator iterator) { + List list = new ArrayList<>(); + + if (iterator != null) { + while (iterator.hasNext()) { + list.add(iterator.next()); + } + } + + return list; + } + + // ─── inner class ──────────────────────────────────────────────────────────── + + public static class DependentUpdate { + private final String guid; + private final String typeName; + private final AtlasVertex vertex; + private final String newUniqueAttribute; + private final Map mappedAttrs; + + public DependentUpdate(String guid, String typeName, AtlasVertex vertex, String newUniqueAttribute, Map mappedAttrs) { + this.guid = guid; + this.typeName = typeName; + this.vertex = vertex; + this.newUniqueAttribute = newUniqueAttribute; + this.mappedAttrs = mappedAttrs; + } + + public String getGuid() { + return guid; + } + + public String getTypeName() { + return typeName; + } + + public AtlasVertex getVertex() { + return vertex; + } + + public String getNewUniqueAttribute() { + return newUniqueAttribute; + } + + public Map getMappedAttrs() { + return mappedAttrs; + } + } +} diff --git a/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java b/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java index 99cab83ab5..2381fc1d77 100644 --- a/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java +++ b/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java @@ -927,6 +927,202 @@ public void testSetServiceTypePatchHandlerComplete() throws Exception { verify(typeDefStore).updateTypesDef(any()); } + @Test + public void testSetAttributeDefOverridesPatchHandlerSupportedActions() { + AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = + new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + + String[] actions = handler.getSupportedActions(); + assertEquals(actions.length, 2); + assertEquals(actions[0], "SET_ATTRIBUTE_DEF_OVERRIDES"); + assertEquals(actions[1], "SET_PROPAGATE_RENAME"); + } + + @Test + public void testSetAttributeDefOverridesPatchHandlerApplyOverrides() throws Exception { + AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = + new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestEntity", "1.0", "2.0"); + + AtlasEntityDef entityDef = new AtlasEntityDef("TestEntity", "desc", "1.0"); + when(typeRegistry.getTypeDefByName("TestEntity")).thenReturn(entityDef); + + AtlasAttributeDef overrideAttr = new AtlasAttributeDef("qualifiedName", "string"); + overrideAttr.setAutoComputeFormat("{db}.{name}"); + setField(patch, "attributeDefs", Arrays.asList(overrideAttr)); + + PatchStatus result = handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch); + assertEquals(result, APPLIED); + verify(typeDefStore).updateEntityDefByName(eq("TestEntity"), any()); + } + + @Test + public void testSetAttributeDefOverridesPatchHandlerApplyOverridesSkipped() throws Exception { + AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = + new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestEntity", "2.0", "3.0"); + + AtlasEntityDef entityDef = new AtlasEntityDef("TestEntity", "desc", "1.0"); + when(typeRegistry.getTypeDefByName("TestEntity")).thenReturn(entityDef); + + AtlasAttributeDef overrideAttr = new AtlasAttributeDef("qualifiedName", "string"); + setField(patch, "attributeDefs", Arrays.asList(overrideAttr)); + + PatchStatus result = handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch); + assertEquals(result, SKIPPED); + verify(typeDefStore, never()).updateEntityDefByName(anyString(), any()); + } + + @Test + public void testSetAttributeDefOverridesPatchHandlerApplyOverridesWrongType() throws Exception { + AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = + new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestRelationship", "1.0", "2.0"); + + AtlasRelationshipDef relationshipDef = new AtlasRelationshipDef(); + relationshipDef.setName("TestRelationship"); + relationshipDef.setTypeVersion("1.0"); + relationshipDef.setEndDef1(new AtlasRelationshipEndDef("E1", "a1", AtlasAttributeDef.Cardinality.SINGLE)); + relationshipDef.setEndDef2(new AtlasRelationshipEndDef("E2", "a2", AtlasAttributeDef.Cardinality.SINGLE)); + when(typeRegistry.getTypeDefByName("TestRelationship")).thenReturn(relationshipDef); + + AtlasAttributeDef overrideAttr = new AtlasAttributeDef("qualifiedName", "string"); + setField(patch, "attributeDefs", Arrays.asList(overrideAttr)); + + expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); + } + + @Test + public void testSetAttributeDefOverridesPatchHandlerPropagateRenameEnd1() throws Exception { + AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = + new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); + + AtlasRelationshipDef relationshipDef = new AtlasRelationshipDef(); + relationshipDef.setName("TestRelationship"); + relationshipDef.setTypeVersion("1.0"); + relationshipDef.setEndDef1(new AtlasRelationshipEndDef("E1", "a1", AtlasAttributeDef.Cardinality.SINGLE)); + relationshipDef.setEndDef2(new AtlasRelationshipEndDef("E2", "a2", AtlasAttributeDef.Cardinality.SINGLE)); + when(typeRegistry.getTypeDefByName("TestRelationship")).thenReturn(relationshipDef); + + Map params = new HashMap<>(); + params.put("endDefNumber", "endDef1"); + setField(patch, "params", params); + + PatchStatus result = handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch); + assertEquals(result, APPLIED); + verify(typeDefStore).updateRelationshipDefByName(eq("TestRelationship"), any()); + } + + @Test + public void testSetAttributeDefOverridesPatchHandlerPropagateRenameEnd2() throws Exception { + AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = + new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); + + AtlasRelationshipDef relationshipDef = new AtlasRelationshipDef(); + relationshipDef.setName("TestRelationship"); + relationshipDef.setTypeVersion("1.0"); + relationshipDef.setEndDef1(new AtlasRelationshipEndDef("E1", "a1", AtlasAttributeDef.Cardinality.SINGLE)); + relationshipDef.setEndDef2(new AtlasRelationshipEndDef("E2", "a2", AtlasAttributeDef.Cardinality.SINGLE)); + when(typeRegistry.getTypeDefByName("TestRelationship")).thenReturn(relationshipDef); + + Map params = new HashMap<>(); + params.put("endDefNumber", "endDef2"); + setField(patch, "params", params); + + PatchStatus result = handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch); + assertEquals(result, APPLIED); + verify(typeDefStore).updateRelationshipDefByName(eq("TestRelationship"), any()); + } + + @Test + public void testSetAttributeDefOverridesPatchHandlerPropagateRenameInvalidEndDef() throws Exception { + AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = + new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); + + AtlasRelationshipDef relationshipDef = new AtlasRelationshipDef(); + relationshipDef.setName("TestRelationship"); + relationshipDef.setTypeVersion("1.0"); + relationshipDef.setEndDef1(new AtlasRelationshipEndDef("E1", "a1", AtlasAttributeDef.Cardinality.SINGLE)); + relationshipDef.setEndDef2(new AtlasRelationshipEndDef("E2", "a2", AtlasAttributeDef.Cardinality.SINGLE)); + when(typeRegistry.getTypeDefByName("TestRelationship")).thenReturn(relationshipDef); + + Map params = new HashMap<>(); + params.put("endDefNumber", "endDef3"); + setField(patch, "params", params); + + expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); + } + + @Test + public void testSetAttributeDefOverridesPatchHandlerPropagateRenameMissingParam() throws Exception { + AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = + new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); + + AtlasRelationshipDef relationshipDef = new AtlasRelationshipDef(); + relationshipDef.setName("TestRelationship"); + relationshipDef.setTypeVersion("1.0"); + relationshipDef.setEndDef1(new AtlasRelationshipEndDef("E1", "a1", AtlasAttributeDef.Cardinality.SINGLE)); + relationshipDef.setEndDef2(new AtlasRelationshipEndDef("E2", "a2", AtlasAttributeDef.Cardinality.SINGLE)); + when(typeRegistry.getTypeDefByName("TestRelationship")).thenReturn(relationshipDef); + + setField(patch, "params", new HashMap()); + + expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); + } + + @Test + public void testSetAttributeDefOverridesPatchHandlerPropagateRenameWrongType() throws Exception { + AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = + new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestEntity", "1.0", "2.0"); + + AtlasEntityDef entityDef = new AtlasEntityDef("TestEntity", "desc", "1.0"); + when(typeRegistry.getTypeDefByName("TestEntity")).thenReturn(entityDef); + + Map params = new HashMap<>(); + params.put("endDefNumber", "endDef1"); + setField(patch, "params", params); + + expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); + } + + @Test + public void testSetAttributeDefOverridesPatchHandlerUnknownType() throws Exception { + AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = + new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "UnknownType", "1.0", "2.0"); + when(typeRegistry.getTypeDefByName("UnknownType")).thenReturn(null); + + expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); + } + + @Test + public void testSetAttributeDefOverridesPatchHandlerInvalidAction() throws Exception { + AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = + new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestEntity", "1.0", "2.0"); + setField(patch, "action", "NOT_A_SUPPORTED_PATCH_ACTION"); + + AtlasEntityDef entityDef = new AtlasEntityDef("TestEntity", "desc", "1.0"); + when(typeRegistry.getTypeDefByName("TestEntity")).thenReturn(entityDef); + + expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); + } + @Test public void testUpdateAttributeMetadataHandlerComprehensive() throws Exception { AtlasTypeDefStoreInitializer.UpdateAttributeMetadataHandler handler = new AtlasTypeDefStoreInitializer.UpdateAttributeMetadataHandler(typeDefStore, typeRegistry, graph); From 0f2ceeffdd1defd5ba236416dadcb1b18f116062 Mon Sep 17 00:00:00 2001 From: "sanket.bhor" Date: Tue, 5 May 2026 10:04:48 +0530 Subject: [PATCH 2/6] ATLAS-5279: code cleanup: improve formatting, refactor structure, and add logging --- .../models/6000-Trino/6000-trino_model.json | 8 +- .../001-rename_propagation_typedef_patch.json | 18 +- .../apache/atlas/repository/Constants.java | 4 +- .../typedef/AtlasRelationshipEndDef.java | 13 +- .../apache/atlas/type/AtlasEntityType.java | 171 ++-- .../atlas/type/RenamePropagationTarget.java | 38 +- .../graph/GraphBackedSearchIndexer.java | 4 +- .../patches/AtlasPatchRegistry.java | 7 - .../AtlasTypeDefStoreInitializer.java | 50 +- .../store/graph/v2/AtlasEntityDefStoreV2.java | 2 +- .../store/graph/v2/AtlasEntityStoreV2.java | 44 +- .../graph/v2/AtlasTypeDefGraphStoreV2.java | 6 +- .../store/graph/v2/EntityRenameHandler.java | 239 ++--- .../AtlasTypeDefStoreInitializerTest.java | 8 +- .../graph/v2/EntityRenameHandlerTest.java | 945 ++++++++++++++++++ 15 files changed, 1280 insertions(+), 277 deletions(-) create mode 100644 repository/src/test/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandlerTest.java diff --git a/addons/models/6000-Trino/6000-trino_model.json b/addons/models/6000-Trino/6000-trino_model.json index 7340385309..d3bb3fc52a 100644 --- a/addons/models/6000-Trino/6000-trino_model.json +++ b/addons/models/6000-Trino/6000-trino_model.json @@ -25,6 +25,12 @@ "superTypes": [ "Asset" ], "serviceType": "trino", "typeVersion": "1.0", + "attributeDefOverrides": [ + { + "name": "qualifiedName", + "autoComputeFormat": "{table.trinoschema.catalog.name}.{table.trinoschema.name}.{table.name}.{name}@{table.trinoschema.catalog.instance.name}" + } + ], "attributeDefs": [ { "name": "parameters", "typeName": "map", "cardinality": "SINGLE", "isUnique": false, "isIndexable": false, "isOptional": true } ] @@ -124,7 +130,7 @@ "typeVersion": "1.0", "relationshipCategory": "COMPOSITION", "propagateTags": "NONE", - "endDef1": { "type": "trino_table", "name": "columns", "isContainer": true, "cardinality": "SET", "isLegacyAttribute": false }, + "endDef1": { "type": "trino_table", "name": "columns", "isContainer": true, "cardinality": "SET", "isLegacyAttribute": false, "propagateRename": true }, "endDef2": { "type": "trino_column", "name": "table", "isContainer": false, "cardinality": "SINGLE", "isLegacyAttribute": false } }, { diff --git a/addons/models/6000-Trino/patches/001-rename_propagation_typedef_patch.json b/addons/models/6000-Trino/patches/001-rename_propagation_typedef_patch.json index e8f61837ab..84ebc5a9e9 100644 --- a/addons/models/6000-Trino/patches/001-rename_propagation_typedef_patch.json +++ b/addons/models/6000-Trino/patches/001-rename_propagation_typedef_patch.json @@ -2,20 +2,6 @@ "patches": [ { "id": "TYPEDEF_PATCH_6000_001_A", - "description": "Persist attributeDefOverrides for trino_table.qualifiedName (upgrade DBs at typeVersion 1.0)", - "action": "SET_ATTRIBUTE_DEF_OVERRIDES", - "typeName": "trino_table", - "applyToVersion": "1.0", - "updateToVersion": "1.1", - "attributeDefs": [ - { - "name": "qualifiedName", - "autoComputeFormat": "{trinoschema.catalog.name}.{trinoschema.name}.{name}@{trinoschema.catalog.instance.name}" - } - ] - }, - { - "id": "TYPEDEF_PATCH_6000_001_B", "description": "Persist attributeDefOverrides for trino_column.qualifiedName (upgrade DBs at typeVersion 1.0)", "action": "SET_ATTRIBUTE_DEF_OVERRIDES", "typeName": "trino_column", @@ -29,14 +15,14 @@ ] }, { - "id": "TYPEDEF_PATCH_6000_001_C", + "id": "TYPEDEF_PATCH_6000_001_B", "description": "Set propagateRename=true on trino_table_columns endDef1 (upgrade DBs at typeVersion 1.0)", "action": "SET_PROPAGATE_RENAME", "typeName": "trino_table_columns", "applyToVersion": "1.0", "updateToVersion": "1.1", "params": { - "endDefNumber": "endDef1" + "endDefToken": "endDef1" } } ] diff --git a/common/src/main/java/org/apache/atlas/repository/Constants.java b/common/src/main/java/org/apache/atlas/repository/Constants.java index 7b62f88b86..b4ebd3f35e 100644 --- a/common/src/main/java/org/apache/atlas/repository/Constants.java +++ b/common/src/main/java/org/apache/atlas/repository/Constants.java @@ -69,11 +69,11 @@ public final class Constants { public static final String TYPEDESCRIPTION_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "type.description"); public static final String TYPEVERSION_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "type.version"); public static final String TYPEOPTIONS_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "type.options"); + public static final String TYPESERVICETYPE_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "type.servicetype"); /** * JSON-serialized {@code List} for entity typedef attribute overrides (e.g. qualifiedName format). */ - public static final String ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "entity.attrDefOverrides"); - public static final String TYPESERVICETYPE_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "type.servicetype"); + public static final String TYPE_ATTR_DEF_OVERRIDES_PROPERTY_KEY = getEncodedTypePropertyKey(INTERNAL_PROPERTY_KEY_PREFIX + "type.attrDefOverrides"); // relationship def constants public static final String RELATIONSHIPTYPE_END1_KEY = "endDef1"; diff --git a/intg/src/main/java/org/apache/atlas/model/typedef/AtlasRelationshipEndDef.java b/intg/src/main/java/org/apache/atlas/model/typedef/AtlasRelationshipEndDef.java index 8067eb0426..c7921b6ca3 100644 --- a/intg/src/main/java/org/apache/atlas/model/typedef/AtlasRelationshipEndDef.java +++ b/intg/src/main/java/org/apache/atlas/model/typedef/AtlasRelationshipEndDef.java @@ -79,14 +79,11 @@ public class AtlasRelationshipEndDef implements Serializable { private boolean propagateRename; /** - * For association relationships, defines which source attributes are mapped to which target - * attributes when a rename is propagated. Each entry is a {@code {"source": "...", "target": "..."}} - * pair. When absent, only the entity's own {@code name} attribute is synchronized. - * - *

Example — Hive DB rename propagated to Trino schema: - *

-     *   [{"source": "name", "target": "name"}, {"source": "clusterName", "target": "sourceCluster"}]
-     * 
+ * Optional mappings between attributes on the two entity types connected by this relationship, + * used when rename propagation applies updates across the relationship. Each element is a map + * with {@code "source"} and {@code "target"} keys naming the attribute on the source side and + * the attribute on the peer entity type, respectively. When {@code null} or empty, only the + * entity {@code name} attribute is synchronized. */ private List> propagateAttributes; diff --git a/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java b/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java index a0c8d3c93f..24fd0dbf22 100644 --- a/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java +++ b/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java @@ -109,14 +109,20 @@ public class AtlasEntityType extends AtlasStructType { private List dynAttributes = Collections.emptyList(); private List dynEvalTriggerAttributes = Collections.emptyList(); private Map> parsedTemplates = Collections.emptyMap(); - private Set tagPropagationEdges = Collections.emptySet(); + private Set tagPropagationEdges = Collections.emptySet(); + /** + * Other entity types that may need updates when an instance of this type is renamed: each entry + * describes how to reach that type (relationship path) from this type. Populated while typedefs + * are resolved from the model. + */ private List renamePropagationTargets = Collections.emptyList(); /** - * Referenced entity type name → dotted slot in {@code qualifiedName} {@code autoComputeFormat} - * for that type's {@code name} (used when that entity is renamed). Built in - * {@link #computeRenamePropagationTemplateMap(AtlasTypeRegistry)}. + * For this type's qualifiedName {@code autoComputeFormat}: each key is another entity type name + * that appears in the template (via relationship hops ending in {@code .name}); the value is the + * dotted path (for example {@code "db.name"}) so rename handling can refresh the right segment + * when that referenced type is renamed. Built in {@link #buildAutoComputeFormatPathByRefTypeNameMap(AtlasTypeRegistry)}. */ - private Map renamePropagationTemplateMap = Collections.emptyMap(); + private Map autoComputeFormatPathByRefTypeNameMap = Collections.emptyMap(); public AtlasEntityType(AtlasEntityDef entityDef) { super(entityDef); @@ -261,8 +267,8 @@ public List getRenamePropagationTargets() { return renamePropagationTargets; } - public Map getRenamePropagationTemplateMap() { - return renamePropagationTemplateMap; + public Map getAutoComputeFormatPathByRefTypeNameMap() { + return autoComputeFormatPathByRefTypeNameMap; } public String[] getTagPropagationEdgesArray() { @@ -478,7 +484,7 @@ void resolveReferencesPhase3(AtlasTypeRegistry typeRegistry) throws AtlasBaseExc populateDynFlagsInfo(); - computeRenamePropagationTemplateMap(typeRegistry); + buildAutoComputeFormatPathByRefTypeNameMap(typeRegistry); LOG.debug("resolveReferencesPhase3({}): tagPropagationEdges={}", getTypeName(), tagPropagationEdges); } @@ -573,8 +579,8 @@ void resolveReferences(AtlasTypeRegistry typeRegistry) throws AtlasBaseException this.relationshipAttributes = new HashMap<>(); // this will be populated in resolveReferencesPhase3() this.businessAttributes = new HashMap<>(); // this will be populated in resolveReferences(), from AtlasBusinessMetadataType this.tagPropagationEdges = new HashSet<>(); // this will be populated in resolveReferencesPhase2() - this.renamePropagationTargets = new ArrayList<>(); // this will be populated in resolveReferencesPhase2() - this.renamePropagationTemplateMap = new HashMap<>(); // this will be populated in resolveReferencesPhase3() + this.renamePropagationTargets = new ArrayList<>(); // this will be populated in resolveReferencesPhase2() + this.autoComputeFormatPathByRefTypeNameMap = new HashMap<>(); // this will be populated in resolveReferencesPhase3() this.typeAndAllSubTypes.add(this.getTypeName()); @@ -888,50 +894,55 @@ void addRelationshipAttribute(String attributeName, AtlasAttribute attribute, At } /** - * Fills {@link #renamePropagationTemplateMap}: parse {@code qualifiedName} {@code autoComputeFormat}, - * for each {@code ….*.name} slot walk relationships to the referenced type, map type → slot - * ({@code putIfAbsent} per type). No-op if no template. + * Looks at this type's qualifiedName {@code autoComputeFormat} template. For each dotted path + * that ends in {@code .name} (for example {@code db.name}), follows relationships to see which + * other entity type that path refers to, then stores {@code that type's name → path} in + * {@link #autoComputeFormatPathByRefTypeNameMap}. If the same type appears more than once, the first + * path wins. Does nothing when there is no qualifiedName attribute, no {@code autoComputeFormat}, or it is blank. */ - private void computeRenamePropagationTemplateMap(AtlasTypeRegistry typeRegistry) { + private void buildAutoComputeFormatPathByRefTypeNameMap(AtlasTypeRegistry typeRegistry) { AtlasAttribute qnAttr = getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME); if (qnAttr == null || qnAttr.getAttributeDef() == null) { return; } - String template = qnAttr.getAttributeDef().getAutoComputeFormat(); + String autoComputeFormat = qnAttr.getAttributeDef().getAutoComputeFormat(); - if (StringUtils.isBlank(template)) { + if (StringUtils.isBlank(autoComputeFormat)) { return; } - List slots = extractTemplateSlots(template); + List placeholders = extractAutoComputeFormatPlaceholders(autoComputeFormat); + + LOG.debug("buildAutoComputeFormatPathByRefTypeNameMap({}): {} qualifiedName autoComputeFormat placeholder(s)", + getTypeName(), placeholders.size()); + Map map = new HashMap<>(); - for (String slot : slots) { - String[] segments = slot.split("\\."); + for (String placeholder : placeholders) { + String[] segments = placeholder.split("\\."); - // Single-segment slots (e.g. "name", "clusterName") are own attributes — not rename-relevant. - // Only process multi-segment slots that end in ".name". + // Single-segment placeholders (e.g. "name", "clusterName") are own attributes — not rename-relevant. + // Only process multi-segment paths that end in ".name". if (segments.length < 2 || !NAME.equals(segments[segments.length - 1])) { continue; } - AtlasEntityType resolved = resolveSlotPath(slot, segments, typeRegistry); + AtlasEntityType resolved = resolveSlotPath(placeholder, segments, typeRegistry); if (resolved != null) { - // putIfAbsent: first matching slot per referenced type wins. - map.putIfAbsent(resolved.getTypeName(), slot); + // putIfAbsent: first matching dotted path per referenced type wins. + map.putIfAbsent(resolved.getTypeName(), placeholder); } } if (!map.isEmpty()) { - this.renamePropagationTemplateMap = Collections.unmodifiableMap(map); + this.autoComputeFormatPathByRefTypeNameMap = Collections.unmodifiableMap(map); } - if (LOG.isDebugEnabled()) { - LOG.debug("computeRenamePropagationTemplateMap({}): renamePropagationTemplateMap={}", getTypeName(), renamePropagationTemplateMap); - } + LOG.debug("buildAutoComputeFormatPathByRefTypeNameMap({}): refTypeNameToPathMap={} (from {} placeholder(s))", + getTypeName(), autoComputeFormatPathByRefTypeNameMap, placeholders.size()); } /** @@ -940,7 +951,7 @@ private void computeRenamePropagationTemplateMap(AtlasTypeRegistry typeRegistry) * on any invalid step (blank segment, missing/non-relationship attribute, unresolvable type, * non-entity attribute type, or degenerate self-resolution). */ - private AtlasEntityType resolveSlotPath(String slot, String[] segments, AtlasTypeRegistry typeRegistry) { + private AtlasEntityType resolveSlotPath(String placeholder, String[] segments, AtlasTypeRegistry typeRegistry) { // Walk segments[0..n-2] as relationship hops; segments[n-1] is "name" (already validated by caller). // Blank-segment check is done inline to avoid a separate loop over segments. AtlasEntityType currentType = this; @@ -948,10 +959,10 @@ private AtlasEntityType resolveSlotPath(String slot, String[] segments, AtlasTyp for (int i = 0; i < segments.length - 1; i++) { String hop = segments[i]; - // Guard: blank segment means a malformed template (e.g. "a..b.name"). + // Guard: blank segment means malformed autoComputeFormat (e.g. "a..b.name"). if (StringUtils.isBlank(hop)) { - LOG.warn("computeRenamePropagationTemplateMap({}): slot '{}' has blank segment at hop {} — skipping", - getTypeName(), slot, i); + LOG.warn("buildAutoComputeFormatPathByRefTypeNameMap({}): path '{}' has blank segment at hop {} — skipping", + getTypeName(), placeholder, i); return null; } @@ -963,8 +974,8 @@ private AtlasEntityType resolveSlotPath(String slot, String[] segments, AtlasTyp String reason = currentType.getAttribute(hop) != null ? "exists but is not a relationship attribute" : "not found on type '" + currentType.getTypeName() + "'"; - LOG.warn("computeRenamePropagationTemplateMap({}): hop {} of slot '{}': relationship attribute '{}' {} — skipping", - getTypeName(), i, slot, hop, reason); + LOG.warn("buildAutoComputeFormatPathByRefTypeNameMap({}): hop {} of path '{}': relationship attribute '{}' {} — skipping", + getTypeName(), i, placeholder, hop, reason); return null; } @@ -974,60 +985,68 @@ private AtlasEntityType resolveSlotPath(String slot, String[] segments, AtlasTyp try { nextType = getReferencedEntityType(typeRegistry.getType(relAttr.getTypeName())); } catch (AtlasBaseException e) { - LOG.warn("computeRenamePropagationTemplateMap({}): hop {} of slot '{}': cannot resolve type '{}' for attribute '{}' on '{}' — skipping", - getTypeName(), i, slot, relAttr.getTypeName(), hop, currentType.getTypeName(), e); + LOG.warn("buildAutoComputeFormatPathByRefTypeNameMap({}): hop {} of path '{}': cannot resolve type '{}' for attribute '{}' on '{}' — skipping", + getTypeName(), i, placeholder, relAttr.getTypeName(), hop, currentType.getTypeName(), e); return null; } // Guard: relationship attribute must point to an entity type (not a primitive or struct). if (nextType == null) { - LOG.warn("computeRenamePropagationTemplateMap({}): hop {} of slot '{}': attribute '{}' on '{}' has type '{}' which is not an entity type — skipping", - getTypeName(), i, slot, hop, currentType.getTypeName(), relAttr.getTypeName()); + LOG.warn("buildAutoComputeFormatPathByRefTypeNameMap({}): hop {} of path '{}': attribute '{}' on '{}' has type '{}' which is not an entity type — skipping", + getTypeName(), i, placeholder, hop, currentType.getTypeName(), relAttr.getTypeName()); return null; } currentType = nextType; } - // Guard: a valid path must reach a different type; resolving back to self means the template + // Guard: a valid path must reach a different type; resolving back to self means the autoComputeFormat // only references own attributes and carries no rename-propagation signal. if (currentType == this) { - LOG.warn("computeRenamePropagationTemplateMap({}): slot '{}' path resolved back to '{}' — skipping", - getTypeName(), slot, getTypeName()); + LOG.warn("buildAutoComputeFormatPathByRefTypeNameMap({}): path '{}' resolved back to '{}' — skipping", + getTypeName(), placeholder, getTypeName()); return null; } + LOG.debug("resolveSlotPath({}): path '{}' references entity type '{}'", + getTypeName(), placeholder, currentType.getTypeName()); + return currentType; } - /** Ordered list of strings inside each brace-delimited placeholder in the template. */ - private static List extractTemplateSlots(String template) { - List slots = new ArrayList<>(); - int pos = 0; + /** + * Returns the text inside each {@code {…}} pair in an {@code autoComputeFormat} string, in order. + * Empty braces are skipped. + */ + private static List extractAutoComputeFormatPlaceholders(String autoComputeFormat) { + List placeholders = new ArrayList<>(); // text inside each {...} pair, in order + int pos = 0; // where we are while scanning autoComputeFormat - while (pos < template.length()) { - int open = template.indexOf(DYN_ATTRIBUTE_OPEN_DELIM, pos); + while (pos < autoComputeFormat.length()) { + int open = autoComputeFormat.indexOf(DYN_ATTRIBUTE_OPEN_DELIM, pos); if (open == -1) { break; } - int close = template.indexOf(DYN_ATTRIBUTE_CLOSE_DELIM, open + 1); + int close = autoComputeFormat.indexOf(DYN_ATTRIBUTE_CLOSE_DELIM, open + 1); if (close == -1) { + LOG.debug("extractAutoComputeFormatPlaceholders: '{' at index {} has no matching '}' — stopping parse", + open); break; } - String slot = template.substring(open + 1, close).trim(); + String placeholder = autoComputeFormat.substring(open + 1, close).trim(); - if (!slot.isEmpty()) { - slots.add(slot); + if (!placeholder.isEmpty()) { + placeholders.add(placeholder); } pos = close + 1; } - return slots; + return placeholders; } /** First relationship attribute named {@code attributeName} on {@code entityType}, or {@code null}. */ @@ -1055,10 +1074,12 @@ private void addRenamePropagationTargetIfTriggered(AtlasRelationshipType relatio AtlasRelationshipEndDef endDef2 = relationshipType.getRelationshipDef().getEndDef2(); if (endDef1 == null || endDef2 == null) { + LOG.debug("addRenamePropagationTargetIfTriggered({}): relationship type '{}' missing endDef — skipping", + getTypeName(), relationshipType.getTypeName()); return; } - String thisTypeName = getTypeName(); + String thisTypeName = getTypeName(); boolean triggerOnEnd1 = StringUtils.equals(relationshipType.getEnd1Type().getTypeName(), thisTypeName) && endDef1.getIsPropagateRename(); boolean triggerOnEnd2 = StringUtils.equals(relationshipType.getEnd2Type().getTypeName(), thisTypeName) && endDef2.getIsPropagateRename(); @@ -1077,28 +1098,28 @@ private void addRenamePropagationTargetIfTriggered(AtlasRelationshipType relatio RelationshipCategory category = relationshipType.getRelationshipDef().getRelationshipCategory(); RenamePropagationTarget target = new RenamePropagationTarget(targetTypeName, category, relationshipAttribute, propagateAttributes != null ? propagateAttributes : Collections.emptyList()); - if (!containsRenamePropagationTarget(target)) { - renamePropagationTargets.add(target); - } - } + String relAttrName = relationshipAttribute.getAttributeDef() != null + ? relationshipAttribute.getAttributeDef().getName() + : null; - /** - * Returns true when an equivalent direct target is already present. - */ - private boolean containsRenamePropagationTarget(RenamePropagationTarget candidate) { - if (candidate == null) { - return false; - } + if (!renamePropagationTargets.contains(target)) { + renamePropagationTargets.add(target); - for (RenamePropagationTarget existing : renamePropagationTargets) { - if (StringUtils.equals(existing.getTargetTypeName(), candidate.getTargetTypeName()) && - existing.getCategory() == candidate.getCategory() && - existing.getRelAttr() == candidate.getRelAttr()) { - return true; - } + LOG.info("addRenamePropagationTargetIfTriggered({}): rename propagation via relationship '{}' attribute '{}' -> target type '{}' (trigger {}, category {}, propagateAttributeMaps={})", + thisTypeName, + relationshipType.getTypeName(), + relAttrName, + targetTypeName, + triggerOnEnd1 ? "end1" : "end2", + category, + target.getPropagateAttributes().size()); + } else { + LOG.debug("addRenamePropagationTargetIfTriggered({}): duplicate propagation target skipped — relationship '{}', attribute '{}', target type '{}'", + thisTypeName, + relationshipType.getTypeName(), + relAttrName, + targetTypeName); } - - return false; } boolean isAssignableFrom(AtlasObjectId objId) { @@ -1136,9 +1157,8 @@ private void getTypeHierarchyInfo(AtlasTypeRegistry typeRegistry, Set al List visitedTypes = new ArrayList<>(); Map attributeToEntityNameMap = new HashMap<>(); - // Pass this entity's attributeDefOverrides so that collectTypeHierarchyInfo can apply - // them inline when it encounters the inherited qualifiedName attribute, avoiding a - // separate post-construction mutation pass in resolveReferences(). + // Pass this type's attributeDefOverrides into collectTypeHierarchyInfo as derivedEntityOverrides + // so inherited qualifiedName is merged while walking supertypes. collectTypeHierarchyInfo(typeRegistry, allSuperTypeNames, allAttributes, attributeToEntityNameMap, visitedTypes, entityDef.getAttributeDefOverrides()); } @@ -1201,6 +1221,7 @@ private static AtlasAttributeDef getAttrDefWithOverrides(AtlasAttributeDef attri if (AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME.equals(override.getName()) && override.getAutoComputeFormat() != null) { AtlasAttributeDef copy = new AtlasAttributeDef(attributeDef); + //TODO : can be removed copy.setAutoComputeFormat(override.getAutoComputeFormat()); return copy; diff --git a/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java b/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java index 1dec9343cb..de5d90110f 100644 --- a/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java +++ b/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java @@ -22,22 +22,23 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; /** * Typedef-time representation of a rename propagation target for an entity type. */ public class RenamePropagationTarget { - private final String targetTypeName; - private final RelationshipCategory category; + private final String targetTypeName; + private final RelationshipCategory category; private final AtlasStructType.AtlasAttribute relAttr; - private final List> propagateAttributes; + private final List> propagateAttributes; public RenamePropagationTarget(String targetTypeName, RelationshipCategory category, AtlasStructType.AtlasAttribute relAttr, List> propagateAttributes) { - this.targetTypeName = targetTypeName; - this.category = category; - this.relAttr = relAttr; - this.propagateAttributes = propagateAttributes != null ? Collections.unmodifiableList(propagateAttributes) : Collections.emptyList(); + this.targetTypeName = targetTypeName; + this.category = category; + this.relAttr = relAttr; + this.propagateAttributes = propagateAttributes != null ? Collections.unmodifiableList(propagateAttributes) : Collections.emptyList(); } public String getTargetTypeName() { @@ -55,4 +56,27 @@ public AtlasStructType.AtlasAttribute getRelAttr() { public List> getPropagateAttributes() { return propagateAttributes; } + + /** + * Same target type, relationship category, and relationship attribute instance as wired on this type. + * {@link #propagateAttributes} is not part of equality — duplicates are detected before add. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RenamePropagationTarget that = (RenamePropagationTarget) o; + return Objects.equals(targetTypeName, that.targetTypeName) + && category == that.category + && relAttr == that.relAttr; + } + + @Override + public int hashCode() { + return Objects.hash(targetTypeName, category, System.identityHashCode(relAttr)); + } } diff --git a/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java b/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java index 3d7c45f5c3..36542378bc 100755 --- a/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java +++ b/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedSearchIndexer.java @@ -101,7 +101,6 @@ import static org.apache.atlas.repository.Constants.CREATED_BY_KEY; import static org.apache.atlas.repository.Constants.CUSTOM_ATTRIBUTES_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.EDGE_INDEX; -import static org.apache.atlas.repository.Constants.ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.ENTITY_DELETED_TIMESTAMP_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.ENTITY_TEXT_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.ENTITY_TYPE_PROPERTY_KEY; @@ -144,6 +143,7 @@ import static org.apache.atlas.repository.Constants.TYPEOPTIONS_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.TYPESERVICETYPE_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.TYPEVERSION_PROPERTY_KEY; +import static org.apache.atlas.repository.Constants.TYPE_ATTR_DEF_OVERRIDES_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.TYPE_CATEGORY_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.VERSION_PROPERTY_KEY; import static org.apache.atlas.repository.Constants.VERTEX_INDEX; @@ -608,7 +608,7 @@ private void initialize(AtlasGraph graph) throws RepositoryException, IndexExcep createPropertyKey(management, TYPEVERSION_PROPERTY_KEY, String.class, SINGLE); createPropertyKey(management, VERSION_PROPERTY_KEY, Long.class, SINGLE); createPropertyKey(management, TYPEOPTIONS_PROPERTY_KEY, String.class, SINGLE); - createPropertyKey(management, ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY, String.class, SINGLE); + createPropertyKey(management, TYPE_ATTR_DEF_OVERRIDES_PROPERTY_KEY, String.class, SINGLE); createPropertyKey(management, IS_PROXY_KEY, Boolean.class, SINGLE); createPropertyKey(management, PROVENANCE_TYPE_KEY, Integer.class, SINGLE); createPropertyKey(management, HOME_ID_KEY, String.class, SINGLE); diff --git a/repository/src/main/java/org/apache/atlas/repository/patches/AtlasPatchRegistry.java b/repository/src/main/java/org/apache/atlas/repository/patches/AtlasPatchRegistry.java index 8d0fdadaae..e4f82425b2 100644 --- a/repository/src/main/java/org/apache/atlas/repository/patches/AtlasPatchRegistry.java +++ b/repository/src/main/java/org/apache/atlas/repository/patches/AtlasPatchRegistry.java @@ -81,13 +81,6 @@ public AtlasPatchRegistry(AtlasGraph graph) { public boolean isApplicable(String incomingId, String patchFile, int index) { String patchId = getId(incomingId, patchFile, index); - // TODO: remove after testing - force typedef patches 020 (rename propagation) to always re-apply - if ("TYPEDEF_PATCH_1000_020_A".equals(patchId) - || "TYPEDEF_PATCH_1000_020_B".equals(patchId) - || "TYPEDEF_PATCH_1000_020_C".equals(patchId)) { - return true; - } - if (MapUtils.isEmpty(patchNameStatusMap) || !patchNameStatusMap.containsKey(patchId)) { return true; } diff --git a/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java b/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java index efb7031985..b16fb0068c 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java @@ -397,14 +397,8 @@ private void loadModelsInFolder(File typesDir, AtlasPatchRegistry patchRegistry) private void startInternal() { try { - // TODO: SANKET:INFO : This is the first step to load the types from the database. - LOG.info("SANKET:INFO : This is the first step to load the types from the database."); typeDefStore.init(); - // TODO: SANKET:INFO : This is the second step to load the types from the models. - LOG.info("SANKET:INFO : This is the second step to load the types from the models."); loadBootstrapTypeDefs(); - // TODO: SANKET:INFO : This is the third step to notify the load completion. - LOG.info("SANKET:INFO : This is the third step to notify the load completion."); typeDefStore.notifyLoadCompletion(); try { AtlasAuthorizerFactory.getAtlasAuthorizer(); @@ -1191,9 +1185,11 @@ public PatchStatus applyPatch(TypeDefPatch patch) throws AtlasBaseException { */ static class SetAttributeDefOverridesPatchHandler extends PatchHandler { private static final String ACTION_SET_ATTRIBUTE_DEF_OVERRIDES = "SET_ATTRIBUTE_DEF_OVERRIDES"; - private static final String ACTION_SET_PROPAGATE_RENAME = "SET_PROPAGATE_RENAME"; + private static final String ACTION_SET_PROPAGATE_RENAME = "SET_PROPAGATE_RENAME"; + /** Patch JSON key under {@code params}: value is {@code "endDef1"} or {@code "endDef2"} (string, not numeric). */ + private static final String PARAM_END_DEF_TOKEN = "endDefToken"; - SetAttributeDefOverridesPatchHandler(AtlasTypeDefStore typeDefStore, AtlasTypeRegistry typeRegistry) { + public SetAttributeDefOverridesPatchHandler(AtlasTypeDefStore typeDefStore, AtlasTypeRegistry typeRegistry) { super(typeDefStore, typeRegistry, new String[] {ACTION_SET_ATTRIBUTE_DEF_OVERRIDES, ACTION_SET_PROPAGATE_RENAME}); } @@ -1207,12 +1203,15 @@ public PatchStatus applyPatch(TypeDefPatch patch) throws AtlasBaseException { } if (!isPatchApplicable(patch, typeDef)) { - LOG.info("patch skipped: typeName={}; applyToVersion={}; updateToVersion={}", - patch.getTypeName(), patch.getApplyToVersion(), patch.getUpdateToVersion()); + LOG.info("patch skipped: typeName={}; applyToVersion={}; updateToVersion={}", patch.getTypeName(), + patch.getApplyToVersion(), patch.getUpdateToVersion()); return SKIPPED; } + LOG.debug("SetAttributeDefOverridesPatchHandler.applyPatch(): id={}; action={}; typeName={}", + patch.getId(), patch.getAction(), typeName); + String action = patch.getAction(); if (ACTION_SET_ATTRIBUTE_DEF_OVERRIDES.equals(action)) { @@ -1240,23 +1239,31 @@ private PatchStatus applyEntityDefOverrides(TypeDefPatch patch, AtlasEntityDef t updatedDef.setAttributeDefOverrides(patch.getAttributeDefs()); updatedDef.setTypeVersion(patch.getUpdateToVersion()); + int overrideCount = CollectionUtils.isEmpty(patch.getAttributeDefs()) ? 0 : patch.getAttributeDefs().size(); + + LOG.debug("SetAttributeDefOverridesPatchHandler.applyEntityDefOverrides(): entityType={}; overrideCount={}; updateToVersion={}", + typeDef.getName(), overrideCount, patch.getUpdateToVersion()); + typeDefStore.updateEntityDefByName(typeDef.getName(), updatedDef); + LOG.info("patch applied: id={}; action={}; entityType={}; attributeDefOverrides={}; typeVersion={}", + patch.getId(), ACTION_SET_ATTRIBUTE_DEF_OVERRIDES, typeDef.getName(), overrideCount, + patch.getUpdateToVersion()); + return APPLIED; } private PatchStatus applyPropagateRename(TypeDefPatch patch, AtlasRelationshipDef typeDef) throws AtlasBaseException { - Object endDefObj = patch.getParams() != null ? patch.getParams().get("endDefNumber") : null; - String endDefNumber = endDefObj != null ? String.valueOf(endDefObj).trim() : null; + Object endDefObj = patch.getParams() != null ? patch.getParams().get(PARAM_END_DEF_TOKEN) : null; + String endDefToken = endDefObj != null ? String.valueOf(endDefObj).trim() : null; + boolean useEndDef2 = "endDef2".equalsIgnoreCase(endDefToken); + boolean useEndDef1 = "endDef1".equalsIgnoreCase(endDefToken); - boolean useEndDef2 = "endDef2".equalsIgnoreCase(endDefNumber); - boolean useEndDef1 = "endDef1".equalsIgnoreCase(endDefNumber); - - if (endDefNumber == null || (!useEndDef1 && !useEndDef2)) { + if (endDefToken == null || (!useEndDef1 && !useEndDef2)) { throw new AtlasBaseException(AtlasErrorCode.INVALID_PARAMETERS, "SET_PROPAGATE_RENAME patch for '" + typeDef.getName() - + "' must set params.endDefNumber to \"endDef1\" or \"endDef2\", got: " - + endDefNumber); + + "' must set params." + PARAM_END_DEF_TOKEN + " to the string \"endDef1\" or \"endDef2\", got: " + + endDefToken); } AtlasRelationshipDef updatedDef = new AtlasRelationshipDef(typeDef); @@ -1274,8 +1281,15 @@ private PatchStatus applyPropagateRename(TypeDefPatch patch, AtlasRelationshipDe } updatedDef.setTypeVersion(patch.getUpdateToVersion()); + + LOG.debug("SetAttributeDefOverridesPatchHandler.applyPropagateRename(): relationshipType={}; endDefToken={}; updateToVersion={}", + typeDef.getName(), endDefToken, patch.getUpdateToVersion()); + typeDefStore.updateRelationshipDefByName(typeDef.getName(), updatedDef); + LOG.info("patch applied: id={}; action={}; relationshipType={}; endDefToken={}; typeVersion={}", + patch.getId(), ACTION_SET_PROPAGATE_RENAME, typeDef.getName(), endDefToken, patch.getUpdateToVersion()); + return APPLIED; } } diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityDefStoreV2.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityDefStoreV2.java index 9b21c553e0..ff1e985126 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityDefStoreV2.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityDefStoreV2.java @@ -318,7 +318,7 @@ private AtlasEntityDef toEntityDef(AtlasVertex vertex) throws AtlasBaseException ret.setSuperTypes(typeDefStore.getSuperTypeNames(vertex)); - String overridesJson = vertex.getProperty(Constants.ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY, String.class); + String overridesJson = vertex.getProperty(Constants.TYPE_ATTR_DEF_OVERRIDES_PROPERTY_KEY, String.class); if (StringUtils.isNotBlank(overridesJson)) { List overrides = AtlasJson.fromJson(overridesJson, diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java index 29998ed797..74b9c9fdbc 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java @@ -1209,39 +1209,49 @@ private EntityMutationResponse createOrUpdate(EntityStream entityStream, boolean } private void handleRenamePropagation(boolean isPartialUpdate, AtlasEntity entity, AtlasEntityType entityType, - AtlasVertex vertex, EntityMutationContext context) throws org.apache.atlas.exception.AtlasBaseException { - try { - if (!isPartialUpdate || entityRenameHandler == null) { - return; - } + AtlasVertex vertex, EntityMutationContext context) throws AtlasBaseException { + if (!isPartialUpdate || entityRenameHandler == null) { + return; + } - if (CollectionUtils.isEmpty(entityType.getRenamePropagationTargets())) { - return; - } + if (CollectionUtils.isEmpty(entityType.getRenamePropagationTargets())) { + return; + } + + String entityGuid = StringUtils.isNotEmpty(entity.getGuid()) ? entity.getGuid() : AtlasGraphUtilsV2.getIdFromVertex(vertex); + + try { + LOG.debug("handleRenamePropagation(): type={}; guid={}; renamePropagationTargetCount={}", + entityType.getTypeName(), entityGuid, entityType.getRenamePropagationTargets().size()); String oldUniqueAttrValue = AtlasGraphUtilsV2.getProperty(vertex, entityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME), String.class); String newUniqueAttrValue = (String) entity.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME); if (StringUtils.isBlank(oldUniqueAttrValue) || StringUtils.isBlank(newUniqueAttrValue)) { + LOG.debug("handleRenamePropagation(): skip — missing qualifiedName on vertex or request (type={}; guid={})", + entityType.getTypeName(), entityGuid); return; } if (StringUtils.equals(oldUniqueAttrValue, newUniqueAttrValue)) { + LOG.debug("handleRenamePropagation(): skip — qualifiedName unchanged (type={}; guid={})", + entityType.getTypeName(), entityGuid); return; } - // TOTDO: - if (!newUniqueAttrValue.toLowerCase().contains("sb_latest")) { - LOG.info("SANKET:DEBUG:HOOK: Skipping rename propagation for entity "); - return; - } + LOG.info("Rename detected (qualifiedName changed): type={}; guid={}; processing dependent entities for rename propagation", + entityType.getTypeName(), entityGuid); + LOG.debug("handleRenamePropagation(): qualifiedName old -> new (guid={}): {} -> {}", entityGuid, oldUniqueAttrValue, + newUniqueAttrValue); - // Rename detected: populate dependent entities into the mutation context entityRenameHandler.addDependentsToContext(context, entityType, vertex, entity); - //context.getUpdatedEntities().clear(); + } catch (AtlasBaseException e) { + LOG.error("handleRenamePropagation(): rename propagation failed for type={}; guid={}", entityType.getTypeName(), entityGuid, e); + throw e; } catch (Exception e) { - // context.getUpdatedEntities().clear(); - throw new RuntimeException(e); + LOG.error("handleRenamePropagation(): unexpected error during rename propagation for type={}; guid={}", + entityType.getTypeName(), entityGuid, e); + throw new AtlasBaseException(e); } } diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasTypeDefGraphStoreV2.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasTypeDefGraphStoreV2.java index 3391c324cf..dac4c97597 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasTypeDefGraphStoreV2.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasTypeDefGraphStoreV2.java @@ -275,7 +275,7 @@ AtlasVertex createTypeVertex(AtlasBaseTypeDef typeDef) { AtlasEntityDef entityDef = (AtlasEntityDef) typeDef; if (CollectionUtils.isNotEmpty(entityDef.getAttributeDefOverrides())) { - ret.setProperty(Constants.ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY, + ret.setProperty(Constants.TYPE_ATTR_DEF_OVERRIDES_PROPERTY_KEY, AtlasType.toJson(entityDef.getAttributeDefOverrides())); } } @@ -305,10 +305,10 @@ void updateTypeVertex(AtlasBaseTypeDef typeDef, AtlasVertex vertex) { AtlasEntityDef entityDef = (AtlasEntityDef) typeDef; if (CollectionUtils.isNotEmpty(entityDef.getAttributeDefOverrides())) { - updateVertexProperty(vertex, Constants.ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY, + updateVertexProperty(vertex, Constants.TYPE_ATTR_DEF_OVERRIDES_PROPERTY_KEY, AtlasType.toJson(entityDef.getAttributeDefOverrides())); } else { - vertex.removeProperty(Constants.ENTITY_ATTR_DEF_OVERRIDES_PROPERTY_KEY); + vertex.removeProperty(Constants.TYPE_ATTR_DEF_OVERRIDES_PROPERTY_KEY); } } diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java index 28d869ea46..51383eeeda 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java @@ -49,11 +49,16 @@ public class EntityRenameHandler { private static final Logger LOG = LoggerFactory.getLogger(EntityRenameHandler.class); - private final AtlasTypeRegistry typeRegistry; + /** Key for trigger-side attribute in relationship end {@code propagateAttributes} map entries. */ + private static final String PROPAGATE_ATTRIBUTES_SOURCE_KEY = "source"; + /** Key for dependent stub attribute in those entries. */ + private static final String PROPAGATE_ATTRIBUTES_TARGET_KEY = "target"; + + private final AtlasTypeRegistry typeRegistry; @Inject public EntityRenameHandler(AtlasTypeRegistry typeRegistry) { - this.typeRegistry = typeRegistry; + this.typeRegistry = typeRegistry; } /** @@ -61,7 +66,7 @@ public EntityRenameHandler(AtlasTypeRegistry typeRegistry) { * them into the mutation context so they are persisted in the same transaction. * * Uses typedef-time metadata on each {@link AtlasEntityType} for O(1) slot-key lookup where - * applicable, without deriving template paths from the graph at runtime. + * applicable, without resolving qualifiedName patterns from the graph at runtime. */ public void addDependentsToContext(EntityMutationContext context, AtlasEntityType sourceEntityType, @@ -77,58 +82,68 @@ public void addDependentsToContext(EntityMutationContext context, collectDependents(sourceVertex, sourceEntityType, renamedTypeName, newName, visitedGuids, dependents); - LOG.info("SANKET:DEBUG:HOOK: dependents: {}", dependents); - LOG.debug("addDependentsToContext(): done — renamedType={}, dependentCount={}", renamedTypeName, dependents.size()); injectDependentsIntoContext(context, dependents); - LOG.info("SANKET:DEBUG:HOOK: context.getUpdatedEntities(): {}", context.getUpdatedEntities()); } - // ─── context injection ────────────────────────────────────────────────────── - + /** + * Registers each dependent rewrite on {@code context} so they persist in the same transaction as the + * renamed root entity. + *

+ * For every entry in {@code dependents}, builds a minimal {@link AtlasEntity} (GUID, new + * {@code qualifiedName}, and any extra attributes from {@code propagateAttributes}), resolves the + * dependent's {@link AtlasEntityType}, and calls {@link EntityMutationContext#addUpdated}. + * + * @param context mutation batch for the current create/update + * @param dependents dependent entity GUID → payload produced by {@link #collectDependents} + */ private void injectDependentsIntoContext(EntityMutationContext context, Map dependents) { - for (DependentUpdate dep : dependents.values()) { - AtlasEntity stub = new AtlasEntity(dep.getTypeName()); + for (DependentUpdate dependentUpdate : dependents.values()) { + AtlasEntity dependentStub = new AtlasEntity(dependentUpdate.getTypeName()); - stub.setGuid(dep.getGuid()); - stub.setAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME, dep.getNewUniqueAttribute()); + dependentStub.setGuid(dependentUpdate.getGuid()); + dependentStub.setAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME, dependentUpdate.getNewUniqueAttribute()); - if (MapUtils.isNotEmpty(dep.getMappedAttrs())) { - for (Map.Entry entry : dep.getMappedAttrs().entrySet()) { - stub.setAttribute(entry.getKey(), entry.getValue()); + if (MapUtils.isNotEmpty(dependentUpdate.getMappedAttrs())) { + for (Map.Entry mappedAttrEntry : dependentUpdate.getMappedAttrs().entrySet()) { + dependentStub.setAttribute(mappedAttrEntry.getKey(), mappedAttrEntry.getValue()); } } - AtlasEntityType dependentEntityType = typeRegistry.getEntityTypeByName(dep.getTypeName()); + AtlasEntityType dependentEntityType = typeRegistry.getEntityTypeByName(dependentUpdate.getTypeName()); - context.addUpdated(dep.getGuid(), stub, dependentEntityType, dep.getVertex()); + context.addUpdated(dependentUpdate.getGuid(), dependentStub, dependentEntityType, dependentUpdate.getVertex()); } } - // ─── core traversal ───────────────────────────────────────────────────────── - /** - * Single recursive traversal over all propagation targets. - * - *

The override strategy is decided purely by whether the target carries - * explicit {@code propagateAttributes} mappings: + * Walks {@link RenamePropagationTarget}s declared on {@code sourceEntityType}, follows each relationship + * from {@code sourceVertex} to neighboring entities, recomputes each neighbor's {@code qualifiedName} when it + * should reflect {@code renamedTypeName}'s new {@code newName}, and records updates in + * {@code dependentUpdatesByGuid}. Recurses when a neighbor's type also declares propagation targets. * + *

How the segment of {@code qualifiedName} to rewrite is chosen: *

    - *
  • propagateAttributes present: the slot patched in the dependent's {@code qualifiedName} - * template is the {@code target} of the mapping whose {@code source} is {@code "name"} - * (see {@link #findQualifiedNameOverrideKey}); {@code newName} is written into that slot. - * Extra stub attributes come from {@link #buildMappedAttrs}. The updated dependent becomes - * the new rename root for any further cascade.
  • - *
  • propagateAttributes absent: the override slot is resolved via O(1) lookup - * in the dependent type's precomputed {@code renamePropagationTemplateMap} using - * {@code renamedTypeName}. The same renamed type and new name are propagated - * unchanged through the cascade.
  • + *
  • {@code propagateAttributes} present: use {@link #findQualifiedNameOverrideKey} (mapping whose + * {@code source} is {@code "name"}), write {@code newName} into that {@code target} path, and add any + * extra attributes via {@link #buildMappedAttrs}. For the next level, {@code newName} is read back from + * the patched path on the new qualified name (falling back to the prior {@code newName} if blank).
  • + *
  • {@code propagateAttributes} absent: use {@link AtlasEntityType#getAutoComputeFormatPathByRefTypeNameMap()} + * on the dependent type to find the dotted path for {@code renamedTypeName}, then recompute with the same + * {@code newName}. Recursion keeps the same {@code renamedTypeName} and {@code newName}.
  • *
+ * + * @param sourceVertex graph vertex for the entity currently acting as the rename root + * @param sourceEntityType typedef for {@code sourceVertex} + * @param renamedTypeName entity type whose name change drives this step (used for map lookup when there is no {@code propagateAttributes}) + * @param newName replacement {@code name} (or a value derived from the patched qualified name when {@code propagateAttributes} is used) + * @param visitedGuids prevents revisiting the same dependent GUID in a cycle + * @param dependentUpdatesByGuid accumulates dependent GUID → {@link DependentUpdate} */ private void collectDependents(AtlasVertex sourceVertex, AtlasEntityType sourceEntityType, String renamedTypeName, String newName, - Set visitedGuids, Map out) throws AtlasBaseException { + Set visitedGuids, Map dependentUpdatesByGuid) throws AtlasBaseException { for (RenamePropagationTarget propagationTarget : sourceEntityType.getRenamePropagationTargets()) { AtlasAttribute relAttr = propagationTarget.getRelAttr(); @@ -153,38 +168,36 @@ private void collectDependents(AtlasVertex sourceVertex, AtlasEntityType sourceE AtlasEntityType dependentEntityType = typeRegistry.getEntityTypeByName(dependentTypeName); if (dependentEntityType == null) { + LOG.debug("collectDependents(): skip — no typedef for dependent type '{}' (guid={})", dependentTypeName, dependentGuid); continue; } - String template = getUniqueAttributeTemplate(dependentEntityType); - String oldQN = AtlasGraphUtilsV2.getProperty(dependentVertex, + String uniqueAttributeAutoComputeFormat = getUniqueAttributeAutoComputeFormat(dependentEntityType); + String oldQualifiedName = AtlasGraphUtilsV2.getProperty(dependentVertex, dependentEntityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME), String.class); - LOG.info("SANKET:DEBUG:HOOK: oldQN: {}", oldQN); - LOG.info("SANKET:DEBUG:HOOK: template: {}", template); - LOG.info("SANKET:DEBUG:HOOK: dependentTypeName: {}", dependentTypeName); - - if (StringUtils.isBlank(template) || StringUtils.isBlank(oldQN)) { + if (StringUtils.isBlank(uniqueAttributeAutoComputeFormat) || StringUtils.isBlank(oldQualifiedName)) { + LOG.debug("collectDependents(): skip — blank unique-attribute autoComputeFormat or stored qualifiedName (dependentType={}; guid={})", + dependentTypeName, dependentGuid); continue; } - String overrideKey; + String qualifiedNameOverrideKey; Map mappedAttrs; if (CollectionUtils.isNotEmpty(propagationTarget.getPropagateAttributes())) { - // QualifiedName slot key comes from the propagateAttributes entry whose source is "name". - overrideKey = findQualifiedNameOverrideKey(propagationTarget.getPropagateAttributes()); - if (overrideKey == null) { - overrideKey = AtlasTypeUtil.ATTRIBUTE_NAME; + // Dotted path comes from propagateAttributes (entry whose source is "name"). + qualifiedNameOverrideKey = findQualifiedNameOverrideKey(propagationTarget.getPropagateAttributes()); + if (qualifiedNameOverrideKey == null) { + qualifiedNameOverrideKey = AtlasTypeUtil.ATTRIBUTE_NAME; } mappedAttrs = buildMappedAttrs(propagationTarget, newName); } else { - // No propagateAttributes: O(1) lookup of the slot key from the typedef-time - // precomputed renamePropagationTemplateMap on the dependent entity type. - overrideKey = dependentEntityType.getRenamePropagationTemplateMap().get(renamedTypeName); + // Typedef map: referenced type → dotted path in this dependent's qualifiedName autoComputeFormat. + qualifiedNameOverrideKey = dependentEntityType.getAutoComputeFormatPathByRefTypeNameMap().get(renamedTypeName); - if (overrideKey == null) { - LOG.debug("collectDependents(): no override key for renamedType={} on dependentType={} — skipping", + if (qualifiedNameOverrideKey == null) { + LOG.debug("collectDependents(): no qualifiedName path for renamedType={} on dependentType={} — skipping", renamedTypeName, dependentTypeName); continue; } @@ -192,38 +205,38 @@ private void collectDependents(AtlasVertex sourceVertex, AtlasEntityType sourceE mappedAttrs = new HashMap<>(); } - String newQN = recomputeUniqueAttribute(oldQN, template, overrideKey, newName); + String newQualifiedName = recomputeUniqueAttribute(oldQualifiedName, uniqueAttributeAutoComputeFormat, qualifiedNameOverrideKey, newName); - if (StringUtils.isBlank(newQN) || StringUtils.equals(oldQN, newQN)) { + if (StringUtils.isBlank(newQualifiedName) || StringUtils.equals(oldQualifiedName, newQualifiedName)) { + LOG.debug("collectDependents(): skip — recomputed qualifiedName empty or unchanged (dependentType={}; guid={})", + dependentTypeName, dependentGuid); continue; } - LOG.debug("collectDependents(): updating {} [{}] {} -> {}", dependentTypeName, dependentGuid, oldQN, newQN); + LOG.debug("collectDependents(): dependent qualifiedName update type={} guid={} {} -> {}", + dependentTypeName, dependentGuid, oldQualifiedName, newQualifiedName); - out.put(dependentGuid, new DependentUpdate(dependentGuid, dependentTypeName, dependentVertex, newQN, mappedAttrs)); + dependentUpdatesByGuid.put(dependentGuid, new DependentUpdate(dependentGuid, dependentTypeName, dependentVertex, newQualifiedName, mappedAttrs)); if (CollectionUtils.isNotEmpty(dependentEntityType.getRenamePropagationTargets())) { if (CollectionUtils.isNotEmpty(propagationTarget.getPropagateAttributes())) { - // The updated dependent becomes the new rename root for its own downstream cascade. - // Read the segment we patched (same key as overrideKey) for the next level's newName. - String dependentNewName = parseUniqueAttribute(newQN, template).get(overrideKey); + // This dependent becomes the next root; use the name segment we just patched (or the prior newName). + String nameForChildPropagation = parseUniqueAttribute(newQualifiedName, uniqueAttributeAutoComputeFormat).get(qualifiedNameOverrideKey); - if (StringUtils.isBlank(dependentNewName)) { - dependentNewName = newName; + if (StringUtils.isBlank(nameForChildPropagation)) { + nameForChildPropagation = newName; } - collectDependents(dependentVertex, dependentEntityType, dependentTypeName, dependentNewName, visitedGuids, out); + collectDependents(dependentVertex, dependentEntityType, dependentTypeName, nameForChildPropagation, visitedGuids, dependentUpdatesByGuid); } else { - // Template-slot cascade: propagate the same renamed type and new name unchanged. - collectDependents(dependentVertex, dependentEntityType, renamedTypeName, newName, visitedGuids, out); + // Same renamed type and name propagate to further dependents. + collectDependents(dependentVertex, dependentEntityType, renamedTypeName, newName, visitedGuids, dependentUpdatesByGuid); } } } } } - // ─── association attribute mapping ────────────────────────────────────────── - /** * Builds the mapped-attribute payload for an association-linked dependent. * Reads {@link RenamePropagationTarget#getPropagateAttributes()} so renamed values can be written @@ -244,8 +257,8 @@ private Map buildMappedAttrs(RenamePropagationTarget target, Str Map mappedAttrs = new HashMap<>(); for (Map mapping : propagateAttributes) { - String sourceAttr = mapping.get("source"); - String targetAttr = mapping.get("target"); + String sourceAttr = mapping.get(PROPAGATE_ATTRIBUTES_SOURCE_KEY); + String targetAttr = mapping.get(PROPAGATE_ATTRIBUTES_TARGET_KEY); // Only "name" is available at this point; other source attributes would need the full sourceEntity. if (AtlasTypeUtil.ATTRIBUTE_NAME.equals(sourceAttr) && StringUtils.isNotBlank(targetAttr)) { @@ -259,10 +272,10 @@ private Map buildMappedAttrs(RenamePropagationTarget target, Str /** * Returns the {@code target} attribute name of the first {@code propagateAttributes} entry whose * {@code source} is {@link AtlasTypeUtil#ATTRIBUTE_NAME "name"}. The returned {@code target} - * identifies which {@code {slot}} in the dependent's {@code qualifiedName} template is patched + * identifies which dotted path in the dependent's unique-attribute {@code autoComputeFormat} is patched * with {@code newName} in {@link #recomputeUniqueAttribute}. * - * @return {@code null} when no mapping has {@code source=name} and a non-blank {@code target} + * @return {@code null} when no mapping has {@code source=name} with a non-blank {@code target} */ private static String findQualifiedNameOverrideKey(List> propagateAttributes) { if (CollectionUtils.isEmpty(propagateAttributes)) { @@ -270,8 +283,8 @@ private static String findQualifiedNameOverrideKey(List> pro } for (Map mapping : propagateAttributes) { - String sourceAttr = mapping.get("source"); - String targetAttr = mapping.get("target"); + String sourceAttr = mapping.get(PROPAGATE_ATTRIBUTES_SOURCE_KEY); + String targetAttr = mapping.get(PROPAGATE_ATTRIBUTES_TARGET_KEY); if (AtlasTypeUtil.ATTRIBUTE_NAME.equals(sourceAttr) && StringUtils.isNotBlank(targetAttr)) { return targetAttr; @@ -281,48 +294,46 @@ private static String findQualifiedNameOverrideKey(List> pro return null; } - // ─── qualifiedName recomputation ──────────────────────────────────────────── - /** * Recomputes a dependent entity's qualifiedName after a rename using a 3-step in-memory approach: *
    - *
  1. Parse — extract each template slot's value from the stored qualifiedName string.
  2. - *
  3. Override — replace the single slot that changed (the renamed entity's name key).
  4. - *
  5. Rebuild — reconstruct the qualifiedName from the updated slots.
  6. + *
  7. Parse — split the stored qualifiedName using the unique attribute's {@code autoComputeFormat}.
  8. + *
  9. Override — replace the path segment that changed (the renamed entity's name key).
  10. + *
  11. Rebuild — stitch the qualifiedName back together from the updated segments.
  12. *
* No extra graph reads are required; all values come from the existing qualifiedName string. */ - private String recomputeUniqueAttribute(String currentUniqueAttr, String template, String overrideKey, String newValue) { - Map slots = parseUniqueAttribute(currentUniqueAttr, template); + private String recomputeUniqueAttribute(String currentUniqueAttr, String uniqueAttributeAutoComputeFormat, String overrideKey, String newValue) { + Map slots = parseUniqueAttribute(currentUniqueAttr, uniqueAttributeAutoComputeFormat); if (StringUtils.isNotBlank(overrideKey) && newValue != null) { slots.put(overrideKey, newValue); } - return buildUniqueAttribute(template, slots); + return buildUniqueAttribute(uniqueAttributeAutoComputeFormat, slots); } /** - * Parses a qualifiedName string into a {@code { templateKey → value }} map by walking - * the template and value strings in parallel with two cursors. + * Parses a qualifiedName string into a {@code { pathKey → value }} map by walking the unique attribute's + * {@code autoComputeFormat} pattern and the stored value in parallel. * *

Literal characters (such as {@code '.'} or {@code '@'}) are delimiters; the character immediately - * after {@code '}'} in the template is used as the value's end delimiter for each slot. - * The last slot captures everything remaining in the qualifiedName. + * after {@code '}'} in the format string is used as the value's end delimiter for each placeholder. + * The last placeholder captures everything remaining in the qualifiedName. * *

-     *   template   = "{db.name}.{name}@{clusterName}"
-     *   uniqueAttr = "mydb.mytable@cluster1"
-     *   result     = { "db.name" → "mydb", "name" → "mytable", "clusterName" → "cluster1" }
+     *   uniqueAttributeAutoComputeFormat = "{db.name}.{name}@{clusterName}"
+     *   uniqueAttr                       = "mydb.mytable@cluster1"
+     *   result                           = { "db.name" → "mydb", "name" → "mytable", "clusterName" → "cluster1" }
      * 
*/ - private Map parseUniqueAttribute(String uniqueAttr, String template) { + private Map parseUniqueAttribute(String uniqueAttr, String uniqueAttributeAutoComputeFormat) { Map slots = new LinkedHashMap<>(); - int tPos = 0; // cursor into template + int tPos = 0; // cursor into uniqueAttributeAutoComputeFormat int uPos = 0; // cursor into uniqueAttr - while (tPos < template.length() && uPos <= uniqueAttr.length()) { - char tc = template.charAt(tPos); + while (tPos < uniqueAttributeAutoComputeFormat.length() && uPos <= uniqueAttr.length()) { + char tc = uniqueAttributeAutoComputeFormat.charAt(tPos); if (tc != '{') { // Literal delimiter — advance both cursors past it. @@ -331,30 +342,30 @@ private Map parseUniqueAttribute(String uniqueAttr, String templ continue; } - int closeBrace = template.indexOf('}', tPos); + int closeBrace = uniqueAttributeAutoComputeFormat.indexOf('}', tPos); if (closeBrace < 0) { - break; // malformed template + break; // malformed autoComputeFormat } - String key = template.substring(tPos + 1, closeBrace); + String key = uniqueAttributeAutoComputeFormat.substring(tPos + 1, closeBrace); tPos = closeBrace + 1; // advance past '}' - // Last slot: nothing follows in the template, so capture the rest of uniqueAttr. - if (tPos >= template.length()) { + // Last placeholder: nothing follows in the format string, so capture the rest of uniqueAttr. + if (tPos >= uniqueAttributeAutoComputeFormat.length()) { slots.put(key, uniqueAttr.substring(uPos)); break; } - // The character immediately after '}' in the template is the value's end delimiter. - char delim = template.charAt(tPos); + // Character after '}' in the format string is the value's end delimiter in uniqueAttr. + char delim = uniqueAttributeAutoComputeFormat.charAt(tPos); int delimPos = uniqueAttr.indexOf(delim, uPos); if (delimPos >= 0) { slots.put(key, uniqueAttr.substring(uPos, delimPos)); uPos = delimPos + 1; // skip past the delimiter in uniqueAttr - tPos++; // skip past the delimiter in template + tPos++; // skip past the delimiter in format string } else { // Delimiter not found — capture the rest (handles malformed / truncated values). slots.put(key, uniqueAttr.substring(uPos)); @@ -366,32 +377,32 @@ private Map parseUniqueAttribute(String uniqueAttr, String templ } /** - * Reconstructs a qualifiedName string from a template and a populated slot map. - * Each {@code {key}} in the template is replaced with its value; unknown keys become {@code ""}. + * Reconstructs a qualifiedName string from the unique attribute's {@code autoComputeFormat} and a populated map. + * Each {@code {key}} in the format string is replaced with its value; unknown keys become {@code ""}. * *
-     *   template = "{db.name}.{name}@{clusterName}"
-     *   slots    = { "db.name" → "mydb", "name" → "new_table", "clusterName" → "cluster1" }
-     *   result   = "mydb.new_table@cluster1"
+     *   uniqueAttributeAutoComputeFormat = "{db.name}.{name}@{clusterName}"
+     *   slots                          = { "db.name" → "mydb", "name" → "new_table", "clusterName" → "cluster1" }
+     *   result                         = "mydb.new_table@cluster1"
      * 
*/ - private String buildUniqueAttribute(String template, Map slots) { - StringBuilder sb = new StringBuilder(template.length() + 32); + private String buildUniqueAttribute(String uniqueAttributeAutoComputeFormat, Map slots) { + StringBuilder sb = new StringBuilder(uniqueAttributeAutoComputeFormat.length() + 32); int tPos = 0; - while (tPos < template.length()) { - if (template.charAt(tPos) != '{') { - sb.append(template.charAt(tPos++)); + while (tPos < uniqueAttributeAutoComputeFormat.length()) { + if (uniqueAttributeAutoComputeFormat.charAt(tPos) != '{') { + sb.append(uniqueAttributeAutoComputeFormat.charAt(tPos++)); continue; } - int closeBrace = template.indexOf('}', tPos); + int closeBrace = uniqueAttributeAutoComputeFormat.indexOf('}', tPos); if (closeBrace < 0) { - break; // malformed template + break; // malformed autoComputeFormat } - String key = template.substring(tPos + 1, closeBrace); + String key = uniqueAttributeAutoComputeFormat.substring(tPos + 1, closeBrace); sb.append(slots.getOrDefault(key, "")); @@ -401,10 +412,8 @@ private String buildUniqueAttribute(String template, Map slots) return sb.toString(); } - // ─── small helpers ────────────────────────────────────────────────────────── - - /** Returns the {@code autoComputeFormat} for the entity's {@code qualifiedName} attribute, trimmed. */ - private String getUniqueAttributeTemplate(AtlasEntityType entityType) { + /** Returns trimmed {@code autoComputeFormat} for the type's unique ({@code qualifiedName}) attribute. */ + private String getUniqueAttributeAutoComputeFormat(AtlasEntityType entityType) { AtlasAttribute uniqueAttr = entityType.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME); if (uniqueAttr == null || uniqueAttr.getAttributeDef() == null) { @@ -435,8 +444,6 @@ private List toList(Iterator iterator) { return list; } - // ─── inner class ──────────────────────────────────────────────────────────── - public static class DependentUpdate { private final String guid; private final String typeName; diff --git a/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java b/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java index 2381fc1d77..84ef8f6031 100644 --- a/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java +++ b/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java @@ -1010,7 +1010,7 @@ public void testSetAttributeDefOverridesPatchHandlerPropagateRenameEnd1() throws when(typeRegistry.getTypeDefByName("TestRelationship")).thenReturn(relationshipDef); Map params = new HashMap<>(); - params.put("endDefNumber", "endDef1"); + params.put("endDefToken", "endDef1"); setField(patch, "params", params); PatchStatus result = handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch); @@ -1033,7 +1033,7 @@ public void testSetAttributeDefOverridesPatchHandlerPropagateRenameEnd2() throws when(typeRegistry.getTypeDefByName("TestRelationship")).thenReturn(relationshipDef); Map params = new HashMap<>(); - params.put("endDefNumber", "endDef2"); + params.put("endDefToken", "endDef2"); setField(patch, "params", params); PatchStatus result = handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch); @@ -1056,7 +1056,7 @@ public void testSetAttributeDefOverridesPatchHandlerPropagateRenameInvalidEndDef when(typeRegistry.getTypeDefByName("TestRelationship")).thenReturn(relationshipDef); Map params = new HashMap<>(); - params.put("endDefNumber", "endDef3"); + params.put("endDefToken", "endDef3"); setField(patch, "params", params); expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); @@ -1092,7 +1092,7 @@ public void testSetAttributeDefOverridesPatchHandlerPropagateRenameWrongType() t when(typeRegistry.getTypeDefByName("TestEntity")).thenReturn(entityDef); Map params = new HashMap<>(); - params.put("endDefNumber", "endDef1"); + params.put("endDefToken", "endDef1"); setField(patch, "params", params); expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); diff --git a/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandlerTest.java b/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandlerTest.java new file mode 100644 index 0000000000..0931f3bd06 --- /dev/null +++ b/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandlerTest.java @@ -0,0 +1,945 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.atlas.repository.store.graph.v2; + +import org.apache.atlas.exception.AtlasBaseException; +import org.apache.atlas.model.instance.AtlasEntity; +import org.apache.atlas.repository.graph.GraphHelper; +import org.apache.atlas.repository.graphdb.AtlasEdge; +import org.apache.atlas.repository.graphdb.AtlasVertex; +import org.apache.atlas.type.AtlasEntityType; +import org.apache.atlas.type.AtlasStructType.AtlasAttribute; +import org.apache.atlas.type.AtlasTypeRegistry; +import org.apache.atlas.type.AtlasTypeUtil; +import org.apache.atlas.type.RenamePropagationTarget; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit tests for {@link EntityRenameHandler}. + * + *

Tests are split into two groups: + *

    + *
  1. Pure-logic tests: {@code parseUniqueAttribute}, {@code buildUniqueAttribute}, + * {@code recomputeUniqueAttribute}, {@code buildMappedAttrs}, and {@code findQualifiedNameOverrideKey} + * are private methods + * exercised directly via reflection — no graph or type-registry mocking required.
  2. + *
  3. Integration tests: {@code addDependentsToContext} (and its private + * {@code collectDependents} worker) are exercised end-to-end by mocking the graph + * layer, the type registry, and the static helpers in {@link GraphHelper} and + * {@link AtlasGraphUtilsV2}.
  4. + *
+ */ +public class EntityRenameHandlerTest { + // ─── shared mocks ──────────────────────────────────────────────────────────── + + @Mock private AtlasTypeRegistry typeRegistry; + + private EntityRenameHandler handler; + + private MockedStatic graphHelperMock; + private MockedStatic graphUtilsMock; + + @BeforeMethod + public void setUp() { + MockitoAnnotations.openMocks(this); + + handler = new EntityRenameHandler(typeRegistry); + } + + @AfterMethod + public void tearDown() { + if (graphHelperMock != null) { + graphHelperMock.close(); + graphHelperMock = null; + } + + if (graphUtilsMock != null) { + graphUtilsMock.close(); + graphUtilsMock = null; + } + } + + // ========================================================================= + // 1. parseUniqueAttribute — pure parsing logic + // ========================================================================= + + @DataProvider(name = "parseUniqueAttributeData") + public Object[][] parseUniqueAttributeData() { + return new Object[][] { + // template, uniqueAttr, expectedSlots + { + "{db.name}.{name}@{clusterName}", + "mydb.mytable@cluster1", + map("db.name", "mydb", "name", "mytable", "clusterName", "cluster1") + }, + { + "{name}", + "standalone", + map("name", "standalone") + }, + { + "{db.name}.{table.name}.{name}@{clusterName}", + "db1.tbl1.col1@prod", + map("db.name", "db1", "table.name", "tbl1", "name", "col1", "clusterName", "prod") + }, + { + "{catalog.name}.{schema.name}.{name}@{clusterName}", + "cat1.sch1.tbl1@prod", + map("catalog.name", "cat1", "schema.name", "sch1", "name", "tbl1", "clusterName", "prod") + }, + }; + } + + @Test(dataProvider = "parseUniqueAttributeData") + public void parseUniqueAttribute_standardTemplates_parsedCorrectly( + String template, String uniqueAttr, Map expectedSlots) throws Exception { + Map result = invokeParseUniqueAttribute(uniqueAttr, template); + + assertEquals(result, expectedSlots); + } + + @Test + public void parseUniqueAttribute_delimiterNotFound_capturesRemainder() throws Exception { + // Template expects '@' after {name} but the QN string has no '@'. + Map result = invokeParseUniqueAttribute("mydb.orphan", "{db.name}.{name}@{clusterName}"); + + assertEquals(result.get("db.name"), "mydb"); + assertEquals(result.get("name"), "orphan"); + } + + @Test + public void parseUniqueAttribute_malformedTemplate_stopsParsingGracefully() throws Exception { + // Missing closing brace — should not throw, just stop parsing. + Map result = invokeParseUniqueAttribute("mydb.mytable", "{db.name}.{name"); + + assertEquals(result.get("db.name"), "mydb"); + assertTrue(!result.containsKey("name")); + } + + // ========================================================================= + // 2. buildUniqueAttribute — pure rebuild logic + // ========================================================================= + + @Test + public void buildUniqueAttribute_allSlotsFilled_rebuildsCorrectly() throws Exception { + String template = "{db.name}.{name}@{clusterName}"; + Map slots = map("db.name", "mydb", "name", "new_table", "clusterName", "prod"); + + String result = invokeBuildUniqueAttribute(template, slots); + + assertEquals(result, "mydb.new_table@prod"); + } + + @Test + public void buildUniqueAttribute_missingSlot_substituteEmptyString() throws Exception { + String template = "{db.name}.{name}@{clusterName}"; + Map slots = map("db.name", "mydb", "name", "tbl"); // "clusterName" absent + + String result = invokeBuildUniqueAttribute(template, slots); + + assertEquals(result, "mydb.tbl@"); + } + + @Test + public void buildUniqueAttribute_noSlots_returnsLiterals() throws Exception { + String result = invokeBuildUniqueAttribute("literal_only", Collections.emptyMap()); + + assertEquals(result, "literal_only"); + } + + // ========================================================================= + // 3. recomputeUniqueAttribute — parse + override + rebuild + // ========================================================================= + + @Test + public void recomputeUniqueAttribute_overridesCorrectSlot() throws Exception { + String result = invokeRecomputeUniqueAttribute( + "old_db.my_table@cluster1", + "{db.name}.{name}@{clusterName}", + "db.name", + "new_db"); + + assertEquals(result, "new_db.my_table@cluster1"); + } + + @Test + public void recomputeUniqueAttribute_nullOverrideKey_qnUnchanged() throws Exception { + String original = "old_db.my_table@cluster1"; + String result = invokeRecomputeUniqueAttribute( + original, "{db.name}.{name}@{clusterName}", null, "new_db"); + + assertEquals(result, original); + } + + @Test + public void recomputeUniqueAttribute_multiLevelTemplate_onlyTargetedSlotChanges() throws Exception { + // hive_column template: {table.db.name}.{table.name}@{clusterName}.{name} + String result = invokeRecomputeUniqueAttribute( + "old_db.my_table@prod.col1", + "{table.db.name}.{table.name}@{clusterName}.{name}", + "table.db.name", + "new_db"); + + assertEquals(result, "new_db.my_table@prod.col1"); + } + + // ========================================================================= + // 4. buildMappedAttrs — propagateAttributes → dependent stub attribute map + // ========================================================================= + + @Test + public void buildMappedAttrs_nameToNameMapping_setsNameOnDependent() throws Exception { + List> propagateAttributes = Collections.singletonList( + map("source", "name", "target", "name")); + + Map result = invokeBuildMappedAttrs(propagateAttributes, "new_source_name"); + + assertEquals(result.get("name"), "new_source_name"); + assertEquals(result.size(), 1); + } + + @Test + public void buildMappedAttrs_nameToCustomTargetMapping_putsCustomTargetKey() throws Exception { + // Mapping source "name" to a differently named target attribute on the dependent. + List> propagateAttributes = Collections.singletonList( + map("source", "name", "target", "sourceCluster")); + + Map result = invokeBuildMappedAttrs(propagateAttributes, "new_name"); + + assertEquals(result.get("sourceCluster"), "new_name"); + assertTrue(!result.containsKey("name")); + } + + @Test + public void buildMappedAttrs_multipleNameMappings_allTargetsFilled() throws Exception { + List> propagateAttributes = Arrays.asList( + map("source", "name", "target", "name"), + map("source", "clusterName", "target", "sourceCluster"), // non-name source — skipped + map("source", "name", "target", "mirrorName")); // second name mapping + + Map result = invokeBuildMappedAttrs(propagateAttributes, "entity_name"); + + assertEquals(result.get("name"), "entity_name"); + assertEquals(result.get("mirrorName"), "entity_name"); + assertTrue(!result.containsKey("sourceCluster")); // clusterName source not available here + } + + @Test + public void buildMappedAttrs_noNameSourceMapping_returnsEmptyMap() throws Exception { + List> propagateAttributes = Collections.singletonList( + map("source", "owner", "target", "schemaOwner")); + + Map result = invokeBuildMappedAttrs(propagateAttributes, "ignored"); + + assertTrue(result.isEmpty()); + } + + // ========================================================================= + // 4b. findQualifiedNameOverrideKey — propagateAttributes → QN slot key + // ========================================================================= + + @Test + public void findQualifiedNameOverrideKey_nameToName_returnsName() throws Exception { + List> propagateAttributes = Collections.singletonList( + map("source", "name", "target", "name")); + + assertEquals(invokeFindQualifiedNameOverrideKey(propagateAttributes), "name"); + } + + @Test + public void findQualifiedNameOverrideKey_nameToCustomTarget_returnsTarget() throws Exception { + List> propagateAttributes = Collections.singletonList( + map("source", "name", "target", "sourceCluster")); + + assertEquals(invokeFindQualifiedNameOverrideKey(propagateAttributes), "sourceCluster"); + } + + @Test + public void findQualifiedNameOverrideKey_firstNameMappingWins() throws Exception { + List> propagateAttributes = Arrays.asList( + map("source", "owner", "target", "schemaOwner"), + map("source", "name", "target", "mirrorName")); + + assertEquals(invokeFindQualifiedNameOverrideKey(propagateAttributes), "mirrorName"); + } + + @Test + public void findQualifiedNameOverrideKey_emptyList_returnsNull() throws Exception { + assertNull(invokeFindQualifiedNameOverrideKey(Collections.emptyList())); + } + + @Test + public void findQualifiedNameOverrideKey_noNameSource_returnsNull() throws Exception { + List> propagateAttributes = Collections.singletonList( + map("source", "clusterName", "target", "sourceCluster")); + + assertNull(invokeFindQualifiedNameOverrideKey(propagateAttributes)); + } + + // ========================================================================= + // 5. addDependentsToContext — integration tests with graph mocking + // ========================================================================= + + @Test + public void addDependentsToContext_noPropagationTargets_contextEmpty() throws AtlasBaseException { + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasEntity srcEntity = new AtlasEntity("hive_db"); + + srcEntity.setAttribute(AtlasTypeUtil.ATTRIBUTE_NAME, "new_db"); + when(sourceType.getTypeName()).thenReturn("hive_db"); + when(sourceType.getRenamePropagationTargets()).thenReturn(Collections.emptyList()); + + EntityMutationContext context = new EntityMutationContext(); + + handler.addDependentsToContext(context, sourceType, srcVertex, srcEntity); + + assertTrue(context.getUpdatedEntities().isEmpty()); + } + + @Test + public void addDependentsToContext_propagationTargetWithNullRelAttr_skipped() throws AtlasBaseException { + RenamePropagationTarget propagationTarget = mock(RenamePropagationTarget.class); + + when(propagationTarget.getRelAttr()).thenReturn(null); + + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasEntity srcEntity = new AtlasEntity("hive_db"); + + srcEntity.setAttribute(AtlasTypeUtil.ATTRIBUTE_NAME, "new_db"); + when(sourceType.getTypeName()).thenReturn("hive_db"); + when(sourceType.getRenamePropagationTargets()).thenReturn(Collections.singletonList(propagationTarget)); + + EntityMutationContext context = new EntityMutationContext(); + + handler.addDependentsToContext(context, sourceType, srcVertex, srcEntity); + + assertTrue(context.getUpdatedEntities().isEmpty()); + } + + @Test + public void addDependentsToContext_noEdgesFromSource_contextEmpty() throws AtlasBaseException { + openStaticMocks(); + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + RenamePropagationTarget propagationTarget = mock(RenamePropagationTarget.class); + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasEntity srcEntity = new AtlasEntity("hive_db"); + + srcEntity.setAttribute(AtlasTypeUtil.ATTRIBUTE_NAME, "new_db"); + when(sourceType.getTypeName()).thenReturn("hive_db"); + when(sourceType.getRenamePropagationTargets()).thenReturn(Collections.singletonList(propagationTarget)); + when(propagationTarget.getRelAttr()).thenReturn(relAttr); + when(propagationTarget.getPropagateAttributes()).thenReturn(Collections.emptyList()); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__hive_table.db"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.emptyList().iterator()); + + EntityMutationContext context = new EntityMutationContext(); + + handler.addDependentsToContext(context, sourceType, srcVertex, srcEntity); + + assertTrue(context.getUpdatedEntities().isEmpty()); + } + + @Test + public void addDependentsToContext_templateMapPath_dependentQnRecomputedCorrectly() throws AtlasBaseException { + openStaticMocks(); + + final String dependentGuid = "dependent-guid-001"; + final String dependentType = "hive_table"; + final String oldQn = "old_db.my_table@cluster1"; + final String expectedQn = "new_db.my_table@cluster1"; + final String template = "{db.name}.{name}@{clusterName}"; + final String qnPropKey = "Referenceable.qualifiedName"; + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + RenamePropagationTarget propagationTarget = mock(RenamePropagationTarget.class); + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasEntityType dependentEntityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasVertex dependentVertex = mock(AtlasVertex.class); + AtlasEdge edge = mock(AtlasEdge.class); + AtlasAttribute qnAttr = mock(AtlasAttribute.class); + org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef attrDef = + mock(org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.class); + + // source setup + when(sourceType.getTypeName()).thenReturn("hive_db"); + when(sourceType.getRenamePropagationTargets()).thenReturn(Collections.singletonList(propagationTarget)); + + // propagation target — no propagateAttributes → uses template-map path + when(propagationTarget.getRelAttr()).thenReturn(relAttr); + when(propagationTarget.getPropagateAttributes()).thenReturn(Collections.emptyList()); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__hive_table.db"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + // edge → dependentVertex + when(edge.getOutVertex()).thenReturn(srcVertex); + when(edge.getInVertex()).thenReturn(dependentVertex); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.singletonList(edge).iterator()); + + // dependent vertex and type setup + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(dependentVertex)).thenReturn(dependentGuid); + graphHelperMock.when(() -> GraphHelper.getTypeName(dependentVertex)).thenReturn(dependentType); + when(typeRegistry.getEntityTypeByName(dependentType)).thenReturn(dependentEntityType); + when(dependentEntityType.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnAttr); + when(qnAttr.getAttributeDef()).thenReturn(attrDef); + when(attrDef.getAutoComputeFormat()).thenReturn(template); + when(dependentEntityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnPropKey); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getProperty(dependentVertex, qnPropKey, String.class)).thenReturn(oldQn); + when(dependentEntityType.getAutoComputeFormatPathByRefTypeNameMap()).thenReturn(Collections.singletonMap("hive_db", "db.name")); + when(dependentEntityType.getRenamePropagationTargets()).thenReturn(Collections.emptyList()); + + AtlasEntity srcEntity = new AtlasEntity("hive_db"); + + srcEntity.setAttribute(AtlasTypeUtil.ATTRIBUTE_NAME, "new_db"); + + EntityMutationContext context = new EntityMutationContext(); + + handler.addDependentsToContext(context, sourceType, srcVertex, srcEntity); + + assertEquals(context.getUpdatedEntities().size(), 1); + + AtlasEntity updatedStub = context.getUpdatedEntities().iterator().next(); + + assertEquals(updatedStub.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME), expectedQn); + assertEquals(updatedStub.getGuid(), dependentGuid); + assertEquals(updatedStub.getTypeName(), dependentType); + } + + @Test + public void addDependentsToContext_templateMapPath_noOverrideKeyForRenamedType_dependentSkipped() throws AtlasBaseException { + openStaticMocks(); + + final String dependentGuid = "dependent-guid-002"; + final String dependentType = "hive_table"; + final String qnPropKey = "Referenceable.qualifiedName"; + final String template = "{db.name}.{name}@{clusterName}"; + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + RenamePropagationTarget propagationTarget = mock(RenamePropagationTarget.class); + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasEntityType dependentEntityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasVertex dependentVertex = mock(AtlasVertex.class); + AtlasEdge edge = mock(AtlasEdge.class); + AtlasAttribute qnAttr = mock(AtlasAttribute.class); + org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef attrDef = + mock(org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.class); + + when(sourceType.getTypeName()).thenReturn("some_other_type"); + when(sourceType.getRenamePropagationTargets()).thenReturn(Collections.singletonList(propagationTarget)); + when(propagationTarget.getRelAttr()).thenReturn(relAttr); + when(propagationTarget.getPropagateAttributes()).thenReturn(Collections.emptyList()); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__hive_table.db"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + when(edge.getOutVertex()).thenReturn(srcVertex); + when(edge.getInVertex()).thenReturn(dependentVertex); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.singletonList(edge).iterator()); + + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(dependentVertex)).thenReturn(dependentGuid); + graphHelperMock.when(() -> GraphHelper.getTypeName(dependentVertex)).thenReturn(dependentType); + when(typeRegistry.getEntityTypeByName(dependentType)).thenReturn(dependentEntityType); + when(dependentEntityType.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnAttr); + when(qnAttr.getAttributeDef()).thenReturn(attrDef); + when(attrDef.getAutoComputeFormat()).thenReturn(template); + when(dependentEntityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnPropKey); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getProperty(dependentVertex, qnPropKey, String.class)) + .thenReturn("old_db.my_table@cluster1"); + // Template map does NOT contain "some_other_type" — overrideKey will be null → dependent skipped. + when(dependentEntityType.getAutoComputeFormatPathByRefTypeNameMap()).thenReturn(Collections.singletonMap("hive_db", "db.name")); + + AtlasEntity srcEntity = new AtlasEntity("some_other_type"); + + srcEntity.setAttribute(AtlasTypeUtil.ATTRIBUTE_NAME, "new_name"); + + EntityMutationContext context = new EntityMutationContext(); + + handler.addDependentsToContext(context, sourceType, srcVertex, srcEntity); + + assertTrue(context.getUpdatedEntities().isEmpty()); + } + + @Test + public void addDependentsToContext_newQnEqualsOldQn_dependentNotAddedToContext() throws AtlasBaseException { + openStaticMocks(); + + final String dependentGuid = "dependent-guid-003"; + final String dependentType = "hive_table"; + final String sameQn = "same_db.my_table@cluster1"; + final String qnPropKey = "Referenceable.qualifiedName"; + final String template = "{db.name}.{name}@{clusterName}"; + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + RenamePropagationTarget propagationTarget = mock(RenamePropagationTarget.class); + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasEntityType dependentEntityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasVertex dependentVertex = mock(AtlasVertex.class); + AtlasEdge edge = mock(AtlasEdge.class); + AtlasAttribute qnAttr = mock(AtlasAttribute.class); + org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef attrDef = + mock(org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.class); + + when(sourceType.getTypeName()).thenReturn("hive_db"); + when(sourceType.getRenamePropagationTargets()).thenReturn(Collections.singletonList(propagationTarget)); + when(propagationTarget.getRelAttr()).thenReturn(relAttr); + when(propagationTarget.getPropagateAttributes()).thenReturn(Collections.emptyList()); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__hive_table.db"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + when(edge.getOutVertex()).thenReturn(srcVertex); + when(edge.getInVertex()).thenReturn(dependentVertex); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.singletonList(edge).iterator()); + + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(dependentVertex)).thenReturn(dependentGuid); + graphHelperMock.when(() -> GraphHelper.getTypeName(dependentVertex)).thenReturn(dependentType); + when(typeRegistry.getEntityTypeByName(dependentType)).thenReturn(dependentEntityType); + when(dependentEntityType.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnAttr); + when(qnAttr.getAttributeDef()).thenReturn(attrDef); + when(attrDef.getAutoComputeFormat()).thenReturn(template); + when(dependentEntityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnPropKey); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getProperty(dependentVertex, qnPropKey, String.class)).thenReturn(sameQn); + // "db.name" slot already equals "same_db" — and new name is also "same_db" → newQN == oldQN. + when(dependentEntityType.getAutoComputeFormatPathByRefTypeNameMap()).thenReturn(Collections.singletonMap("hive_db", "db.name")); + when(dependentEntityType.getRenamePropagationTargets()).thenReturn(Collections.emptyList()); + + AtlasEntity srcEntity = new AtlasEntity("hive_db"); + + srcEntity.setAttribute(AtlasTypeUtil.ATTRIBUTE_NAME, "same_db"); + + EntityMutationContext context = new EntityMutationContext(); + + handler.addDependentsToContext(context, sourceType, srcVertex, srcEntity); + + assertTrue(context.getUpdatedEntities().isEmpty()); + } + + @Test + public void addDependentsToContext_propagateAttributesPath_dependentQnAndMappedAttrsUpdated() throws AtlasBaseException { + openStaticMocks(); + + final String dependentGuid = "assoc-dependent-guid"; + final String dependentType = "trino_table"; + final String oldQn = "cat1.pub.old_table@prod"; + final String expectedQn = "cat1.pub.new_table@prod"; + final String template = "{catalog.name}.{schema.name}.{name}@{clusterName}"; + final String qnPropKey = "trino_table.qualifiedName"; + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + RenamePropagationTarget propagationTarget = mock(RenamePropagationTarget.class); + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasEntityType dependentEntityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasVertex dependentVertex = mock(AtlasVertex.class); + AtlasEdge edge = mock(AtlasEdge.class); + AtlasAttribute qnAttr = mock(AtlasAttribute.class); + org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef attrDef = + mock(org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.class); + + List> propagateAttributes = Collections.singletonList( + map("source", "name", "target", "name")); + + when(sourceType.getTypeName()).thenReturn("hive_table"); + when(sourceType.getRenamePropagationTargets()).thenReturn(Collections.singletonList(propagationTarget)); + when(propagationTarget.getRelAttr()).thenReturn(relAttr); + when(propagationTarget.getPropagateAttributes()).thenReturn(propagateAttributes); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__trino_table_hive_table"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + when(edge.getOutVertex()).thenReturn(srcVertex); + when(edge.getInVertex()).thenReturn(dependentVertex); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.singletonList(edge).iterator()); + + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(dependentVertex)).thenReturn(dependentGuid); + graphHelperMock.when(() -> GraphHelper.getTypeName(dependentVertex)).thenReturn(dependentType); + when(typeRegistry.getEntityTypeByName(dependentType)).thenReturn(dependentEntityType); + when(dependentEntityType.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnAttr); + when(qnAttr.getAttributeDef()).thenReturn(attrDef); + when(attrDef.getAutoComputeFormat()).thenReturn(template); + when(dependentEntityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnPropKey); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getProperty(dependentVertex, qnPropKey, String.class)).thenReturn(oldQn); + when(dependentEntityType.getRenamePropagationTargets()).thenReturn(Collections.emptyList()); + + AtlasEntity srcEntity = new AtlasEntity("hive_table"); + + srcEntity.setAttribute(AtlasTypeUtil.ATTRIBUTE_NAME, "new_table"); + + EntityMutationContext context = new EntityMutationContext(); + + handler.addDependentsToContext(context, sourceType, srcVertex, srcEntity); + + assertEquals(context.getUpdatedEntities().size(), 1); + + AtlasEntity stub = context.getUpdatedEntities().iterator().next(); + + assertEquals(stub.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME), expectedQn); + assertEquals(stub.getAttribute("name"), "new_table"); + assertEquals(stub.getGuid(), dependentGuid); + } + + @Test + public void addDependentsToContext_propagateAttributesPath_nameMappedToCustomQnSlot_updatesCorrectSegment() throws AtlasBaseException { + openStaticMocks(); + + final String dependentGuid = "assoc-dependent-guid-2"; + final String dependentType = "trino_schema"; + final String oldQn = "c1.oldCluster.n1@prod"; + final String expectedQn = "c1.newCluster.n1@prod"; + final String template = "{catalog}.{sourceCluster}.{name}@prod"; + final String qnPropKey = "trino_schema.qualifiedName"; + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + RenamePropagationTarget propagationTarget = mock(RenamePropagationTarget.class); + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasEntityType dependentEntityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasVertex dependentVertex = mock(AtlasVertex.class); + AtlasEdge edge = mock(AtlasEdge.class); + AtlasAttribute qnAttr = mock(AtlasAttribute.class); + org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef attrDef = + mock(org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.class); + + List> propagateAttributes = Collections.singletonList( + map("source", "name", "target", "sourceCluster")); + + when(sourceType.getTypeName()).thenReturn("hive_db"); + when(sourceType.getRenamePropagationTargets()).thenReturn(Collections.singletonList(propagationTarget)); + when(propagationTarget.getRelAttr()).thenReturn(relAttr); + when(propagationTarget.getPropagateAttributes()).thenReturn(propagateAttributes); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__trino_schema_hive_db"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + when(edge.getOutVertex()).thenReturn(srcVertex); + when(edge.getInVertex()).thenReturn(dependentVertex); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.singletonList(edge).iterator()); + + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(dependentVertex)).thenReturn(dependentGuid); + graphHelperMock.when(() -> GraphHelper.getTypeName(dependentVertex)).thenReturn(dependentType); + when(typeRegistry.getEntityTypeByName(dependentType)).thenReturn(dependentEntityType); + when(dependentEntityType.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnAttr); + when(qnAttr.getAttributeDef()).thenReturn(attrDef); + when(attrDef.getAutoComputeFormat()).thenReturn(template); + when(dependentEntityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnPropKey); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getProperty(dependentVertex, qnPropKey, String.class)).thenReturn(oldQn); + when(dependentEntityType.getRenamePropagationTargets()).thenReturn(Collections.emptyList()); + + AtlasEntity srcEntity = new AtlasEntity("hive_db"); + + srcEntity.setAttribute(AtlasTypeUtil.ATTRIBUTE_NAME, "newCluster"); + + EntityMutationContext context = new EntityMutationContext(); + + handler.addDependentsToContext(context, sourceType, srcVertex, srcEntity); + + assertEquals(context.getUpdatedEntities().size(), 1); + + AtlasEntity stub = context.getUpdatedEntities().iterator().next(); + + assertEquals(stub.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME), expectedQn); + assertEquals(stub.getAttribute("sourceCluster"), "newCluster"); + } + + @Test + public void addDependentsToContext_visitedGuard_sameDependentNotProcessedTwice() throws AtlasBaseException { + openStaticMocks(); + + final String dependentGuid = "shared-dependent-guid"; + final String dependentType = "hive_table"; + final String oldQn = "old_db.tbl@cluster1"; + final String qnPropKey = "Referenceable.qualifiedName"; + final String template = "{db.name}.{name}@{clusterName}"; + + AtlasAttribute relAttr1 = mock(AtlasAttribute.class); + AtlasAttribute relAttr2 = mock(AtlasAttribute.class); + RenamePropagationTarget propagationTarget1 = mock(RenamePropagationTarget.class); + RenamePropagationTarget propagationTarget2 = mock(RenamePropagationTarget.class); + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasEntityType dependentEntityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasVertex dependentVertex = mock(AtlasVertex.class); + AtlasEdge edge = mock(AtlasEdge.class); + AtlasAttribute qnAttr = mock(AtlasAttribute.class); + org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef attrDef = + mock(org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.class); + + // Two propagation targets both resolve to the same dependent vertex/guid. + when(sourceType.getTypeName()).thenReturn("hive_db"); + when(sourceType.getRenamePropagationTargets()).thenReturn(Arrays.asList(propagationTarget1, propagationTarget2)); + + for (RenamePropagationTarget propagationTarget : Arrays.asList(propagationTarget1, propagationTarget2)) { + when(propagationTarget.getPropagateAttributes()).thenReturn(Collections.emptyList()); + } + + when(propagationTarget1.getRelAttr()).thenReturn(relAttr1); + when(relAttr1.getRelationshipEdgeLabel()).thenReturn("edge_label_1"); + when(relAttr1.getRelationshipEdgeDirection()).thenReturn(null); + + when(propagationTarget2.getRelAttr()).thenReturn(relAttr2); + when(relAttr2.getRelationshipEdgeLabel()).thenReturn("edge_label_2"); + when(relAttr2.getRelationshipEdgeDirection()).thenReturn(null); + + when(edge.getOutVertex()).thenReturn(srcVertex); + when(edge.getInVertex()).thenReturn(dependentVertex); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.singletonList(edge).iterator()); + + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(dependentVertex)).thenReturn(dependentGuid); + graphHelperMock.when(() -> GraphHelper.getTypeName(dependentVertex)).thenReturn(dependentType); + when(typeRegistry.getEntityTypeByName(dependentType)).thenReturn(dependentEntityType); + when(dependentEntityType.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnAttr); + when(qnAttr.getAttributeDef()).thenReturn(attrDef); + when(attrDef.getAutoComputeFormat()).thenReturn(template); + when(dependentEntityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnPropKey); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getProperty(dependentVertex, qnPropKey, String.class)).thenReturn(oldQn); + when(dependentEntityType.getAutoComputeFormatPathByRefTypeNameMap()).thenReturn(Collections.singletonMap("hive_db", "db.name")); + when(dependentEntityType.getRenamePropagationTargets()).thenReturn(Collections.emptyList()); + + AtlasEntity srcEntity = new AtlasEntity("hive_db"); + + srcEntity.setAttribute(AtlasTypeUtil.ATTRIBUTE_NAME, "new_db"); + + EntityMutationContext context = new EntityMutationContext(); + + handler.addDependentsToContext(context, sourceType, srcVertex, srcEntity); + + // Even though two targets both reach the same dependent, it must appear only once. + assertEquals(context.getUpdatedEntities().size(), 1); + } + + @Test + public void addDependentsToContext_twoLevelStructuralCascade_bothLevelsUpdated() throws AtlasBaseException { + openStaticMocks(); + + final String tableGuid = "table-guid"; + final String tableType = "hive_table"; + final String tableOld = "old_db.my_table@cluster1"; + final String tableNew = "new_db.my_table@cluster1"; + final String tableTmpl = "{db.name}.{name}@{clusterName}"; + + final String colGuid = "col-guid"; + final String colType = "hive_column"; + final String colOld = "old_db.my_table@cluster1.col1"; + final String colNew = "new_db.my_table@cluster1.col1"; + final String colTmpl = "{table.db.name}.{table.name}@{clusterName}.{name}"; + + final String qnPropKey = "Referenceable.qualifiedName"; + + // Source: hive_db + AtlasEntityType dbType = mock(AtlasEntityType.class); + AtlasVertex dbVertex = mock(AtlasVertex.class); + RenamePropagationTarget dbToTableTarget = mock(RenamePropagationTarget.class); + AtlasAttribute dbRelAttr = mock(AtlasAttribute.class); + + // Level 1 dependent: hive_table + AtlasEntityType tableEntityType = mock(AtlasEntityType.class); + AtlasVertex tableVertex = mock(AtlasVertex.class); + AtlasEdge dbToTableEdge = mock(AtlasEdge.class); + AtlasAttribute tableQnAttr = mock(AtlasAttribute.class); + org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef tableAttrDef = + mock(org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.class); + RenamePropagationTarget tableToColTarget = mock(RenamePropagationTarget.class); + AtlasAttribute tableRelAttr = mock(AtlasAttribute.class); + + // Level 2 dependent: hive_column + AtlasEntityType colEntityType = mock(AtlasEntityType.class); + AtlasVertex colVertex = mock(AtlasVertex.class); + AtlasEdge tableToColEdge = mock(AtlasEdge.class); + AtlasAttribute colQnAttr = mock(AtlasAttribute.class); + org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef colAttrDef = + mock(org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.class); + + // ── source (hive_db) ── + when(dbType.getTypeName()).thenReturn("hive_db"); + when(dbType.getRenamePropagationTargets()).thenReturn(Collections.singletonList(dbToTableTarget)); + when(dbToTableTarget.getRelAttr()).thenReturn(dbRelAttr); + when(dbToTableTarget.getPropagateAttributes()).thenReturn(Collections.emptyList()); + when(dbRelAttr.getRelationshipEdgeLabel()).thenReturn("__hive_table.db"); + when(dbRelAttr.getRelationshipEdgeDirection()).thenReturn(null); + + when(dbToTableEdge.getOutVertex()).thenReturn(dbVertex); + when(dbToTableEdge.getInVertex()).thenReturn(tableVertex); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(dbVertex), eq("__hive_table.db"), any())) + .thenReturn(Collections.singletonList(dbToTableEdge).iterator()); + + // ── level 1 dependent: hive_table ── + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(tableVertex)).thenReturn(tableGuid); + graphHelperMock.when(() -> GraphHelper.getTypeName(tableVertex)).thenReturn(tableType); + when(typeRegistry.getEntityTypeByName(tableType)).thenReturn(tableEntityType); + when(tableEntityType.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(tableQnAttr); + when(tableQnAttr.getAttributeDef()).thenReturn(tableAttrDef); + when(tableAttrDef.getAutoComputeFormat()).thenReturn(tableTmpl); + when(tableEntityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnPropKey); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getProperty(tableVertex, qnPropKey, String.class)).thenReturn(tableOld); + when(tableEntityType.getAutoComputeFormatPathByRefTypeNameMap()).thenReturn(Collections.singletonMap("hive_db", "db.name")); + when(tableEntityType.getRenamePropagationTargets()).thenReturn(Collections.singletonList(tableToColTarget)); + + when(tableToColTarget.getRelAttr()).thenReturn(tableRelAttr); + when(tableToColTarget.getPropagateAttributes()).thenReturn(Collections.emptyList()); + when(tableRelAttr.getRelationshipEdgeLabel()).thenReturn("__hive_column.table"); + when(tableRelAttr.getRelationshipEdgeDirection()).thenReturn(null); + + when(tableToColEdge.getOutVertex()).thenReturn(tableVertex); + when(tableToColEdge.getInVertex()).thenReturn(colVertex); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(tableVertex), eq("__hive_column.table"), any())) + .thenReturn(Collections.singletonList(tableToColEdge).iterator()); + + // ── level 2 dependent: hive_column ── + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(colVertex)).thenReturn(colGuid); + graphHelperMock.when(() -> GraphHelper.getTypeName(colVertex)).thenReturn(colType); + when(typeRegistry.getEntityTypeByName(colType)).thenReturn(colEntityType); + when(colEntityType.getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(colQnAttr); + when(colQnAttr.getAttributeDef()).thenReturn(colAttrDef); + when(colAttrDef.getAutoComputeFormat()).thenReturn(colTmpl); + when(colEntityType.getVertexPropertyName(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME)).thenReturn(qnPropKey); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getProperty(colVertex, qnPropKey, String.class)).thenReturn(colOld); + when(colEntityType.getAutoComputeFormatPathByRefTypeNameMap()).thenReturn(Collections.singletonMap("hive_db", "table.db.name")); + when(colEntityType.getRenamePropagationTargets()).thenReturn(Collections.emptyList()); + + // ── execute ── + AtlasEntity srcEntity = new AtlasEntity("hive_db"); + + srcEntity.setAttribute(AtlasTypeUtil.ATTRIBUTE_NAME, "new_db"); + + EntityMutationContext context = new EntityMutationContext(); + + handler.addDependentsToContext(context, dbType, dbVertex, srcEntity); + + assertEquals(context.getUpdatedEntities().size(), 2); + + Map byGuid = new HashMap<>(); + + for (AtlasEntity entity : context.getUpdatedEntities()) { + byGuid.put(entity.getGuid(), entity); + } + + assertNotNull(byGuid.get(tableGuid)); + assertNotNull(byGuid.get(colGuid)); + assertEquals(byGuid.get(tableGuid).getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME), tableNew); + assertEquals(byGuid.get(colGuid).getAttribute(AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME), colNew); + } + + // ========================================================================= + // helpers + // ========================================================================= + + private void openStaticMocks() { + graphHelperMock = mockStatic(GraphHelper.class); + graphUtilsMock = mockStatic(AtlasGraphUtilsV2.class); + } + + // ── reflection wrappers for private methods ─────────────────────────────── + + @SuppressWarnings("unchecked") + private Map invokeParseUniqueAttribute(String uniqueAttr, String template) throws Exception { + Method m = EntityRenameHandler.class.getDeclaredMethod("parseUniqueAttribute", String.class, String.class); + + m.setAccessible(true); + + return (Map) m.invoke(handler, uniqueAttr, template); + } + + private String invokeBuildUniqueAttribute(String template, Map slots) throws Exception { + Method m = EntityRenameHandler.class.getDeclaredMethod("buildUniqueAttribute", String.class, Map.class); + + m.setAccessible(true); + + return (String) m.invoke(handler, template, slots); + } + + private String invokeRecomputeUniqueAttribute(String currentQN, String template, + String overrideKey, String newValue) throws Exception { + Method m = EntityRenameHandler.class.getDeclaredMethod( + "recomputeUniqueAttribute", String.class, String.class, String.class, String.class); + + m.setAccessible(true); + + return (String) m.invoke(handler, currentQN, template, overrideKey, newValue); + } + + @SuppressWarnings("unchecked") + private Map invokeBuildMappedAttrs(List> propagateAttributes, + String newSourceEntityName) throws Exception { + Method m = EntityRenameHandler.class.getDeclaredMethod( + "buildMappedAttrs", RenamePropagationTarget.class, String.class); + + m.setAccessible(true); + + RenamePropagationTarget propagationTarget = mock(RenamePropagationTarget.class); + + when(propagationTarget.getPropagateAttributes()).thenReturn(propagateAttributes); + + return (Map) m.invoke(handler, propagationTarget, newSourceEntityName); + } + + private String invokeFindQualifiedNameOverrideKey(List> propagateAttributes) throws Exception { + Method m = EntityRenameHandler.class.getDeclaredMethod("findQualifiedNameOverrideKey", List.class); + + m.setAccessible(true); + + return (String) m.invoke(null, propagateAttributes); + } + + // ── Map factory helper ──────────────────────────────────────────────────── + + private static Map map(Object... keysAndValues) { + Map result = new HashMap<>(); + + for (int i = 0; i < keysAndValues.length; i += 2) { + //noinspection unchecked + result.put((K) keysAndValues[i], (V) keysAndValues[i + 1]); + } + + return result; + } +} From 7506b42eecdb45c515fc3037a9bbc1b56402934a Mon Sep 17 00:00:00 2001 From: "sanket.bhor" Date: Wed, 6 May 2026 15:57:42 +0530 Subject: [PATCH 3/6] ATLAS-5279: changes to add Rename Propagation README file, updated solrconfig for newly added field and handled deleted column use-case --- docs/src/documents/RenamePropagation.md | 98 +++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 docs/src/documents/RenamePropagation.md diff --git a/docs/src/documents/RenamePropagation.md b/docs/src/documents/RenamePropagation.md new file mode 100644 index 0000000000..0fb47e0c5a --- /dev/null +++ b/docs/src/documents/RenamePropagation.md @@ -0,0 +1,98 @@ +--- + +## name: Rename Propagation +route: /RenamePropagation +menu: Documentation +submenu: Features + +# Rename propagation + +**Rename propagation** is an Atlas feature for **partial entity updates**: when a renamed instance’s `**name`** (or a mapped source attribute) changes, the server can find **related entities** whose **unique attribute** (for most types, `**qualifiedName`**) still embeds the old segment, recompute that unique attribute from the type’s `**autoComputeFormat**`, and **persist those updates in the same transaction** as the triggering entity. Discovery is driven by **typedefs** (relationship ends and templates), not by hard-coded type names in application code. + +--- + +## Why it exists + +The unique attribute (qualified name) is often a **composite string** built from ancestor names (database, table, cluster, and so on). If only the root entity is updated in a hook payload, **downstream** instances can keep an outdated unique attribute until something rewrites them. Rename propagation closes that gap by following **marked relationship edges** from the updated vertex and applying the same **parse → replace one template segment → rebuild** logic per dependent type. + +--- + +## Example: Hive database rename + +Assume a Hive model where: + +- `**hive_db`** has unique attribute (qualified name) shaped like `{name}@{clusterName}` (for example `sales@cm`). +- `**hive_table**` uses `{db.name}.{name}@{clusterName}` (for example `sales.orders@cm`). +- `**hive_column**` uses `{table.db.name}.{table.name}.{name}@{clusterName}`. + +The `**hive_table_db**` relationship links each table to its database; `**hive_table_columns**` links columns to a table. Relationship ends that opt in with `**propagateRename**` tell Atlas: when the **trigger** side is renamed, walk to neighbors and refresh their unique attribute (qualified name) where the template depends on that rename. + +1. An integration renames the database `**sales`** → `**sales_archive**` (partial update; `**name**` and thus the unique attribute on the `hive_db` vertex change). +2. Atlas detects that the entity’s **unique attribute (qualified name)** changed and that `**hive_db`** has **rename propagation targets** from typedef resolution. +3. `**EntityRenameHandler`** follows the configured relationship from the database vertex to related `**hive_table**` vertices, recomputes each table’s string (for example `sales.orders@cm` → `sales_archive.orders@cm`), and registers those rows on the mutation context. +4. If `**hive_table**` also declares propagation targets, the same process continues to `**hive_column**` rows (for example `sales.orders.col1@cm` → `sales_archive.orders.col1@cm`). + +All of this happens **without** requiring the hook to send every table and column in one batch. + +--- + +## How it works (two stages) + +### 1. Typedef resolution (startup / type updates) + +While the type system resolves references, Atlas builds **per-entity-type** metadata used later at runtime: + +- `**propagateRename`** on a `**AtlasRelationshipEndDef**` marks which end acts as the **rename source** for that relationship so instances reached through that edge can be updated when the source side’s name changes. +- `**attributeDefOverrides`** on `**AtlasEntityDef**` can set `**autoComputeFormat**` for the **unique attribute** (qualified name), describing how the string is composed from named segments. +- For paths that do not use explicit `**propagateAttributes`** maps on the relationship end, `**AtlasEntityType**` also maintains `**autoComputeFormatPathByRefTypeNameMap**`: referenced type name → **dotted path** in the template for the segment to replace when that referenced type was renamed. +- Optional `**propagateAttributes`** on a relationship end lists `**source**` / `**target**` attribute pairs so values such as `**name**` can be written to the right stub fields on dependents when models need more than template substitution alone. + +Together, these produce a list of `**RenamePropagationTarget**` entries on each `**AtlasEntityType**`. + +### 2. Entity update (runtime) + +On **partial update**, `**AtlasEntityStoreV2`** compares the stored **unique attribute (qualified name)** on the graph vertex to the incoming value. If it changed and the type has **rename propagation targets**, `**EntityRenameHandler`** walks the graph along those relationships, recomputes each dependent’s unique attribute from its effective `**autoComputeFormat**`, and adds minimal **dependent stubs** (guid, new unique attribute, optional mapped attributes) to the **same** mutation context so they are saved with the root change. + +--- + +## Key typedef concepts + + +| Concept | Where it lives | Role | +| ------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `**attributeDefOverrides`** | `AtlasEntityDef` | Supplies per-type overrides such as `**autoComputeFormat**` for the unique attribute (qualified name); merged during hierarchy resolution and stored on the entity type vertex when supported. | +| `**propagateRename**` | `AtlasRelationshipEndDef` | Marks the end whose entity type is the **trigger** for propagation across that relationship. | +| `**propagateAttributes`** | `AtlasRelationshipEndDef` | Optional maps from a **source** attribute on the trigger side to **target** attribute names on the dependent entity. | +| `**RenamePropagationTarget`** | `AtlasEntityType` | Precomputed link: relationship attribute, category, and propagate-attribute list. | +| `**autoComputeFormatPathByRefTypeNameMap**` | `AtlasEntityType` | Referenced entity type name → dotted path in the dependent’s **autoComputeFormat** for the segment to rewrite when that type was renamed (when `**propagateAttributes`** is not used for that path). | + + +--- + +## Implementation reference + + +| Component | Role | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `**AtlasEntityType**` | Registers rename propagation targets and builds `**autoComputeFormatPathByRefTypeNameMap**` during reference resolution. | +| `**EntityRenameHandler**` | Traverses edges, parses/rebuilds the unique attribute string, registers `**DependentUpdate**` entries on the mutation context. | +| `**AtlasEntityStoreV2**` | Detects a changed unique attribute (qualified name) on partial update and invokes the handler. | +| `**AtlasEntityDefStoreV2` / `AtlasTypeDefGraphStoreV2**` | Persist and read `**attributeDefOverrides**` via the type vertex property keyed by `**TYPE_ATTR_DEF_OVERRIDES_PROPERTY_KEY**`. | +| `**AtlasTypeDefStoreInitializer**` | Applies typedef patches such as `**SET_ATTRIBUTE_DEF_OVERRIDES**` and `**SET_PROPAGATE_RENAME**` (patch params use `**endDefToken**`: `"endDef1"` or `"endDef2"`). | +| `**GraphBackedSearchIndexer**` | Indexes the overrides property for search where applicable. | + + +--- + +## Models and patches + +- **Bootstrap model JSON** — Declare `**attributeDefOverrides`** for the unique attribute (qualified name) and `**propagateRename**` / `**propagateAttributes**` on relationship ends as required by your connectors (for example Hive, Trino). +- **Patch files** — Ship one-off typedef updates under `**addons/.../patches/`** so each patch runs once and is tracked (for example `**AtlasPatchRegistry**`), avoiding reliance on version-only bumps on every restart. + +--- + +## Tests + +- `**AtlasTypeDefStoreInitializerTest**` — Patch handlers for overrides and `**SET_PROPAGATE_RENAME**`. +- `**EntityRenameHandlerTest**` — End-to-end behavior: unique attribute recompute, mapped attributes, and multi-hop propagation through typedefs. + From 9c0f85ee0e8cdc469dc1188dbe6e125dddbbae65 Mon Sep 17 00:00:00 2001 From: "sanket.bhor" Date: Wed, 6 May 2026 16:52:37 +0530 Subject: [PATCH 4/6] ATLAS-5279: changes to add Rename Propagation README file, updated solrconfig for newly added field and handled deleted column use-case --- .../apache/atlas/type/AtlasEntityType.java | 1 - .../atlas/type/RenamePropagationTarget.java | 8 ++--- .../store/graph/v2/AtlasEntityStoreV2.java | 6 ++-- .../store/graph/v2/EntityRenameHandler.java | 34 ++++++++++++------- .../solr/core-template/solrconfig.xml | 2 +- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java b/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java index 24fd0dbf22..2ca1f91a64 100644 --- a/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java +++ b/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java @@ -1221,7 +1221,6 @@ private static AtlasAttributeDef getAttrDefWithOverrides(AtlasAttributeDef attri if (AtlasTypeUtil.ATTRIBUTE_QUALIFIED_NAME.equals(override.getName()) && override.getAutoComputeFormat() != null) { AtlasAttributeDef copy = new AtlasAttributeDef(attributeDef); - //TODO : can be removed copy.setAutoComputeFormat(override.getAutoComputeFormat()); return copy; diff --git a/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java b/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java index de5d90110f..913d8a1ff1 100644 --- a/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java +++ b/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java @@ -35,10 +35,10 @@ public class RenamePropagationTarget { public RenamePropagationTarget(String targetTypeName, RelationshipCategory category, AtlasStructType.AtlasAttribute relAttr, List> propagateAttributes) { - this.targetTypeName = targetTypeName; - this.category = category; - this.relAttr = relAttr; - this.propagateAttributes = propagateAttributes != null ? Collections.unmodifiableList(propagateAttributes) : Collections.emptyList(); + this.targetTypeName = targetTypeName; + this.category = category; + this.relAttr = relAttr; + this.propagateAttributes = propagateAttributes != null ? Collections.unmodifiableList(propagateAttributes) : Collections.emptyList(); } public String getTargetTypeName() { diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java index 74b9c9fdbc..578c116328 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasEntityStoreV2.java @@ -1239,10 +1239,8 @@ private void handleRenamePropagation(boolean isPartialUpdate, AtlasEntity entity return; } - LOG.info("Rename detected (qualifiedName changed): type={}; guid={}; processing dependent entities for rename propagation", - entityType.getTypeName(), entityGuid); - LOG.debug("handleRenamePropagation(): qualifiedName old -> new (guid={}): {} -> {}", entityGuid, oldUniqueAttrValue, - newUniqueAttrValue); + LOG.info("Rename detected (qualifiedName changed): type={}; guid={}; oldQualifiedName={}; newQualifiedName={}; processing dependent entities for rename propagation", + entityType.getTypeName(), entityGuid, oldUniqueAttrValue, newUniqueAttrValue); entityRenameHandler.addDependentsToContext(context, entityType, vertex, entity); } catch (AtlasBaseException e) { diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java index 51383eeeda..96a4ab775b 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityRenameHandler.java @@ -125,11 +125,11 @@ private void injectDependentsIntoContext(EntityMutationContext context, MapHow the segment of {@code qualifiedName} to rewrite is chosen: *
    - *
  • {@code propagateAttributes} present: use {@link #findQualifiedNameOverrideKey} (mapping whose + *
  • When {@code propagateAttributes} is present: use {@link #findQualifiedNameOverrideKey} (mapping whose * {@code source} is {@code "name"}), write {@code newName} into that {@code target} path, and add any * extra attributes via {@link #buildMappedAttrs}. For the next level, {@code newName} is read back from * the patched path on the new qualified name (falling back to the prior {@code newName} if blank).
  • - *
  • {@code propagateAttributes} absent: use {@link AtlasEntityType#getAutoComputeFormatPathByRefTypeNameMap()} + *
  • When {@code propagateAttributes} is absent: use {@link AtlasEntityType#getAutoComputeFormatPathByRefTypeNameMap()} * on the dependent type to find the dotted path for {@code renamedTypeName}, then recompute with the same * {@code newName}. Recursion keeps the same {@code renamedTypeName} and {@code newName}.
  • *
@@ -152,12 +152,25 @@ private void collectDependents(AtlasVertex sourceVertex, AtlasEntityType sourceE } for (AtlasEdge edge : toList(GraphHelper.getEdgesForLabel(sourceVertex, relAttr.getRelationshipEdgeLabel(), relAttr.getRelationshipEdgeDirection()))) { + if (GraphHelper.getStatus(edge) == AtlasEntity.Status.DELETED) { + LOG.debug("collectDependents(): skip — soft-deleted edge (label={}, relationshipType={}, relationshipGuid={})", + relAttr.getRelationshipEdgeLabel(), + AtlasGraphUtilsV2.getTypeName(edge), + GraphHelper.getRelationshipGuid(edge)); + continue; + } + AtlasVertex dependentVertex = getOtherVertex(edge, sourceVertex); if (dependentVertex == null) { continue; } + if (GraphHelper.getStatus(dependentVertex) == AtlasEntity.Status.DELETED) { + LOG.debug("collectDependents(): skip — soft-deleted dependent vertex (guid={})", AtlasGraphUtilsV2.getIdFromVertex(dependentVertex)); + continue; + } + String dependentGuid = AtlasGraphUtilsV2.getIdFromVertex(dependentVertex); if (StringUtils.isBlank(dependentGuid) || !visitedGuids.add(dependentGuid)) { @@ -220,14 +233,9 @@ private void collectDependents(AtlasVertex sourceVertex, AtlasEntityType sourceE if (CollectionUtils.isNotEmpty(dependentEntityType.getRenamePropagationTargets())) { if (CollectionUtils.isNotEmpty(propagationTarget.getPropagateAttributes())) { - // This dependent becomes the next root; use the name segment we just patched (or the prior newName). - String nameForChildPropagation = parseUniqueAttribute(newQualifiedName, uniqueAttributeAutoComputeFormat).get(qualifiedNameOverrideKey); - - if (StringUtils.isBlank(nameForChildPropagation)) { - nameForChildPropagation = newName; - } - - collectDependents(dependentVertex, dependentEntityType, dependentTypeName, nameForChildPropagation, visitedGuids, dependentUpdatesByGuid); + // This dependent becomes the next root; recurse with the propagated name so + // further dependents are updated relative to this entity's new name. + collectDependents(dependentVertex, dependentEntityType, dependentTypeName, newName, visitedGuids, dependentUpdatesByGuid); } else { // Same renamed type and name propagate to further dependents. collectDependents(dependentVertex, dependentEntityType, renamedTypeName, newName, visitedGuids, dependentUpdatesByGuid); @@ -297,9 +305,9 @@ private static String findQualifiedNameOverrideKey(List> pro /** * Recomputes a dependent entity's qualifiedName after a rename using a 3-step in-memory approach: *
    - *
  1. Parse — split the stored qualifiedName using the unique attribute's {@code autoComputeFormat}.
  2. - *
  3. Override — replace the path segment that changed (the renamed entity's name key).
  4. - *
  5. Rebuild — stitch the qualifiedName back together from the updated segments.
  6. + *
  7. {@code Parse}: split the stored qualifiedName using the unique attribute's {@code autoComputeFormat}.
  8. + *
  9. {@code Override}: replace the path segment that changed (the renamed entity's name key).
  10. + *
  11. {@code Rebuild}: stitch the qualifiedName back together from the updated segments.
  12. *
* No extra graph reads are required; all values come from the existing qualifiedName string. */ diff --git a/test-tools/src/main/resources/solr/core-template/solrconfig.xml b/test-tools/src/main/resources/solr/core-template/solrconfig.xml index e39ca9e81c..71b26edb19 100644 --- a/test-tools/src/main/resources/solr/core-template/solrconfig.xml +++ b/test-tools/src/main/resources/solr/core-template/solrconfig.xml @@ -445,7 +445,7 @@ --> edismax - 35x_t 5j9_t 7wl_t a9x_t but_t dfp_l f0l_t i6d_l iyt_l jr9_t kjp_s lc5_t m4l_s mx1_t ohx_t xz9_i 1151_t 12px_t 14at_l 15vp_t 1891_t 19tx_t 1bet_t 1czp_t 1ekl_t 1gxx_t 1iit_l 1k3p_t 1lol_t 1o1x_t 1qf9_t 1ssl_t 1v5x_t 1wqt_t 1z45_t 20p1_t 4wzp_t 59mt_s 581x_s 55ol_s 543p_s 579h_t 5f5x_t 5b7p_t 5csl_l 5dl1_t 5jwl_t 5ibp_t 5lhh_t 5slh_t 5y4l_t 62v9_t 60hx_t 61ad_t 5zph_t 696t_t 68ed_t 6gat_l 6hvp_l 69z9_t 6fid_t 6qkl_l 6s5h_l 6ozp_t 6net_t 6tqd_t 6jgl_t 6k91_t 6ltx_t 7405_t 74sl_l 70ud_t 6xol_l 6z9h_l 6uit_t 72f9_t 7e9x_t 7f2d_t 7gn9_t 76dh_t 7i85_t 7dhh_t 7fut_t 7hfp_t 7bwl_t a879_t a0at_t a9s5_t a5tx_t a8zp_t a7et_t aj9h_t abd1_t almt_t agw5_t aih1_t akud_t av45_t an7p_t asqt_t aubp_t f8cl_t f18l_t f5z9_t f7k5_t f4ed_t eznp_l f2th_t f56t_t f0g5_l f9xh_l feo5_t ffgl_t f951_l fapx_t fbid_i fdvp_t fimd_t fhtx_t fjet_t fkzp_i foxx_t fqit_t fuh1_t fv9h_i fsw5_t fwud_t fw1x_t g1l1_t g3yd_t fz7p_t g2dh_t g0sl_t fxmt_l g35x_t fyf9_l hssl_l hpmt_t hr7p_l hwqt_l hxj9_t i0p1_i i32d_t hz45_t j5s5_t jhmt_l jj7p_i jc3p_t jk05_i jksl_i jll1_f jrwl_d jr45_l kqo5_t kw79_l kzd1_t l2it_l l43p_t l0xx_t l5ol_l l79h_t + 35x_t 5j9_t 7wl_t a9x_t but_t dfp_l f0l_t i6d_l iyt_l jr9_t kjp_s lc5_t m4l_s mx1_t ohx_t xz9_i 1151_t 12px_t 14at_l 15vp_t 1891_t 19tx_t 1bet_t 1czp_t 1ekl_t 1gxx_t 1iit_l 1k3p_t 1lol_t 1o1x_t 1qf9_t 1ssl_t 1v5x_t 1wqt_t 1z45_t 20p1_t 4xs5_t 5af9_s 58ud_s 56h1_s 54w5_s 581x_t 5fyd_t 5c05_t 5dl1_l 5edh_t 5kp1_t 5j45_t 5m9x_t 5tdx_t 5yx1_t 63np_t 61ad_t 622t_t 60hx_t 69z9_t 696t_t 6h39_l 6io5_l 6arp_t 6gat_t 6rd1_l 6sxx_l 6ps5_t 6o79_t 6uit_t 6k91_t 6l1h_t 6mmd_t 74sl_t 75l1_l 71mt_t 6yh1_l 701x_l 6vb9_t 737p_t 7f2d_t 7fut_t 7hfp_t 775x_t 7j0l_t 7e9x_t 7gn9_t 7i85_t 7cp1_t a8zp_t a139_t aakl_t a6md_t a9s5_t a879_t ak1x_t ac5h_t amf9_t ahol_t aj9h_t almt_t avwl_t ao05_t atj9_t av45_t f951_t f211_t f6rp_t f8cl_t f56t_t f0g5_l f3lx_t f5z9_t f18l_l fapx_l ffgl_t fg91_t f9xh_l fbid_t fcat_i feo5_t fjet_t fimd_t fk79_t fls5_i fpqd_t frb9_t fv9h_t fw1x_i ftol_t fxmt_t fwud_t g2dh_t g4qt_t g005_t g35x_t g1l1_t fyf9_l g3yd_t fz7p_l htl1_l hqf9_t hs05_l hxj9_l hybp_t i1hh_i i3ut_t hzwl_t j6kl_t jif9_l jk05_i jcw5_t jksl_i jll1_i jmdh_f jsp1_d jrwl_l krgl_t kwzp_l l05h_t l3b9_l l4w5_t l1qd_t l6h1_l l81x_t * true true From d3ba5094cdd8e3ef1f79cf462f8d5b9d6270b1a2 Mon Sep 17 00:00:00 2001 From: "sanket.bhor" Date: Tue, 12 May 2026 11:20:47 +0530 Subject: [PATCH 5/6] ATLAS-5279: changes to resolve review comments --- docs/src/documents/RenamePropagation.md | 113 +++++++++++++++--- .../apache/atlas/type/AtlasEntityType.java | 2 +- .../atlas/type/RenamePropagationTarget.java | 23 ++-- .../AtlasTypeDefStoreInitializer.java | 14 +-- .../AtlasTypeDefStoreInitializerTest.java | 66 +++++----- 5 files changed, 146 insertions(+), 72 deletions(-) diff --git a/docs/src/documents/RenamePropagation.md b/docs/src/documents/RenamePropagation.md index 0fb47e0c5a..9946b42e87 100644 --- a/docs/src/documents/RenamePropagation.md +++ b/docs/src/documents/RenamePropagation.md @@ -1,13 +1,14 @@ --- ## name: Rename Propagation + route: /RenamePropagation menu: Documentation submenu: Features # Rename propagation -**Rename propagation** is an Atlas feature for **partial entity updates**: when a renamed instance’s `**name`** (or a mapped source attribute) changes, the server can find **related entities** whose **unique attribute** (for most types, `**qualifiedName`**) still embeds the old segment, recompute that unique attribute from the type’s `**autoComputeFormat**`, and **persist those updates in the same transaction** as the triggering entity. Discovery is driven by **typedefs** (relationship ends and templates), not by hard-coded type names in application code. +**Rename propagation** is an Atlas feature for **partial entity updates**: when a renamed instance’s `**name`** (or a mapped source attribute) changes, the server can find **related entities** whose **unique attribute** (for most types, `**qualifiedName`**) still embeds the old segment, recompute that unique attribute from the type’s `**autoComputeFormat`**, and **persist those updates in the same transaction** as the triggering entity. Discovery is driven by **typedefs** (relationship ends and templates), not by hard-coded type names in application code. --- @@ -22,14 +23,14 @@ The unique attribute (qualified name) is often a **composite string** built from Assume a Hive model where: - `**hive_db`** has unique attribute (qualified name) shaped like `{name}@{clusterName}` (for example `sales@cm`). -- `**hive_table**` uses `{db.name}.{name}@{clusterName}` (for example `sales.orders@cm`). +- `**hive_table`** uses `{db.name}.{name}@{clusterName}` (for example `sales.orders@cm`). - `**hive_column**` uses `{table.db.name}.{table.name}.{name}@{clusterName}`. The `**hive_table_db**` relationship links each table to its database; `**hive_table_columns**` links columns to a table. Relationship ends that opt in with `**propagateRename**` tell Atlas: when the **trigger** side is renamed, walk to neighbors and refresh their unique attribute (qualified name) where the template depends on that rename. -1. An integration renames the database `**sales`** → `**sales_archive**` (partial update; `**name**` and thus the unique attribute on the `hive_db` vertex change). +1. An integration renames the database `**sales`** → `**sales_archive`** (partial update; `**name**` and thus the unique attribute on the `hive_db` vertex change). 2. Atlas detects that the entity’s **unique attribute (qualified name)** changed and that `**hive_db`** has **rename propagation targets** from typedef resolution. -3. `**EntityRenameHandler`** follows the configured relationship from the database vertex to related `**hive_table**` vertices, recomputes each table’s string (for example `sales.orders@cm` → `sales_archive.orders@cm`), and registers those rows on the mutation context. +3. `**EntityRenameHandler`** follows the configured relationship from the database vertex to related `**hive_table`** vertices, recomputes each table’s string (for example `sales.orders@cm` → `sales_archive.orders@cm`), and registers those rows on the mutation context. 4. If `**hive_table**` also declares propagation targets, the same process continues to `**hive_column**` rows (for example `sales.orders.col1@cm` → `sales_archive.orders.col1@cm`). All of this happens **without** requiring the hook to send every table and column in one batch. @@ -42,16 +43,16 @@ All of this happens **without** requiring the hook to send every table and colum While the type system resolves references, Atlas builds **per-entity-type** metadata used later at runtime: -- `**propagateRename`** on a `**AtlasRelationshipEndDef**` marks which end acts as the **rename source** for that relationship so instances reached through that edge can be updated when the source side’s name changes. -- `**attributeDefOverrides`** on `**AtlasEntityDef**` can set `**autoComputeFormat**` for the **unique attribute** (qualified name), describing how the string is composed from named segments. -- For paths that do not use explicit `**propagateAttributes`** maps on the relationship end, `**AtlasEntityType**` also maintains `**autoComputeFormatPathByRefTypeNameMap**`: referenced type name → **dotted path** in the template for the segment to replace when that referenced type was renamed. -- Optional `**propagateAttributes`** on a relationship end lists `**source**` / `**target**` attribute pairs so values such as `**name**` can be written to the right stub fields on dependents when models need more than template substitution alone. +- `**propagateRename`** on a `**AtlasRelationshipEndDef`** marks which end acts as the **rename source** for that relationship so instances reached through that edge can be updated when the source side’s name changes. +- `**attributeDefOverrides`** on `**AtlasEntityDef`** can set `**autoComputeFormat**` for the **unique attribute** (qualified name), describing how the string is composed from named segments. +- For paths that do not use explicit `**propagateAttributes`** maps on the relationship end, `**AtlasEntityType`** also maintains `**autoComputeFormatPathByRefTypeNameMap**`: referenced type name → **dotted path** in the template for the segment to replace when that referenced type was renamed. +- Optional `**propagateAttributes`** on a relationship end lists `**source`** / `**target**` attribute pairs so values such as `**name**` can be written to the right stub fields on dependents when models need more than template substitution alone. Together, these produce a list of `**RenamePropagationTarget**` entries on each `**AtlasEntityType**`. ### 2. Entity update (runtime) -On **partial update**, `**AtlasEntityStoreV2`** compares the stored **unique attribute (qualified name)** on the graph vertex to the incoming value. If it changed and the type has **rename propagation targets**, `**EntityRenameHandler`** walks the graph along those relationships, recomputes each dependent’s unique attribute from its effective `**autoComputeFormat**`, and adds minimal **dependent stubs** (guid, new unique attribute, optional mapped attributes) to the **same** mutation context so they are saved with the root change. +On **partial update**, `**AtlasEntityStoreV2`** compares the stored **unique attribute (qualified name)** on the graph vertex to the incoming value. If it changed and the type has **rename propagation targets**, `**EntityRenameHandler`** walks the graph along those relationships, recomputes each dependent’s unique attribute from its effective `**autoComputeFormat`**, and adds minimal **dependent stubs** (guid, new unique attribute, optional mapped attributes) to the **same** mutation context so they are saved with the root change. --- @@ -60,11 +61,11 @@ On **partial update**, `**AtlasEntityStoreV2`** compares the stored **unique att | Concept | Where it lives | Role | | ------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `**attributeDefOverrides`** | `AtlasEntityDef` | Supplies per-type overrides such as `**autoComputeFormat**` for the unique attribute (qualified name); merged during hierarchy resolution and stored on the entity type vertex when supported. | +| `**attributeDefOverrides`** | `AtlasEntityDef` | Supplies per-type overrides such as `**autoComputeFormat`** for the unique attribute (qualified name); merged during hierarchy resolution and stored on the entity type vertex when supported. | | `**propagateRename**` | `AtlasRelationshipEndDef` | Marks the end whose entity type is the **trigger** for propagation across that relationship. | | `**propagateAttributes`** | `AtlasRelationshipEndDef` | Optional maps from a **source** attribute on the trigger side to **target** attribute names on the dependent entity. | -| `**RenamePropagationTarget`** | `AtlasEntityType` | Precomputed link: relationship attribute, category, and propagate-attribute list. | -| `**autoComputeFormatPathByRefTypeNameMap**` | `AtlasEntityType` | Referenced entity type name → dotted path in the dependent’s **autoComputeFormat** for the segment to rewrite when that type was renamed (when `**propagateAttributes`** is not used for that path). | +| `**RenamePropagationTarget**` | `AtlasEntityType` | Precomputed link: target type name, relationship attribute for edge traversal, and propagate-attribute list. | +| `**autoComputeFormatPathByRefTypeNameMap`** | `AtlasEntityType` | Referenced entity type name → dotted path in the dependent’s **autoComputeFormat** for the segment to rewrite when that type was renamed (when `**propagateAttributes`** is not used for that path). | --- @@ -74,7 +75,7 @@ On **partial update**, `**AtlasEntityStoreV2`** compares the stored **unique att | Component | Role | | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `**AtlasEntityType**` | Registers rename propagation targets and builds `**autoComputeFormatPathByRefTypeNameMap**` during reference resolution. | +| `**AtlasEntityType`** | Registers rename propagation targets and builds `**autoComputeFormatPathByRefTypeNameMap**` during reference resolution. | | `**EntityRenameHandler**` | Traverses edges, parses/rebuilds the unique attribute string, registers `**DependentUpdate**` entries on the mutation context. | | `**AtlasEntityStoreV2**` | Detects a changed unique attribute (qualified name) on partial update and invokes the handler. | | `**AtlasEntityDefStoreV2` / `AtlasTypeDefGraphStoreV2**` | Persist and read `**attributeDefOverrides**` via the type vertex property keyed by `**TYPE_ATTR_DEF_OVERRIDES_PROPERTY_KEY**`. | @@ -86,8 +87,90 @@ On **partial update**, `**AtlasEntityStoreV2`** compares the stored **unique att ## Models and patches -- **Bootstrap model JSON** — Declare `**attributeDefOverrides`** for the unique attribute (qualified name) and `**propagateRename**` / `**propagateAttributes**` on relationship ends as required by your connectors (for example Hive, Trino). -- **Patch files** — Ship one-off typedef updates under `**addons/.../patches/`** so each patch runs once and is tracked (for example `**AtlasPatchRegistry**`), avoiding reliance on version-only bumps on every restart. +- **Bootstrap model JSON** — Declare `**attributeDefOverrides`** for the unique attribute (qualified name) and `**propagateRename`** / `**propagateAttributes**` on relationship ends as required by your connectors (for example Hive, Trino). +- **Patch files** — Ship one-off typedef updates under `**addons/.../patches/`** so each patch runs once and is tracked (for example `**AtlasPatchRegistry`**), avoiding reliance on version-only bumps on every restart. + +--- + +## Configuring `autoComputeFormat` on an entity type + +Rename propagation recomputes a dependent’s **unique attribute** (usually `**qualifiedName**`) by **parsing** the stored string against the type’s `**autoComputeFormat**`, **replacing** the segment that came from the renamed entity, and **rebuilding** the string. You supply that template on the **entity typedef** as an override on the unique attribute, for **any** entity type whose composite id should stay aligned after upstream renames. + +### Where to declare it + +In **bootstrap model JSON** add an `**attributeDefOverrides`** array on the `**entityDefs**` entry. Each override targets an attribute by name; set `**autoComputeFormat**` on the **unique** attribute (conventionally `**qualifiedName`**): + +```json +{ + "name": "my_column", + "superTypes": ["DataSet"], + "attributeDefOverrides": [ + { + "name": "qualifiedName", + "autoComputeFormat": "{table.db.name}.{table.name}.{name}@{clusterName}" + } + ], + "attributeDefs": [ ] +} +``` + +Atlas merges overrides with inherited definitions so the resolved `**AtlasEntityDef**` carries `**autoComputeFormat**` on `**qualifiedName**`. + +### Placeholder syntax + +- Use **curly braces** for each slot: `{segmentKey}`. +- **Literals** between placeholders (for example `.`, `/`, `@`) are **delimiters**. When parsing an existing value, the character **immediately after** each `}` in the format marks where that slot ends in the stored string (the **last** placeholder consumes the rest of the string). Pick delimiters that do not appear inside real slot values, or parsing becomes ambiguous. +- **Dots inside a key** are part of the key string only (they are not JSON nesting in the model file). + +### How to design the format for a new entity type + +Start from **one real qualified-name string** your connector already produces (for example `sales.orders@prod`). Notice how it is built from smaller parts and which **fixed characters** separate those parts (here: `.`, `.`, and `@`). + +1. Write the **same separators in the same order** in `**autoComputeFormat**`. +2. Replace each **variable part** with `{...}`: + - **`{name}`** — this entity’s own **name** (always a single word inside the braces). + - **`{db.name}`**, **`{table.db.name}`**, and similar — each segment **before** the final `.name` is one **relationship hop** (the relationship attribute name on the typedef at that step). Together they lead to a related entity; the trailing `.name` means “use that entity’s **name** attribute.” + - **`{clusterName}`** (one word inside the braces) — a normal attribute on **this** same entity, not a relationship hop. + +Dots **inside** `{...}` are part of the placeholder key, not nested JSON in the model file. Choose separators that will not appear inside a single part of the real string, so parsing stays unambiguous. + +### Example: table under a database (Hive-style shape) + +If `**my_table**` links to a database via relationship attribute `**db**` and carries a cluster label `**clusterName**`, a common pattern is: + +```json +"attributeDefOverrides": [ + { + "name": "qualifiedName", + "autoComputeFormat": "{db.name}.{name}@{clusterName}" + } +] +``` + +- `{db.name}` — database’s `**name**` reached through `**db**`. +- `{name}` — this table’s `**name**`. +- `@{clusterName}` — literal `@` then this entity’s `**clusterName**` attribute. + +### Example: deeper chain (Trino column) + +For `**trino_column**`, the unique string chains catalog, schema, table, column, and instance: + +```json +"attributeDefOverrides": [ + { + "name": "qualifiedName", + "autoComputeFormat": "{table.trinoschema.catalog.name}.{table.trinoschema.name}.{table.name}.{name}@{table.trinoschema.catalog.instance.name}" + } +] +``` + +Meaning of each slot: + +- `{table.trinoschema.catalog.name}` — from this column, follow `**table**` → `**trinoschema**` → `**catalog**`, then use that catalog entity’s `**name**`. +- `{table.trinoschema.name}` — schema `**name**` along the same branch. +- `{table.name}` — table `**name**`. +- `{name}` — this column’s own `**name**`. +- `@{table.trinoschema.catalog.instance.name}` — after `@`, follow `**instance**` from the catalog and use that instance’s `**name**`. --- diff --git a/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java b/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java index 2ca1f91a64..9309d1c254 100644 --- a/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java +++ b/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java @@ -1096,7 +1096,7 @@ private void addRenamePropagationTargetIfTriggered(AtlasRelationshipType relatio : endDef2.getPropagateAttributes(); RelationshipCategory category = relationshipType.getRelationshipDef().getRelationshipCategory(); - RenamePropagationTarget target = new RenamePropagationTarget(targetTypeName, category, relationshipAttribute, propagateAttributes != null ? propagateAttributes : Collections.emptyList()); + RenamePropagationTarget target = new RenamePropagationTarget(targetTypeName, relationshipAttribute, propagateAttributes != null ? propagateAttributes : Collections.emptyList()); String relAttrName = relationshipAttribute.getAttributeDef() != null ? relationshipAttribute.getAttributeDef().getName() diff --git a/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java b/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java index 913d8a1ff1..446455aa43 100644 --- a/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java +++ b/intg/src/main/java/org/apache/atlas/type/RenamePropagationTarget.java @@ -17,8 +17,6 @@ */ package org.apache.atlas.type; -import org.apache.atlas.model.typedef.AtlasRelationshipDef.RelationshipCategory; - import java.util.Collections; import java.util.List; import java.util.Map; @@ -29,26 +27,20 @@ */ public class RenamePropagationTarget { private final String targetTypeName; - private final RelationshipCategory category; private final AtlasStructType.AtlasAttribute relAttr; private final List> propagateAttributes; - public RenamePropagationTarget(String targetTypeName, RelationshipCategory category, - AtlasStructType.AtlasAttribute relAttr, List> propagateAttributes) { - this.targetTypeName = targetTypeName; - this.category = category; - this.relAttr = relAttr; - this.propagateAttributes = propagateAttributes != null ? Collections.unmodifiableList(propagateAttributes) : Collections.emptyList(); + public RenamePropagationTarget(String targetTypeName, AtlasStructType.AtlasAttribute relAttr, + List> propagateAttributes) { + this.targetTypeName = targetTypeName; + this.relAttr = relAttr; + this.propagateAttributes = propagateAttributes != null ? Collections.unmodifiableList(propagateAttributes) : Collections.emptyList(); } public String getTargetTypeName() { return targetTypeName; } - public RelationshipCategory getCategory() { - return category; - } - public AtlasStructType.AtlasAttribute getRelAttr() { return relAttr; } @@ -58,7 +50,7 @@ public List> getPropagateAttributes() { } /** - * Same target type, relationship category, and relationship attribute instance as wired on this type. + * Same target type and relationship attribute instance as wired on this type. * {@link #propagateAttributes} is not part of equality — duplicates are detected before add. */ @Override @@ -71,12 +63,11 @@ public boolean equals(Object o) { } RenamePropagationTarget that = (RenamePropagationTarget) o; return Objects.equals(targetTypeName, that.targetTypeName) - && category == that.category && relAttr == that.relAttr; } @Override public int hashCode() { - return Objects.hash(targetTypeName, category, System.identityHashCode(relAttr)); + return Objects.hash(targetTypeName, System.identityHashCode(relAttr)); } } diff --git a/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java b/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java index b16fb0068c..91dff99d14 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializer.java @@ -462,7 +462,7 @@ private void applyTypePatches(String typesDirName, AtlasPatchRegistry patchRegis new RemoveLegacyRefAttributesPatchHandler(typeDefStore, typeRegistry), new UpdateTypeDefOptionsPatchHandler(typeDefStore, typeRegistry), new SetServiceTypePatchHandler(typeDefStore, typeRegistry), - new SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry), + new AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry), new UpdateAttributeMetadataHandler(typeDefStore, typeRegistry, graph), new AddSuperTypePatchHandler(typeDefStore, typeRegistry), new AddMandatoryAttributePatchHandler(typeDefStore, typeRegistry) @@ -1181,15 +1181,15 @@ public PatchStatus applyPatch(TypeDefPatch patch) throws AtlasBaseException { } /** - * Applies {@code SET_ATTRIBUTE_DEF_OVERRIDES} on entity typedefs and {@code SET_PROPAGATE_RENAME} on relationship typedefs. + * Handles {@code SET_ATTRIBUTE_DEF_OVERRIDES} on entity typedefs and {@code SET_PROPAGATE_RENAME} on relationship typedef end defs. */ - static class SetAttributeDefOverridesPatchHandler extends PatchHandler { + static class AttributeDefOverridesAndPropagateRenamePatchHandler extends PatchHandler { private static final String ACTION_SET_ATTRIBUTE_DEF_OVERRIDES = "SET_ATTRIBUTE_DEF_OVERRIDES"; private static final String ACTION_SET_PROPAGATE_RENAME = "SET_PROPAGATE_RENAME"; /** Patch JSON key under {@code params}: value is {@code "endDef1"} or {@code "endDef2"} (string, not numeric). */ private static final String PARAM_END_DEF_TOKEN = "endDefToken"; - public SetAttributeDefOverridesPatchHandler(AtlasTypeDefStore typeDefStore, AtlasTypeRegistry typeRegistry) { + public AttributeDefOverridesAndPropagateRenamePatchHandler(AtlasTypeDefStore typeDefStore, AtlasTypeRegistry typeRegistry) { super(typeDefStore, typeRegistry, new String[] {ACTION_SET_ATTRIBUTE_DEF_OVERRIDES, ACTION_SET_PROPAGATE_RENAME}); } @@ -1209,7 +1209,7 @@ public PatchStatus applyPatch(TypeDefPatch patch) throws AtlasBaseException { return SKIPPED; } - LOG.debug("SetAttributeDefOverridesPatchHandler.applyPatch(): id={}; action={}; typeName={}", + LOG.debug("AttributeDefOverridesAndPropagateRenamePatchHandler.applyPatch(): id={}; action={}; typeName={}", patch.getId(), patch.getAction(), typeName); String action = patch.getAction(); @@ -1241,7 +1241,7 @@ private PatchStatus applyEntityDefOverrides(TypeDefPatch patch, AtlasEntityDef t int overrideCount = CollectionUtils.isEmpty(patch.getAttributeDefs()) ? 0 : patch.getAttributeDefs().size(); - LOG.debug("SetAttributeDefOverridesPatchHandler.applyEntityDefOverrides(): entityType={}; overrideCount={}; updateToVersion={}", + LOG.debug("AttributeDefOverridesAndPropagateRenamePatchHandler.applyEntityDefOverrides(): entityType={}; overrideCount={}; updateToVersion={}", typeDef.getName(), overrideCount, patch.getUpdateToVersion()); typeDefStore.updateEntityDefByName(typeDef.getName(), updatedDef); @@ -1282,7 +1282,7 @@ private PatchStatus applyPropagateRename(TypeDefPatch patch, AtlasRelationshipDe updatedDef.setTypeVersion(patch.getUpdateToVersion()); - LOG.debug("SetAttributeDefOverridesPatchHandler.applyPropagateRename(): relationshipType={}; endDefToken={}; updateToVersion={}", + LOG.debug("AttributeDefOverridesAndPropagateRenamePatchHandler.applyPropagateRename(): relationshipType={}; endDefToken={}; updateToVersion={}", typeDef.getName(), endDefToken, patch.getUpdateToVersion()); typeDefStore.updateRelationshipDefByName(typeDef.getName(), updatedDef); diff --git a/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java b/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java index 84ef8f6031..9a2ee1639e 100644 --- a/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java +++ b/repository/src/test/java/org/apache/atlas/repository/store/bootstrap/AtlasTypeDefStoreInitializerTest.java @@ -928,9 +928,9 @@ public void testSetServiceTypePatchHandlerComplete() throws Exception { } @Test - public void testSetAttributeDefOverridesPatchHandlerSupportedActions() { - AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = - new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndPropagateRenamePatchHandlerSupportedActions() { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); String[] actions = handler.getSupportedActions(); assertEquals(actions.length, 2); @@ -939,9 +939,9 @@ public void testSetAttributeDefOverridesPatchHandlerSupportedActions() { } @Test - public void testSetAttributeDefOverridesPatchHandlerApplyOverrides() throws Exception { - AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = - new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndPropagateRenamePatchHandlerApplyOverrides() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestEntity", "1.0", "2.0"); @@ -958,9 +958,9 @@ public void testSetAttributeDefOverridesPatchHandlerApplyOverrides() throws Exce } @Test - public void testSetAttributeDefOverridesPatchHandlerApplyOverridesSkipped() throws Exception { - AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = - new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndPropagateRenamePatchHandlerApplyOverridesSkipped() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestEntity", "2.0", "3.0"); @@ -976,9 +976,9 @@ public void testSetAttributeDefOverridesPatchHandlerApplyOverridesSkipped() thro } @Test - public void testSetAttributeDefOverridesPatchHandlerApplyOverridesWrongType() throws Exception { - AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = - new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndPropagateRenamePatchHandlerApplyOverridesWrongType() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestRelationship", "1.0", "2.0"); @@ -996,9 +996,9 @@ public void testSetAttributeDefOverridesPatchHandlerApplyOverridesWrongType() th } @Test - public void testSetAttributeDefOverridesPatchHandlerPropagateRenameEnd1() throws Exception { - AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = - new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenameEnd1() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); @@ -1019,9 +1019,9 @@ public void testSetAttributeDefOverridesPatchHandlerPropagateRenameEnd1() throws } @Test - public void testSetAttributeDefOverridesPatchHandlerPropagateRenameEnd2() throws Exception { - AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = - new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenameEnd2() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); @@ -1042,9 +1042,9 @@ public void testSetAttributeDefOverridesPatchHandlerPropagateRenameEnd2() throws } @Test - public void testSetAttributeDefOverridesPatchHandlerPropagateRenameInvalidEndDef() throws Exception { - AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = - new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenameInvalidEndDef() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); @@ -1063,9 +1063,9 @@ public void testSetAttributeDefOverridesPatchHandlerPropagateRenameInvalidEndDef } @Test - public void testSetAttributeDefOverridesPatchHandlerPropagateRenameMissingParam() throws Exception { - AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = - new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenameMissingParam() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); @@ -1082,9 +1082,9 @@ public void testSetAttributeDefOverridesPatchHandlerPropagateRenameMissingParam( } @Test - public void testSetAttributeDefOverridesPatchHandlerPropagateRenameWrongType() throws Exception { - AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = - new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenameWrongType() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestEntity", "1.0", "2.0"); @@ -1099,9 +1099,9 @@ public void testSetAttributeDefOverridesPatchHandlerPropagateRenameWrongType() t } @Test - public void testSetAttributeDefOverridesPatchHandlerUnknownType() throws Exception { - AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = - new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndPropagateRenamePatchHandlerUnknownType() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "UnknownType", "1.0", "2.0"); when(typeRegistry.getTypeDefByName("UnknownType")).thenReturn(null); @@ -1110,9 +1110,9 @@ public void testSetAttributeDefOverridesPatchHandlerUnknownType() throws Excepti } @Test - public void testSetAttributeDefOverridesPatchHandlerInvalidAction() throws Exception { - AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler handler = - new AtlasTypeDefStoreInitializer.SetAttributeDefOverridesPatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndPropagateRenamePatchHandlerInvalidAction() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestEntity", "1.0", "2.0"); setField(patch, "action", "NOT_A_SUPPORTED_PATCH_ACTION"); From 0ca96d6fbb080c5c65c09abab714754526633f6c Mon Sep 17 00:00:00 2001 From: "sanket.bhor" Date: Wed, 13 May 2026 09:30:29 +0530 Subject: [PATCH 6/6] ATLAS-5279: changes to add attributeDefOverrides for trino column --- addons/models/6000-Trino/6000-trino_model.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/models/6000-Trino/6000-trino_model.json b/addons/models/6000-Trino/6000-trino_model.json index d3bb3fc52a..3a7a5c8bea 100644 --- a/addons/models/6000-Trino/6000-trino_model.json +++ b/addons/models/6000-Trino/6000-trino_model.json @@ -25,12 +25,6 @@ "superTypes": [ "Asset" ], "serviceType": "trino", "typeVersion": "1.0", - "attributeDefOverrides": [ - { - "name": "qualifiedName", - "autoComputeFormat": "{table.trinoschema.catalog.name}.{table.trinoschema.name}.{table.name}.{name}@{table.trinoschema.catalog.instance.name}" - } - ], "attributeDefs": [ { "name": "parameters", "typeName": "map", "cardinality": "SINGLE", "isUnique": false, "isIndexable": false, "isOptional": true } ] @@ -51,6 +45,12 @@ "superTypes": [ "DataSet" ], "serviceType": "trino", "typeVersion": "1.0", + "attributeDefOverrides": [ + { + "name": "qualifiedName", + "autoComputeFormat": "{table.trinoschema.catalog.name}.{table.trinoschema.name}.{table.name}.{name}@{table.trinoschema.catalog.instance.name}" + } + ], "attributeDefs": [ { "name": "data_type", "typeName": "string", "cardinality": "SINGLE", "isUnique": false, "isIndexable": true, "isOptional": true }, { "name": "ordinal_position", "typeName": "int", "cardinality": "SINGLE", "isUnique": false, "isIndexable": false, "isOptional": true },