diff --git a/bootstrap/sql/migrations/native/1.13.0/mysql/schemaChanges.sql b/bootstrap/sql/migrations/native/1.13.0/mysql/schemaChanges.sql
index ee41e5655fab..1d7d13817319 100644
--- a/bootstrap/sql/migrations/native/1.13.0/mysql/schemaChanges.sql
+++ b/bootstrap/sql/migrations/native/1.13.0/mysql/schemaChanges.sql
@@ -129,3 +129,13 @@ SELECT ue.id, re.id, 'user', 'role', 10
FROM user_entity ue, role_entity re
WHERE ue.name = 'mcpapplicationbot'
AND re.name = 'ApplicationBotImpersonationRole';
+
+-- Add FQN hash columns to entity_relationship to enable fast prefix-based bulk deletion.
+-- This allows deleting all relationships for an entire entity subtree in a single indexed query
+-- instead of walking the tree entity-by-entity.
+ALTER TABLE entity_relationship
+ ADD COLUMN fromFQNHash VARCHAR(768) DEFAULT NULL,
+ ADD COLUMN toFQNHash VARCHAR(768) DEFAULT NULL;
+
+CREATE INDEX idx_er_from_fqn_hash ON entity_relationship (fromFQNHash(768));
+CREATE INDEX idx_er_to_fqn_hash ON entity_relationship (toFQNHash(768));
diff --git a/bootstrap/sql/migrations/native/1.13.0/postgres/schemaChanges.sql b/bootstrap/sql/migrations/native/1.13.0/postgres/schemaChanges.sql
index 87918fb2a7d1..5aa37757b52a 100644
--- a/bootstrap/sql/migrations/native/1.13.0/postgres/schemaChanges.sql
+++ b/bootstrap/sql/migrations/native/1.13.0/postgres/schemaChanges.sql
@@ -150,3 +150,20 @@ FROM user_entity ue, role_entity re
WHERE ue.name = 'mcpapplicationbot'
AND re.name = 'ApplicationBotImpersonationRole'
ON CONFLICT DO NOTHING;
+
+-- Add FQN hash columns to entity_relationship to enable fast prefix-based bulk deletion.
+-- This allows deleting all relationships for an entire entity subtree in a single indexed query
+-- instead of walking the tree entity-by-entity.
+ALTER TABLE entity_relationship
+ ADD COLUMN IF NOT EXISTS fromFQNHash VARCHAR(768) DEFAULT NULL,
+ ADD COLUMN IF NOT EXISTS toFQNHash VARCHAR(768) DEFAULT NULL;
+
+CREATE INDEX IF NOT EXISTS idx_er_from_fqn_hash ON entity_relationship (fromFQNHash);
+CREATE INDEX IF NOT EXISTS idx_er_to_fqn_hash ON entity_relationship (toFQNHash);
+
+-- Fix entity_deletion_lock column types: id and entityId were created as native UUID
+-- in 1.9.0 but the rest of the codebase uses VARCHAR(36) for UUID columns so that
+-- BindUUID (which binds via UUID.toString()) can compare them without an explicit cast.
+ALTER TABLE entity_deletion_lock
+ ALTER COLUMN id TYPE VARCHAR(36) USING id::VARCHAR,
+ ALTER COLUMN entityId TYPE VARCHAR(36) USING entityId::VARCHAR;
diff --git a/bootstrap/sql/migrations/native/1.14.1/mysql/schemaChanges.sql b/bootstrap/sql/migrations/native/1.14.1/mysql/schemaChanges.sql
new file mode 100644
index 000000000000..3afbbac80252
--- /dev/null
+++ b/bootstrap/sql/migrations/native/1.14.1/mysql/schemaChanges.sql
@@ -0,0 +1 @@
+-- No schema changes in this version.
diff --git a/bootstrap/sql/migrations/native/1.14.1/postgres/schemaChanges.sql b/bootstrap/sql/migrations/native/1.14.1/postgres/schemaChanges.sql
new file mode 100644
index 000000000000..3afbbac80252
--- /dev/null
+++ b/bootstrap/sql/migrations/native/1.14.1/postgres/schemaChanges.sql
@@ -0,0 +1 @@
+-- No schema changes in this version.
diff --git a/bootstrap/sql/schema/mysql.sql b/bootstrap/sql/schema/mysql.sql
index c76d44ffb301..b809fcff2c13 100644
--- a/bootstrap/sql/schema/mysql.sql
+++ b/bootstrap/sql/schema/mysql.sql
@@ -352,9 +352,13 @@ CREATE TABLE `entity_relationship` (
`jsonSchema` varchar(256) DEFAULT NULL,
`json` json DEFAULT NULL,
`deleted` tinyint(1) NOT NULL DEFAULT '0',
+ `fromFQNHash` varchar(768) DEFAULT NULL,
+ `toFQNHash` varchar(768) DEFAULT NULL,
PRIMARY KEY (`fromId`,`toId`,`relation`),
KEY `from_index` (`fromId`,`relation`),
- KEY `to_index` (`toId`,`relation`)
+ KEY `to_index` (`toId`,`relation`),
+ KEY `idx_er_from_fqn_hash` (`fromFQNHash`(768)),
+ KEY `idx_er_to_fqn_hash` (`toFQNHash`(768))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
diff --git a/bootstrap/sql/schema/postgres.sql b/bootstrap/sql/schema/postgres.sql
index 0c6b64b116ed..7336a90ba23a 100644
--- a/bootstrap/sql/schema/postgres.sql
+++ b/bootstrap/sql/schema/postgres.sql
@@ -325,7 +325,9 @@ CREATE TABLE public.entity_relationship (
relation smallint NOT NULL,
jsonschema character varying(256),
json jsonb,
- deleted boolean DEFAULT false NOT NULL
+ deleted boolean DEFAULT false NOT NULL,
+ fromfqnhash character varying(768),
+ tofqnhash character varying(768)
);
@@ -1921,6 +1923,20 @@ CREATE INDEX entity_relationship_from_index ON public.entity_relationship USING
CREATE INDEX entity_relationship_to_index ON public.entity_relationship USING btree (toid, relation);
+--
+-- Name: idx_er_from_fqn_hash; Type: INDEX; Schema: public; Owner: openmetadata_user
+--
+
+CREATE INDEX idx_er_from_fqn_hash ON public.entity_relationship USING btree (fromfqnhash);
+
+
+--
+-- Name: idx_er_to_fqn_hash; Type: INDEX; Schema: public; Owner: openmetadata_user
+--
+
+CREATE INDEX idx_er_to_fqn_hash ON public.entity_relationship USING btree (tofqnhash);
+
+
--
-- Name: field_relationship_from_index; Type: INDEX; Schema: public; Owner: openmetadata_user
--
diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/factories/DatabaseSchemaTestFactory.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/factories/DatabaseSchemaTestFactory.java
index c3910fb9d9fe..b767160728aa 100644
--- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/factories/DatabaseSchemaTestFactory.java
+++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/factories/DatabaseSchemaTestFactory.java
@@ -49,6 +49,14 @@ public static DatabaseSchema createSimple(TestNamespace ns) {
return createSimple(ns, service);
}
+ /**
+ * Create a schema with a namespaced base name using fluent API.
+ */
+ public static DatabaseSchema createWithName(
+ TestNamespace ns, String databaseFqn, String baseName) {
+ return DatabaseSchemas.create().name(ns.prefix(baseName)).in(databaseFqn).execute();
+ }
+
/**
* Create a schema with a custom name using fluent API.
* Useful for tests that need short names to avoid FQN length limits.
diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/factories/DatabaseTestFactory.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/factories/DatabaseTestFactory.java
index fb7e4427f789..ac7c8fdc9aab 100644
--- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/factories/DatabaseTestFactory.java
+++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/factories/DatabaseTestFactory.java
@@ -29,4 +29,15 @@ public static Database create(TestNamespace ns, String serviceFqn) {
public static Database createWithName(String serviceFqn, String name) {
return Databases.create().name(name).in(serviceFqn).execute();
}
+
+ /**
+ * Create database with a namespaced base name using fluent API.
+ */
+ public static Database createWithName(TestNamespace ns, String serviceFqn, String baseName) {
+ return Databases.create()
+ .name(ns.prefix(baseName))
+ .in(serviceFqn)
+ .withDescription("Test database created by integration test")
+ .execute();
+ }
}
diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/PrefixDeletionBenchmarkIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/PrefixDeletionBenchmarkIT.java
new file mode 100644
index 000000000000..cb02ea0d376d
--- /dev/null
+++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/PrefixDeletionBenchmarkIT.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2021 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmetadata.it.tests;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.openmetadata.it.factories.DatabaseSchemaTestFactory;
+import org.openmetadata.it.factories.DatabaseServiceTestFactory;
+import org.openmetadata.it.factories.DatabaseTestFactory;
+import org.openmetadata.it.factories.TableTestFactory;
+import org.openmetadata.it.util.SdkClients;
+import org.openmetadata.it.util.TestNamespace;
+import org.openmetadata.it.util.TestNamespaceExtension;
+import org.openmetadata.schema.entity.data.Database;
+import org.openmetadata.schema.entity.data.DatabaseSchema;
+import org.openmetadata.schema.entity.services.DatabaseService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Benchmark comparing old recursive hard delete vs new FQN prefix hard delete.
+ *
+ *
Default topology: 5 databases × 5 schemas × 400 tables = 10,000 tables per service
+ * (~10,031 total entities including service, databases, schemas).
+ *
+ *
Run manually against a local stack:
+ *
+ *
+ * mvn verify -pl openmetadata-integration-tests \
+ * -Dgroups=benchmark \
+ * -Dit.test=PrefixDeletionBenchmarkIT \
+ * -Dtest.databases=5 # databases per service (default: 5)
+ * -Dtest.schemas=5 # schemas per database (default: 5)
+ * -Dtest.tables=400 # tables per schema (default: 400)
+ * -Dtest.seedThreads=32 # parallel seed threads (default: 32)
+ *
+ *
+ * NOTE: Setup creates entities in parallel (default 32 threads, tunable via
+ * -Dtest.seedThreads). At ~50ms/call and 32 threads, 10k tables seed in ~20 s.
+ *
+ *
Both deletions are timed end-to-end: the old delete is synchronous; the new prefix
+ * delete is async (202), so we poll until the service is gone before recording elapsed time.
+ */
+@Tag("benchmark")
+@Disabled("Manual benchmark — run explicitly against a local mysql/postgres stack")
+@ExtendWith(TestNamespaceExtension.class)
+class PrefixDeletionBenchmarkIT {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PrefixDeletionBenchmarkIT.class);
+
+ private static final int DATABASES_PER_SERVICE = Integer.getInteger("test.databases", 5);
+ private static final int SCHEMAS_PER_DATABASE = Integer.getInteger("test.schemas", 5);
+ private static final int TABLES_PER_SCHEMA = Integer.getInteger("test.tables", 400);
+ private static final int SEED_THREADS = Integer.getInteger("test.seedThreads", 32);
+
+ private static final Duration DELETE_POLL_TIMEOUT = Duration.ofMinutes(10);
+ private static final Duration DELETE_POLL_INTERVAL = Duration.ofSeconds(2);
+
+ @BeforeAll
+ static void setup() {
+ SdkClients.adminClient();
+ }
+
+ @Test
+ void benchmark_oldRecursiveHardDelete_vs_newPrefixDelete(TestNamespace ns) throws Exception {
+ int totalTables = DATABASES_PER_SERVICE * SCHEMAS_PER_DATABASE * TABLES_PER_SCHEMA;
+ int totalEntities =
+ 1 + DATABASES_PER_SERVICE + DATABASES_PER_SERVICE * SCHEMAS_PER_DATABASE + totalTables;
+ LOG.info(
+ "Benchmark topology: {} databases × {} schemas × {} tables = {} tables, {} total entities per service",
+ DATABASES_PER_SERVICE,
+ SCHEMAS_PER_DATABASE,
+ TABLES_PER_SCHEMA,
+ totalTables,
+ totalEntities);
+
+ DatabaseService oldService = buildHierarchy(ns, "old");
+ long oldMs = timeOldDelete(oldService);
+
+ DatabaseService newService = buildHierarchy(ns, "new");
+ long newMs = timeNewDelete(newService);
+
+ double speedup = (double) oldMs / Math.max(newMs, 1);
+ LOG.info("=== Deletion Benchmark Results ({} entities per service) ===", totalEntities);
+ LOG.info(" Old recursive hard delete : {} ms", oldMs);
+ LOG.info(" New FQN prefix hard delete: {} ms", newMs);
+ LOG.info(" Speedup : {}x", String.format("%.2f", speedup));
+ }
+
+ private DatabaseService buildHierarchy(TestNamespace ns, String tag) throws Exception {
+ DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns);
+ int totalEntities =
+ 1
+ + DATABASES_PER_SERVICE
+ + DATABASES_PER_SERVICE * SCHEMAS_PER_DATABASE
+ + DATABASES_PER_SERVICE * SCHEMAS_PER_DATABASE * TABLES_PER_SCHEMA;
+ LOG.info(
+ "[{}] Seeding {} entities under service {} using {} threads ...",
+ tag,
+ totalEntities,
+ service.getName(),
+ SEED_THREADS);
+ long seedStart = System.currentTimeMillis();
+
+ ExecutorService pool = Executors.newFixedThreadPool(SEED_THREADS);
+ try {
+ List> dbFutures = new ArrayList<>();
+ for (int d = 0; d < DATABASES_PER_SERVICE; d++) {
+ final int dIdx = d;
+ dbFutures.add(
+ pool.submit(
+ () ->
+ DatabaseTestFactory.createWithName(
+ ns, service.getFullyQualifiedName(), tag + "db" + dIdx)));
+ }
+ List databases = new ArrayList<>();
+ for (Future f : dbFutures) {
+ databases.add(f.get());
+ }
+
+ List> schemaFutures = new ArrayList<>();
+ for (int d = 0; d < databases.size(); d++) {
+ final Database database = databases.get(d);
+ final int dIdx = d;
+ for (int s = 0; s < SCHEMAS_PER_DATABASE; s++) {
+ final int sIdx = s;
+ schemaFutures.add(
+ pool.submit(
+ () ->
+ DatabaseSchemaTestFactory.createWithName(
+ ns, database.getFullyQualifiedName(), tag + "sc" + dIdx + "x" + sIdx)));
+ }
+ }
+ List schemas = new ArrayList<>();
+ for (Future f : schemaFutures) {
+ schemas.add(f.get());
+ }
+
+ List> tableFutures = new ArrayList<>();
+ for (int s = 0; s < schemas.size(); s++) {
+ final DatabaseSchema schema = schemas.get(s);
+ final int sIdx = s;
+ for (int t = 0; t < TABLES_PER_SCHEMA; t++) {
+ final int tIdx = t;
+ tableFutures.add(
+ pool.submit(
+ () -> {
+ TableTestFactory.createWithName(
+ ns, schema.getFullyQualifiedName(), tag + "tbl" + sIdx + "x" + tIdx);
+ return null;
+ }));
+ }
+ }
+ for (Future> f : tableFutures) {
+ f.get();
+ }
+ } finally {
+ pool.shutdown();
+ pool.awaitTermination(30, TimeUnit.MINUTES);
+ }
+
+ long seedMs = System.currentTimeMillis() - seedStart;
+ LOG.info(
+ "[{}] Hierarchy seeded in {} ms ({} ms/entity avg)",
+ tag,
+ seedMs,
+ seedMs / Math.max(totalEntities, 1));
+ return service;
+ }
+
+ private long timeOldDelete(DatabaseService service) throws Exception {
+ LOG.info("Timing OLD recursive hard delete for service {} ...", service.getName());
+ long start = System.currentTimeMillis();
+
+ String url =
+ SdkClients.getServerUrl()
+ + "/v1/services/databaseServices/"
+ + service.getId()
+ + "?hardDelete=true&recursive=true";
+ sendDelete(url);
+
+ long elapsed = System.currentTimeMillis() - start;
+ LOG.info("OLD recursive hard delete completed in {} ms", elapsed);
+ return elapsed;
+ }
+
+ private long timeNewDelete(DatabaseService service) throws Exception {
+ LOG.info("Timing NEW FQN prefix hard delete for service {} ...", service.getName());
+ long start = System.currentTimeMillis();
+
+ String url =
+ SdkClients.getServerUrl() + "/v1/services/databaseServices/prefix/" + service.getId();
+ sendDelete(url);
+
+ // Prefix delete is async — poll until the service is actually gone so we measure
+ // real deletion time, not just the time to hand off the job to the executor.
+ UUID serviceId = service.getId();
+ Awaitility.await("Wait for prefix deletion of " + service.getName() + " to complete")
+ .atMost(DELETE_POLL_TIMEOUT)
+ .pollInterval(DELETE_POLL_INTERVAL)
+ .until(
+ () -> {
+ try {
+ SdkClients.adminClient().databaseServices().get(serviceId.toString());
+ return false;
+ } catch (Exception e) {
+ return true;
+ }
+ });
+
+ long elapsed = System.currentTimeMillis() - start;
+ LOG.info("NEW FQN prefix hard delete completed in {} ms", elapsed);
+ return elapsed;
+ }
+
+ private void sendDelete(String url) throws Exception {
+ HttpRequest request =
+ HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .header("Authorization", "Bearer " + SdkClients.getAdminToken())
+ .DELETE()
+ .build();
+ HttpResponse response =
+ HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() >= 300) {
+ throw new RuntimeException(
+ "Delete failed with status " + response.statusCode() + ": " + response.body());
+ }
+ }
+}
diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/PrefixDeletionIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/PrefixDeletionIT.java
new file mode 100644
index 000000000000..7d2b51d59086
--- /dev/null
+++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/PrefixDeletionIT.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2021 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmetadata.it.tests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.openmetadata.it.factories.DatabaseSchemaTestFactory;
+import org.openmetadata.it.factories.DatabaseServiceTestFactory;
+import org.openmetadata.it.factories.DatabaseTestFactory;
+import org.openmetadata.it.factories.TableTestFactory;
+import org.openmetadata.it.util.SdkClients;
+import org.openmetadata.it.util.TestNamespace;
+import org.openmetadata.it.util.TestNamespaceExtension;
+import org.openmetadata.schema.entity.data.Database;
+import org.openmetadata.schema.entity.data.DatabaseSchema;
+import org.openmetadata.schema.entity.data.Table;
+import org.openmetadata.schema.entity.services.DatabaseService;
+
+/**
+ * Integration tests for the FQN prefix-based hard deletion endpoints at each hierarchy level:
+ *
+ *
+ * - {@code DELETE /v1/services/databaseServices/prefix/{id}} — deletes service + all descendants
+ *
- {@code DELETE /v1/databases/prefix/{id}} — deletes database + schemas + tables, leaving
+ * sibling databases intact
+ *
- {@code DELETE /v1/databaseSchemas/prefix/{id}} — deletes schema + tables, leaving sibling
+ * schemas intact
+ *
+ *
+ * The endpoint is async (returns 202 with a jobId). All assertions use Awaitility to poll until
+ * the background deletion actually completes.
+ */
+@ExtendWith(TestNamespaceExtension.class)
+public class PrefixDeletionIT {
+
+ private static final Duration DELETE_TIMEOUT = Duration.ofSeconds(30);
+ private static final Duration POLL_INTERVAL = Duration.ofSeconds(1);
+
+ @BeforeAll
+ static void setup() {
+ SdkClients.adminClient();
+ }
+
+ // ── Service-level ────────────────────────────────────────────────────────────
+
+ @Test
+ void prefixDelete_service_removesServiceAndAllDescendants(TestNamespace ns) throws Exception {
+ DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns);
+ Database database =
+ DatabaseTestFactory.createWithName(ns, service.getFullyQualifiedName(), "db");
+ DatabaseSchema schema =
+ DatabaseSchemaTestFactory.createWithName(ns, database.getFullyQualifiedName(), "sc");
+
+ List tableIds = new ArrayList<>();
+ for (int i = 0; i < 5; i++) {
+ Table table = TableTestFactory.createWithName(ns, schema.getFullyQualifiedName(), "t" + i);
+ tableIds.add(table.getId());
+ }
+
+ prefixDelete("/v1/services/databaseServices/prefix/", service.getId());
+
+ awaitGone(
+ "service",
+ () -> SdkClients.adminClient().databaseServices().get(service.getId().toString()));
+ awaitGone(
+ "database", () -> SdkClients.adminClient().databases().get(database.getId().toString()));
+ awaitGone(
+ "schema", () -> SdkClients.adminClient().databaseSchemas().get(schema.getId().toString()));
+ for (UUID tableId : tableIds) {
+ awaitGone("table", () -> SdkClients.adminClient().tables().get(tableId.toString()));
+ }
+ }
+
+ @Test
+ void prefixDelete_service_withMultipleDatabasesAndSchemas(TestNamespace ns) throws Exception {
+ DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns);
+
+ List dbIds = new ArrayList<>();
+ List schemaIds = new ArrayList<>();
+ List tableIds = new ArrayList<>();
+
+ for (int d = 0; d < 2; d++) {
+ Database database =
+ DatabaseTestFactory.createWithName(ns, service.getFullyQualifiedName(), "db" + d);
+ dbIds.add(database.getId());
+ for (int s = 0; s < 2; s++) {
+ DatabaseSchema schema =
+ DatabaseSchemaTestFactory.createWithName(
+ ns, database.getFullyQualifiedName(), "sc" + d + s);
+ schemaIds.add(schema.getId());
+ for (int t = 0; t < 3; t++) {
+ Table table =
+ TableTestFactory.createWithName(ns, schema.getFullyQualifiedName(), "t" + d + s + t);
+ tableIds.add(table.getId());
+ }
+ }
+ }
+
+ prefixDelete("/v1/services/databaseServices/prefix/", service.getId());
+
+ awaitGone(
+ "service",
+ () -> SdkClients.adminClient().databaseServices().get(service.getId().toString()));
+ for (UUID id : dbIds) {
+ awaitGone("database", () -> SdkClients.adminClient().databases().get(id.toString()));
+ }
+ for (UUID id : schemaIds) {
+ awaitGone("schema", () -> SdkClients.adminClient().databaseSchemas().get(id.toString()));
+ }
+ for (UUID id : tableIds) {
+ awaitGone("table", () -> SdkClients.adminClient().tables().get(id.toString()));
+ }
+ }
+
+ // ── Database-level ────────────────────────────────────────────────────────────
+
+ @Test
+ void prefixDelete_database_removesDatabaseAndDescendantsLeavingSiblingDatabaseIntact(
+ TestNamespace ns) throws Exception {
+ DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns);
+
+ Database targetDb =
+ DatabaseTestFactory.createWithName(ns, service.getFullyQualifiedName(), "tgtdb");
+ DatabaseSchema targetSchema =
+ DatabaseSchemaTestFactory.createWithName(ns, targetDb.getFullyQualifiedName(), "tgtsc");
+ Table targetTable =
+ TableTestFactory.createWithName(ns, targetSchema.getFullyQualifiedName(), "tgttbl");
+
+ Database siblingDb =
+ DatabaseTestFactory.createWithName(ns, service.getFullyQualifiedName(), "sibdb");
+ DatabaseSchema siblingSchema =
+ DatabaseSchemaTestFactory.createWithName(ns, siblingDb.getFullyQualifiedName(), "sibsc");
+ Table siblingTable =
+ TableTestFactory.createWithName(ns, siblingSchema.getFullyQualifiedName(), "sibtbl");
+
+ prefixDelete("/v1/databases/prefix/", targetDb.getId());
+
+ awaitGone(
+ "target database",
+ () -> SdkClients.adminClient().databases().get(targetDb.getId().toString()));
+ awaitGone(
+ "target schema",
+ () -> SdkClients.adminClient().databaseSchemas().get(targetSchema.getId().toString()));
+ awaitGone(
+ "target table",
+ () -> SdkClients.adminClient().tables().get(targetTable.getId().toString()));
+
+ assertNotNull(
+ SdkClients.adminClient().databases().get(siblingDb.getId().toString()),
+ "sibling database should survive");
+ assertNotNull(
+ SdkClients.adminClient().databaseSchemas().get(siblingSchema.getId().toString()),
+ "sibling schema should survive");
+ assertNotNull(
+ SdkClients.adminClient().tables().get(siblingTable.getId().toString()),
+ "sibling table should survive");
+ assertNotNull(
+ SdkClients.adminClient().databaseServices().get(service.getId().toString()),
+ "service should survive");
+ }
+
+ // ── Schema-level ─────────────────────────────────────────────────────────────
+
+ @Test
+ void prefixDelete_schema_removesSchemaAndTablesLeavingSiblingSchemaIntact(TestNamespace ns)
+ throws Exception {
+ DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns);
+ Database database =
+ DatabaseTestFactory.createWithName(ns, service.getFullyQualifiedName(), "db");
+
+ DatabaseSchema targetSchema =
+ DatabaseSchemaTestFactory.createWithName(ns, database.getFullyQualifiedName(), "tgtsc");
+ List targetTableIds = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ Table table =
+ TableTestFactory.createWithName(ns, targetSchema.getFullyQualifiedName(), "tgt" + i);
+ targetTableIds.add(table.getId());
+ }
+
+ DatabaseSchema siblingSchema =
+ DatabaseSchemaTestFactory.createWithName(ns, database.getFullyQualifiedName(), "sibsc");
+ Table siblingTable =
+ TableTestFactory.createWithName(ns, siblingSchema.getFullyQualifiedName(), "sibtbl");
+
+ prefixDelete("/v1/databaseSchemas/prefix/", targetSchema.getId());
+
+ awaitGone(
+ "target schema",
+ () -> SdkClients.adminClient().databaseSchemas().get(targetSchema.getId().toString()));
+ for (UUID tableId : targetTableIds) {
+ awaitGone("target table", () -> SdkClients.adminClient().tables().get(tableId.toString()));
+ }
+
+ assertNotNull(
+ SdkClients.adminClient().databaseSchemas().get(siblingSchema.getId().toString()),
+ "sibling schema should survive");
+ assertNotNull(
+ SdkClients.adminClient().tables().get(siblingTable.getId().toString()),
+ "sibling table should survive");
+ assertNotNull(
+ SdkClients.adminClient().databases().get(database.getId().toString()),
+ "database should survive");
+ assertNotNull(
+ SdkClients.adminClient().databaseServices().get(service.getId().toString()),
+ "service should survive");
+ }
+
+ // ── Helpers ───────────────────────────────────────────────────────────────────
+
+ private void prefixDelete(String path, UUID id) throws Exception {
+ String url = SdkClients.getServerUrl() + path + id;
+ HttpRequest request =
+ HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .header("Authorization", "Bearer " + SdkClients.getAdminToken())
+ .DELETE()
+ .build();
+
+ HttpResponse response =
+ HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
+ assertEquals(
+ 202,
+ response.statusCode(),
+ "Expected 202 Accepted from prefix delete "
+ + path
+ + id
+ + ", got "
+ + response.statusCode()
+ + ": "
+ + response.body());
+ }
+
+ /**
+ * Polls until the given fetch throws (entity gone) or the timeout elapses.
+ * The prefix delete API is async — the 202 only means the job was queued.
+ */
+ private void awaitGone(String entityType, ThrowingSupplier> fetch) {
+ Awaitility.await("Wait for " + entityType + " to be deleted")
+ .atMost(DELETE_TIMEOUT)
+ .pollInterval(POLL_INTERVAL)
+ .until(
+ () -> {
+ try {
+ fetch.get();
+ return false;
+ } catch (Exception e) {
+ return true;
+ }
+ });
+ }
+
+ @FunctionalInterface
+ private interface ThrowingSupplier {
+ T get() throws Exception;
+ }
+}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java
index 4b0eab6398da..b76d094d231f 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java
@@ -102,6 +102,7 @@
import org.openmetadata.service.exception.OMErrorPageHandler;
import org.openmetadata.service.fernet.Fernet;
import org.openmetadata.service.governance.workflows.WorkflowHandler;
+import org.openmetadata.service.initialization.LockManagerInitializer;
import org.openmetadata.service.jdbi3.BulkExecutor;
import org.openmetadata.service.jdbi3.CollectionDAO;
import org.openmetadata.service.jdbi3.EntityRelationshipRepository;
@@ -275,6 +276,8 @@ public void run(OpenMetadataApplicationConfig catalogConfig, Environment environ
ResourceRegistry.addResource(
Entity.AUDIT_LOG, List.of(MetadataOperation.AUDIT_LOGS), Collections.emptySet());
+ LockManagerInitializer.initialize();
+
// Configure the Fernet instance
Fernet.getInstance().setFernetKey(catalogConfig);
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/initialization/LockManagerInitializer.java b/openmetadata-service/src/main/java/org/openmetadata/service/initialization/LockManagerInitializer.java
index 93cab31a2be0..4886d7837ff9 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/initialization/LockManagerInitializer.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/initialization/LockManagerInitializer.java
@@ -3,6 +3,7 @@
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.service.Entity;
import org.openmetadata.service.jdbi3.EntityRepository;
+import org.openmetadata.service.jdbi3.PrefixDeletionService;
import org.openmetadata.service.lock.HierarchicalLockManager;
/**
@@ -44,6 +45,9 @@ public static void initialize() {
// Set it on EntityRepository
EntityRepository.setLockManager(lockManager);
+ // Initialize PrefixDeletionService with the same lock manager
+ PrefixDeletionService.initialize(lockManager);
+
initialized = true;
LOG.info("Hierarchical lock manager initialized successfully");
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java
index 561e405b7dc9..d1df298eb95b 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java
@@ -1419,7 +1419,14 @@ List getExtensionsWithOffset(
void deleteAll(@BindUUID("id") UUID id);
@SqlUpdate("DELETE FROM entity_extension WHERE id IN ()")
- void deleteAllBatch(@BindList("ids") List ids);
+ void deleteAllBatchChunk(@BindList("ids") List ids);
+
+ default void deleteAllBatch(List ids) {
+ int chunkSize = 50_000;
+ for (int i = 0; i < ids.size(); i += chunkSize) {
+ deleteAllBatchChunk(ids.subList(i, Math.min(i + chunkSize, ids.size())));
+ }
+ }
}
class EntityVersionPair {
@@ -1556,15 +1563,21 @@ default void bulkRemoveFromRelationship(
@ConnectionAwareSqlUpdate(
value =
- "INSERT INTO entity_relationship(fromId, toId, fromEntity, toEntity, relation, json) "
- + "VALUES (:fromId, :toId, :fromEntity, :toEntity, :relation, :json) "
- + "ON DUPLICATE KEY UPDATE json = :json",
+ "INSERT INTO entity_relationship"
+ + "(fromId, toId, fromEntity, toEntity, relation, json, fromFQNHash, toFQNHash) "
+ + "VALUES (:fromId, :toId, :fromEntity, :toEntity, :relation, :json, :fromFQNHash, :toFQNHash) "
+ + "ON DUPLICATE KEY UPDATE json = :json, "
+ + "fromFQNHash = COALESCE(:fromFQNHash, fromFQNHash), "
+ + "toFQNHash = COALESCE(:toFQNHash, toFQNHash)",
connectionType = MYSQL)
@ConnectionAwareSqlUpdate(
value =
- "INSERT INTO entity_relationship(fromId, toId, fromEntity, toEntity, relation, json) VALUES "
- + "(:fromId, :toId, :fromEntity, :toEntity, :relation, (:json :: jsonb)) "
- + "ON CONFLICT (fromId, toId, relation) DO UPDATE SET json = EXCLUDED.json",
+ "INSERT INTO entity_relationship"
+ + "(fromId, toId, fromEntity, toEntity, relation, json, fromFQNHash, toFQNHash) "
+ + "VALUES (:fromId, :toId, :fromEntity, :toEntity, :relation, (:json :: jsonb), :fromFQNHash, :toFQNHash) "
+ + "ON CONFLICT (fromId, toId, relation) DO UPDATE SET json = EXCLUDED.json, "
+ + "fromFQNHash = COALESCE(EXCLUDED.fromFQNHash, entity_relationship.fromFQNHash), "
+ + "toFQNHash = COALESCE(EXCLUDED.toFQNHash, entity_relationship.toFQNHash)",
connectionType = POSTGRES)
void insert(
@BindUUID("fromId") UUID fromId,
@@ -1572,7 +1585,14 @@ void insert(
@Bind("fromEntity") String fromEntity,
@Bind("toEntity") String toEntity,
@Bind("relation") int relation,
- @Bind("json") String json);
+ @Bind("json") String json,
+ @Bind("fromFQNHash") String fromFQNHash,
+ @Bind("toFQNHash") String toFQNHash);
+
+ default void insert(
+ UUID fromId, UUID toId, String fromEntity, String toEntity, int relation, String json) {
+ insert(fromId, toId, fromEntity, toEntity, relation, json, null, null);
+ }
@ConnectionAwareSqlUpdate(
value =
@@ -2421,6 +2441,32 @@ default void batchDeleteRelationships(List entityIds, String entityType) {
@SqlUpdate("DELETE from entity_relationship WHERE fromId = :id or toId = :id")
void deleteAllWithId(@BindUUID("id") UUID id);
+ @SqlUpdate(
+ "UPDATE entity_relationship SET fromFQNHash = :fqnHash "
+ + "WHERE fromId = :id AND fromFQNHash IS NULL")
+ void backfillFromFqnHash(@Bind("id") String id, @Bind("fqnHash") String fqnHash);
+
+ @SqlUpdate(
+ "UPDATE entity_relationship SET toFQNHash = :fqnHash "
+ + "WHERE toId = :id AND toFQNHash IS NULL")
+ void backfillToFqnHash(@Bind("id") String id, @Bind("fqnHash") String fqnHash);
+
+ @SqlUpdate(
+ "DELETE FROM entity_relationship WHERE fromFQNHash = :exact OR fromFQNHash LIKE :prefix")
+ void deleteByFromFqnHashPrefix(
+ @Bind("exact") String exactHash, @Bind("prefix") String prefixPattern);
+
+ @SqlUpdate("DELETE FROM entity_relationship WHERE toFQNHash = :exact OR toFQNHash LIKE :prefix")
+ void deleteByToFqnHashPrefix(
+ @Bind("exact") String exactHash, @Bind("prefix") String prefixPattern);
+
+ @Transaction
+ default void deleteAllByFqnHashPrefix(String fqnHashPrefix) {
+ String prefixPattern = fqnHashPrefix + ".%";
+ deleteByFromFqnHashPrefix(fqnHashPrefix, prefixPattern);
+ deleteByToFqnHashPrefix(fqnHashPrefix, prefixPattern);
+ }
+
@ConnectionAwareSqlUpdate(
value =
"DELETE FROM entity_relationship "
@@ -3195,6 +3241,19 @@ List> listCountThreadsByGlossaryAndTerms(
@SqlQuery("select id from thread_entity where entityId = :entityId")
List findByEntityId(@Bind("entityId") String entityId);
+ @SqlQuery("SELECT id FROM thread_entity WHERE entityId IN ()")
+ List findByEntityIdsChunk(@BindList("entityIds") List entityIds);
+
+ default List findByEntityIds(List entityIds) {
+ int chunkSize = 50_000;
+ List result = new ArrayList<>();
+ for (int i = 0; i < entityIds.size(); i += chunkSize) {
+ result.addAll(
+ findByEntityIdsChunk(entityIds.subList(i, Math.min(i + chunkSize, entityIds.size()))));
+ }
+ return result;
+ }
+
@ConnectionAwareSqlUpdate(
value =
"UPDATE thread_entity SET json = JSON_SET(json, '$.about', :newEntityLink)\n"
@@ -6168,6 +6227,16 @@ List getUsageById(
@SqlUpdate("DELETE FROM entity_usage WHERE id = :id")
void delete(@BindUUID("id") UUID id);
+ @SqlUpdate("DELETE FROM entity_usage WHERE id IN ()")
+ void deleteBatchChunk(@BindList("ids") List ids);
+
+ default void deleteBatch(List ids) {
+ int chunkSize = 50_000;
+ for (int i = 0; i < ids.size(); i += chunkSize) {
+ deleteBatchChunk(ids.subList(i, Math.min(i + chunkSize, ids.size())));
+ }
+ }
+
/**
* TODO: Not sure I get what the next comment means, but tests now use mysql 8 so maybe tests can be improved here
* Note not using in following percentile computation PERCENT_RANK function as unit tests use mysql5.7, and it does
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java
index cbc426e6c81a..08018062026e 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java
@@ -1725,6 +1725,21 @@ public FeedRepository.TaskWorkflow getTaskWorkflow(FeedRepository.ThreadContext
return super.getTaskWorkflow(threadContext);
}
+ @Override
+ protected void preDeleteByFqnHashPrefix(String fqnHashPrefix, String deletedBy) {
+ List ids = getDao().findIdsByFqnHashPrefix(fqnHashPrefix);
+ for (UUID id : ids) {
+ try {
+ DataContract contract = find(id, Include.ALL);
+ if (!nullOrEmpty(contract.getQualityExpectations())) {
+ deleteTestSuite(contract);
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed cleanup for data contract {}: {}", id, e.getMessage());
+ }
+ }
+ }
+
@Override
protected void preDelete(DataContract entity, String deletedBy) {
// Inherited contracts cannot be deleted - they are virtual contracts derived from Data Product
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DatabaseSchemaRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DatabaseSchemaRepository.java
index 205c84e54095..8106cd2169fc 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DatabaseSchemaRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DatabaseSchemaRepository.java
@@ -129,10 +129,14 @@ public void storeRelationships(DatabaseSchema schema) {
EntityReference database = schema.getDatabase();
addRelationship(
database.getId(),
+ database.getFullyQualifiedName(),
schema.getId(),
+ schema.getFullyQualifiedName(),
database.getType(),
Entity.DATABASE_SCHEMA,
- Relationship.CONTAINS);
+ Relationship.CONTAINS,
+ null,
+ false);
}
@Override
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityDAO.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityDAO.java
index 0e8c8d69e562..8e8d918ec70a 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityDAO.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityDAO.java
@@ -191,6 +191,19 @@ default void updateFqn(String oldPrefix, String newPrefix) {
void updateFqnInternal(
@Define("mySqlUpdate") String mySqlUpdate, @Define("postgresUpdate") String postgresUpdate);
+ @SqlQuery("SELECT id FROM WHERE LIKE :prefix")
+ List findIdsByFqnHashPrefixInternal(
+ @Define("table") String table, @Define("col") String col, @Bind("prefix") String prefix);
+
+ default List findIdsByFqnHashPrefix(String fqnHashPrefix) {
+ if (!"fqnHash".equals(getNameHashColumn())) {
+ return List.of();
+ }
+ return findIdsByFqnHashPrefixInternal(getTableName(), "fqnHash", fqnHashPrefix + ".%").stream()
+ .map(UUID::fromString)
+ .toList();
+ }
+
@SqlQuery("SELECT json FROM WHERE id = :id ")
String findById(
@Define("table") String table, @BindUUID("id") UUID id, @Define("cond") String cond);
@@ -686,6 +699,16 @@ default void delete(UUID id) {
}
}
+ @SqlUpdate("DELETE FROM WHERE id IN ()")
+ void deleteBatchInternal(@Define("table") String table, @BindList("ids") List ids);
+
+ default void deleteBatch(List ids) {
+ if (ids == null || ids.isEmpty()) {
+ return;
+ }
+ deleteBatchInternal(getTableName(), ids);
+ }
+
record EntityNameColumnHashJsonPair(String nameColumnHash, String json) {}
class EntityNameColumnHashJsonPairMapper implements RowMapper {
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java
index 2e1ab6552f44..539923c9d2df 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java
@@ -422,6 +422,10 @@ public static void setLockManager(HierarchicalLockManager manager) {
lockManager = manager;
}
+ public static HierarchicalLockManager getLockManager() {
+ return lockManager;
+ }
+
public boolean isSupportsOwners() {
return supportsOwners;
}
@@ -1105,7 +1109,15 @@ protected void setInheritedFields(List entities, Fields fields) {
protected final void addServiceRelationship(T entity, EntityReference service) {
if (service != null) {
addRelationship(
- service.getId(), entity.getId(), service.getType(), entityType, Relationship.CONTAINS);
+ service.getId(),
+ service.getFullyQualifiedName(),
+ entity.getId(),
+ entity.getFullyQualifiedName(),
+ service.getType(),
+ entityType,
+ Relationship.CONTAINS,
+ null,
+ false);
}
}
@@ -3435,6 +3447,19 @@ protected void postDelete(T entity, boolean hardDelete) {
}
}
+ /**
+ * Called once per entity type before prefix deletion removes entities from the DB.
+ * Override for bulk-efficient cleanup (e.g. calling an external system like Airflow).
+ * Entities matching the prefix are still in the DB when this runs.
+ */
+ protected void preDeleteByFqnHashPrefix(String fqnHashPrefix, String deletedBy) {}
+
+ /**
+ * Called once per entity type after prefix deletion has removed entities from the DB.
+ * Override for post-deletion metadata updates that do not require loading the deleted entities.
+ */
+ protected void postDeleteByFqnHashPrefix(String fqnHashPrefix) {}
+
public final void deleteFromSearch(T entity, boolean hardDelete) {
try (var ignored = phase("lifecycleDispatch")) {
if (hardDelete) {
@@ -3785,6 +3810,16 @@ protected final void cleanup(T entityInterface) {
protected void entitySpecificCleanup(T entityInterface) {}
+ @SuppressWarnings("unchecked")
+ final void callPreDelete(EntityInterface entity, String deletedBy) {
+ preDelete((T) entity, deletedBy);
+ }
+
+ @SuppressWarnings("unchecked")
+ final void invalidateEntity(EntityInterface entity) {
+ invalidate((T) entity);
+ }
+
private void invalidate(T entity) {
CACHE_WITH_ID.invalidate(new ImmutablePair<>(entityType, entity.getId()));
CACHE_WITH_NAME.invalidate(new ImmutablePair<>(entityType, entity.getFullyQualifiedName()));
@@ -4917,17 +4952,34 @@ public final void addRelationship(
Relationship relationship,
String json,
boolean bidirectional) {
+ addRelationship(
+ fromId, null, toId, null, fromEntity, toEntity, relationship, json, bidirectional);
+ }
+
+ @Transaction
+ public final void addRelationship(
+ UUID fromId,
+ String fromFqn,
+ UUID toId,
+ String toFqn,
+ String fromEntity,
+ String toEntity,
+ Relationship relationship,
+ String json,
+ boolean bidirectional) {
UUID from = fromId;
UUID to = toId;
+ String fromHash = fromFqn != null ? FullyQualifiedName.buildHash(fromFqn) : null;
+ String toHash = toFqn != null ? FullyQualifiedName.buildHash(toFqn) : null;
if (bidirectional && fromId.compareTo(toId) > 0) {
- // For bidirectional relationship, instead of adding two row fromId -> toId and toId ->
- // fromId, just add one row where fromId is alphabetically less than toId
from = toId;
to = fromId;
+ fromHash = toFqn != null ? FullyQualifiedName.buildHash(toFqn) : null;
+ toHash = fromFqn != null ? FullyQualifiedName.buildHash(fromFqn) : null;
}
daoCollection
.relationshipDAO()
- .insert(from, to, fromEntity, toEntity, relationship.ordinal(), json);
+ .insert(from, to, fromEntity, toEntity, relationship.ordinal(), json, fromHash, toHash);
// Update RDF
EntityRelationship entityRelationship =
@@ -4940,7 +4992,6 @@ public final void addRelationship(
RdfUpdater.addRelationship(entityRelationship);
if (bidirectional) {
- // Also add the reverse relationship to RDF
EntityRelationship reverseRelationship =
new EntityRelationship()
.withFromId(toId)
@@ -4952,6 +5003,10 @@ public final void addRelationship(
}
}
+ protected void deleteTimeSeriesByFqnPrefix(String fqnHashPrefix) {
+ // No-op default — override in repositories with associated time-series tables
+ }
+
@Transaction
public final void bulkAddToRelationship(
UUID fromId, List toId, String fromEntity, String toEntity, Relationship relationship) {
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityTimeSeriesDAO.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityTimeSeriesDAO.java
index 96ea3e06a08f..65da83fff948 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityTimeSeriesDAO.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityTimeSeriesDAO.java
@@ -530,6 +530,17 @@ default void delete(String entityFQNHash, String extension) {
delete(getTimeSeriesTableName(), entityFQNHash, extension);
}
+ @SqlUpdate(
+ "DELETE FROM WHERE entityFQNHash = :exactHash OR entityFQNHash LIKE :prefixHash")
+ void deleteByFqnHashPrefixInternal(
+ @Define("table") String table,
+ @Bind("exactHash") String exactHash,
+ @Bind("prefixHash") String prefixHash);
+
+ default void deleteByFqnHashPrefix(String fqnHashPrefix) {
+ deleteByFqnHashPrefixInternal(getTimeSeriesTableName(), fqnHashPrefix, fqnHashPrefix + ".%");
+ }
+
@SqlUpdate(
"DELETE FROM WHERE entityFQNHash = :entityFQNHash AND extension = :extension AND timestamp = :timestamp")
void deleteAtTimestamp(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/FeedRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/FeedRepository.java
index 0364eb316305..04fa428f4609 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/FeedRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/FeedRepository.java
@@ -772,6 +772,22 @@ public void deleteByAbout(UUID entityId) {
}
}
+ @Transaction
+ public void deleteByAboutBatch(List entityIds) {
+ if (nullOrEmpty(entityIds)) {
+ return;
+ }
+ List ids = entityIds.stream().map(UUID::toString).toList();
+ List threadIds = listOrEmpty(dao.feedDAO().findByEntityIds(ids));
+ for (String threadId : threadIds) {
+ try {
+ deleteThreadInternal(UUID.fromString(threadId));
+ } catch (Exception ex) {
+ // Continue deletion
+ }
+ }
+ }
+
public List getThreadsCount(String link) {
List> result;
EntityLink entityLink = EntityLink.parse(link);
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java
index a6ad876c7eab..c9c2339f9891 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java
@@ -488,6 +488,24 @@ protected void postDelete(IngestionPipeline entity, boolean hardDelete) {
.delete(entity.getFullyQualifiedName(), PIPELINE_STATUS_EXTENSION);
}
+ @Override
+ protected void preDeleteByFqnHashPrefix(String fqnHashPrefix, String deletedBy) {
+ List ids = getDao().findIdsByFqnHashPrefix(fqnHashPrefix);
+ for (UUID id : ids) {
+ try {
+ IngestionPipeline pipeline = find(id, Include.ALL);
+ if (pipelineServiceClient != null) {
+ pipelineServiceClient.deletePipeline(pipeline);
+ }
+ daoCollection
+ .entityExtensionTimeSeriesDao()
+ .delete(pipeline.getFullyQualifiedName(), PIPELINE_STATUS_EXTENSION);
+ } catch (Exception e) {
+ LOG.warn("Failed cleanup for ingestion pipeline {}: {}", id, e.getMessage());
+ }
+ }
+ }
+
@Override
protected EntityReference getParentReference(IngestionPipeline entity) {
return entity.getService();
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/PrefixDeletionService.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/PrefixDeletionService.java
new file mode 100644
index 000000000000..9fff9e217e4e
--- /dev/null
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/PrefixDeletionService.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2021 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmetadata.service.jdbi3;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import lombok.extern.slf4j.Slf4j;
+import org.openmetadata.schema.EntityInterface;
+import org.openmetadata.service.Entity;
+import org.openmetadata.service.events.lifecycle.EntityLifecycleEventDispatcher;
+import org.openmetadata.service.lock.HierarchicalLockManager;
+import org.openmetadata.service.search.SearchRepository;
+import org.openmetadata.service.util.FullyQualifiedName;
+
+/**
+ * Orchestrates fast prefix-based hard deletion of an entity and all its descendants.
+ *
+ * Uses 7 phases to atomically delete an entire subtree by FQN prefix rather than walking the
+ * entity tree one-by-one. This is orders of magnitude faster for large hierarchies and eliminates
+ * the race condition where concurrent ingestion creates orphaned entities during slow cascade
+ * deletion.
+ *
+ *
This service handles hard delete only. Soft delete still uses the existing tree walk in
+ * {@link EntityRepository} to preserve relationship data for restoration.
+ */
+@Slf4j
+public final class PrefixDeletionService {
+
+ private static volatile PrefixDeletionService instance;
+
+ private final HierarchicalLockManager lockManager;
+
+ private PrefixDeletionService(HierarchicalLockManager lockManager) {
+ this.lockManager = lockManager;
+ }
+
+ public static void initialize(HierarchicalLockManager lockMgr) {
+ if (instance == null) {
+ synchronized (PrefixDeletionService.class) {
+ if (instance == null) {
+ instance = new PrefixDeletionService(lockMgr);
+ }
+ }
+ }
+ }
+
+ public static PrefixDeletionService getInstance() {
+ return instance;
+ }
+
+ /**
+ * Hard-deletes the given root entity and all descendants whose FQN starts with the root's FQN.
+ * Works at any hierarchy level (service, database, schema, etc.).
+ */
+ public void deletePrefixHard(EntityInterface rootEntity, String deletedBy) {
+ String rootFqn = rootEntity.getFullyQualifiedName();
+ String fqnHashPrefix = FullyQualifiedName.buildHash(rootFqn);
+ DeletionLock lock = acquireLock(rootEntity, deletedBy);
+ try {
+ Map> descendantsByType = collectDescendantIds(fqnHashPrefix);
+ List allIds = buildAllIds(rootEntity.getId(), descendantsByType);
+ deleteDependencyTables(rootFqn, fqnHashPrefix, allIds);
+ runEntityHooks(rootEntity, deletedBy, fqnHashPrefix);
+ deleteEntityTables(descendantsByType, rootEntity);
+ runPostEntityHooks(fqnHashPrefix);
+ cleanSearchIndex(rootEntity, descendantsByType.keySet());
+ emitDeleteEvent(rootEntity);
+ } finally {
+ releaseLock(lock, rootEntity);
+ }
+ }
+
+ private DeletionLock acquireLock(EntityInterface entity, String deletedBy) {
+ if (lockManager == null) {
+ return null;
+ }
+ try {
+ return lockManager.acquireDeletionLock(entity, deletedBy, true);
+ } catch (Exception e) {
+ LOG.warn(
+ "Could not acquire deletion lock for {}: {}",
+ entity.getFullyQualifiedName(),
+ e.getMessage());
+ return null;
+ }
+ }
+
+ private Map> collectDescendantIds(String fqnHashPrefix) {
+ Map> descendantsByType = new HashMap<>();
+ for (String entityType : Entity.getEntityList()) {
+ try {
+ EntityRepository> repo = Entity.getEntityRepository(entityType);
+ List ids = repo.getDao().findIdsByFqnHashPrefix(fqnHashPrefix);
+ if (!ids.isEmpty()) {
+ descendantsByType.put(entityType, ids);
+ }
+ } catch (Exception e) {
+ LOG.debug("Skipping type {} during descendant collection: {}", entityType, e.getMessage());
+ }
+ }
+ return descendantsByType;
+ }
+
+ private List buildAllIds(UUID rootId, Map> descendantsByType) {
+ List allIds = new ArrayList<>();
+ allIds.add(rootId.toString());
+ for (List ids : descendantsByType.values()) {
+ for (UUID id : ids) {
+ allIds.add(id.toString());
+ }
+ }
+ return allIds;
+ }
+
+ private void deleteDependencyTables(String rootFqn, String fqnHashPrefix, List allIds) {
+ CollectionDAO dao = Entity.getCollectionDAO();
+ dao.relationshipDAO().deleteAllByFqnHashPrefix(fqnHashPrefix);
+ dao.fieldRelationshipDAO().deleteAllByPrefix(rootFqn);
+ dao.entityExtensionDAO().deleteAllBatch(allIds);
+ dao.entityExtensionTimeSeriesDao().deleteByFqnHashPrefix(fqnHashPrefix);
+ dao.tagUsageDAO().deleteTagLabelsByTargetPrefix(rootFqn);
+ dao.usageDAO().deleteBatch(allIds);
+ List allUuids = allIds.stream().map(UUID::fromString).toList();
+ Entity.getFeedRepository().deleteByAboutBatch(allUuids);
+ }
+
+ private void runEntityHooks(EntityInterface rootEntity, String deletedBy, String fqnHashPrefix) {
+ String rootType = rootEntity.getEntityReference().getType();
+ try {
+ Entity.getEntityRepository(rootType).callPreDelete(rootEntity, deletedBy);
+ } catch (Exception e) {
+ LOG.warn(
+ "preDelete hook failed for {}: {}", rootEntity.getFullyQualifiedName(), e.getMessage());
+ }
+ for (String entityType : Entity.getEntityList()) {
+ try {
+ Entity.getEntityRepository(entityType).deleteTimeSeriesByFqnPrefix(fqnHashPrefix);
+ } catch (Exception e) {
+ LOG.debug("deleteTimeSeriesByFqnPrefix failed for type {}: {}", entityType, e.getMessage());
+ }
+ try {
+ Entity.getEntityRepository(entityType).preDeleteByFqnHashPrefix(fqnHashPrefix, deletedBy);
+ } catch (Exception e) {
+ LOG.debug("preDeleteByFqnHashPrefix failed for type {}: {}", entityType, e.getMessage());
+ }
+ }
+ }
+
+ private void runPostEntityHooks(String fqnHashPrefix) {
+ for (String entityType : Entity.getEntityList()) {
+ try {
+ Entity.getEntityRepository(entityType).postDeleteByFqnHashPrefix(fqnHashPrefix);
+ } catch (Exception e) {
+ LOG.debug("postDeleteByFqnHashPrefix failed for type {}: {}", entityType, e.getMessage());
+ }
+ }
+ }
+
+ private void deleteEntityTables(
+ Map> descendantsByType, EntityInterface rootEntity) {
+ for (Map.Entry> entry : descendantsByType.entrySet()) {
+ String entityType = entry.getKey();
+ List ids = entry.getValue().stream().map(UUID::toString).toList();
+ try {
+ Entity.getEntityRepository(entityType).getDao().deleteBatch(ids);
+ } catch (Exception e) {
+ LOG.warn(
+ "Failed to delete {} entities of type {}: {}", ids.size(), entityType, e.getMessage());
+ }
+ }
+ String rootType = rootEntity.getEntityReference().getType();
+ EntityRepository> rootRepo = Entity.getEntityRepository(rootType);
+ rootRepo.getDao().delete(rootEntity.getId());
+ rootRepo.invalidateEntity(rootEntity);
+ }
+
+ private void cleanSearchIndex(EntityInterface rootEntity, Set descendantTypes) {
+ SearchRepository searchRepo = Entity.getSearchRepository();
+ if (searchRepo == null) {
+ return;
+ }
+ String rootFqn = rootEntity.getFullyQualifiedName();
+ String rootType = rootEntity.getEntityReference().getType();
+ searchRepo.deleteByEntityTypeFqnPrefix(rootType, rootFqn);
+ for (String entityType : descendantTypes) {
+ searchRepo.deleteByEntityTypeFqnPrefix(entityType, rootFqn);
+ }
+ }
+
+ private void emitDeleteEvent(EntityInterface rootEntity) {
+ try {
+ EntityLifecycleEventDispatcher.getInstance().onEntityDeleted(rootEntity, null);
+ } catch (Exception e) {
+ LOG.warn(
+ "Failed to emit delete event for {}: {}",
+ rootEntity.getFullyQualifiedName(),
+ e.getMessage());
+ }
+ }
+
+ private void releaseLock(DeletionLock lock, EntityInterface entity) {
+ if (lock == null || lockManager == null) {
+ return;
+ }
+ try {
+ lockManager.releaseDeletionLock(entity.getId(), entity.getEntityReference().getType());
+ } catch (Exception e) {
+ LOG.warn(
+ "Failed to release deletion lock for {}: {}",
+ entity.getFullyQualifiedName(),
+ e.getMessage());
+ }
+ }
+}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TableRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TableRepository.java
index 81f4fa8c1ab8..bea11248d43d 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TableRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TableRepository.java
@@ -1632,13 +1632,16 @@ private void collectColumnFqns(List columns, List columnFqns) {
@Override
public void storeRelationships(Table table) {
- // Add relationship from database to table
addRelationship(
table.getDatabaseSchema().getId(),
+ table.getDatabaseSchema().getFullyQualifiedName(),
table.getId(),
+ table.getFullyQualifiedName(),
DATABASE_SCHEMA,
TABLE,
- Relationship.CONTAINS);
+ Relationship.CONTAINS,
+ null,
+ false);
}
@Override
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/WorkflowDefinitionRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/WorkflowDefinitionRepository.java
index 3c3178a2ab83..2e3f5e8cc437 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/WorkflowDefinitionRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/WorkflowDefinitionRepository.java
@@ -14,6 +14,7 @@
import org.openmetadata.schema.governance.workflows.elements.EdgeDefinition;
import org.openmetadata.schema.governance.workflows.elements.WorkflowNodeDefinitionInterface;
import org.openmetadata.schema.type.EntityReference;
+import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.change.ChangeSource;
import org.openmetadata.schema.utils.JsonUtils;
import org.openmetadata.service.Entity;
@@ -54,6 +55,22 @@ protected void postUpdate(WorkflowDefinition original, WorkflowDefinition update
WorkflowHandler.getInstance().deploy(new Workflow(updated));
}
+ @Override
+ protected void preDeleteByFqnHashPrefix(String fqnHashPrefix, String deletedBy) {
+ if (!WorkflowHandler.isInitialized()) {
+ return;
+ }
+ List ids = getDao().findIdsByFqnHashPrefix(fqnHashPrefix);
+ for (UUID id : ids) {
+ try {
+ WorkflowDefinition definition = find(id, Include.ALL);
+ WorkflowHandler.getInstance().deleteWorkflowDefinition(definition);
+ } catch (Exception e) {
+ LOG.warn("Failed to delete workflow definition {}: {}", id, e.getMessage());
+ }
+ }
+ }
+
@Override
protected void postDelete(WorkflowDefinition entity, boolean hardDelete) {
super.postDelete(entity, hardDelete);
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/WorkflowRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/WorkflowRepository.java
index d1b3ca6173d1..d6d5728a674b 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/WorkflowRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/WorkflowRepository.java
@@ -4,8 +4,11 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
+import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.schema.entity.automations.Workflow;
+import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.change.ChangeSource;
import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.automations.WorkflowResource;
@@ -14,6 +17,7 @@
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.EntityUtil.RelationIncludes;
+@Slf4j
public class WorkflowRepository extends EntityRepository {
private static final String PATCH_FIELDS = "status,response";
@@ -80,6 +84,19 @@ public void storeEntities(List workflows) {
dao.insertMany(dao.getTableName(), dao.getNameHashColumn(), fqns, jsons);
}
+ @Override
+ protected void preDeleteByFqnHashPrefix(String fqnHashPrefix, String deletedBy) {
+ List ids = getDao().findIdsByFqnHashPrefix(fqnHashPrefix);
+ for (UUID id : ids) {
+ try {
+ Workflow workflow = find(id, Include.ALL);
+ SecretsManagerFactory.getSecretsManager().deleteSecretsFromWorkflow(workflow);
+ } catch (Exception e) {
+ LOG.warn("Failed to delete secrets for workflow {}: {}", id, e.getMessage());
+ }
+ }
+ }
+
/** Remove the secrets from the secret manager */
@Override
protected void postDelete(Workflow workflow, boolean hardDelete) {
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java
index d146890469f6..b887c80ee789 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java
@@ -1,10 +1,12 @@
package org.openmetadata.service.migration.mysql.v1130;
import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
import org.openmetadata.service.migration.api.MigrationProcessImpl;
import org.openmetadata.service.migration.utils.MigrationFile;
import org.openmetadata.service.migration.utils.v1130.MigrationUtil;
+@Slf4j
public class Migration extends MigrationProcessImpl {
public Migration(MigrationFile migrationFile) {
@@ -15,5 +17,13 @@ public Migration(MigrationFile migrationFile) {
@SneakyThrows
public void runDataMigration() {
MigrationUtil.updateOwnerChartFormulas();
+ try {
+ MigrationUtil.backfillRelationshipFqnHashes(handle);
+ } catch (Exception e) {
+ LOG.error(
+ "Failed to backfill FQN hashes in entity_relationship during v1130 migration. "
+ + "Fast prefix deletion may not work correctly for pre-existing relationships.",
+ e);
+ }
}
}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1141/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1141/Migration.java
new file mode 100644
index 000000000000..7d381de87393
--- /dev/null
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1141/Migration.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2021 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmetadata.service.migration.mysql.v1141;
+
+import org.openmetadata.service.migration.api.MigrationProcessImpl;
+import org.openmetadata.service.migration.utils.MigrationFile;
+
+public class Migration extends MigrationProcessImpl {
+
+ public Migration(MigrationFile migrationFile) {
+ super(migrationFile);
+ }
+}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java
index 909ea509319d..570eda6bf4ab 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java
@@ -1,10 +1,12 @@
package org.openmetadata.service.migration.postgres.v1130;
import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
import org.openmetadata.service.migration.api.MigrationProcessImpl;
import org.openmetadata.service.migration.utils.MigrationFile;
import org.openmetadata.service.migration.utils.v1130.MigrationUtil;
+@Slf4j
public class Migration extends MigrationProcessImpl {
public Migration(MigrationFile migrationFile) {
@@ -15,5 +17,13 @@ public Migration(MigrationFile migrationFile) {
@SneakyThrows
public void runDataMigration() {
MigrationUtil.updateOwnerChartFormulas();
+ try {
+ MigrationUtil.backfillRelationshipFqnHashes(handle);
+ } catch (Exception e) {
+ LOG.error(
+ "Failed to backfill FQN hashes in entity_relationship during v1130 migration. "
+ + "Fast prefix deletion may not work correctly for pre-existing relationships.",
+ e);
+ }
}
}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1141/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1141/Migration.java
new file mode 100644
index 000000000000..e0c6d4f761f8
--- /dev/null
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1141/Migration.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2021 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openmetadata.service.migration.postgres.v1141;
+
+import org.openmetadata.service.migration.api.MigrationProcessImpl;
+import org.openmetadata.service.migration.utils.MigrationFile;
+
+public class Migration extends MigrationProcessImpl {
+
+ public Migration(MigrationFile migrationFile) {
+ super(migrationFile);
+ }
+}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1130/MigrationUtil.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1130/MigrationUtil.java
index f5005e76f4b3..d6ab1addbc73 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1130/MigrationUtil.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1130/MigrationUtil.java
@@ -1,17 +1,87 @@
package org.openmetadata.service.migration.utils.v1130;
import lombok.extern.slf4j.Slf4j;
+import org.jdbi.v3.core.Handle;
import org.openmetadata.schema.dataInsight.custom.DataInsightCustomChart;
+import org.openmetadata.service.Entity;
import org.openmetadata.service.jdbi3.DataInsightSystemChartRepository;
+import org.openmetadata.service.jdbi3.EntityRepository;
import org.openmetadata.service.util.EntityUtil;
@Slf4j
public class MigrationUtil {
private MigrationUtil() {}
+ private static final String FQNHASH_COL = "fqnHash";
+ private static final String NAMEHASH_COL = "nameHash";
+
private static final String OLD_FIELD = "owners.name.keyword";
private static final String NEW_FIELD = "ownerName";
+ /**
+ * Backfills fromFQNHash and toFQNHash in entity_relationship for all registered entity types.
+ * Uses a direct correlated-subquery UPDATE (one per direction per entity type) to avoid
+ * LIMIT/OFFSET pagination ordering bugs that could silently skip rows.
+ */
+ public static void backfillRelationshipFqnHashes(Handle handle) {
+ for (String entityType : Entity.getEntityList()) {
+ try {
+ backfillForEntityType(handle, entityType);
+ } catch (Exception e) {
+ LOG.warn(
+ "Failed to backfill FQN hashes for entity type {}: {}", entityType, e.getMessage());
+ }
+ }
+ }
+
+ private static void backfillForEntityType(Handle handle, String entityType) {
+ EntityRepository> repo = Entity.getEntityRepository(entityType);
+ String hashCol = repo.getDao().getNameHashColumn();
+ if (!FQNHASH_COL.equals(hashCol) && !NAMEHASH_COL.equals(hashCol)) {
+ return;
+ }
+ String tableName = repo.getDao().getTableName();
+ int fromUpdated = updateFromHashes(handle, entityType, tableName, hashCol);
+ int toUpdated = updateToHashes(handle, entityType, tableName, hashCol);
+ if (fromUpdated + toUpdated > 0) {
+ LOG.info(
+ "Backfilled FQN hashes for entity type {}: {} fromFQNHash, {} toFQNHash",
+ entityType,
+ fromUpdated,
+ toUpdated);
+ }
+ }
+
+ private static int updateFromHashes(
+ Handle handle, String entityType, String tableName, String hashCol) {
+ return handle
+ .createUpdate(
+ "UPDATE entity_relationship SET fromFQNHash = ("
+ + "SELECT CAST(t."
+ + hashCol
+ + " AS CHAR(768)) FROM "
+ + tableName
+ + " t WHERE CAST(t.id AS CHAR(36)) = entity_relationship.fromId"
+ + ") WHERE fromEntity = :entityType AND fromFQNHash IS NULL")
+ .bind("entityType", entityType)
+ .execute();
+ }
+
+ private static int updateToHashes(
+ Handle handle, String entityType, String tableName, String hashCol) {
+ return handle
+ .createUpdate(
+ "UPDATE entity_relationship SET toFQNHash = ("
+ + "SELECT CAST(t."
+ + hashCol
+ + " AS CHAR(768)) FROM "
+ + tableName
+ + " t WHERE CAST(t.id AS CHAR(36)) = entity_relationship.toId"
+ + ") WHERE toEntity = :entityType AND toFQNHash IS NULL")
+ .bind("entityType", entityType)
+ .execute();
+ }
+
public static void updateOwnerChartFormulas() {
DataInsightSystemChartRepository repository = new DataInsightSystemChartRepository();
String[] chartNames = {
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java
index bee61836e6c8..a8f3c21945d9 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java
@@ -73,6 +73,7 @@
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.jdbi3.EntityRepository;
import org.openmetadata.service.jdbi3.ListFilter;
+import org.openmetadata.service.jdbi3.PrefixDeletionService;
import org.openmetadata.service.limits.Limits;
import org.openmetadata.service.mapper.EntityMapper;
import org.openmetadata.service.monitoring.LatencyPhase;
@@ -704,6 +705,43 @@ public Response deleteByIdAsync(
return response;
}
+ public Response deletePrefixHardById(UriInfo uriInfo, SecurityContext securityContext, UUID id) {
+ String jobId = UUID.randomUUID().toString();
+ OperationContext operationContext = new OperationContext(entityType, MetadataOperation.DELETE);
+ authorizer.authorize(
+ securityContext,
+ operationContext,
+ getResourceContextById(id, ResourceContextInterface.Operation.DELETE));
+ T entity = repository.get(uriInfo, id, repository.getFields("name"), Include.ALL, false);
+ String userName = securityContext.getUserPrincipal().getName();
+ ExecutorService executorService = AsyncService.getInstance().getExecutorService();
+ executorService.submit(
+ RequestLatencyContext.wrapWithContext(
+ () -> {
+ try {
+ PrefixDeletionService.getInstance().deletePrefixHard(entity, userName);
+ limits.invalidateCache(entityType);
+ WebsocketNotificationHandler.sendDeleteOperationCompleteNotification(
+ jobId, securityContext, entity);
+ } catch (Exception e) {
+ WebsocketNotificationHandler.sendDeleteOperationFailedNotification(
+ jobId,
+ securityContext,
+ entity,
+ e.getMessage() == null ? e.toString() : e.getMessage());
+ }
+ }));
+ return Response.accepted()
+ .entity(
+ new DeleteEntityResponse(
+ jobId,
+ "Fast prefix deletion initiated for " + entity.getName(),
+ entity.getName(),
+ true,
+ true))
+ .build();
+ }
+
public Response deleteByName(
UriInfo uriInfo,
SecurityContext securityContext,
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/AIApplicationResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/AIApplicationResource.java
index 2a8d91b6ac11..6e1f364bd038 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/AIApplicationResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/AIApplicationResource.java
@@ -510,6 +510,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteAiApplicationPrefixHard",
+ summary = "Hard-delete a AI application and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this AI application and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the AI application", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/AIGovernancePolicyResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/AIGovernancePolicyResource.java
index a469b705e839..4cbf1a456a2b 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/AIGovernancePolicyResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/AIGovernancePolicyResource.java
@@ -497,6 +497,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteAiGovernancePolicyPrefixHard",
+ summary = "Hard-delete a AI governance policy and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this AI governance policy and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the AI governance policy", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/LLMModelResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/LLMModelResource.java
index 1bfbc7aeb4fe..ce357b08fb4e 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/LLMModelResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/LLMModelResource.java
@@ -477,6 +477,27 @@ public Response delete(
return delete(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteLlmModelPrefixHard",
+ summary = "Hard-delete a LLM model and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this LLM model and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the LLM model", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/McpServerResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/McpServerResource.java
index 7a602ee98364..26ca0249bf47 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/McpServerResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/McpServerResource.java
@@ -531,6 +531,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteMcpServerPrefixHard",
+ summary = "Hard-delete a MCP server and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this MCP server and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the MCP server", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/PromptTemplateResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/PromptTemplateResource.java
index 416ec1d43bdb..6980877e2559 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/PromptTemplateResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/ai/PromptTemplateResource.java
@@ -494,6 +494,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deletePromptTemplatePrefixHard",
+ summary = "Hard-delete a prompt template and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this prompt template and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the prompt template", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/analytics/WebAnalyticEventResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/analytics/WebAnalyticEventResource.java
index d7471083d91c..8b002e216747 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/analytics/WebAnalyticEventResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/analytics/WebAnalyticEventResource.java
@@ -352,6 +352,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteWebAnalyticEventPrefixHard",
+ summary = "Hard-delete a web analytic event and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this web analytic event and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the web analytic event", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apis/APICollectionResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apis/APICollectionResource.java
index 3f575db7f5c2..82c90e3ce9d1 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apis/APICollectionResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apis/APICollectionResource.java
@@ -545,6 +545,27 @@ public Response updateVote(
.toResponse();
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteApiCollectionPrefixHard",
+ summary = "Hard-delete a API collection and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this API collection and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the API collection", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apis/APIEndpointResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apis/APIEndpointResource.java
index 4f76cd9e7123..aac911877929 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apis/APIEndpointResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apis/APIEndpointResource.java
@@ -591,6 +591,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteApiEndpointPrefixHard",
+ summary = "Hard-delete a API endpoint and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this API endpoint and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the API endpoint", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppMarketPlaceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppMarketPlaceResource.java
index 168650c7a4cf..4cb59f641ec2 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppMarketPlaceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppMarketPlaceResource.java
@@ -374,6 +374,29 @@ public Response createOrUpdate(
return createOrUpdate(uriInfo, securityContext, app);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteAppMarketPlacePrefixHard",
+ summary = "Hard-delete a app marketplace definition and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this app marketplace definition and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(
+ description = "Id of the app marketplace definition",
+ schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java
index 5ab4f8ac5e5e..b67f5b27c3ab 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java
@@ -921,6 +921,26 @@ public Response createOrUpdate(
return createOrUpdate(uriInfo, securityContext, app);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteAppPrefixHard",
+ summary = "Hard-delete a app and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this app and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the app", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java
index 6e97b951fc10..180816170730 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java
@@ -529,6 +529,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteWorkflowPrefixHard",
+ summary = "Hard-delete a workflow and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this workflow and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the workflow", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/bots/BotResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/bots/BotResource.java
index 6175e4eac8fd..c2cad05c9c3a 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/bots/BotResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/bots/BotResource.java
@@ -435,6 +435,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, true, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteBotPrefixHard",
+ summary = "Hard-delete a bot and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this bot and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the bot", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java
index 96ff75878e9e..ece8379534d1 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java
@@ -530,6 +530,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteChartPrefixHard",
+ summary = "Hard-delete a chart and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this chart and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the chart", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dashboards/DashboardResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dashboards/DashboardResource.java
index 3ac05657402a..b6fd4aba0a54 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dashboards/DashboardResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dashboards/DashboardResource.java
@@ -575,6 +575,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDashboardPrefixHard",
+ summary = "Hard-delete a dashboard and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this dashboard and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the dashboard", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java
index eaffd7591f8c..f7c48ca20976 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java
@@ -550,6 +550,27 @@ public Response delete(
return delete(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDataContractPrefixHard",
+ summary = "Hard-delete a data contract and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this data contract and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the data contract", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java
index 200170085ec4..6335a77b9bb4 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java
@@ -715,6 +715,27 @@ public Response updateVote(
.toResponse();
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDatabasePrefixHard",
+ summary = "Hard-delete a database and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this database and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the database", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseSchemaResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseSchemaResource.java
index 7be0e5fa9441..8efee5b4af2c 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseSchemaResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseSchemaResource.java
@@ -778,6 +778,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDatabaseSchemaPrefixHard",
+ summary = "Hard-delete a database schema and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this database schema and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the database schema", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/StoredProcedureResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/StoredProcedureResource.java
index 760da9de6b55..c1557e331983 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/StoredProcedureResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/StoredProcedureResource.java
@@ -14,6 +14,8 @@
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.ws.rs.*;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.*;
import java.util.List;
import java.util.UUID;
@@ -566,6 +568,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteStoredProcedurePrefixHard",
+ summary = "Hard-delete a stored procedure and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this stored procedure and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the stored procedure", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/TableResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/TableResource.java
index fea914d1726f..79c590bb2e0d 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/TableResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/TableResource.java
@@ -750,6 +750,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteTablePrefixHard",
+ summary = "Hard-delete a table and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this table and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the table", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/DataInsightChartResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/DataInsightChartResource.java
index 9a28ee188715..a29a8753576a 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/DataInsightChartResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/DataInsightChartResource.java
@@ -454,6 +454,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDataInsightChartPrefixHard",
+ summary = "Hard-delete a data insight chart and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this data insight chart and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the data insight chart", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/system/DataInsightSystemChartResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/system/DataInsightSystemChartResource.java
index dbb6468ee460..33f1c1b09e06 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/system/DataInsightSystemChartResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/system/DataInsightSystemChartResource.java
@@ -24,6 +24,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import org.openmetadata.schema.dataInsight.DataInsightChart;
import org.openmetadata.schema.dataInsight.custom.DataInsightCustomChart;
import org.openmetadata.schema.dataInsight.custom.DataInsightCustomChartResultList;
@@ -305,4 +306,27 @@ public Response stopChartDataStreaming(
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(errorResponse).build();
}
}
+
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDataInsightSystemChartPrefixHard",
+ summary = "Hard-delete a data insight system chart and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this data insight system chart and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(
+ description = "Id of the data insight system chart",
+ schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java
index 8a69e00aa088..06d7629daca2 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java
@@ -588,6 +588,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDashboardDataModelPrefixHard",
+ summary = "Hard-delete a dashboard data model and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this dashboard data model and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the dashboard data model", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/docstore/DocStoreResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/docstore/DocStoreResource.java
index cd5e19efe834..2324efe73476 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/docstore/DocStoreResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/docstore/DocStoreResource.java
@@ -452,6 +452,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, true);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDocStorePrefixHard",
+ summary = "Hard-delete a doc store and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this doc store and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the doc store", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DataProductResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DataProductResource.java
index 967d470c9ab5..405cf0040dd6 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DataProductResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DataProductResource.java
@@ -921,6 +921,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, true, true);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDataProductPrefixHard",
+ summary = "Hard-delete a data product and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this data product and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the data product", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DomainResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DomainResource.java
index 028df9d98a95..1142c80fc1fd 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DomainResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DomainResource.java
@@ -496,6 +496,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, true, true);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDomainPrefixHard",
+ summary = "Hard-delete a domain and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this domain and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the domain", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java
index f5140b493459..99f8d6f06f0b 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java
@@ -924,6 +924,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteTestCasePrefixHard",
+ summary = "Hard-delete a test case and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this test case and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the test case", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestDefinitionResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestDefinitionResource.java
index 923e699ee405..3c4a9fa0dbe7 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestDefinitionResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestDefinitionResource.java
@@ -459,6 +459,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteTestDefinitionPrefixHard",
+ summary = "Hard-delete a test definition and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this test definition and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the test definition", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java
index fed7540d002f..4e9ae67964ce 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java
@@ -806,6 +806,27 @@ public Response deleteAsync(
return repository.deleteLogicalTestSuiteAsync(securityContext, testSuite, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteTestSuitePrefixHard",
+ summary = "Hard-delete a test suite and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this test suite and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the test suite", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/DirectoryResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/DirectoryResource.java
index 016c1fdef4ab..e48c0da1a958 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/DirectoryResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/DirectoryResource.java
@@ -425,6 +425,27 @@ public Response delete(
return delete(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDirectoryPrefixHard",
+ summary = "Hard-delete a directory and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this directory and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the directory", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/FileResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/FileResource.java
index bcbbdd6088a9..7761ddd02bf3 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/FileResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/FileResource.java
@@ -418,6 +418,26 @@ public Response delete(
return delete(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteFilePrefixHard",
+ summary = "Hard-delete a file and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this file and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the file", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/SpreadsheetResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/SpreadsheetResource.java
index 28335091455c..8426dd2aecbb 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/SpreadsheetResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/SpreadsheetResource.java
@@ -429,6 +429,27 @@ public Response delete(
return delete(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteSpreadsheetPrefixHard",
+ summary = "Hard-delete a spreadsheet and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this spreadsheet and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the spreadsheet", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/WorksheetResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/WorksheetResource.java
index c094d0bbb595..602f78617b41 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/WorksheetResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/drives/WorksheetResource.java
@@ -412,6 +412,27 @@ public Response delete(
return delete(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteWorksheetPrefixHard",
+ summary = "Hard-delete a worksheet and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this worksheet and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the worksheet", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/NotificationTemplateResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/NotificationTemplateResource.java
index 48886f978a64..d3d557dfcea3 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/NotificationTemplateResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/NotificationTemplateResource.java
@@ -635,6 +635,27 @@ public Response delete(
return super.delete(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteNotificationTemplatePrefixHard",
+ summary = "Hard-delete a notification template and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this notification template and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the notification template", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/subscription/EventSubscriptionResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/subscription/EventSubscriptionResource.java
index 9833213eeacd..ce4339e4b3de 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/subscription/EventSubscriptionResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/subscription/EventSubscriptionResource.java
@@ -561,6 +561,27 @@ public Response deleteEventSubscriptionAsync(
return deleteByIdAsync(uriInfo, securityContext, id, true, true);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteEventSubscriptionPrefixHard",
+ summary = "Hard-delete a event subscription and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this event subscription and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the event subscription", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java
index 57b8c03e391f..12be16ac4fb7 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java
@@ -474,6 +474,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteGlossaryPrefixHard",
+ summary = "Hard-delete a glossary and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this glossary and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the glossary", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java
index e7e2eef75782..4997fcbc4cea 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java
@@ -1046,6 +1046,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteGlossaryTermPrefixHard",
+ summary = "Hard-delete a glossary term and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this glossary term and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the glossary term", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/governance/WorkflowDefinitionResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/governance/WorkflowDefinitionResource.java
index e52a68bd1c84..b32c384ce0a4 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/governance/WorkflowDefinitionResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/governance/WorkflowDefinitionResource.java
@@ -484,6 +484,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteWorkflowDefinitionPrefixHard",
+ summary = "Hard-delete a workflow definition and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this workflow definition and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the workflow definition", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/kpi/KpiResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/kpi/KpiResource.java
index e8d345599fb6..e0d119dd244a 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/kpi/KpiResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/kpi/KpiResource.java
@@ -379,6 +379,26 @@ public Response createOrUpdate(
return createOrUpdate(uriInfo, securityContext, kpi);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteKpiPrefixHard",
+ summary = "Hard-delete a KPI and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this KPI and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the KPI", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/learning/LearningResourceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/learning/LearningResourceResource.java
index 3874474e37cd..41deb13beef2 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/learning/LearningResourceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/learning/LearningResourceResource.java
@@ -451,4 +451,25 @@ private LearningResource toEntity(CreateLearningResource create, String updatedB
: LearningResource.Status.fromValue(create.getStatus().value()));
return resource;
}
+
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteLearningResourcePrefixHard",
+ summary = "Hard-delete a learning resource and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this learning resource and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the learning resource", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/metrics/MetricResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/metrics/MetricResource.java
index 9865a2218bd3..14a4c4253e85 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/metrics/MetricResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/metrics/MetricResource.java
@@ -541,6 +541,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteMetricPrefixHard",
+ summary = "Hard-delete a metric and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this metric and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the metric", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/mlmodels/MlModelResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/mlmodels/MlModelResource.java
index 785c2358cba3..2357ee44d35c 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/mlmodels/MlModelResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/mlmodels/MlModelResource.java
@@ -584,6 +584,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteMlModelPrefixHard",
+ summary = "Hard-delete a ML model and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this ML model and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the ML model", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/pipelines/PipelineResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/pipelines/PipelineResource.java
index c307cdd2e5e3..fcff5edac6aa 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/pipelines/PipelineResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/pipelines/PipelineResource.java
@@ -1218,6 +1218,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deletePipelinePrefixHard",
+ summary = "Hard-delete a pipeline and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this pipeline and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the pipeline", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/policies/PolicyResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/policies/PolicyResource.java
index 80c59d85b093..906ae17d3200 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/policies/PolicyResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/policies/PolicyResource.java
@@ -506,6 +506,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deletePolicyPrefixHard",
+ summary = "Hard-delete a policy and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this policy and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the policy", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/query/QueryResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/query/QueryResource.java
index 695855121df7..fd93f795eca3 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/query/QueryResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/query/QueryResource.java
@@ -697,6 +697,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, true);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteQueryPrefixHard",
+ summary = "Hard-delete a query and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this query and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the query", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/reports/ReportResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/reports/ReportResource.java
index 3f0fef5416bc..d73d315eadbc 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/reports/ReportResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/reports/ReportResource.java
@@ -23,6 +23,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
@@ -227,4 +228,24 @@ private void addToReport(SecurityContext securityContext, Report report) {
.withUpdatedBy(securityContext.getUserPrincipal().getName())
.withUpdatedAt(System.currentTimeMillis());
}
+
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteReportPrefixHard",
+ summary = "Hard-delete a report and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this report and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the report", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/searchindex/SearchIndexResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/searchindex/SearchIndexResource.java
index 2570ba07642a..0cc38a0e29de 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/searchindex/SearchIndexResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/searchindex/SearchIndexResource.java
@@ -663,6 +663,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteSearchIndexPrefixHard",
+ summary = "Hard-delete a search index and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this search index and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the search index", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/apiservices/APIServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/apiservices/APIServiceResource.java
index b8555d192487..67c8313986ec 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/apiservices/APIServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/apiservices/APIServiceResource.java
@@ -522,6 +522,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteApiServicePrefixHard",
+ summary = "Hard-delete a API service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this API service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the API service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/connections/TestConnectionDefinitionResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/connections/TestConnectionDefinitionResource.java
index b13a03e7a461..f1e348b3e45b 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/connections/TestConnectionDefinitionResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/connections/TestConnectionDefinitionResource.java
@@ -12,6 +12,7 @@
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@@ -20,6 +21,7 @@
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.core.UriInfo;
import java.io.IOException;
@@ -217,4 +219,27 @@ public TestConnectionDefinition getByName(
Include include) {
return getByNameInternal(uriInfo, securityContext, name, fieldsParam, include);
}
+
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteTestConnectionDefinitionPrefixHard",
+ summary = "Hard-delete a test connection definition and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this test connection definition and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(
+ description = "Id of the test connection definition",
+ schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/dashboard/DashboardServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/dashboard/DashboardServiceResource.java
index 5ebcc81c1391..978b95ec70f8 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/dashboard/DashboardServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/dashboard/DashboardServiceResource.java
@@ -573,6 +573,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDashboardServicePrefixHard",
+ summary = "Hard-delete a dashboard service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this dashboard service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the dashboard service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/database/DatabaseServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/database/DatabaseServiceResource.java
index 394a145c493f..3e9fae1315cb 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/database/DatabaseServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/database/DatabaseServiceResource.java
@@ -732,6 +732,30 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDatabaseServicePrefixHard",
+ summary = "Hard-delete a database service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this database service and all descendants (databases, schemas, tables) "
+ + "whose FQN starts with this service's FQN. Significantly faster than recursive "
+ + "delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(
+ responseCode = "404",
+ description = "DatabaseService for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the database service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/drive/DriveServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/drive/DriveServiceResource.java
index fa3b7c45231c..41b0557f5385 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/drive/DriveServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/drive/DriveServiceResource.java
@@ -650,6 +650,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteDriveServicePrefixHard",
+ summary = "Hard-delete a drive service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this drive service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the drive service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java
index 9216cdd4eb2b..10c92cf58936 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java
@@ -904,6 +904,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteIngestionPipelinePrefixHard",
+ summary = "Hard-delete a ingestion pipeline and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this ingestion pipeline and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the ingestion pipeline", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/llm/LLMServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/llm/LLMServiceResource.java
index f2fe8332044e..4a9acd9b2760 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/llm/LLMServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/llm/LLMServiceResource.java
@@ -575,6 +575,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteLlmServicePrefixHard",
+ summary = "Hard-delete a LLM service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this LLM service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the LLM service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mcp/McpServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mcp/McpServiceResource.java
index 826b5505f385..3a12dc3ccf3d 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mcp/McpServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mcp/McpServiceResource.java
@@ -568,6 +568,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteMcpServicePrefixHard",
+ summary = "Hard-delete a MCP service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this MCP service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the MCP service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/messaging/MessagingServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/messaging/MessagingServiceResource.java
index 5ac5acc91193..b2ba489fe805 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/messaging/MessagingServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/messaging/MessagingServiceResource.java
@@ -576,6 +576,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteMessagingServicePrefixHard",
+ summary = "Hard-delete a messaging service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this messaging service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the messaging service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/metadata/MetadataServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/metadata/MetadataServiceResource.java
index 9cab5010d6d8..fc36dd07ccac 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/metadata/MetadataServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/metadata/MetadataServiceResource.java
@@ -616,6 +616,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteMetadataServicePrefixHard",
+ summary = "Hard-delete a metadata service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this metadata service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the metadata service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mlmodel/MlModelServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mlmodel/MlModelServiceResource.java
index 1568140a2bf4..1627cb02527d 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mlmodel/MlModelServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mlmodel/MlModelServiceResource.java
@@ -589,6 +589,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteMlModelServicePrefixHard",
+ summary = "Hard-delete a ML model service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this ML model service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the ML model service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/pipeline/PipelineServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/pipeline/PipelineServiceResource.java
index c8d79e38a3b5..cabdbc86994d 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/pipeline/PipelineServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/pipeline/PipelineServiceResource.java
@@ -589,6 +589,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deletePipelineServicePrefixHard",
+ summary = "Hard-delete a pipeline service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this pipeline service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the pipeline service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/searchIndexes/SearchServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/searchIndexes/SearchServiceResource.java
index 7d3b9adb9b6f..7339d8b8a081 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/searchIndexes/SearchServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/searchIndexes/SearchServiceResource.java
@@ -575,6 +575,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteSearchIndexServicePrefixHard",
+ summary = "Hard-delete a search index service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this search index service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the search index service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/security/SecurityServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/security/SecurityServiceResource.java
index 1b86dd974a49..5e8c73ac768f 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/security/SecurityServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/security/SecurityServiceResource.java
@@ -700,6 +700,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteSecurityServicePrefixHard",
+ summary = "Hard-delete a security service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this security service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the security service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/storage/StorageServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/storage/StorageServiceResource.java
index a9e24a8bdfc5..04ffdc6c1f53 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/storage/StorageServiceResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/storage/StorageServiceResource.java
@@ -573,6 +573,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteStorageServicePrefixHard",
+ summary = "Hard-delete a storage service and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this storage service and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the storage service", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/storages/ContainerResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/storages/ContainerResource.java
index a2e65b5beb10..90b3469c3402 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/storages/ContainerResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/storages/ContainerResource.java
@@ -584,6 +584,27 @@ public Response updateVote(
.toResponse();
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteContainerPrefixHard",
+ summary = "Hard-delete a container and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this container and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the container", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java
index 89ebc8ab3f30..c41f376c60b9 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java
@@ -437,6 +437,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteClassificationPrefixHard",
+ summary = "Hard-delete a classification and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this classification and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the classification", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/TagResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/TagResource.java
index 00f943464369..310ef39da624 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/TagResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/TagResource.java
@@ -517,6 +517,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteTagPrefixHard",
+ summary = "Hard-delete a tag and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this tag and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the tag", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/PersonaResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/PersonaResource.java
index 3c9b4cd8a2c6..059dfcb5e02d 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/PersonaResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/PersonaResource.java
@@ -30,6 +30,8 @@
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.ws.rs.*;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.*;
import java.util.List;
import java.util.UUID;
@@ -414,6 +416,27 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, true);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deletePersonaPrefixHard",
+ summary = "Hard-delete a persona and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this persona and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the persona", schema = @Schema(type = "UUID"))
+ @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/RoleResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/RoleResource.java
index 5f9dde00c015..a265019257dd 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/RoleResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/RoleResource.java
@@ -475,6 +475,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, true, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteRolePrefixHard",
+ summary = "Hard-delete a role and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this role and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the role", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java
index c5143f25fa32..294bebaf1e61 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java
@@ -662,6 +662,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteTeamPrefixHard",
+ summary = "Hard-delete a team and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this team and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the team", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java
index c1610ad70230..30957c4e97fa 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java
@@ -1127,6 +1127,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteUserPrefixHard",
+ summary = "Hard-delete a user and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this user and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the user", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/topics/TopicResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/topics/TopicResource.java
index 6b092a86f0c8..9b95788a7180 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/topics/TopicResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/topics/TopicResource.java
@@ -631,6 +631,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, hardDelete);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteTopicPrefixHard",
+ summary = "Hard-delete a topic and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this topic and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the topic", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{fqn}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java
index 9546182a40b0..4059e284f1d9 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java
@@ -481,6 +481,26 @@ public Response deleteByIdAsync(
return deleteByIdAsync(uriInfo, securityContext, id, false, true);
}
+ @DELETE
+ @Path("/prefix/{id}")
+ @Operation(
+ operationId = "deleteTypePrefixHard",
+ summary = "Hard-delete a type and all descendants by FQN prefix",
+ description =
+ "Bulk hard-delete this type and all descendants whose FQN starts with this "
+ + "entity's FQN. Significantly faster than recursive delete for large hierarchies.",
+ responses = {
+ @ApiResponse(responseCode = "202", description = "Deletion accepted and running"),
+ @ApiResponse(responseCode = "404", description = "Entity for instance {id} is not found")
+ })
+ public Response deletePrefixHardById(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @Parameter(description = "Id of the type", schema = @Schema(type = "UUID")) @PathParam("id")
+ UUID id) {
+ return super.deletePrefixHardById(uriInfo, securityContext, id);
+ }
+
@DELETE
@Path("/name/{name}")
@Operation(
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
index b480a07eaaa3..8c4b6693d5f0 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
@@ -2178,6 +2178,28 @@ public void deleteEntityByFQNPrefix(EntityInterface entity) {
}
}
+ public void deleteByEntityTypeFqnPrefix(String entityType, String fqnPrefix) {
+ if (!checkIfIndexingIsSupported(entityType)) {
+ return;
+ }
+ if (!getSearchClient().isClientAvailable()) {
+ return;
+ }
+ IndexMapping indexMapping = entityIndexMap.get(entityType);
+ Timer.Sample searchSample = RequestLatencyContext.startSearchOperation();
+ try {
+ searchClient.deleteEntityByFQNPrefix(indexMapping.getIndexName(clusterAlias), fqnPrefix);
+ } catch (Exception ie) {
+ LOG.error(
+ "Issue deleting search documents for entityType [{}] with FQN prefix [{}]",
+ entityType,
+ fqnPrefix,
+ ie);
+ } finally {
+ RequestLatencyContext.endSearchOperation(searchSample);
+ }
+ }
+
public void deleteTimeSeriesEntityById(EntityTimeSeriesInterface entity) {
if (entity != null) {
String entityId = entity.getId().toString();
diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/migration/mysql/v1130/MigrationTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/migration/mysql/v1130/MigrationTest.java
new file mode 100644
index 000000000000..af0b0c89a89b
--- /dev/null
+++ b/openmetadata-service/src/test/java/org/openmetadata/service/migration/mysql/v1130/MigrationTest.java
@@ -0,0 +1,55 @@
+package org.openmetadata.service.migration.mysql.v1130;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Answers.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+
+import java.lang.reflect.Field;
+import org.jdbi.v3.core.Handle;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.openmetadata.service.migration.api.MigrationProcessImpl;
+import org.openmetadata.service.migration.utils.MigrationFile;
+import org.openmetadata.service.migration.utils.v1130.MigrationUtil;
+
+class MigrationTest {
+
+ private Migration createMigrationWithHandle(Handle handle) throws Exception {
+ MigrationFile migrationFile = mock(MigrationFile.class);
+ Migration migration = new Migration(migrationFile);
+ Field handleField = MigrationProcessImpl.class.getDeclaredField("handle");
+ handleField.setAccessible(true);
+ handleField.set(migration, handle);
+ return migration;
+ }
+
+ @Test
+ void runDataMigrationCallsBackfillRelationshipFqnHashes() throws Exception {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+ Migration migration = createMigrationWithHandle(handle);
+
+ try (MockedStatic util = mockStatic(MigrationUtil.class)) {
+ util.when(MigrationUtil::updateOwnerChartFormulas).then(inv -> null);
+ util.when(() -> MigrationUtil.backfillRelationshipFqnHashes(handle)).then(inv -> null);
+
+ migration.runDataMigration();
+
+ util.verify(() -> MigrationUtil.backfillRelationshipFqnHashes(handle));
+ }
+ }
+
+ @Test
+ void runDataMigrationDoesNotThrowWhenBackfillFails() throws Exception {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+ Migration migration = createMigrationWithHandle(handle);
+
+ try (MockedStatic util = mockStatic(MigrationUtil.class)) {
+ util.when(MigrationUtil::updateOwnerChartFormulas).then(inv -> null);
+ util.when(() -> MigrationUtil.backfillRelationshipFqnHashes(handle))
+ .thenThrow(new RuntimeException("DB error"));
+
+ assertDoesNotThrow(migration::runDataMigration);
+ }
+ }
+}
diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/migration/postgres/v1130/MigrationTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/migration/postgres/v1130/MigrationTest.java
new file mode 100644
index 000000000000..04207aec3361
--- /dev/null
+++ b/openmetadata-service/src/test/java/org/openmetadata/service/migration/postgres/v1130/MigrationTest.java
@@ -0,0 +1,55 @@
+package org.openmetadata.service.migration.postgres.v1130;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Answers.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+
+import java.lang.reflect.Field;
+import org.jdbi.v3.core.Handle;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.openmetadata.service.migration.api.MigrationProcessImpl;
+import org.openmetadata.service.migration.utils.MigrationFile;
+import org.openmetadata.service.migration.utils.v1130.MigrationUtil;
+
+class MigrationTest {
+
+ private Migration createMigrationWithHandle(Handle handle) throws Exception {
+ MigrationFile migrationFile = mock(MigrationFile.class);
+ Migration migration = new Migration(migrationFile);
+ Field handleField = MigrationProcessImpl.class.getDeclaredField("handle");
+ handleField.setAccessible(true);
+ handleField.set(migration, handle);
+ return migration;
+ }
+
+ @Test
+ void runDataMigrationCallsBackfillRelationshipFqnHashes() throws Exception {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+ Migration migration = createMigrationWithHandle(handle);
+
+ try (MockedStatic util = mockStatic(MigrationUtil.class)) {
+ util.when(MigrationUtil::updateOwnerChartFormulas).then(inv -> null);
+ util.when(() -> MigrationUtil.backfillRelationshipFqnHashes(handle)).then(inv -> null);
+
+ migration.runDataMigration();
+
+ util.verify(() -> MigrationUtil.backfillRelationshipFqnHashes(handle));
+ }
+ }
+
+ @Test
+ void runDataMigrationDoesNotThrowWhenBackfillFails() throws Exception {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+ Migration migration = createMigrationWithHandle(handle);
+
+ try (MockedStatic util = mockStatic(MigrationUtil.class)) {
+ util.when(MigrationUtil::updateOwnerChartFormulas).then(inv -> null);
+ util.when(() -> MigrationUtil.backfillRelationshipFqnHashes(handle))
+ .thenThrow(new RuntimeException("DB error"));
+
+ assertDoesNotThrow(migration::runDataMigration);
+ }
+ }
+}
diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/migration/utils/v1130/MigrationUtilTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/migration/utils/v1130/MigrationUtilTest.java
new file mode 100644
index 000000000000..06aee6ae7281
--- /dev/null
+++ b/openmetadata-service/src/test/java/org/openmetadata/service/migration/utils/v1130/MigrationUtilTest.java
@@ -0,0 +1,150 @@
+package org.openmetadata.service.migration.utils.v1130;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Answers.RETURNS_DEEP_STUBS;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Set;
+import org.jdbi.v3.core.Handle;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.openmetadata.service.Entity;
+import org.openmetadata.service.jdbi3.EntityDAO;
+import org.openmetadata.service.jdbi3.EntityRepository;
+
+class MigrationUtilTest {
+
+ private Handle handleWithUpdateReturning(int rowCount) {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+ when(handle.createUpdate(any()).bind(anyString(), anyString()).execute()).thenReturn(rowCount);
+ return handle;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private EntityRepository> repoWithHashCol(String tableName, String hashCol) {
+ EntityDAO dao = mock(EntityDAO.class);
+ when(dao.getTableName()).thenReturn(tableName);
+ when(dao.getNameHashColumn()).thenReturn(hashCol);
+ EntityRepository repo = mock(EntityRepository.class);
+ doReturn(dao).when(repo).getDao();
+ return repo;
+ }
+
+ @Test
+ void backfillDoesNothingWhenEntityListIsEmpty() {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+
+ try (MockedStatic entity = mockStatic(Entity.class)) {
+ entity.when(Entity::getEntityList).thenReturn(Set.of());
+
+ assertDoesNotThrow(() -> MigrationUtil.backfillRelationshipFqnHashes(handle));
+ verify(handle, never()).createUpdate(any());
+ }
+ }
+
+ @Test
+ void backfillIssuesFromAndToUpdatesForFqnHashEntity() {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+ EntityRepository> repo = repoWithHashCol("table_entity", "fqnHash");
+
+ try (MockedStatic entity = mockStatic(Entity.class)) {
+ entity.when(Entity::getEntityList).thenReturn(Set.of("table"));
+ entity.when(() -> Entity.getEntityRepository("table")).thenReturn(repo);
+
+ assertDoesNotThrow(() -> MigrationUtil.backfillRelationshipFqnHashes(handle));
+
+ verify(handle).createUpdate(contains("fromFQNHash"));
+ verify(handle).createUpdate(contains("toFQNHash"));
+ }
+ }
+
+ @Test
+ void backfillIssuesFromAndToUpdatesForNameHashEntity() {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+ EntityRepository> repo = repoWithHashCol("user_entity", "nameHash");
+
+ try (MockedStatic entity = mockStatic(Entity.class)) {
+ entity.when(Entity::getEntityList).thenReturn(Set.of("user"));
+ entity.when(() -> Entity.getEntityRepository("user")).thenReturn(repo);
+
+ assertDoesNotThrow(() -> MigrationUtil.backfillRelationshipFqnHashes(handle));
+
+ verify(handle).createUpdate(contains("fromFQNHash"));
+ verify(handle).createUpdate(contains("toFQNHash"));
+ }
+ }
+
+ @Test
+ void backfillSkipsEntityTypeWithUnrecognisedHashColumn() {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+ EntityRepository> repo = repoWithHashCol("some_entity", "otherHash");
+
+ try (MockedStatic entity = mockStatic(Entity.class)) {
+ entity.when(Entity::getEntityList).thenReturn(Set.of("someEntity"));
+ entity.when(() -> Entity.getEntityRepository("someEntity")).thenReturn(repo);
+
+ assertDoesNotThrow(() -> MigrationUtil.backfillRelationshipFqnHashes(handle));
+
+ verify(handle, never()).createUpdate(any());
+ }
+ }
+
+ @Test
+ void backfillSkipsTimeSeriesEntityWhenRepositoryNotFound() {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+
+ try (MockedStatic entity = mockStatic(Entity.class)) {
+ entity.when(Entity::getEntityList).thenReturn(Set.of("testCaseResolutionStatus"));
+ entity
+ .when(() -> Entity.getEntityRepository("testCaseResolutionStatus"))
+ .thenThrow(new RuntimeException("not a regular entity"));
+
+ assertDoesNotThrow(() -> MigrationUtil.backfillRelationshipFqnHashes(handle));
+
+ verify(handle, never()).createUpdate(any());
+ }
+ }
+
+ @Test
+ void backfillContinuesToNextEntityWhenOneEntityUpdateFails() {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+ when(handle.createUpdate(any()).bind(anyString(), anyString()).execute())
+ .thenThrow(new RuntimeException("DB error"));
+
+ EntityRepository> tableRepo = repoWithHashCol("table_entity", "fqnHash");
+ EntityRepository> userRepo = repoWithHashCol("user_entity", "nameHash");
+
+ try (MockedStatic entity = mockStatic(Entity.class)) {
+ entity.when(Entity::getEntityList).thenReturn(Set.of("table", "user"));
+ entity.when(() -> Entity.getEntityRepository("table")).thenReturn(tableRepo);
+ entity.when(() -> Entity.getEntityRepository("user")).thenReturn(userRepo);
+
+ assertDoesNotThrow(() -> MigrationUtil.backfillRelationshipFqnHashes(handle));
+ }
+ }
+
+ @Test
+ void backfillSqlContainsCorrelatedSubqueryWithCast() {
+ Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
+ EntityRepository> repo = repoWithHashCol("table_entity", "fqnHash");
+
+ try (MockedStatic entity = mockStatic(Entity.class)) {
+ entity.when(Entity::getEntityList).thenReturn(Set.of("table"));
+ entity.when(() -> Entity.getEntityRepository("table")).thenReturn(repo);
+
+ MigrationUtil.backfillRelationshipFqnHashes(handle);
+
+ verify(handle, times(2)).createUpdate(contains("CAST(t.id AS CHAR(36))"));
+ verify(handle, times(2)).createUpdate(contains("CAST(t.fqnHash AS CHAR(768))"));
+ }
+ }
+}