Skip to content

Core, Spark: guard DeleteOrphanFiles against shared table locations#17254

Open
terrytlu wants to merge 1 commit into
apache:mainfrom
terrytlu:delete-orphan-shared-location
Open

Core, Spark: guard DeleteOrphanFiles against shared table locations#17254
terrytlu wants to merge 1 commit into
apache:mainfrom
terrytlu:delete-orphan-shared-location

Conversation

@terrytlu

@terrytlu terrytlu commented Jul 16, 2026

Copy link
Copy Markdown

Fix #17213

Approach

Every Iceberg metadata.json (including .metadata.json.gz) carries the creating table's immutable table-uuid. So, before deleting anything, we inspect the table's metadata directory:

  • if it contains a metadata.json that is not part of this table's own history (current + previousFiles()), and
  • its table-uuid differs from, or cannot be read for, this table's uuid,

→ we conclude that another table shares the location and stop before any file is deleted.

Constraint — requires SupportsPrefixOperations: the detection relies on table.io().listPrefix(...) to walk the metadata directory. This means the table's FileIO must implement the SupportsPrefixOperations interface.

Fail-safe bias: a missing or unreadable uuid (legacy file without a uuid, corrupt file, or compressed file) is always treated as a conflict (preferring false positives over silently deleting another table's data).

Changes

  • core: new OrphanFileUtils.hasOtherTableInLocation + readTableUuid (the latter @VisibleForTesting, transparently handling gzip); new TestOrphanFileUtils covering conflict / no-conflict / null-uuid / compressed / corrupt / unsupported-FileIO cases.
  • api: DeleteOrphanFiles interface extension (above).
  • spark 3.5 / 4.0 / 4.1: DeleteOrphanFilesSparkAction extracts shouldSkipDueToLocationConflict() (called at the top of doExecute, keeping the main flow clean); RemoveOrphanFilesProcedure exposes a location_conflict_mode parameter and forwards it.
  • docs: docs/spark-procedures.md documents the new parameter.

API changes

In api's DeleteOrphanFiles interface:

  • New enum LocationConflictMode { ERROR, IGNORE, DELETE } plus fromString(...) (consistent with the existing PrefixMismatchMode).

Mode semantics:

Condition Mode Behavior
Conflict ERROR (default) Abort with ValidationException; protects the other table (recommended default).
Conflict IGNORE Skip the cleanup entirely (returns success without deleting anything); preserve the other table's files without erroring.
Conflict DELETE Skip the shared-location check entirely and proceed with deletion (legacy behavior); use only when you are certain no other table shares the location.
No conflict all modes Cleanup proceeds normally; real orphans are deleted.
Check cannot run (e.g. FileIO lacks SupportsPrefixOperations) all modes logged as "unable to determine" and skip the cleanup; To force the cleanup, set the mode to DELETE.

Tests

  • core: TestOrphanFileUtils (foreign metadata with same/foreign uuid, missing uuid, compressed, corrupt, unsupported-FileIO throws).
  • spark: TestRemoveOrphanFilesProcedure exercises all three modes across catalog variants.

AI Disclosure

Model: hy3
Platform/Tool: CodeBuddy
Human Oversight: fully reviewed
Prompt Summary: Implement a shared-location guard for DeleteOrphanFiles (core detection helper + api LocationConflictMode enum + Spark 3.5/4.0/4.1 action/procedure wiring) and fix the resulting CI checkstyle/test failures.

@terrytlu
terrytlu force-pushed the delete-orphan-shared-location branch 8 times, most recently from d4c1156 to 450230f Compare July 17, 2026 01:39
When two Iceberg tables share the same underlying location (for example a test
table created by copying SHOW CREATE TABLE but forgetting to change the
LOCATION), running orphan-file cleanup for one table walks the whole location
and deletes every file not reachable from that single table's metadata,
silently corrupting the other table.
Add a locationConflictMode(LocationConflictMode) option to DeleteOrphanFiles,
defaulting to ERROR. Before deleting anything, it detects whether another table
shares the metadata location by comparing the immutable table-uuid stored in
each metadata.json (also handling .metadata.json.gz). A missing or unreadable
uuid (legacy file without a uuid, corrupt, or compressed) is always treated as
a conflict (fail-safe). In ERROR mode it aborts when a conflict is detected; in
IGNORE mode the cleanup is skipped entirely and the other table's files are
preserved; in DELETE mode cleanup proceeds even if a conflict is detected
(legacy behavior), and should be used only when you are certain no other table
shares the location.
The detection helper lives in core (OrphanFileUtils.hasOtherTableInLocation) and
is wired into DeleteOrphanFilesSparkAction for Spark 3.5/4.0/4.1. The Spark
remove_orphan_files procedure also exposes a location_conflict_mode parameter,
and docs/spark-procedures.md documents it. Core and Spark procedure tests cover
the conflict / no-conflict / null-uuid / compressed / corrupt cases.
Closes apache#17213
Signed-off-by: terrytlu <terrytlu@tencent.com>

Generated-by: Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DeleteOrphanFiles can corrupt other tables that share the same table location

1 participant