From 7f492728c74322b77895303673ee2bc1cdc3cd3d Mon Sep 17 00:00:00 2001 From: "sanket.bhor" Date: Fri, 12 Jun 2026 14:41:41 +0530 Subject: [PATCH 1/2] ATLAS-5312: Add delete propagation support via propagateDelete flag on relationship endDefs --- .../models/6000-Trino/6000-trino_model.json | 6 +- .../typedef/AtlasRelationshipEndDef.java | 26 +- .../apache/atlas/type/AtlasEntityType.java | 57 +++ .../atlas/type/DeletePropagationTarget.java | 59 +++ .../typedef/TestAtlasRelationshipEndDef.java | 66 +++ .../type/TestDeletePropagationTarget.java | 123 +++++ .../AtlasTypeDefStoreInitializer.java | 61 ++- .../store/graph/v1/DeleteHandlerV1.java | 40 ++ .../v2/EntityDeletePropagationHandler.java | 132 +++++ .../AtlasTypeDefStoreInitializerTest.java | 234 +++++++-- .../EntityDeletePropagationHandlerTest.java | 477 ++++++++++++++++++ 11 files changed, 1228 insertions(+), 53 deletions(-) create mode 100644 intg/src/main/java/org/apache/atlas/type/DeletePropagationTarget.java create mode 100644 intg/src/test/java/org/apache/atlas/type/TestDeletePropagationTarget.java create mode 100644 repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityDeletePropagationHandler.java create mode 100644 repository/src/test/java/org/apache/atlas/repository/store/graph/v2/EntityDeletePropagationHandlerTest.java diff --git a/addons/models/6000-Trino/6000-trino_model.json b/addons/models/6000-Trino/6000-trino_model.json index aa254ee6ed8..a8aa885de08 100644 --- a/addons/models/6000-Trino/6000-trino_model.json +++ b/addons/models/6000-Trino/6000-trino_model.json @@ -128,7 +128,7 @@ "relationshipCategory": "AGGREGATION", "propagateTags": "NONE", "endDef1": { "type": "trino_table", "name": "trinoschema", "isContainer": false, "cardinality": "SINGLE", "isLegacyAttribute": false }, - "endDef2": { "type": "trino_schema", "name": "tables", "isContainer": true, "cardinality": "SET" } + "endDef2": { "type": "trino_schema", "name": "tables", "isContainer": true, "cardinality": "SET", "propagateDelete": true } }, { "name": "trino_table_columns", @@ -172,7 +172,7 @@ "typeVersion": "1.0", "relationshipCategory": "ASSOCIATION", "propagateTags": "BOTH", - "endDef1": { "type": "hive_db", "name": "trino_schema", "cardinality": "SET" }, + "endDef1": { "type": "hive_db", "name": "trino_schema", "cardinality": "SET", "propagateDelete": true }, "endDef2": { "type": "trino_schema", "name": "hive_db", "cardinality": "SINGLE" } }, { @@ -181,7 +181,7 @@ "typeVersion": "1.0", "relationshipCategory": "ASSOCIATION", "propagateTags": "BOTH", - "endDef1": { "type": "hive_table", "name": "trino_table", "cardinality": "SET", "propagateRename": true, "propagateAttributes": [ { "source": "name", "target": "name" } ] }, + "endDef1": { "type": "hive_table", "name": "trino_table", "cardinality": "SET", "propagateRename": true, "propagateDelete": true, "propagateAttributes": [ { "source": "name", "target": "name" } ] }, "endDef2": { "type": "trino_table", "name": "hive_table", "cardinality": "SINGLE" } }, { 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 c7921b6ca36..568b7535ff9 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 @@ -86,6 +86,15 @@ public class AtlasRelationshipEndDef implements Serializable { * entity {@code name} attribute is synchronized. */ private List> propagateAttributes; + /** + * When set, deleting an entity of this end's type cascades the delete to entities on the + * other end of the relationship. Use with caution: enabling this flag means ALL related + * entities reachable via this relationship will be deleted — the effect is recursive if + * the target type also has propagateDelete configured on its relationships. This is + * irreversible in a single transaction; ensure the modeled semantics truly require cascade + * (e.g., the target entity cannot meaningfully exist without the source). + */ + private boolean propagateDelete; /** * Base constructor @@ -177,6 +186,7 @@ public AtlasRelationshipEndDef(AtlasRelationshipEndDef other) { setDescription(other.description); setIsPropagateRename(other.propagateRename); setPropagateAttributes(other.propagateAttributes); + setIsPropagateDelete(other.propagateDelete); } } @@ -254,6 +264,7 @@ public StringBuilder toString(StringBuilder sb) { sb.append(", isLegacyAttribute==>'").append(isLegacyAttribute).append('\''); sb.append(", propagateRename==>'").append(propagateRename).append('\''); sb.append(", propagateAttributes==>'").append(propagateAttributes).append('\''); + sb.append(", propagateDelete==>'").append(propagateDelete).append('\''); sb.append('}'); return sb; @@ -261,7 +272,7 @@ public StringBuilder toString(StringBuilder sb) { @Override public int hashCode() { - return Objects.hash(type, getName(), description, isContainer, cardinality, isLegacyAttribute, propagateRename, propagateAttributes); + return Objects.hash(type, getName(), description, isContainer, cardinality, isLegacyAttribute, propagateRename, propagateAttributes, propagateDelete); } @Override @@ -281,7 +292,8 @@ public boolean equals(Object o) { cardinality == that.cardinality && isLegacyAttribute == that.isLegacyAttribute && propagateRename == that.propagateRename && - Objects.equals(propagateAttributes, that.propagateAttributes); + Objects.equals(propagateAttributes, that.propagateAttributes) && + propagateDelete == that.propagateDelete; } @JsonProperty("propagateRename") @@ -302,6 +314,16 @@ public void setPropagateAttributes(List> propagateAttributes this.propagateAttributes = (propagateAttributes != null) ? Collections.unmodifiableList(propagateAttributes) : null; } + @JsonProperty("propagateDelete") + public boolean getIsPropagateDelete() { + return propagateDelete; + } + + @JsonProperty("propagateDelete") + public void setIsPropagateDelete(boolean propagateDelete) { + this.propagateDelete = propagateDelete; + } + @Override public String toString() { return toString(new StringBuilder()).toString(); 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 9309d1c254e..922aada3d48 100644 --- a/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java +++ b/intg/src/main/java/org/apache/atlas/type/AtlasEntityType.java @@ -116,6 +116,7 @@ public class AtlasEntityType extends AtlasStructType { * are resolved from the model. */ private List renamePropagationTargets = Collections.emptyList(); + private List deletePropagationTargets = Collections.emptyList(); /** * 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 @@ -267,6 +268,10 @@ public List getRenamePropagationTargets() { return renamePropagationTargets; } + public List getDeletePropagationTargets() { + return deletePropagationTargets; + } + public Map getAutoComputeFormatPathByRefTypeNameMap() { return autoComputeFormatPathByRefTypeNameMap; } @@ -403,6 +408,7 @@ void resolveReferencesPhase3(AtlasTypeRegistry typeRegistry) throws AtlasBaseExc tagPropagationEdges.addAll(superType.tagPropagationEdges); renamePropagationTargets.addAll(superType.renamePropagationTargets); + deletePropagationTargets.addAll(superType.deletePropagationTargets); } ownedRefAttributes = new ArrayList<>(); @@ -430,6 +436,7 @@ void resolveReferencesPhase3(AtlasTypeRegistry typeRegistry) throws AtlasBaseExc ownedRefAttributes = Collections.unmodifiableList(ownedRefAttributes); tagPropagationEdges = Collections.unmodifiableSet(tagPropagationEdges); renamePropagationTargets = Collections.unmodifiableList(renamePropagationTargets); + deletePropagationTargets = Collections.unmodifiableList(deletePropagationTargets); entityDef.setSubTypes(subTypes); @@ -580,6 +587,7 @@ void resolveReferences(AtlasTypeRegistry typeRegistry) throws AtlasBaseException 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.deletePropagationTargets = new ArrayList<>(); // this will be populated in resolveReferencesPhase2() this.autoComputeFormatPathByRefTypeNameMap = new HashMap<>(); // this will be populated in resolveReferencesPhase3() this.typeAndAllSubTypes.add(this.getTypeName()); @@ -891,6 +899,7 @@ void addRelationshipAttribute(String attributeName, AtlasAttribute attribute, At } addRenamePropagationTargetIfTriggered(relationshipType, attribute); + addDeletePropagationTargetIfTriggered(relationshipType, attribute); } /** @@ -1122,6 +1131,54 @@ private void addRenamePropagationTargetIfTriggered(AtlasRelationshipType relatio } } + private void addDeletePropagationTargetIfTriggered(AtlasRelationshipType relationshipType, AtlasAttribute relationshipAttribute) { + AtlasRelationshipEndDef endDef1 = relationshipType.getRelationshipDef().getEndDef1(); + AtlasRelationshipEndDef endDef2 = relationshipType.getRelationshipDef().getEndDef2(); + + if (endDef1 == null || endDef2 == null) { + LOG.debug("addDeletePropagationTargetIfTriggered({}): relationship type '{}' missing endDef — skipping", + getTypeName(), relationshipType.getTypeName()); + return; + } + + String thisTypeName = getTypeName(); + boolean triggerOnEnd1 = StringUtils.equals(relationshipType.getEnd1Type().getTypeName(), thisTypeName) && endDef1.getIsPropagateDelete(); + boolean triggerOnEnd2 = StringUtils.equals(relationshipType.getEnd2Type().getTypeName(), thisTypeName) && endDef2.getIsPropagateDelete(); + + if (!triggerOnEnd1 && !triggerOnEnd2) { + return; + } + + String targetTypeName = triggerOnEnd1 + ? relationshipType.getEnd2Type().getTypeName() + : relationshipType.getEnd1Type().getTypeName(); + + RelationshipCategory category = relationshipType.getRelationshipDef().getRelationshipCategory(); + DeletePropagationTarget target = new DeletePropagationTarget(targetTypeName, relationshipAttribute); + + String relAttrName = relationshipAttribute.getAttributeDef() != null + ? relationshipAttribute.getAttributeDef().getName() + : null; + + if (!deletePropagationTargets.contains(target)) { + deletePropagationTargets.add(target); + + LOG.info("addDeletePropagationTargetIfTriggered({}): delete propagation via relationship '{}' attribute '{}' -> target type '{}' (trigger {}, category {})", + thisTypeName, + relationshipType.getTypeName(), + relAttrName, + targetTypeName, + triggerOnEnd1 ? "end1" : "end2", + category); + } else { + LOG.debug("addDeletePropagationTargetIfTriggered({}): duplicate propagation target skipped — relationship '{}', attribute '{}', target type '{}'", + thisTypeName, + relationshipType.getTypeName(), + relAttrName, + targetTypeName); + } + } + boolean isAssignableFrom(AtlasObjectId objId) { return AtlasTypeUtil.isValid(objId) && (StringUtils.equals(objId.getTypeName(), getTypeName()) || isSuperTypeOf(objId.getTypeName())); } diff --git a/intg/src/main/java/org/apache/atlas/type/DeletePropagationTarget.java b/intg/src/main/java/org/apache/atlas/type/DeletePropagationTarget.java new file mode 100644 index 00000000000..66d26745011 --- /dev/null +++ b/intg/src/main/java/org/apache/atlas/type/DeletePropagationTarget.java @@ -0,0 +1,59 @@ +/** + * 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 java.util.Objects; + +/** + * Typedef-time representation of a delete propagation target for an entity type. + */ +public class DeletePropagationTarget { + private final String targetTypeName; + private final AtlasStructType.AtlasAttribute relAttr; + + public DeletePropagationTarget(String targetTypeName, AtlasStructType.AtlasAttribute relAttr) { + this.targetTypeName = targetTypeName; + this.relAttr = relAttr; + } + + public String getTargetTypeName() { + return targetTypeName; + } + + public AtlasStructType.AtlasAttribute getRelAttr() { + return relAttr; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeletePropagationTarget that = (DeletePropagationTarget) o; + return Objects.equals(targetTypeName, that.targetTypeName) + && relAttr == that.relAttr; + } + + @Override + public int hashCode() { + return Objects.hash(targetTypeName, System.identityHashCode(relAttr)); + } +} diff --git a/intg/src/test/java/org/apache/atlas/model/typedef/TestAtlasRelationshipEndDef.java b/intg/src/test/java/org/apache/atlas/model/typedef/TestAtlasRelationshipEndDef.java index 1185275019e..1eabbcf8483 100644 --- a/intg/src/test/java/org/apache/atlas/model/typedef/TestAtlasRelationshipEndDef.java +++ b/intg/src/test/java/org/apache/atlas/model/typedef/TestAtlasRelationshipEndDef.java @@ -363,4 +363,70 @@ public void testBooleanGettersSetters() { endDef.setIsLegacyAttribute(false); assertFalse(endDef.getIsLegacyAttribute()); } + + @Test + public void testPropagateDeleteDefaultFalse() { + AtlasRelationshipEndDef endDef = new AtlasRelationshipEndDef(); + + assertFalse(endDef.getIsPropagateDelete()); + } + + @Test + public void testPropagateDeleteSetterGetter() { + AtlasRelationshipEndDef endDef = new AtlasRelationshipEndDef(); + + endDef.setIsPropagateDelete(true); + assertTrue(endDef.getIsPropagateDelete()); + + endDef.setIsPropagateDelete(false); + assertFalse(endDef.getIsPropagateDelete()); + } + + @Test + public void testPropagateDeleteCopyConstructor() { + AtlasRelationshipEndDef original = new AtlasRelationshipEndDef("Type1", "attr1", Cardinality.SINGLE); + original.setIsPropagateDelete(true); + + AtlasRelationshipEndDef copy = new AtlasRelationshipEndDef(original); + + assertTrue(copy.getIsPropagateDelete()); + } + + @Test + public void testPropagateDeleteEquality() { + AtlasRelationshipEndDef endDef1 = new AtlasRelationshipEndDef("Type1", "attr1", Cardinality.SINGLE); + AtlasRelationshipEndDef endDef2 = new AtlasRelationshipEndDef("Type1", "attr1", Cardinality.SINGLE); + + assertEquals(endDef1, endDef2); + + endDef1.setIsPropagateDelete(true); + assertNotEquals(endDef1, endDef2); + + endDef2.setIsPropagateDelete(true); + assertEquals(endDef1, endDef2); + assertEquals(endDef1.hashCode(), endDef2.hashCode()); + } + + @Test + public void testPropagateDeleteSerialization() { + AtlasRelationshipEndDef original = new AtlasRelationshipEndDef("Database", "tables", Cardinality.SET, true); + original.setIsPropagateDelete(true); + + String jsonString = AtlasType.toJson(original); + assertTrue(jsonString.contains("propagateDelete")); + + AtlasRelationshipEndDef deserialized = AtlasType.fromJson(jsonString, AtlasRelationshipEndDef.class); + assertTrue(deserialized.getIsPropagateDelete()); + assertEquals(deserialized, original); + } + + @Test + public void testPropagateDeleteToString() { + AtlasRelationshipEndDef endDef = new AtlasRelationshipEndDef("Type1", "attr1", Cardinality.SINGLE); + endDef.setIsPropagateDelete(true); + + String str = endDef.toString(); + assertTrue(str.contains("propagateDelete")); + assertTrue(str.contains("true")); + } } diff --git a/intg/src/test/java/org/apache/atlas/type/TestDeletePropagationTarget.java b/intg/src/test/java/org/apache/atlas/type/TestDeletePropagationTarget.java new file mode 100644 index 00000000000..148fccb10d7 --- /dev/null +++ b/intg/src/test/java/org/apache/atlas/type/TestDeletePropagationTarget.java @@ -0,0 +1,123 @@ +/** + * 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.type.AtlasStructType.AtlasAttribute; +import org.testng.annotations.Test; + +import static org.mockito.Mockito.mock; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +public class TestDeletePropagationTarget { + + @Test + public void testConstructorAndGetters() { + AtlasAttribute relAttr = mock(AtlasAttribute.class); + + DeletePropagationTarget target = new DeletePropagationTarget("trino_table", relAttr); + + assertEquals(target.getTargetTypeName(), "trino_table"); + assertEquals(target.getRelAttr(), relAttr); + } + + @Test + public void testConstructorWithNullValues() { + DeletePropagationTarget target = new DeletePropagationTarget(null, null); + + assertNull(target.getTargetTypeName()); + assertNull(target.getRelAttr()); + } + + @Test + public void testEqualsSameInstance() { + AtlasAttribute relAttr = mock(AtlasAttribute.class); + DeletePropagationTarget target = new DeletePropagationTarget("trino_table", relAttr); + + assertTrue(target.equals(target)); + } + + @Test + public void testEqualsSameValues() { + AtlasAttribute relAttr = mock(AtlasAttribute.class); + DeletePropagationTarget target1 = new DeletePropagationTarget("trino_table", relAttr); + DeletePropagationTarget target2 = new DeletePropagationTarget("trino_table", relAttr); + + assertEquals(target1, target2); + assertEquals(target1.hashCode(), target2.hashCode()); + } + + @Test + public void testEqualsIdentityOnRelAttr() { + AtlasAttribute relAttr1 = mock(AtlasAttribute.class); + AtlasAttribute relAttr2 = mock(AtlasAttribute.class); + DeletePropagationTarget target1 = new DeletePropagationTarget("trino_table", relAttr1); + DeletePropagationTarget target2 = new DeletePropagationTarget("trino_table", relAttr2); + + assertNotEquals(target1, target2); + } + + @Test + public void testEqualsDifferentTypeName() { + AtlasAttribute relAttr = mock(AtlasAttribute.class); + DeletePropagationTarget target1 = new DeletePropagationTarget("trino_table", relAttr); + DeletePropagationTarget target2 = new DeletePropagationTarget("trino_schema", relAttr); + + assertNotEquals(target1, target2); + } + + @Test + public void testEqualsNull() { + AtlasAttribute relAttr = mock(AtlasAttribute.class); + DeletePropagationTarget target = new DeletePropagationTarget("trino_table", relAttr); + + assertFalse(target.equals(null)); + } + + @Test + public void testEqualsDifferentClass() { + AtlasAttribute relAttr = mock(AtlasAttribute.class); + DeletePropagationTarget target = new DeletePropagationTarget("trino_table", relAttr); + + assertFalse(target.equals("not a target")); + } + + @Test + public void testHashCodeConsistency() { + AtlasAttribute relAttr = mock(AtlasAttribute.class); + DeletePropagationTarget target = new DeletePropagationTarget("trino_table", relAttr); + + int hash1 = target.hashCode(); + int hash2 = target.hashCode(); + + assertEquals(hash1, hash2); + } + + @Test + public void testHashCodeDifferentForDifferentTargets() { + AtlasAttribute relAttr1 = mock(AtlasAttribute.class); + AtlasAttribute relAttr2 = mock(AtlasAttribute.class); + DeletePropagationTarget target1 = new DeletePropagationTarget("trino_table", relAttr1); + DeletePropagationTarget target2 = new DeletePropagationTarget("trino_schema", relAttr2); + + assertNotEquals(target1.hashCode(), target2.hashCode()); + } +} 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 b7483f4ba82..31e748096c3 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 @@ -465,7 +465,7 @@ private void applyTypePatches(String typesDirName, AtlasPatchRegistry patchRegis new RemoveLegacyRefAttributesPatchHandler(typeDefStore, typeRegistry), new UpdateTypeDefOptionsPatchHandler(typeDefStore, typeRegistry), new SetServiceTypePatchHandler(typeDefStore, typeRegistry), - new AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry), + new AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry), new UpdateAttributeMetadataHandler(typeDefStore, typeRegistry, graph), new AddSuperTypePatchHandler(typeDefStore, typeRegistry), new AddMandatoryAttributePatchHandler(typeDefStore, typeRegistry) @@ -1184,19 +1184,20 @@ public PatchStatus applyPatch(TypeDefPatch patch) throws AtlasBaseException { } /** - * Handles {@code SET_ATTRIBUTE_DEF_OVERRIDES} on entity typedefs and {@code SET_PROPAGATE_RENAME} on relationship typedef end defs. + * Handles {@code SET_ATTRIBUTE_DEF_OVERRIDES} on entity typedefs, {@code SET_PROPAGATE_RENAME} and {@code SET_PROPAGATE_DELETE} on relationship typedef end defs. * Optional {@code params.propagateAttributes} for {@code SET_PROPAGATE_RENAME}: a JSON array of objects, each copied to a {@code HashMap} on the patched end. */ - static class AttributeDefOverridesAndPropagateRenamePatchHandler extends PatchHandler { + static class AttributeDefOverridesAndOperationPropagationPatchHandler 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_DELETE = "SET_PROPAGATE_DELETE"; /** 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"; /** Optional: JSON array of objects under {@code params}; each object becomes one {@code Map}. */ private static final String PARAM_PROPAGATE_ATTRIBUTES = "propagateAttributes"; - public AttributeDefOverridesAndPropagateRenamePatchHandler(AtlasTypeDefStore typeDefStore, AtlasTypeRegistry typeRegistry) { - super(typeDefStore, typeRegistry, new String[] {ACTION_SET_ATTRIBUTE_DEF_OVERRIDES, ACTION_SET_PROPAGATE_RENAME}); + public AttributeDefOverridesAndOperationPropagationPatchHandler(AtlasTypeDefStore typeDefStore, AtlasTypeRegistry typeRegistry) { + super(typeDefStore, typeRegistry, new String[] {ACTION_SET_ATTRIBUTE_DEF_OVERRIDES, ACTION_SET_PROPAGATE_RENAME, ACTION_SET_PROPAGATE_DELETE}); } @Override @@ -1215,7 +1216,7 @@ public PatchStatus applyPatch(TypeDefPatch patch) throws AtlasBaseException { return SKIPPED; } - LOG.debug("AttributeDefOverridesAndPropagateRenamePatchHandler.applyPatch(): id={}; action={}; typeName={}", + LOG.debug("AttributeDefOverridesAndOperationPropagationPatchHandler.applyPatch(): id={}; action={}; typeName={}", patch.getId(), patch.getAction(), typeName); String action = patch.getAction(); @@ -1234,6 +1235,13 @@ public PatchStatus applyPatch(TypeDefPatch patch) throws AtlasBaseException { } return applyPropagateRename(patch, (AtlasRelationshipDef) typeDef); + } else if (ACTION_SET_PROPAGATE_DELETE.equals(action)) { + if (!(typeDef instanceof AtlasRelationshipDef)) { + throw new AtlasBaseException(AtlasErrorCode.PATCH_NOT_APPLICABLE_FOR_TYPE, patch.getAction(), + typeDef.getClass().getSimpleName()); + } + + return applyPropagateDelete(patch, (AtlasRelationshipDef) typeDef); } throw new AtlasBaseException(AtlasErrorCode.PATCH_INVALID_DATA, patch.getAction(), typeName); @@ -1247,7 +1255,7 @@ private PatchStatus applyEntityDefOverrides(TypeDefPatch patch, AtlasEntityDef t int overrideCount = CollectionUtils.isEmpty(patch.getAttributeDefs()) ? 0 : patch.getAttributeDefs().size(); - LOG.debug("AttributeDefOverridesAndPropagateRenamePatchHandler.applyEntityDefOverrides(): entityType={}; overrideCount={}; updateToVersion={}", + LOG.debug("AttributeDefOverridesAndOperationPropagationPatchHandler.applyEntityDefOverrides(): entityType={}; overrideCount={}; updateToVersion={}", typeDef.getName(), overrideCount, patch.getUpdateToVersion()); typeDefStore.updateEntityDefByName(typeDef.getName(), updatedDef); @@ -1290,7 +1298,7 @@ private PatchStatus applyPropagateRename(TypeDefPatch patch, AtlasRelationshipDe updatedDef.setTypeVersion(patch.getUpdateToVersion()); - LOG.debug("AttributeDefOverridesAndPropagateRenamePatchHandler.applyPropagateRename(): relationshipType={}; endDefToken={}; updateToVersion={}", + LOG.debug("AttributeDefOverridesAndOperationPropagationPatchHandler.applyPropagateRename(): relationshipType={}; endDefToken={}; updateToVersion={}", typeDef.getName(), endDefToken, patch.getUpdateToVersion()); typeDefStore.updateRelationshipDefByName(typeDef.getName(), updatedDef); @@ -1301,6 +1309,43 @@ private PatchStatus applyPropagateRename(TypeDefPatch patch, AtlasRelationshipDe return APPLIED; } + private PatchStatus applyPropagateDelete(TypeDefPatch patch, AtlasRelationshipDef typeDef) throws AtlasBaseException { + 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); + + if (endDefToken == null || (!useEndDef1 && !useEndDef2)) { + throw new AtlasBaseException(AtlasErrorCode.INVALID_PARAMETERS, + "SET_PROPAGATE_DELETE patch for '" + typeDef.getName() + + "' must set params." + PARAM_END_DEF_TOKEN + " to the string \"endDef1\" or \"endDef2\", got: " + + endDefToken); + } + + AtlasRelationshipDef updatedDef = new AtlasRelationshipDef(typeDef); + + if (useEndDef2) { + AtlasRelationshipEndDef end2 = new AtlasRelationshipEndDef(updatedDef.getEndDef2()); + + end2.setIsPropagateDelete(true); + updatedDef.setEndDef2(end2); + } else { + AtlasRelationshipEndDef end1 = new AtlasRelationshipEndDef(updatedDef.getEndDef1()); + + end1.setIsPropagateDelete(true); + updatedDef.setEndDef1(end1); + } + + updatedDef.setTypeVersion(patch.getUpdateToVersion()); + + typeDefStore.updateRelationshipDefByName(typeDef.getName(), updatedDef); + + LOG.info("patch applied: id={}; action={}; relationshipType={}; endDefToken={}; typeVersion={}", + patch.getId(), ACTION_SET_PROPAGATE_DELETE, typeDef.getName(), endDefToken, patch.getUpdateToVersion()); + + return APPLIED; + } + /** If {@code params} contains {@code propagateAttributes}, coerces via {@link AtlasJson#getMapper()} {@code convertValue} to {@code List>}. */ private static void setPropagateAttributesFromParamsIfPresent(Map params, AtlasRelationshipEndDef endDef) throws AtlasBaseException { if (MapUtils.isEmpty(params) || !params.containsKey(PARAM_PROPAGATE_ATTRIBUTES)) { diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1.java index fd7b42f9fea..43956e21838 100644 --- a/repository/src/main/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1.java +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1.java @@ -39,6 +39,7 @@ import org.apache.atlas.repository.graphdb.AtlasUniqueKeyHandler; import org.apache.atlas.repository.graphdb.AtlasVertex; import org.apache.atlas.repository.store.graph.v2.AtlasGraphUtilsV2; +import org.apache.atlas.repository.store.graph.v2.EntityDeletePropagationHandler; import org.apache.atlas.repository.store.graph.v2.EntityGraphRetriever; import org.apache.atlas.repository.store.graph.v2.tasks.ClassificationTask; import org.apache.atlas.tasks.TaskManagement; @@ -143,6 +144,7 @@ public abstract class DeleteHandlerV1 { private final boolean softDelete; private final TaskManagement taskManagement; private final AtlasUniqueKeyHandler uniqueKeyHandler; + private final EntityDeletePropagationHandler deletePropagationHandler; public DeleteHandlerV1(AtlasGraph graph, AtlasTypeRegistry typeRegistry, boolean shouldUpdateInverseReference, boolean softDelete, TaskManagement taskManagement) { this.typeRegistry = typeRegistry; @@ -152,6 +154,7 @@ public DeleteHandlerV1(AtlasGraph graph, AtlasTypeRegistry typeRegistry, boolean this.softDelete = softDelete; this.taskManagement = taskManagement; this.uniqueKeyHandler = graph.getUniqueKeyHandler(); + this.deletePropagationHandler = new EntityDeletePropagationHandler(typeRegistry); } /** @@ -215,9 +218,46 @@ public Set accumulateDeletionCandidates(Collection ins getColumnLineageEntities(instanceVertex, deletionCandidateVertices); } } + + // Propagation applies only during delete (ACTIVE→DELETED), not purge (cleanup of already-DELETED entities) + if (!requestContext.isPurgeRequested()) { + collectDeletePropagationCandidates(requestContext, deletionCandidateVertices, instanceVertexGuids); + } + return deletionCandidateVertices; } + private void collectDeletePropagationCandidates(RequestContext requestContext, Set deletionCandidateVertices, + Set visitedGuids) throws AtlasBaseException { + Set propagationSources = new HashSet<>(deletionCandidateVertices); + + for (AtlasVertex vertex : propagationSources) { + String typeName = GraphHelper.getTypeName(vertex); + AtlasEntityType entityType = typeRegistry.getEntityTypeByName(typeName); + + if (entityType == null || CollectionUtils.isEmpty(entityType.getDeletePropagationTargets())) { + continue; + } + + LOG.debug("collectDeletePropagationCandidates(): checking propagation targets for entity type='{}', guid='{}'", + typeName, AtlasGraphUtilsV2.getIdFromVertex(vertex)); + + Set propagatedVertices = deletePropagationHandler.collectPropagatedVertices(vertex, entityType, visitedGuids); + + for (AtlasVertex propagatedVertex : propagatedVertices) { + for (GraphHelper.VertexInfo vertexInfo : getOwnedVertices(propagatedVertex)) { + requestContext.recordEntityDelete(vertexInfo.getEntity()); + deletionCandidateVertices.add(vertexInfo.getVertex()); + } + } + } + + if (deletionCandidateVertices.size() > propagationSources.size()) { + LOG.info("collectDeletePropagationCandidates(): added {} entities via delete propagation", + deletionCandidateVertices.size() - propagationSources.size()); + } + } + /* actually delete traits and then the vertex along its references */ diff --git a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityDeletePropagationHandler.java b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityDeletePropagationHandler.java new file mode 100644 index 00000000000..0466d1ae52f --- /dev/null +++ b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityDeletePropagationHandler.java @@ -0,0 +1,132 @@ +/** + * 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.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.DeletePropagationTarget; +import org.apache.commons.collections.CollectionUtils; +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.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +@Component +public class EntityDeletePropagationHandler { + private static final Logger LOG = LoggerFactory.getLogger(EntityDeletePropagationHandler.class); + + private final AtlasTypeRegistry typeRegistry; + + @Inject + public EntityDeletePropagationHandler(AtlasTypeRegistry typeRegistry) { + this.typeRegistry = typeRegistry; + } + + public Set collectPropagatedVertices(AtlasVertex sourceVertex, AtlasEntityType entityType, Set visitedGuids) { + Set propagatedVertices = new HashSet<>(); + + collectPropagatedVerticesRecursive(sourceVertex, entityType, visitedGuids, propagatedVertices); + + return propagatedVertices; + } + + private void collectPropagatedVerticesRecursive(AtlasVertex sourceVertex, AtlasEntityType sourceEntityType, + Set visitedGuids, Set propagatedVertices) { + for (DeletePropagationTarget propagationTarget : sourceEntityType.getDeletePropagationTargets()) { + AtlasAttribute relAttr = propagationTarget.getRelAttr(); + + if (relAttr == null) { + continue; + } + + for (AtlasEdge edge : toList(GraphHelper.getEdgesForLabel(sourceVertex, relAttr.getRelationshipEdgeLabel(), relAttr.getRelationshipEdgeDirection()))) { + if (GraphHelper.getStatus(edge) == AtlasEntity.Status.DELETED) { + LOG.debug("collectPropagatedVertices(): skip — soft-deleted edge (label={})", relAttr.getRelationshipEdgeLabel()); + continue; + } + + AtlasVertex targetVertex = getOtherVertex(edge, sourceVertex); + + if (targetVertex == null) { + continue; + } + + if (GraphHelper.getStatus(targetVertex) == AtlasEntity.Status.DELETED) { + LOG.debug("collectPropagatedVertices(): skip — already-deleted target vertex (guid={})", + AtlasGraphUtilsV2.getIdFromVertex(targetVertex)); + continue; + } + + String targetGuid = AtlasGraphUtilsV2.getIdFromVertex(targetVertex); + + if (StringUtils.isBlank(targetGuid) || !visitedGuids.add(targetGuid)) { + LOG.debug("collectPropagatedVertices(): skip — already visited (guid={})", targetGuid); + continue; + } + + String targetTypeName = GraphHelper.getTypeName(targetVertex); + AtlasEntityType targetEntityType = typeRegistry.getEntityTypeByName(targetTypeName); + + if (targetEntityType == null) { + LOG.debug("collectPropagatedVertices(): skip — no typedef for target type '{}' (guid={})", targetTypeName, targetGuid); + continue; + } + + propagatedVertices.add(targetVertex); + + LOG.info("collectPropagatedVertices(): propagating delete from '{}' to '{}' (guid={})", + sourceEntityType.getTypeName(), targetTypeName, targetGuid); + + if (CollectionUtils.isNotEmpty(targetEntityType.getDeletePropagationTargets())) { + collectPropagatedVerticesRecursive(targetVertex, targetEntityType, visitedGuids, propagatedVertices); + } + } + } + } + + 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; + } +} 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 8e43bcb1c28..6c1ce5fcfdf 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 @@ -929,20 +929,21 @@ public void testSetServiceTypePatchHandlerComplete() throws Exception { } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerSupportedActions() { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerSupportedActions() { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); String[] actions = handler.getSupportedActions(); - assertEquals(actions.length, 2); + assertEquals(actions.length, 3); assertEquals(actions[0], "SET_ATTRIBUTE_DEF_OVERRIDES"); assertEquals(actions[1], "SET_PROPAGATE_RENAME"); + assertEquals(actions[2], "SET_PROPAGATE_DELETE"); } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerApplyOverrides() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerApplyOverrides() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestEntity", "1.0", "2.0"); @@ -959,9 +960,9 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerApplyOverride } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerApplyOverridesSkipped() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerApplyOverridesSkipped() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestEntity", "2.0", "3.0"); @@ -977,9 +978,9 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerApplyOverride } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerApplyOverridesWrongType() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerApplyOverridesWrongType() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestRelationship", "1.0", "2.0"); @@ -997,9 +998,9 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerApplyOverride } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenameEnd2WithPropagateAttributes() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerPropagateRenameEnd2WithPropagateAttributes() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); @@ -1031,9 +1032,9 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRena } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenamePropagateAttributesInvalidShape() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerPropagateRenamePropagateAttributesInvalidShape() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); @@ -1053,9 +1054,9 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRena } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenameEnd1() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerPropagateRenameEnd1() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); @@ -1076,9 +1077,9 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRena } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenameEnd2() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerPropagateRenameEnd2() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); @@ -1099,9 +1100,9 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRena } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenameInvalidEndDef() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerPropagateRenameInvalidEndDef() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); @@ -1120,9 +1121,9 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRena } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenameMissingParam() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerPropagateRenameMissingParam() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestRelationship", "1.0", "2.0"); @@ -1139,9 +1140,9 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRena } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRenameWrongType() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerPropagateRenameWrongType() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_PROPAGATE_RENAME", "TestEntity", "1.0", "2.0"); @@ -1156,9 +1157,9 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerPropagateRena } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerUnknownType() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerUnknownType() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "UnknownType", "1.0", "2.0"); when(typeRegistry.getTypeDefByName("UnknownType")).thenReturn(null); @@ -1167,9 +1168,9 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerUnknownType() } @Test - public void testAttributeDefOverridesAndPropagateRenamePatchHandlerInvalidAction() throws Exception { - AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler handler = - new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndPropagateRenamePatchHandler(typeDefStore, typeRegistry); + public void testAttributeDefOverridesAndOperationPropagationPatchHandlerInvalidAction() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); Object patch = createMockTypeDefPatch("SET_ATTRIBUTE_DEF_OVERRIDES", "TestEntity", "1.0", "2.0"); setField(patch, "action", "NOT_A_SUPPORTED_PATCH_ACTION"); @@ -1180,6 +1181,159 @@ public void testAttributeDefOverridesAndPropagateRenamePatchHandlerInvalidAction expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); } + @Test + public void testPropagateDeletePatchHandlerApplyEnd1() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_DELETE", "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("endDefToken", "endDef1"); + setField(patch, "params", params); + + PatchStatus result = handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch); + assertEquals(result, APPLIED); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AtlasRelationshipDef.class); + verify(typeDefStore).updateRelationshipDefByName(eq("TestRelationship"), captor.capture()); + assertTrue(captor.getValue().getEndDef1().getIsPropagateDelete()); + assertFalse(captor.getValue().getEndDef2().getIsPropagateDelete()); + } + + @Test + public void testPropagateDeletePatchHandlerApplyEnd2() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_DELETE", "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("endDefToken", "endDef2"); + setField(patch, "params", params); + + PatchStatus result = handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch); + assertEquals(result, APPLIED); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AtlasRelationshipDef.class); + verify(typeDefStore).updateRelationshipDefByName(eq("TestRelationship"), captor.capture()); + assertFalse(captor.getValue().getEndDef1().getIsPropagateDelete()); + assertTrue(captor.getValue().getEndDef2().getIsPropagateDelete()); + } + + @Test + public void testPropagateDeletePatchHandlerInvalidEndDef() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_DELETE", "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("endDefToken", "endDef3"); + setField(patch, "params", params); + + expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); + } + + @Test + public void testPropagateDeletePatchHandlerMissingParam() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_DELETE", "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 testPropagateDeletePatchHandlerNullParams() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_DELETE", "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", null); + + expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); + } + + @Test + public void testPropagateDeletePatchHandlerWrongType() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_DELETE", "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("endDefToken", "endDef1"); + setField(patch, "params", params); + + expectThrows(AtlasBaseException.class, () -> handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch)); + } + + @Test + public void testPropagateDeletePatchHandlerSkipped() throws Exception { + AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler handler = + new AtlasTypeDefStoreInitializer.AttributeDefOverridesAndOperationPropagationPatchHandler(typeDefStore, typeRegistry); + + Object patch = createMockTypeDefPatch("SET_PROPAGATE_DELETE", "TestRelationship", "2.0", "3.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("endDefToken", "endDef1"); + setField(patch, "params", params); + + PatchStatus result = handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch); + assertEquals(result, SKIPPED); + verify(typeDefStore, never()).updateRelationshipDefByName(anyString(), any()); + } + @Test public void testUpdateAttributeMetadataHandlerComprehensive() throws Exception { AtlasTypeDefStoreInitializer.UpdateAttributeMetadataHandler handler = new AtlasTypeDefStoreInitializer.UpdateAttributeMetadataHandler(typeDefStore, typeRegistry, graph); diff --git a/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/EntityDeletePropagationHandlerTest.java b/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/EntityDeletePropagationHandlerTest.java new file mode 100644 index 00000000000..23c57f8e2f8 --- /dev/null +++ b/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/EntityDeletePropagationHandlerTest.java @@ -0,0 +1,477 @@ +/** + * 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.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.DeletePropagationTarget; +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.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +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.assertTrue; + +public class EntityDeletePropagationHandlerTest { + + @Mock private AtlasTypeRegistry typeRegistry; + + private EntityDeletePropagationHandler handler; + + private MockedStatic graphHelperMock; + private MockedStatic graphUtilsMock; + + @BeforeMethod + public void setUp() { + MockitoAnnotations.openMocks(this); + handler = new EntityDeletePropagationHandler(typeRegistry); + } + + @AfterMethod + public void tearDown() { + if (graphHelperMock != null) { + graphHelperMock.close(); + graphHelperMock = null; + } + if (graphUtilsMock != null) { + graphUtilsMock.close(); + graphUtilsMock = null; + } + } + + @Test + public void testNoPropagationTargets_returnsEmpty() { + AtlasEntityType entityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + + when(entityType.getDeletePropagationTargets()).thenReturn(Collections.emptyList()); + + Set visitedGuids = new HashSet<>(); + Set result = handler.collectPropagatedVertices(srcVertex, entityType, visitedGuids); + + assertTrue(result.isEmpty()); + } + + @Test + public void testNullRelAttr_skipped() { + openStaticMocks(); + + DeletePropagationTarget target = mock(DeletePropagationTarget.class); + when(target.getRelAttr()).thenReturn(null); + + AtlasEntityType entityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + when(entityType.getDeletePropagationTargets()).thenReturn(Collections.singletonList(target)); + + Set visitedGuids = new HashSet<>(); + Set result = handler.collectPropagatedVertices(srcVertex, entityType, visitedGuids); + + assertTrue(result.isEmpty()); + } + + @Test + public void testNoEdges_returnsEmpty() { + openStaticMocks(); + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__trino_table.schema"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + DeletePropagationTarget target = mock(DeletePropagationTarget.class); + when(target.getRelAttr()).thenReturn(relAttr); + + AtlasEntityType entityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + when(entityType.getDeletePropagationTargets()).thenReturn(Collections.singletonList(target)); + + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.emptyList().iterator()); + + Set visitedGuids = new HashSet<>(); + Set result = handler.collectPropagatedVertices(srcVertex, entityType, visitedGuids); + + assertTrue(result.isEmpty()); + } + + @Test + public void testDeletedEdge_skipped() { + openStaticMocks(); + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__trino_table.schema"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + DeletePropagationTarget target = mock(DeletePropagationTarget.class); + when(target.getRelAttr()).thenReturn(relAttr); + + AtlasEntityType entityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasEdge edge = mock(AtlasEdge.class); + when(entityType.getDeletePropagationTargets()).thenReturn(Collections.singletonList(target)); + + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.singletonList(edge).iterator()); + graphHelperMock.when(() -> GraphHelper.getStatus(edge)).thenReturn(AtlasEntity.Status.DELETED); + + Set visitedGuids = new HashSet<>(); + Set result = handler.collectPropagatedVertices(srcVertex, entityType, visitedGuids); + + assertTrue(result.isEmpty()); + } + + @Test + public void testDeletedTargetVertex_skipped() { + openStaticMocks(); + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__trino_table.schema"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + DeletePropagationTarget target = mock(DeletePropagationTarget.class); + when(target.getRelAttr()).thenReturn(relAttr); + + AtlasEntityType entityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasVertex targetVertex = mock(AtlasVertex.class); + AtlasEdge edge = mock(AtlasEdge.class); + + when(entityType.getDeletePropagationTargets()).thenReturn(Collections.singletonList(target)); + when(edge.getOutVertex()).thenReturn(srcVertex); + when(edge.getInVertex()).thenReturn(targetVertex); + + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.singletonList(edge).iterator()); + graphHelperMock.when(() -> GraphHelper.getStatus(edge)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getStatus(targetVertex)).thenReturn(AtlasEntity.Status.DELETED); + + Set visitedGuids = new HashSet<>(); + Set result = handler.collectPropagatedVertices(srcVertex, entityType, visitedGuids); + + assertTrue(result.isEmpty()); + } + + @Test + public void testAlreadyVisitedGuid_skipped() { + openStaticMocks(); + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__trino_table.schema"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + DeletePropagationTarget target = mock(DeletePropagationTarget.class); + when(target.getRelAttr()).thenReturn(relAttr); + + AtlasEntityType entityType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasVertex targetVertex = mock(AtlasVertex.class); + AtlasEdge edge = mock(AtlasEdge.class); + + when(entityType.getDeletePropagationTargets()).thenReturn(Collections.singletonList(target)); + when(edge.getOutVertex()).thenReturn(srcVertex); + when(edge.getInVertex()).thenReturn(targetVertex); + + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.singletonList(edge).iterator()); + graphHelperMock.when(() -> GraphHelper.getStatus(edge)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getStatus(targetVertex)).thenReturn(AtlasEntity.Status.ACTIVE); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(targetVertex)).thenReturn("already-visited-guid"); + + Set visitedGuids = new HashSet<>(); + visitedGuids.add("already-visited-guid"); + + Set result = handler.collectPropagatedVertices(srcVertex, entityType, visitedGuids); + + assertTrue(result.isEmpty()); + } + + @Test + public void testSingleTargetPropagated() { + openStaticMocks(); + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__trino_table.schema"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + DeletePropagationTarget target = mock(DeletePropagationTarget.class); + when(target.getRelAttr()).thenReturn(relAttr); + + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasEntityType targetType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasVertex targetVertex = mock(AtlasVertex.class); + AtlasEdge edge = mock(AtlasEdge.class); + + when(sourceType.getDeletePropagationTargets()).thenReturn(Collections.singletonList(target)); + when(sourceType.getTypeName()).thenReturn("trino_schema"); + when(edge.getOutVertex()).thenReturn(srcVertex); + when(edge.getInVertex()).thenReturn(targetVertex); + + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.singletonList(edge).iterator()); + graphHelperMock.when(() -> GraphHelper.getStatus(edge)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getStatus(targetVertex)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getTypeName(targetVertex)).thenReturn("trino_table"); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(targetVertex)).thenReturn("target-guid-001"); + + when(typeRegistry.getEntityTypeByName("trino_table")).thenReturn(targetType); + when(targetType.getDeletePropagationTargets()).thenReturn(Collections.emptyList()); + + Set visitedGuids = new HashSet<>(); + Set result = handler.collectPropagatedVertices(srcVertex, sourceType, visitedGuids); + + assertEquals(result.size(), 1); + assertTrue(result.contains(targetVertex)); + assertTrue(visitedGuids.contains("target-guid-001")); + } + + @Test + public void testRecursivePropagation() { + openStaticMocks(); + + // Level 1: trino_schema -> trino_table + AtlasAttribute relAttr1 = mock(AtlasAttribute.class); + when(relAttr1.getRelationshipEdgeLabel()).thenReturn("__trino_table.schema"); + when(relAttr1.getRelationshipEdgeDirection()).thenReturn(null); + + DeletePropagationTarget target1 = mock(DeletePropagationTarget.class); + when(target1.getRelAttr()).thenReturn(relAttr1); + + // Level 2: trino_table -> trino_column + AtlasAttribute relAttr2 = mock(AtlasAttribute.class); + when(relAttr2.getRelationshipEdgeLabel()).thenReturn("__trino_column.table"); + when(relAttr2.getRelationshipEdgeDirection()).thenReturn(null); + + DeletePropagationTarget target2 = mock(DeletePropagationTarget.class); + when(target2.getRelAttr()).thenReturn(relAttr2); + + AtlasEntityType schemaType = mock(AtlasEntityType.class); + AtlasEntityType tableType = mock(AtlasEntityType.class); + AtlasEntityType columnType = mock(AtlasEntityType.class); + AtlasVertex schemaVertex = mock(AtlasVertex.class); + AtlasVertex tableVertex = mock(AtlasVertex.class); + AtlasVertex columnVertex = mock(AtlasVertex.class); + AtlasEdge edge1 = mock(AtlasEdge.class); + AtlasEdge edge2 = mock(AtlasEdge.class); + + when(schemaType.getDeletePropagationTargets()).thenReturn(Collections.singletonList(target1)); + when(schemaType.getTypeName()).thenReturn("trino_schema"); + when(tableType.getDeletePropagationTargets()).thenReturn(Collections.singletonList(target2)); + when(tableType.getTypeName()).thenReturn("trino_table"); + when(columnType.getDeletePropagationTargets()).thenReturn(Collections.emptyList()); + + // Edge from schema -> table + when(edge1.getOutVertex()).thenReturn(schemaVertex); + when(edge1.getInVertex()).thenReturn(tableVertex); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(schemaVertex), eq("__trino_table.schema"), any())) + .thenReturn(Collections.singletonList(edge1).iterator()); + graphHelperMock.when(() -> GraphHelper.getStatus(edge1)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getStatus(tableVertex)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getTypeName(tableVertex)).thenReturn("trino_table"); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(tableVertex)).thenReturn("table-guid"); + + // Edge from table -> column + when(edge2.getOutVertex()).thenReturn(tableVertex); + when(edge2.getInVertex()).thenReturn(columnVertex); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(tableVertex), eq("__trino_column.table"), any())) + .thenReturn(Collections.singletonList(edge2).iterator()); + graphHelperMock.when(() -> GraphHelper.getStatus(edge2)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getStatus(columnVertex)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getTypeName(columnVertex)).thenReturn("trino_column"); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(columnVertex)).thenReturn("column-guid"); + + when(typeRegistry.getEntityTypeByName("trino_table")).thenReturn(tableType); + when(typeRegistry.getEntityTypeByName("trino_column")).thenReturn(columnType); + + Set visitedGuids = new HashSet<>(); + Set result = handler.collectPropagatedVertices(schemaVertex, schemaType, visitedGuids); + + assertEquals(result.size(), 2); + assertTrue(result.contains(tableVertex)); + assertTrue(result.contains(columnVertex)); + } + + @Test + public void testCyclePrevention() { + openStaticMocks(); + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__cyclic_edge"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + DeletePropagationTarget target = mock(DeletePropagationTarget.class); + when(target.getRelAttr()).thenReturn(relAttr); + + AtlasEntityType entityType = mock(AtlasEntityType.class); + AtlasVertex vertexA = mock(AtlasVertex.class); + AtlasVertex vertexB = mock(AtlasVertex.class); + AtlasEdge edgeAtoB = mock(AtlasEdge.class); + AtlasEdge edgeBtoA = mock(AtlasEdge.class); + + when(entityType.getDeletePropagationTargets()).thenReturn(Collections.singletonList(target)); + when(entityType.getTypeName()).thenReturn("cyclic_type"); + + // A -> B + when(edgeAtoB.getOutVertex()).thenReturn(vertexA); + when(edgeAtoB.getInVertex()).thenReturn(vertexB); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(vertexA), anyString(), any())) + .thenReturn(Collections.singletonList(edgeAtoB).iterator()); + graphHelperMock.when(() -> GraphHelper.getStatus(edgeAtoB)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getStatus(vertexB)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getTypeName(vertexB)).thenReturn("cyclic_type"); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(vertexB)).thenReturn("guid-B"); + + // B -> A (cycle): vertexA guid is already in visitedGuids since it's the source + when(edgeBtoA.getOutVertex()).thenReturn(vertexB); + when(edgeBtoA.getInVertex()).thenReturn(vertexA); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(vertexB), anyString(), any())) + .thenReturn(Collections.singletonList(edgeBtoA).iterator()); + graphHelperMock.when(() -> GraphHelper.getStatus(edgeBtoA)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getStatus(vertexA)).thenReturn(AtlasEntity.Status.ACTIVE); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(vertexA)).thenReturn("guid-A"); + + when(typeRegistry.getEntityTypeByName("cyclic_type")).thenReturn(entityType); + + Set visitedGuids = new HashSet<>(); + visitedGuids.add("guid-A"); + + Set result = handler.collectPropagatedVertices(vertexA, entityType, visitedGuids); + + assertEquals(result.size(), 1); + assertTrue(result.contains(vertexB)); + } + + @Test + public void testMultipleTargetsFromSameSource() { + openStaticMocks(); + + AtlasAttribute relAttr1 = mock(AtlasAttribute.class); + when(relAttr1.getRelationshipEdgeLabel()).thenReturn("__edge_label_1"); + when(relAttr1.getRelationshipEdgeDirection()).thenReturn(null); + + AtlasAttribute relAttr2 = mock(AtlasAttribute.class); + when(relAttr2.getRelationshipEdgeLabel()).thenReturn("__edge_label_2"); + when(relAttr2.getRelationshipEdgeDirection()).thenReturn(null); + + DeletePropagationTarget target1 = mock(DeletePropagationTarget.class); + when(target1.getRelAttr()).thenReturn(relAttr1); + + DeletePropagationTarget target2 = mock(DeletePropagationTarget.class); + when(target2.getRelAttr()).thenReturn(relAttr2); + + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasEntityType targetType1 = mock(AtlasEntityType.class); + AtlasEntityType targetType2 = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasVertex vertex1 = mock(AtlasVertex.class); + AtlasVertex vertex2 = mock(AtlasVertex.class); + AtlasEdge edge1 = mock(AtlasEdge.class); + AtlasEdge edge2 = mock(AtlasEdge.class); + + when(sourceType.getDeletePropagationTargets()).thenReturn(Arrays.asList(target1, target2)); + when(sourceType.getTypeName()).thenReturn("source_type"); + + when(edge1.getOutVertex()).thenReturn(srcVertex); + when(edge1.getInVertex()).thenReturn(vertex1); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), eq("__edge_label_1"), any())) + .thenReturn(Collections.singletonList(edge1).iterator()); + graphHelperMock.when(() -> GraphHelper.getStatus(edge1)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getStatus(vertex1)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getTypeName(vertex1)).thenReturn("target_type_1"); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(vertex1)).thenReturn("guid-1"); + + when(edge2.getOutVertex()).thenReturn(srcVertex); + when(edge2.getInVertex()).thenReturn(vertex2); + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), eq("__edge_label_2"), any())) + .thenReturn(Collections.singletonList(edge2).iterator()); + graphHelperMock.when(() -> GraphHelper.getStatus(edge2)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getStatus(vertex2)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getTypeName(vertex2)).thenReturn("target_type_2"); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(vertex2)).thenReturn("guid-2"); + + when(typeRegistry.getEntityTypeByName("target_type_1")).thenReturn(targetType1); + when(typeRegistry.getEntityTypeByName("target_type_2")).thenReturn(targetType2); + when(targetType1.getDeletePropagationTargets()).thenReturn(Collections.emptyList()); + when(targetType2.getDeletePropagationTargets()).thenReturn(Collections.emptyList()); + + Set visitedGuids = new HashSet<>(); + Set result = handler.collectPropagatedVertices(srcVertex, sourceType, visitedGuids); + + assertEquals(result.size(), 2); + assertTrue(result.contains(vertex1)); + assertTrue(result.contains(vertex2)); + } + + @Test + public void testNullTargetEntityType_skipped() { + openStaticMocks(); + + AtlasAttribute relAttr = mock(AtlasAttribute.class); + when(relAttr.getRelationshipEdgeLabel()).thenReturn("__edge_label"); + when(relAttr.getRelationshipEdgeDirection()).thenReturn(null); + + DeletePropagationTarget target = mock(DeletePropagationTarget.class); + when(target.getRelAttr()).thenReturn(relAttr); + + AtlasEntityType sourceType = mock(AtlasEntityType.class); + AtlasVertex srcVertex = mock(AtlasVertex.class); + AtlasVertex targetVertex = mock(AtlasVertex.class); + AtlasEdge edge = mock(AtlasEdge.class); + + when(sourceType.getDeletePropagationTargets()).thenReturn(Collections.singletonList(target)); + when(edge.getOutVertex()).thenReturn(srcVertex); + when(edge.getInVertex()).thenReturn(targetVertex); + + graphHelperMock.when(() -> GraphHelper.getEdgesForLabel(eq(srcVertex), anyString(), any())) + .thenReturn(Collections.singletonList(edge).iterator()); + graphHelperMock.when(() -> GraphHelper.getStatus(edge)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getStatus(targetVertex)).thenReturn(AtlasEntity.Status.ACTIVE); + graphHelperMock.when(() -> GraphHelper.getTypeName(targetVertex)).thenReturn("unknown_type"); + graphUtilsMock.when(() -> AtlasGraphUtilsV2.getIdFromVertex(targetVertex)).thenReturn("unknown-guid"); + + when(typeRegistry.getEntityTypeByName("unknown_type")).thenReturn(null); + + Set visitedGuids = new HashSet<>(); + Set result = handler.collectPropagatedVertices(srcVertex, sourceType, visitedGuids); + + assertTrue(result.isEmpty()); + } + + private void openStaticMocks() { + graphHelperMock = mockStatic(GraphHelper.class); + graphUtilsMock = mockStatic(AtlasGraphUtilsV2.class); + } +} From 0cf3e84d71b6225228f392006121c750cff34c60 Mon Sep 17 00:00:00 2001 From: "sanket.bhor" Date: Tue, 16 Jun 2026 15:42:26 +0530 Subject: [PATCH 2/2] ATLAS-5312: changes to add patch for delete_propagation --- .../002-delete_propagation_typedef_patch.json | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 addons/models/6000-Trino/patches/002-delete_propagation_typedef_patch.json diff --git a/addons/models/6000-Trino/patches/002-delete_propagation_typedef_patch.json b/addons/models/6000-Trino/patches/002-delete_propagation_typedef_patch.json new file mode 100644 index 00000000000..21b95d5d478 --- /dev/null +++ b/addons/models/6000-Trino/patches/002-delete_propagation_typedef_patch.json @@ -0,0 +1,37 @@ +{ + "patches": [ + { + "id": "TYPEDEF_PATCH_6000_002_A", + "description": "Set propagateDelete=true on trino_table_hive_table endDef1 (hive_table → trino_table)", + "action": "SET_PROPAGATE_DELETE", + "typeName": "trino_table_hive_table", + "applyToVersion": "1.1", + "updateToVersion": "1.2", + "params": { + "endDefToken": "endDef1" + } + }, + { + "id": "TYPEDEF_PATCH_6000_002_B", + "description": "Set propagateDelete=true on trino_schema_hive_db endDef1 (hive_db → trino_schema)", + "action": "SET_PROPAGATE_DELETE", + "typeName": "trino_schema_hive_db", + "applyToVersion": "1.0", + "updateToVersion": "1.1", + "params": { + "endDefToken": "endDef1" + } + }, + { + "id": "TYPEDEF_PATCH_6000_002_C", + "description": "Set propagateDelete=true on trino_table_schema endDef2 (trino_schema → trino_table)", + "action": "SET_PROPAGATE_DELETE", + "typeName": "trino_table_schema", + "applyToVersion": "1.0", + "updateToVersion": "1.1", + "params": { + "endDefToken": "endDef2" + } + } + ] +}