nevzheng commented on code in PR #12257: URL: https://github.com/apache/gravitino/pull/12257#discussion_r3708420063
########## design-docs/deletion-v2/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` | + Review Comment: nack. non-substantive naming debate. undrop is in the PRD, and is also a route used in other systems see appendix a. review comments should try to make the design substantativelty better, debating naming is typically bike shedding. -- 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]
