lasdf1234 commented on code in PR #12257:
URL: https://github.com/apache/gravitino/pull/12257#discussion_r3708627496


##########
design-docs/iceberg-table-soft-deletion.md:
##########
@@ -0,0 +1,479 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# Design: Soft Deletion for Iceberg Tables
+
+| Field   | Value                                                              
                 |
+| ------- | 
-----------------------------------------------------------------------------------
 |
+| Status  | Draft — for discussion                                             
                 |
+| Author  | Nevin Zheng                                                        
                 |
+| Created | 2026-07-29                                                         
                 |
+| Module  | `core`, `server`, `iceberg/iceberg-rest-server`                    
                 |
+| Related | [Asynchronous Hard 
Deletion](./async-iceberg-rest-hard-deletion.md) (§3 Non-Goal 1) |
+
+**Scope.** The deletion record, the metadata model, and the API for 
discovering and
+undeleting a dropped Iceberg table, and the purge that runs when the window 
closes.
+Purge reuses the shipped cleanup worker; its scheduling and operator tooling 
are
+deferred (§5.6). Existing drop and purge behavior is unchanged.
+
+---
+
+## 1. Background
+
+Dropping an Iceberg table is terminal — no window exists in which a mistaken 
drop
+can be undone.
+
+| Request                                       | What happens                 
                 | Recoverable |
+| --------------------------------------------- | 
--------------------------------------------- | ----------- |
+| `DELETE …` (no purge)                         | Registration removed; data 
files orphaned     | No          |
+| `DELETE …?purgeRequested=true` (synchronous)  | Files deleted on the request 
thread           | No          |
+| `DELETE …?purgeRequested=true` (asynchronous) | A cleanup job deletes files 
in the background | No          |
+
+The asynchronous path shipped with
+[Asynchronous Hard Deletion](./async-iceberg-rest-hard-deletion.md), which 
moved
+cleanup off the request thread. It deliberately left recovery out — its §3 
Non-Goal
+1 destroys files with no undrop path and defers soft delete to a follow-up. 
This is
+that follow-up.
+
+Two existing mechanisms look adjacent but are not recovery. **Relational
+tombstones** (`deleted_at` plus `RelationalGarbageCollector`) are storage 
hygiene:
+nothing reads those rows back, there is no restore verb, and the name frees up 
at
+once. **The purge tombstone** holds an identifier only while a cleanup job 
runs, to
+stop a recreate landing on the old storage prefix.
+
+Gravitino keeps deleted rows and reserves deleted names, yet a user cannot see 
what
+they dropped or get it back. Missing is a durable record saying *this was 
deleted,
+it can still be restored, and here is when that stops being true.*
+
+---
+
+## 2. Goals
+
+1. **Recoverable window**: a dropped table is restorable to its *original* 
identity
+   — same id, name, and attached metadata — until a persisted deadline.
+2. **Bounded**: recoverability ends at that deadline whether or not purge has 
run.
+3. **Reserved name**: the name stays occupied until purge, so no new table can 
take
+   it and make restore ambiguous.
+4. **Discoverable**: users can list what they dropped and how long they have 
left,
+   through the existing Gravitino metadata API.
+5. **No Iceberg REST wire change**: standard clients drop tables exactly as 
today.
+6. **Automatic expiry**: when the window closes, purge removes the files and 
the
+   metadata with no operator action.
+
+---
+
+## 3. Non-Goals
+
+1. **Purge internals**: §5.6 defines the purge lifecycle and its transactions;
+   scheduling parameters, retry tuning, and operator repair tooling are 
deferred.
+2. **Changing existing drop or purge behavior**: the synchronous and 
asynchronous
+   hard-delete paths are untouched, including their defaults and 
`purgeRequested`.
+3. **Non-Iceberg connectors**: JDBC, Kafka, and Paimon drops destroy the 
object at
+   the source. Iceberg is recoverable because a saved metadata pointer can be
+   re-registered.
+4. **Namespace, schema, and view recovery**: different containment rules; a 
follow-up.
+5. **Per-catalog retention**: phase 1 ships one server-level window.
+6. **Trash / recycle-bin UX**: no Web UI work.
+
+---
+
+## 4. Solution Investigations
+
+The decision is **where deletion state lives**. Everything else follows.
+
+| Approach                                            | Pros                   
                                     | Cons                                     
                                                                                
         | Decision   |
+| --------------------------------------------------- | 
----------------------------------------------------------- | 
---------------------------------------------------------------------------------------------------------------------------------
 | ---------- |
+| **A.** Extend the entity row (today's `deleted_at`) | No new table; the 
column already exists nearly everywhere    | Not obviously correct — one row 
means two things; every entity table needs the same new columns; restore 
becomes un-editing fields |  Rejected  |
+| **B.** Extend the async purge job row               | Already carries the 
Iceberg recovery input and the name lock | Conflates a *cleanup job* with a 
*recoverable deletion*; exists only on the purge path; cannot describe 
file-less objects          | Rejected   |
+| **C.** Derive state from the audit / change log     | No new authoritative 
state                                   | A log explains history; it cannot 
arbitrate a race between restore and expiry                                     
                | Rejected   |
+| **D. Dedicated deletion action row**                | Obviously correct, 
reversible, extensible                    | One new table and one nullable 
pointer                                                                         
                   | **Chosen** |
+
+**Obviously correct.** A deletion has its own lifetime, so it gets its own 
row. The
+table row means one thing — the table. The deletion row means one thing — this 
drop,
+and whether it can still be undone. Neither requires knowing which combination 
of
+nullable columns is currently live, which matters in a lifecycle where a wrong
+answer either destroys recoverable data or resurrects data meant to be gone.
+
+**Reversible.** Restore *removes the deletion record*; the table row and 
everything
+keyed to it are untouched. Under option A restore is the inverse of a 
multi-column
+edit, and that inverse is only as trustworthy as the completeness of the field 
list.
+
+**Extensible.** The row describes a deletion, not a table, so adding a type 
later is
+a new column value rather than a migration on another entity table. Option B 
cannot
+follow — a cleanup job presupposes files to clean up.
+
+---
+
+## 5. Proposal
+
+### 5.1 Overview
+
+A drop with soft delete enabled writes a **deletion record** and leaves the 
table
+row in place. The record holds the deadline and is the only thing making the 
table
+recoverable. Restore deletes the record; passing the deadline ends 
recoverability.
+
+```mermaid
+stateDiagram-v2
+    [*] --> Live
+    Live --> Deleted: DELETE (soft delete enabled)
+    Deleted --> Live: UNDROP (before deadline)
+    Deleted --> Expired: deadline passes
+    Expired --> Purging: claimed by purge (§5.6)
+    Purging --> [*]: purge completes
+```
+
+`Expired` is derived, not stored — it is `DELETED` with the deadline behind it.
+`Purging` is stored and is the hard cutoff: once a purge owns the record, 
restore is
+refused even if no file has been deleted. There is no `Restoring` state; 
restore is
+a single transaction.
+
+### 5.2 API
+
+All new surface extends the **Gravitino metadata routes**. No new route tree, 
and no
+change to the Iceberg REST wire protocol.
+
+| Operation    | Route                                                         
             |
+| ------------ | 
-------------------------------------------------------------------------- |
+| Drop         | `DELETE 
/api/metalakes/{m}/catalogs/{c}/schemas/{s}/tables/{t}`             |
+| List deleted | `GET  
/api/metalakes/{m}/catalogs/{c}/schemas/{s}/tables?deleted=true`      |
+| Load deleted | `GET  
/api/metalakes/{m}/catalogs/{c}/schemas/{s}/tables/{t}?deleted=true`  |
+| Restore      | `POST 
/api/metalakes/{m}/catalogs/{c}/schemas/{s}/tables/{t}/undrop`        |
+
+`deleted=true` reuses the existing table routes rather than adding a 
`/deletions`
+tree: the table is addressed by the name it still holds, so clients need no 
second
+identifier. The response carries safe fields only — never a metadata location,
+FileIO properties, or credentials:
+
+```json
+{
+  "name": "orders",
+  "entityId": "984273",
+  "deletedAt": 1784800000000,
+  "retentionExpiresAt": 1784886400000,
+  "deletedBy": "alice",
+  "purgeRequested": true,
+  "recoverable": true
+}
+```
+
+**Drop behavior.** With soft delete disabled, all three paths in §1 are 
untouched.
+With it enabled, a deletion record is created and the table row is retained;
+`purgeRequested` is *captured on the record* and consumed by purge when the 
window
+closes (§5.6), so the parameter keeps its meaning — files still die if it was
+`true`, just later.
+
+**Errors.** No conditional headers, so no `412` or `428`.
+
+| Code  | Condition                                                            
      |
+| ----- | 
-------------------------------------------------------------------------- |
+| `204` | Drop accepted                                                        
      |
+| `200` | Deleted read or restore succeeded                                    
      |
+| `400` | Malformed request                                                    
      |
+| `403` | Caller may not read deleted metadata or restore                      
      |
+| `404` | No live table, or no retained deletion — including after a completed 
purge  |
+| `409` | Create, register, or rename targets a name held by a retained 
deletion      |
+| `410` | The deadline has passed, or a purge already owns the record          
       |
+
+### 5.3 Metadata model
+
+One new table, one nullable pointer on the table row.
+
+```mermaid
+erDiagram
+    table_meta {
+        bigint table_id PK
+        varchar table_name
+        bigint deleted_at "stays 0 while retained"
+        varchar deletion_id FK "nullable, unique"
+    }
+    entity_deletion {
+        varchar deletion_id PK

Review Comment:
   I don't think the table_meta table needs to be modified, because for the IRC 
scenario, it is very likely that table_meta does not store the metadata of 
iceberg table. So, a new table is added to store the iceberg tables that have 
been softly deleted.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to