This is an automated email from the ASF dual-hosted git repository.
pinal-shah pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/atlas.git
The following commit(s) were added to refs/heads/master by this push:
new eda0c1976 ATLAS-5312: Add delete propagation support via
propagateDelete flag o… (#670)
eda0c1976 is described below
commit eda0c19765d2a61c1e59cf46d54aea76b0ca3f6d
Author: bhor-sanket <[email protected]>
AuthorDate: Mon Aug 10 09:19:24 2026 +0530
ATLAS-5312: Add delete propagation support via propagateDelete flag o…
(#670)
---
addons/models/6000-Trino/6000-trino_model.json | 6 +-
.../002-delete_propagation_typedef_patch.json | 37 ++
docs/src/documents/RenamePropagation.md | 200 ---------
docs/src/documents/TypeSystem.md | 155 +++++++
.../model/typedef/AtlasRelationshipEndDef.java | 26 +-
.../org/apache/atlas/type/AtlasEntityType.java | 57 +++
.../apache/atlas/type/DeletePropagationTarget.java | 59 +++
.../model/typedef/TestAtlasRelationshipEndDef.java | 66 +++
.../atlas/type/TestDeletePropagationTarget.java | 122 ++++++
.../bootstrap/AtlasTypeDefStoreInitializer.java | 61 ++-
.../repository/store/graph/v1/DeleteHandlerV1.java | 40 ++
.../graph/v2/EntityDeletePropagationHandler.java | 133 ++++++
.../AtlasTypeDefStoreInitializerTest.java | 234 ++++++++--
.../v2/EntityDeletePropagationHandlerTest.java | 476 +++++++++++++++++++++
14 files changed, 1419 insertions(+), 253 deletions(-)
diff --git a/addons/models/6000-Trino/6000-trino_model.json
b/addons/models/6000-Trino/6000-trino_model.json
index aa254ee6e..a8aa885de 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/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 000000000..21b95d5d4
--- /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"
+ }
+ }
+ ]
+}
diff --git a/docs/src/documents/RenamePropagation.md
b/docs/src/documents/RenamePropagation.md
deleted file mode 100644
index b261c2adf..000000000
--- a/docs/src/documents/RenamePropagation.md
+++ /dev/null
@@ -1,200 +0,0 @@
----
-
-## name: Rename Propagation
-
-route: /RenamePropagation
-menu: Documentation
-submenu: Features
-
-# Rename propagation
-
-**Rename propagation** is an Atlas feature for **partial entity updates**:
when a renamed instance’s `**name`** (or a mapped source attribute) changes,
the server can find **related entities** whose **unique attribute** (for most
types, `**qualifiedName`**) still embeds the old segment, recompute that unique
attribute from the type’s `**autoComputeFormat`**, and **persist those updates
in the same transaction** as the triggering entity. Discovery is driven by
**typedefs** (relationship e [...]
-
----
-
-## Why it exists
-
-The unique attribute (qualified name) is often a **composite string** built
from ancestor names (database, table, cluster, and so on). If only the root
entity is updated in a hook payload, **downstream** instances can keep an
outdated unique attribute until something rewrites them. Rename propagation
closes that gap by following **marked relationship edges** from the updated
vertex and applying the same **parse → replace one template segment → rebuild**
logic per dependent type.
-
----
-
-## Example: Hive database rename
-
-Assume a Hive model where:
-
-- `**hive_db`** has unique attribute (qualified name) shaped like
`{name}@{clusterName}` (for example `sales@cm`).
-- `**hive_table`** uses `{db.name}.{name}@{clusterName}` (for example
`sales.orders@cm`).
-- `**hive_column**` uses `{table.db.name}.{table.name}.{name}@{clusterName}`.
-
-The `**hive_table_db**` relationship links each table to its database;
`**hive_table_columns**` links columns to a table. Relationship ends that opt
in with `**propagateRename**` tell Atlas: when the **trigger** side is renamed,
walk to neighbors and refresh their unique attribute (qualified name) where the
template depends on that rename.
-
-1. An integration renames the database `**sales`** → `**sales_archive`**
(partial update; `**name**` and thus the unique attribute on the `hive_db`
vertex change).
-2. Atlas detects that the entity’s **unique attribute (qualified name)**
changed and that `**hive_db`** has **rename propagation targets** from typedef
resolution.
-3. `**EntityRenameHandler`** follows the configured relationship from the
database vertex to related `**hive_table`** vertices, recomputes each table’s
string (for example `sales.orders@cm` → `sales_archive.orders@cm`), and
registers those rows on the mutation context.
-4. If `**hive_table**` also declares propagation targets, the same process
continues to `**hive_column**` rows (for example `sales.orders.col1@cm` →
`sales_archive.orders.col1@cm`).
-
-All of this happens **without** requiring the hook to send every table and
column in one batch.
-
----
-
-## Example: Hive table → Trino table → Trino column
-
-Assume a Hive and Trino model where:
-
-- `**hive_table**` has unique attribute (qualified name) shaped like
`{db.name}.{name}@{clusterName}` (for example `default.orders@cluster`).
-- `**trino_table**` uses
`{trinoschema.catalog.name}.{trinoschema.name}.{name}@{trinoschema.catalog.instance.name}`
(for example `cat1.schema1.orders@inst1`).
-- `**trino_column**` uses
`{table.trinoschema.catalog.name}.{table.trinoschema.name}.{table.name}.{name}@{table.trinoschema.catalog.instance.name}`
(for example `cat1.schema1.orders.col1@inst1`).
-
-The `**trino_table_hive_table**` relationship links each Trino table to its
Hive table; `**trino_table_columns**` links columns to a table. The
`**propagateRename**` flag on the `**hive_table**` end of
`**trino_table_hive_table**` tells Atlas: when the **trigger** Hive table is
renamed, walk to linked `**trino_table**` vertices and refresh their unique
attribute (qualified name) where the `**autoComputeFormat**` template depends
on that rename.
-
-1. An integration renames the Hive table `**orders**` → `**orders_v2**`
(partial update; `**name**` and thus the unique attribute on the
`**hive_table**` vertex change).
-2. Atlas detects that the entity's **unique attribute (qualified name)**
changed and that `**hive_table**` has **rename propagation targets** from
typedef resolution.
-3. `**EntityRenameHandler**` follows `**trino_table_hive_table**` from the
Hive table vertex to related `**trino_table**` vertices, recomputes each
table's string (for example `cat1.schema1.orders@inst1` →
`cat1.schema1.orders_v2@inst1`), and registers those rows on the mutation
context.
-4. If `**trino_table**` also declares propagation targets, the same process
continues to `**trino_column**` rows (for example
`cat1.schema1.orders.col1@inst1` → `cat1.schema1.orders_v2.col1@inst1`).
-
-All of this happens **without** requiring the hook to send every Trino table
and column in one batch.
-
----
-
-## How it works (two stages)
-
-### 1. Typedef resolution (startup / type updates)
-
-While the type system resolves references, Atlas builds **per-entity-type**
metadata used later at runtime:
-
-- `**propagateRename`** on a `**AtlasRelationshipEndDef`** marks which end
acts as the **rename source** for that relationship so instances reached
through that edge can be updated when the source side’s name changes.
-- `**attributeDefOverrides`** on `**AtlasEntityDef`** can set
`**autoComputeFormat**` for the **unique attribute** (qualified name),
describing how the string is composed from named segments.
-- For paths that do not use explicit `**propagateAttributes`** maps on the
relationship end, `**AtlasEntityType`** also maintains
`**autoComputeFormatPathByRefTypeNameMap**`: referenced type name → **dotted
path** in the template for the segment to replace when that referenced type was
renamed.
-- Optional `**propagateAttributes`** on a relationship end lists `**source`**
/ `**target**` attribute pairs so values such as `**name**` can be written to
the right stub fields on dependents when models need more than template
substitution alone.
-
-Together, these produce a list of `**RenamePropagationTarget**` entries on
each `**AtlasEntityType**`.
-
-### 2. Entity update (runtime)
-
-On **partial update**, `**AtlasEntityStoreV2`** compares the stored **unique
attribute (qualified name)** on the graph vertex to the incoming value. If it
changed and the type has **rename propagation targets**,
`**EntityRenameHandler`** walks the graph along those relationships, recomputes
each dependent’s unique attribute from its effective `**autoComputeFormat`**,
and adds minimal **dependent stubs** (guid, new unique attribute, optional
mapped attributes) to the **same** mutation con [...]
-
----
-
-## Key typedef concepts
-
-
-| Concept | Where it lives |
Role
|
-| ------------------------------------------- | ------------------------- |
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
-| `**attributeDefOverrides`** | `AtlasEntityDef` |
Supplies per-type overrides such as `**autoComputeFormat`** for the unique
attribute (qualified name); merged during hierarchy resolution and stored on
the entity type vertex when supported. |
-| `**propagateRename**` | `AtlasRelationshipEndDef` |
Marks the end whose entity type is the **trigger** for propagation across that
relationship.
|
-| `**propagateAttributes`** | `AtlasRelationshipEndDef` |
Optional maps from a **source** attribute on the trigger side to **target**
attribute names on the dependent entity.
|
-| `**RenamePropagationTarget**` | `AtlasEntityType` |
Precomputed link: target type name, relationship attribute for edge traversal,
and propagate-attribute list.
|
-| `**autoComputeFormatPathByRefTypeNameMap`** | `AtlasEntityType` |
Referenced entity type name → dotted path in the dependent’s
**autoComputeFormat** for the segment to rewrite when that type was renamed
(when `**propagateAttributes`** is not used for that path). |
-
-
----
-
-## Implementation reference
-
-
-| Component | Role
|
-| -------------------------------------------------------- |
------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
-| `**AtlasEntityType`** | Registers rename
propagation targets and builds `**autoComputeFormatPathByRefTypeNameMap**`
during reference resolution. |
-| `**EntityRenameHandler**` | Traverses edges,
parses/rebuilds the unique attribute string, registers `**DependentUpdate**`
entries on the mutation context. |
-| `**AtlasEntityStoreV2**` | Detects a changed
unique attribute (qualified name) on partial update and invokes the handler.
|
-| `**AtlasEntityDefStoreV2` / `AtlasTypeDefGraphStoreV2**` | Persist and read
`**attributeDefOverrides**` via the type vertex property keyed by
`**TYPE_ATTR_DEF_OVERRIDES_PROPERTY_KEY**`.
|
-| `**AtlasTypeDefStoreInitializer**` | Applies typedef
patches such as `**SET_ATTRIBUTE_DEF_OVERRIDES**` and
`**SET_PROPAGATE_RENAME**` (patch params use `**endDefToken**`: `"endDef1"` or
`"endDef2"`). |
-| `**GraphBackedSearchIndexer**` | Indexes the
overrides property for search where applicable.
|
-
-
----
-
-## Models and patches
-
-- **Bootstrap model JSON** — Declare `**attributeDefOverrides`** for the
unique attribute (qualified name) and `**propagateRename`** /
`**propagateAttributes**` on relationship ends as required by your connectors
(for example Hive, Trino).
-- **Patch files** — Ship one-off typedef updates under
`**addons/.../patches/`** so each patch runs once and is tracked (for example
`**AtlasPatchRegistry`**), avoiding reliance on version-only bumps on every
restart.
-
----
-
-## Configuring `autoComputeFormat` on an entity type
-
-Rename propagation recomputes a dependent’s **unique attribute** (usually
`**qualifiedName**`) by **parsing** the stored string against the type’s
`**autoComputeFormat**`, **replacing** the segment that came from the renamed
entity, and **rebuilding** the string. You supply that template on the **entity
typedef** as an override on the unique attribute, for **any** entity type whose
composite id should stay aligned after upstream renames.
-
-### Where to declare it
-
-In **bootstrap model JSON** add an `**attributeDefOverrides`** array on the
`**entityDefs**` entry. Each override targets an attribute by name; set
`**autoComputeFormat**` on the **unique** attribute (conventionally
`**qualifiedName`**):
-
-```json
-{
- "name": "my_column",
- "superTypes": ["DataSet"],
- "attributeDefOverrides": [
- {
- "name": "qualifiedName",
- "autoComputeFormat": "{table.db.name}.{table.name}.{name}@{clusterName}"
- }
- ],
- "attributeDefs": [ ]
-}
-```
-
-Atlas merges overrides with inherited definitions so the resolved
`**AtlasEntityDef**` carries `**autoComputeFormat**` on `**qualifiedName**`.
-
-### Placeholder syntax
-
-- Use **curly braces** for each slot: `{segmentKey}`.
-- **Literals** between placeholders (for example `.`, `/`, `@`) are
**delimiters**. When parsing an existing value, the character **immediately
after** each `}` in the format marks where that slot ends in the stored string
(the **last** placeholder consumes the rest of the string). Pick delimiters
that do not appear inside real slot values, or parsing becomes ambiguous.
-- **Dots inside a key** are part of the key string only (they are not JSON
nesting in the model file).
-
-### How to design the format for a new entity type
-
-Start from **one real qualified-name string** your connector already produces
(for example `sales.orders@prod`). Notice how it is built from smaller parts
and which **fixed characters** separate those parts (here: `.`, `.`, and `@`).
-
-1. Write the **same separators in the same order** in `**autoComputeFormat**`.
-2. Replace each **variable part** with `{...}`:
- - **`{name}`** — this entity’s own **name** (always a single word inside
the braces).
- - **`{db.name}`**, **`{table.db.name}`**, and similar — each segment
**before** the final `.name` is one **relationship hop** (the relationship
attribute name on the typedef at that step). Together they lead to a related
entity; the trailing `.name` means “use that entity’s **name** attribute.”
- - **`{clusterName}`** (one word inside the braces) — a normal attribute on
**this** same entity, not a relationship hop.
-
-Dots **inside** `{...}` are part of the placeholder key, not nested JSON in
the model file. Choose separators that will not appear inside a single part of
the real string, so parsing stays unambiguous.
-
-### Example: table under a database (Hive-style shape)
-
-If `**my_table**` links to a database via relationship attribute `**db**` and
carries a cluster label `**clusterName**`, a common pattern is:
-
-```json
-"attributeDefOverrides": [
- {
- "name": "qualifiedName",
- "autoComputeFormat": "{db.name}.{name}@{clusterName}"
- }
-]
-```
-
-- `{db.name}` — database’s `**name**` reached through `**db**`.
-- `{name}` — this table’s `**name**`.
-- `@{clusterName}` — literal `@` then this entity’s `**clusterName**`
attribute.
-
-### Example: deeper chain (Trino column)
-
-For `**trino_column**`, the unique string chains catalog, schema, table,
column, and instance:
-
-```json
-"attributeDefOverrides": [
- {
- "name": "qualifiedName",
- "autoComputeFormat":
"{table.trinoschema.catalog.name}.{table.trinoschema.name}.{table.name}.{name}@{table.trinoschema.catalog.instance.name}"
- }
-]
-```
-
-Meaning of each slot:
-
-- `{table.trinoschema.catalog.name}` — from this column, follow `**table**` →
`**trinoschema**` → `**catalog**`, then use that catalog entity’s `**name**`.
-- `{table.trinoschema.name}` — schema `**name**` along the same branch.
-- `{table.name}` — table `**name**`.
-- `{name}` — this column’s own `**name**`.
-- `@{table.trinoschema.catalog.instance.name}` — after `@`, follow
`**instance**` from the catalog and use that instance’s `**name**`.
-
----
-
-## Tests
-
-- `**AtlasTypeDefStoreInitializerTest**` — Patch handlers for overrides and
`**SET_PROPAGATE_RENAME**`.
-- `**EntityRenameHandlerTest**` — End-to-end behavior: unique attribute
recompute, mapped attributes, and multi-hop propagation through typedefs.
-
diff --git a/docs/src/documents/TypeSystem.md b/docs/src/documents/TypeSystem.md
index 7cfbc70d1..0c5dad69b 100644
--- a/docs/src/documents/TypeSystem.md
+++ b/docs/src/documents/TypeSystem.md
@@ -204,3 +204,158 @@ example, an ETL process that transforms a hive table with
raw data to another hi
be a specific type that extends the Process type. A Process type has two
specific attributes, inputs and outputs. Both
inputs and outputs are arrays of DataSet entities. Thus, an instance of a
Process type can use these inputs and outputs
to capture how the lineage of a DataSet evolves.
+
+## Relationship propagation model extensions
+
+Atlas supports automatic propagation of rename and delete operations through
relationships. This is driven by fields on
+**entity type definitions** and **relationship end definitions** in the model
JSON.
+
+### Entity definition: attributeDefOverrides
+
+An entity type can declare `attributeDefOverrides` to specify how its unique
attribute (qualifiedName) is composed from
+related entities. The `autoComputeFormat` template tells Atlas how to parse
and rebuild the qualifiedName when an
+upstream entity is renamed.
+
+<SyntaxHighlighter wrapLines={true} language="json" style={theme.dark}>
+{`"entityDefs": [{
+ "name": "trino_table",
+ "superTypes": ["DataSet"],
+ "attributeDefOverrides": [
+ {
+ "name": "qualifiedName",
+ "autoComputeFormat":
"{trinoschema.catalog.name}.{trinoschema.name}.{name}@{trinoschema.catalog.instance.name}"
+ }
+ ]
+}]`}
+</SyntaxHighlighter>
+
+Each `{placeholder}` maps to an attribute path: `{name}` refers to this
entity's own name, while dotted paths like
+`{trinoschema.catalog.name}` traverse relationship attributes to reach a
related entity's name.
+
+#### Placeholder syntax
+
+- Use **curly braces** for each slot: `{segmentKey}`.
+- **Literals** between placeholders (e.g. `.`, `/`, `@`) act as
**delimiters**. When parsing an existing qualifiedName,
+ the character immediately after each `}` marks where that slot ends in the
stored string. The **last** placeholder
+ consumes the rest of the string. Choose delimiters that do not appear inside
real slot values to avoid ambiguity.
+- **Dots inside a key** (e.g. `{table.db.name}`) are part of the key string —
they represent relationship hops, not
+ JSON nesting.
+
+#### How to design autoComputeFormat for a new entity type
+
+Start from **one real qualifiedName string** your connector already produces
(e.g. `sales.orders@prod`). Notice how it
+is built from smaller parts and which **fixed characters** separate those
parts (here `.` and `@`).
+
+1. Write the **same separators in the same order** in `autoComputeFormat`.
+2. Replace each **variable part** with a `{placeholder}`:
+ - **`{name}`** — this entity's own `name` attribute.
+ - **`{db.name}`**, **`{table.db.name}`**, etc. — each segment before the
final `.name` is one **relationship hop**
+ (the relationship attribute name on the typedef at that step). Together
they navigate to a related entity; the
+ trailing `.name` means "use that entity's `name` attribute."
+ - **`{clusterName}`** (single word, no dots) — a plain attribute on
**this** entity, not a relationship hop.
+
+##### Example: table under a database (Hive-style)
+
+If `my_table` links to a database via relationship attribute `db` and carries
a cluster label `clusterName`:
+
+<SyntaxHighlighter wrapLines={true} language="json" style={theme.dark}>
+{`"attributeDefOverrides": [
+ {
+ "name": "qualifiedName",
+ "autoComputeFormat": "{db.name}.{name}@{clusterName}"
+ }
+]`}
+</SyntaxHighlighter>
+
+- `{db.name}` — the database's `name`, reached through relationship attribute
`db`.
+- `{name}` — this table's own `name`.
+- `@{clusterName}` — literal `@` followed by this entity's `clusterName`
attribute.
+
+##### Example: deeper chain (Trino column)
+
+For `trino_column`, the qualifiedName chains catalog, schema, table, column,
and instance:
+
+<SyntaxHighlighter wrapLines={true} language="json" style={theme.dark}>
+{`"attributeDefOverrides": [
+ {
+ "name": "qualifiedName",
+ "autoComputeFormat":
"{table.trinoschema.catalog.name}.{table.trinoschema.name}.{table.name}.{name}@{table.trinoschema.catalog.instance.name}"
+ }
+]`}
+</SyntaxHighlighter>
+
+- `{table.trinoschema.catalog.name}` — from this column, follow `table` →
`trinoschema` → `catalog`, use that entity's `name`.
+- `{table.trinoschema.name}` — schema `name` along the same path.
+- `{table.name}` — the parent table's `name`.
+- `{name}` — this column's own `name`.
+- `@{table.trinoschema.catalog.instance.name}` — after `@`, follow `instance`
from the catalog and use its `name`.
+
+
+### Relationship end definition: propagateRename
+
+Setting `propagateRename: true` on a relationship end marks that end as the
**trigger** for rename propagation. When an
+entity on that end is renamed, Atlas traverses the relationship to dependent
entities and recomputes their qualifiedName
+using their `autoComputeFormat`.
+
+<SyntaxHighlighter wrapLines={true} language="json" style={theme.dark}>
+{`"relationshipDefs": [{
+ "name": "trino_table_hive_table",
+ "endDef1": {
+ "type": "hive_table",
+ "name": "trinoTables",
+ "propagateRename": true
+ },
+ "endDef2": {
+ "type": "trino_table",
+ "name": "hiveTable"
+ }
+}]`}
+</SyntaxHighlighter>
+
+An optional `propagateAttributes` list on the same end can map source
attributes to target attributes for cases where
+simple template substitution is not sufficient:
+
+<SyntaxHighlighter wrapLines={true} language="json" style={theme.dark}>
+{`"endDef1": {
+ "type": "hive_table",
+ "name": "trinoTables",
+ "propagateRename": true,
+ "propagateAttributes": [
+ { "source": "name", "target": "hiveTableName" }
+ ]
+}`}
+</SyntaxHighlighter>
+
+### Relationship end definition: propagateDelete
+
+Setting `propagateDelete: true` on a relationship end marks that end as the
**trigger** for delete propagation. When an
+entity on that end is deleted, Atlas propagates the deletion to dependent
entities reached through the relationship.
+
+<SyntaxHighlighter wrapLines={true} language="json" style={theme.dark}>
+{`"relationshipDefs": [{
+ "name": "trino_table_hive_table",
+ "endDef1": {
+ "type": "hive_table",
+ "name": "trinoTables",
+ "propagateRename": true,
+ "propagateDelete": true
+ },
+ "endDef2": {
+ "type": "trino_table",
+ "name": "hiveTable"
+ }
+}]`}
+</SyntaxHighlighter>
+
+In this example, deleting a `hive_table` entity will automatically propagate
deletion to any linked `trino_table`
+entities. If those targets also have delete propagation configured on their
own relationships (e.g.,
+`trino_table_columns`), the propagation continues recursively.
+
+### Summary of propagation fields
+
+| Field | Location | Purpose |
+|-------|----------|---------|
+| `attributeDefOverrides.autoComputeFormat` | Entity definition | Template for
computing qualifiedName from related entities |
+| `propagateRename` | Relationship end definition | Triggers qualifiedName
recomputation on dependents when this end is renamed |
+| `propagateAttributes` | Relationship end definition | Maps source attributes
to target attributes during rename |
+| `propagateDelete` | Relationship end definition | Triggers propagation of
deletion to dependents when this end is deleted |
\ No newline at end of file
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 c7921b6ca..568b7535f 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<Map<String, String>> 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 class AtlasRelationshipEndDef implements
Serializable {
setDescription(other.description);
setIsPropagateRename(other.propagateRename);
setPropagateAttributes(other.propagateAttributes);
+ setIsPropagateDelete(other.propagateDelete);
}
}
@@ -254,6 +264,7 @@ public class AtlasRelationshipEndDef implements
Serializable {
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 class AtlasRelationshipEndDef implements
Serializable {
@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 class AtlasRelationshipEndDef implements
Serializable {
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 class AtlasRelationshipEndDef implements
Serializable {
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 9309d1c25..922aada3d 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<RenamePropagationTarget>
renamePropagationTargets = Collections.emptyList();
+ private List<DeletePropagationTarget>
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 class AtlasEntityType extends AtlasStructType {
return renamePropagationTargets;
}
+ public List<DeletePropagationTarget> getDeletePropagationTargets() {
+ return deletePropagationTargets;
+ }
+
public Map<String, String> getAutoComputeFormatPathByRefTypeNameMap() {
return autoComputeFormatPathByRefTypeNameMap;
}
@@ -403,6 +408,7 @@ public class AtlasEntityType extends AtlasStructType {
tagPropagationEdges.addAll(superType.tagPropagationEdges);
renamePropagationTargets.addAll(superType.renamePropagationTargets);
+
deletePropagationTargets.addAll(superType.deletePropagationTargets);
}
ownedRefAttributes = new ArrayList<>();
@@ -430,6 +436,7 @@ public class AtlasEntityType extends AtlasStructType {
ownedRefAttributes =
Collections.unmodifiableList(ownedRefAttributes);
tagPropagationEdges =
Collections.unmodifiableSet(tagPropagationEdges);
renamePropagationTargets =
Collections.unmodifiableList(renamePropagationTargets);
+ deletePropagationTargets =
Collections.unmodifiableList(deletePropagationTargets);
entityDef.setSubTypes(subTypes);
@@ -580,6 +587,7 @@ public class AtlasEntityType extends AtlasStructType {
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 @@ public class AtlasEntityType extends AtlasStructType {
}
addRenamePropagationTargetIfTriggered(relationshipType, attribute);
+ addDeletePropagationTargetIfTriggered(relationshipType, attribute);
}
/**
@@ -1122,6 +1131,54 @@ public class AtlasEntityType extends AtlasStructType {
}
}
+ 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 000000000..66d267450
--- /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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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 118527501..1eabbcf84 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 class TestAtlasRelationshipEndDef {
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 000000000..6a3120d24
--- /dev/null
+++ b/intg/src/test/java/org/apache/atlas/type/TestDeletePropagationTarget.java
@@ -0,0 +1,122 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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 b7483f4ba..31e748096 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 @@ public class AtlasTypeDefStoreInitializer implements
ActiveStateChangeHandler {
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 class AtlasTypeDefStoreInitializer implements
ActiveStateChangeHandler {
}
/**
- * 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<String,String>} 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<String,String>}. */
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 class AtlasTypeDefStoreInitializer implements
ActiveStateChangeHandler {
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 class AtlasTypeDefStoreInitializer implements
ActiveStateChangeHandler {
}
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 @@ public class AtlasTypeDefStoreInitializer implements
ActiveStateChangeHandler {
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 @@ public class AtlasTypeDefStoreInitializer implements
ActiveStateChangeHandler {
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 @@ public class AtlasTypeDefStoreInitializer implements
ActiveStateChangeHandler {
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<Map<String,String>>}. */
private static void
setPropagateAttributesFromParamsIfPresent(Map<String, Object> 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 4477a3099..8ddd9372a 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.AtlasGraph;
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 abstract class DeleteHandlerV1 {
this.softDelete = softDelete;
this.taskManagement = taskManagement;
this.uniqueKeyHandler = graph.getUniqueKeyHandler();
+ this.deletePropagationHandler = new
EntityDeletePropagationHandler(typeRegistry);
}
/**
@@ -215,9 +218,46 @@ public abstract class DeleteHandlerV1 {
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<AtlasVertex> deletionCandidateVertices,
+ Set<String> visitedGuids)
throws AtlasBaseException {
+ Set<AtlasVertex> 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<AtlasVertex> 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 000000000..a5c45b870
--- /dev/null
+++
b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/EntityDeletePropagationHandler.java
@@ -0,0 +1,133 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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<AtlasVertex> collectPropagatedVertices(AtlasVertex
sourceVertex, AtlasEntityType entityType, Set<String> visitedGuids) {
+ Set<AtlasVertex> propagatedVertices = new HashSet<>();
+
+ collectPropagatedVerticesRecursive(sourceVertex, entityType,
visitedGuids, propagatedVertices);
+
+ return propagatedVertices;
+ }
+
+ private void collectPropagatedVerticesRecursive(AtlasVertex sourceVertex,
AtlasEntityType sourceEntityType,
+ Set<String> visitedGuids,
Set<AtlasVertex> 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 <T> List<T> toList(Iterator<T> iterator) {
+ List<T> 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 8e43bcb1c..6c1ce5fcf 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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
}
@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 class AtlasTypeDefStoreInitializerTest {
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<String, Object> params = new HashMap<>();
+ params.put("endDefToken", "endDef1");
+ setField(patch, "params", params);
+
+ PatchStatus result =
handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch);
+ assertEquals(result, APPLIED);
+
+ ArgumentCaptor<AtlasRelationshipDef> 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<String, Object> params = new HashMap<>();
+ params.put("endDefToken", "endDef2");
+ setField(patch, "params", params);
+
+ PatchStatus result =
handler.applyPatch((AtlasTypeDefStoreInitializer.TypeDefPatch) patch);
+ assertEquals(result, APPLIED);
+
+ ArgumentCaptor<AtlasRelationshipDef> 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<String, Object> 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<String, Object>());
+
+ 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<String, Object> 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<String, Object> 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 000000000..37769794d
--- /dev/null
+++
b/repository/src/test/java/org/apache/atlas/repository/store/graph/v2/EntityDeletePropagationHandlerTest.java
@@ -0,0 +1,476 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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<GraphHelper> graphHelperMock;
+ private MockedStatic<AtlasGraphUtilsV2> 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<String> visitedGuids = new HashSet<>();
+ Set<AtlasVertex> 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<String> visitedGuids = new HashSet<>();
+ Set<AtlasVertex> 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.<AtlasEdge>emptyList().iterator());
+
+ Set<String> visitedGuids = new HashSet<>();
+ Set<AtlasVertex> 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<String> visitedGuids = new HashSet<>();
+ Set<AtlasVertex> 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<String> visitedGuids = new HashSet<>();
+ Set<AtlasVertex> 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<String> visitedGuids = new HashSet<>();
+ visitedGuids.add("already-visited-guid");
+
+ Set<AtlasVertex> 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<String> visitedGuids = new HashSet<>();
+ Set<AtlasVertex> 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<String> visitedGuids = new HashSet<>();
+ Set<AtlasVertex> 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<String> visitedGuids = new HashSet<>();
+ visitedGuids.add("guid-A");
+
+ Set<AtlasVertex> 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<String> visitedGuids = new HashSet<>();
+ Set<AtlasVertex> 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<String> visitedGuids = new HashSet<>();
+ Set<AtlasVertex> result = handler.collectPropagatedVertices(srcVertex,
sourceType, visitedGuids);
+
+ assertTrue(result.isEmpty());
+ }
+
+ private void openStaticMocks() {
+ graphHelperMock = mockStatic(GraphHelper.class);
+ graphUtilsMock = mockStatic(AtlasGraphUtilsV2.class);
+ }
+}