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