You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@iceberg.apache.org by Alexander Bailey <za...@gmail.com> on 2026/09/11 10:20:16 UTC
[DISCUSS] Defining gc.enabled semantics across engines and clients
Hi all,
I have a PR open that relaxes `gc.enabled` for metadata-only snapshot
expiry, and review feedback has convinced me the underlying problem is
that we've never defined what `gc.enabled` means in full. I'd like to try
and settle the
definition on-list before the behaviour change goes anywhere.
https://github.com/apache/iceberg/pull/17791
The property is not in the spec. The only normative-ish text is one line
in docs/docs/configuration.md:
> gc.enabled | true | Allows garbage collection operations such as
> expiring snapshots and removing orphan files
That wording arguably supports the strictest reading ("expiry is a GC
operation, so it is blocked"), which is why I don't think of this as a
bug fix -- it's a semantic decision the community should make.
Java implements at least three different meanings today:
1. `CatalogUtil.dropTableData` proceeds when `gc.enabled=false`: it
skips data files but still deletes manifests, manifest lists,
statistics, previous metadata and the metadata file. No error. That
is exactly the `METADATA_ONLY` semantic we just added to
`ExpireSnapshots.CleanupLevel`.
2. `RemoveSnapshots` (before this PR) hard-failed in its constructor,
even for an expiry configured to delete nothing at all.
3. `SparkCatalog.purgeTable`, `ExpireSnapshotsSparkAction`,
`DeleteOrphanFilesSparkAction` and `DeleteReachableFilesSparkAction`
all hard-fail on construction.
4. `HMSTablePropertyHelper` maps `gc.enabled` to Hive's
`external.table.purge`, i.e. treats it as a drop-purge flag. Its own
javadoc already concedes the two flags can disagree and produce
surprising behaviour.
So within one repository the same flag means "skip data files, delete
metadata", "refuse the whole operation", and "control DROP TABLE purge".
The clients have inherited the ambiguity, not resolved it:
- iceberg-rust rejects expiry at commit time in
crates/iceberg/src/transaction/expire_snapshots.rs, with the comment
"Expiring metadata defeats a user's explicit decision to disable GC
(Java refuses too)" -- citing Java as the authority. Meanwhile
table_properties.rs documents the same key as "enabling garbage
collection on drop... data files will not be deleted when a table is
dropped". Two definitions in one crate.
- iceberg-go documents it as "Snapshot expiration is rejected when
false, and data-file cleanup skips deletion."
- iceberg-cpp rejects expiry in the `ExpireSnapshots` constructor, with
the identical error string to Java's pre-PR check ("Cannot expire
snapshots: GC is disabled (deleting files may corrupt other tables)")
in the same construction-time position. It is a direct port of the
behaviour this PR removes.
src/iceberg/update/expire_snapshots.cc
- iceberg-python has a full `expire_snapshots` implementation and no
`gc.enabled` check that I can find under any spelling, so it already
permits what Rust, Go and C++ reject.
So two clients have hard-coded Java's current behaviour -- one citing
Java in a comment, the other copying its error message verbatim -- while
a third ignores the property for this operation entirely. Whichever
definition we pick, the worst outcome for those implementations is Java
changing quietly, which is why I'd rather settle it here than in a Java
PR.
Proposal. Define `gc.enabled=false` as:
> Physical file deletion is disabled for this table. No client may
> delete a file from the table's storage, whether or not the table's
> metadata still references it. Operations that only rewrite table
> metadata are unaffected.
Consequences:
- Snapshot expiry that deletes no files is permitted; expiry that would
delete files is rejected.
- `remove_orphan_files` and drop-with-purge stay blocked, as today.
- `dropTableData` currently proceeds under `gc.enabled=false` and
deletes manifests, manifest lists, statistics and metadata files,
skipping only data files. Under this definition that becomes
non-conformant. As far as I can tell nothing in-tree depends on it --
Nessie ignores `purge` entirely -- but it is a behaviour change well
outside the scope of the PR above and would need to be its own
discussion.
- The rule is stated once, in the spec's table-property appendix, so
clients implement one thing.
The alternative, raised by Amogh in review and worth taking seriously:
keep `gc.enabled` as a blunt "no snapshot removal at all" switch, on the
grounds that catalogs and platform operators may already rely on it that
way, and introduce a separate, finer-grained property for cleanup
control. I lean against adding a property when `CleanupLevel` already
expresses the granularity, but I don't think the concern about existing
opinionated deployments is wrong.
Questions to answer:
1. Which definition do we want -- "no physical deletion" or "no snapshot
removal"?
2. Should this land in the spec (table-property appendix) or stay
documentation-only?
3. Error or silent skip when a cleanup operation is blocked? Java
currently does both.
4. Should the strict reading extend to metadata files on drop, making
`dropTableData` skip manifests and metadata too, or do we grandfather
its current behaviour?
5. Does anything need saying about REST catalogs, which can reject the
remove-snapshot update server-side regardless of what clients do?
Happy to write the spec PR and open tracking issues in the Python, Rust,
Go and C++ repos once there's a direction.
Thanks,
Xander
Re: [DISCUSS] Defining gc.enabled semantics across engines and clients
Posted by Alexander Bailey <za...@gmail.com>.
Thanks Dan and Russell. I'm happy to leave the formal specification of the
property aside.
Dan, could you clarify what you mean by relying on the catalog to enforce
this? Do you mean that, where preventing deletion is a requirement, the
catalog should vend credentials without delete permissions, leaving
deletion to catalog-managed maintenance? That would make sense to me:
rejecting a snapshot update alone doesn't prevent a client with storage
delete permissions from deleting files.
For #17791, the change is narrower: when gc.enabled=false, allow snapshot
expiration only with CleanupLevel.NONE, which skips file cleanup. The
default cleanup mode and METADATA_ONLY would remain blocked.
This preserves the existing protection for data files and shared metadata
files while allowing snapshot entries to be removed without deleting their
underlying files.
Would either of you object to proceeding with that limited change, leaving
the broader definition and enforcement questions separate?
https://github.com/apache/iceberg/pull/17791
On Fri, 11 Sept 2026 at 21:39, Daniel Weeks <dw...@apache.org> wrote:
> The origins of 'gc.enabled' go back quite far and fall more into the
> category of convention rather than specification. The surrounding
> ecosystem has changed significantly and while I think clients
> should generally abide by the convention, I don't think codifying this is
> the right approach. There are also FileIO flags that disable deletes, but
> similarly, we wouldn't want to codify those behaviors.
>
> I'd rather rely on catalog implementations to enforce requirements when
> necessary; not on clients to do the right thing.
>
> -Dan
>
> On Fri, Sep 11, 2026 at 8:33 AM Russell Spitzer <ru...@gmail.com>
> wrote:
>
>>
>> We added this when we started snapshotting existing data: an Iceberg
>> table that only represents files owned by another system. The metadata
>> would be all new and owned by the Iceberg Table but the data files would
>> still be owned by another system. We did not want Iceberg maintenance to
>> touch those files. I have no problem writing that down. I don't think it
>> matters much how an implementation enforces it, as long as it does not
>> delete data files. We could put this in the spec appendix; when we added
>> the property we simply were not putting table properties in the spec.
>>
>> I don't really see this as ambiguous in the Java Repo. The operations
>> that fail (or skip work) under gc.enabled=false are the ones *that might
>> delete a data file*. How that is enforced differs but that doesn't
>> really bother me. It is true that in some cases you could run the same API
>> without any data-file risk if we checked the cleanup mode instead of
>> failing in the constructor but we just haven't done that yet.
>>
>> So following for q4, the behavior is fine
>> And Q5 - It would be too late, if a client already removed the files, it
>> doesn't matter if the catalog rejects the commit removing those snapshots.
>> If the Catalog is removing data files (through a purge or it's own
>> maintenance) then it should respect the GC flag.
>>
>> TL;DR - The invariant here is "*Don't remove this table's data files"* and
>> I don't think anything needs to change about that.
>>
>> On Fri, Sep 11, 2026 at 5:20 AM Alexander Bailey <za...@gmail.com>
>> wrote:
>>
>>> Hi all,
>>>
>>> I have a PR open that relaxes `gc.enabled` for metadata-only snapshot
>>> expiry, and review feedback has convinced me the underlying problem is
>>> that we've never defined what `gc.enabled` means in full. I'd like to
>>> try and settle the
>>> definition on-list before the behaviour change goes anywhere.
>>>
>>> https://github.com/apache/iceberg/pull/17791
>>>
>>> The property is not in the spec. The only normative-ish text is one line
>>> in docs/docs/configuration.md:
>>>
>>> > gc.enabled | true | Allows garbage collection operations such as
>>> > expiring snapshots and removing orphan files
>>>
>>> That wording arguably supports the strictest reading ("expiry is a GC
>>> operation, so it is blocked"), which is why I don't think of this as a
>>> bug fix -- it's a semantic decision the community should make.
>>>
>>> Java implements at least three different meanings today:
>>>
>>> 1. `CatalogUtil.dropTableData` proceeds when `gc.enabled=false`: it
>>> skips data files but still deletes manifests, manifest lists,
>>> statistics, previous metadata and the metadata file. No error. That
>>> is exactly the `METADATA_ONLY` semantic we just added to
>>> `ExpireSnapshots.CleanupLevel`.
>>>
>>> 2. `RemoveSnapshots` (before this PR) hard-failed in its constructor,
>>> even for an expiry configured to delete nothing at all.
>>>
>>> 3. `SparkCatalog.purgeTable`, `ExpireSnapshotsSparkAction`,
>>> `DeleteOrphanFilesSparkAction` and `DeleteReachableFilesSparkAction`
>>> all hard-fail on construction.
>>>
>>> 4. `HMSTablePropertyHelper` maps `gc.enabled` to Hive's
>>> `external.table.purge`, i.e. treats it as a drop-purge flag. Its own
>>> javadoc already concedes the two flags can disagree and produce
>>> surprising behaviour.
>>>
>>> So within one repository the same flag means "skip data files, delete
>>> metadata", "refuse the whole operation", and "control DROP TABLE purge".
>>>
>>> The clients have inherited the ambiguity, not resolved it:
>>>
>>> - iceberg-rust rejects expiry at commit time in
>>> crates/iceberg/src/transaction/expire_snapshots.rs, with the comment
>>> "Expiring metadata defeats a user's explicit decision to disable GC
>>> (Java refuses too)" -- citing Java as the authority. Meanwhile
>>> table_properties.rs documents the same key as "enabling garbage
>>> collection on drop... data files will not be deleted when a table is
>>> dropped". Two definitions in one crate.
>>>
>>> - iceberg-go documents it as "Snapshot expiration is rejected when
>>> false, and data-file cleanup skips deletion."
>>>
>>> - iceberg-cpp rejects expiry in the `ExpireSnapshots` constructor, with
>>> the identical error string to Java's pre-PR check ("Cannot expire
>>> snapshots: GC is disabled (deleting files may corrupt other tables)")
>>> in the same construction-time position. It is a direct port of the
>>> behaviour this PR removes.
>>> src/iceberg/update/expire_snapshots.cc
>>>
>>> - iceberg-python has a full `expire_snapshots` implementation and no
>>> `gc.enabled` check that I can find under any spelling, so it already
>>> permits what Rust, Go and C++ reject.
>>>
>>> So two clients have hard-coded Java's current behaviour -- one citing
>>> Java in a comment, the other copying its error message verbatim -- while
>>> a third ignores the property for this operation entirely. Whichever
>>> definition we pick, the worst outcome for those implementations is Java
>>> changing quietly, which is why I'd rather settle it here than in a Java
>>> PR.
>>>
>>> Proposal. Define `gc.enabled=false` as:
>>>
>>> > Physical file deletion is disabled for this table. No client may
>>> > delete a file from the table's storage, whether or not the table's
>>> > metadata still references it. Operations that only rewrite table
>>> > metadata are unaffected.
>>>
>>> Consequences:
>>>
>>> - Snapshot expiry that deletes no files is permitted; expiry that would
>>> delete files is rejected.
>>>
>>> - `remove_orphan_files` and drop-with-purge stay blocked, as today.
>>>
>>> - `dropTableData` currently proceeds under `gc.enabled=false` and
>>> deletes manifests, manifest lists, statistics and metadata files,
>>> skipping only data files. Under this definition that becomes
>>> non-conformant. As far as I can tell nothing in-tree depends on it --
>>> Nessie ignores `purge` entirely -- but it is a behaviour change well
>>> outside the scope of the PR above and would need to be its own
>>> discussion.
>>>
>>> - The rule is stated once, in the spec's table-property appendix, so
>>> clients implement one thing.
>>>
>>> The alternative, raised by Amogh in review and worth taking seriously:
>>> keep `gc.enabled` as a blunt "no snapshot removal at all" switch, on the
>>> grounds that catalogs and platform operators may already rely on it that
>>> way, and introduce a separate, finer-grained property for cleanup
>>> control. I lean against adding a property when `CleanupLevel` already
>>> expresses the granularity, but I don't think the concern about existing
>>> opinionated deployments is wrong.
>>>
>>> Questions to answer:
>>>
>>> 1. Which definition do we want -- "no physical deletion" or "no snapshot
>>> removal"?
>>>
>>> 2. Should this land in the spec (table-property appendix) or stay
>>> documentation-only?
>>>
>>> 3. Error or silent skip when a cleanup operation is blocked? Java
>>> currently does both.
>>>
>>> 4. Should the strict reading extend to metadata files on drop, making
>>> `dropTableData` skip manifests and metadata too, or do we grandfather
>>> its current behaviour?
>>>
>>> 5. Does anything need saying about REST catalogs, which can reject the
>>> remove-snapshot update server-side regardless of what clients do?
>>>
>>> Happy to write the spec PR and open tracking issues in the Python, Rust,
>>> Go and C++ repos once there's a direction.
>>>
>>> Thanks,
>>> Xander
>>>
>>
Re: [DISCUSS] Defining gc.enabled semantics across engines and clients
Posted by Daniel Weeks <dw...@apache.org>.
The origins of 'gc.enabled' go back quite far and fall more into the
category of convention rather than specification. The surrounding
ecosystem has changed significantly and while I think clients
should generally abide by the convention, I don't think codifying this is
the right approach. There are also FileIO flags that disable deletes, but
similarly, we wouldn't want to codify those behaviors.
I'd rather rely on catalog implementations to enforce requirements when
necessary; not on clients to do the right thing.
-Dan
On Fri, Sep 11, 2026 at 8:33 AM Russell Spitzer <ru...@gmail.com>
wrote:
>
> We added this when we started snapshotting existing data: an Iceberg table
> that only represents files owned by another system. The metadata would be
> all new and owned by the Iceberg Table but the data files would still be
> owned by another system. We did not want Iceberg maintenance to touch those
> files. I have no problem writing that down. I don't think it matters much
> how an implementation enforces it, as long as it does not delete data
> files. We could put this in the spec appendix; when we added the property
> we simply were not putting table properties in the spec.
>
> I don't really see this as ambiguous in the Java Repo. The operations that
> fail (or skip work) under gc.enabled=false are the ones *that might
> delete a data file*. How that is enforced differs but that doesn't really
> bother me. It is true that in some cases you could run the same API without
> any data-file risk if we checked the cleanup mode instead of failing in the
> constructor but we just haven't done that yet.
>
> So following for q4, the behavior is fine
> And Q5 - It would be too late, if a client already removed the files, it
> doesn't matter if the catalog rejects the commit removing those snapshots.
> If the Catalog is removing data files (through a purge or it's own
> maintenance) then it should respect the GC flag.
>
> TL;DR - The invariant here is "*Don't remove this table's data files"* and
> I don't think anything needs to change about that.
>
> On Fri, Sep 11, 2026 at 5:20 AM Alexander Bailey <za...@gmail.com>
> wrote:
>
>> Hi all,
>>
>> I have a PR open that relaxes `gc.enabled` for metadata-only snapshot
>> expiry, and review feedback has convinced me the underlying problem is
>> that we've never defined what `gc.enabled` means in full. I'd like to try
>> and settle the
>> definition on-list before the behaviour change goes anywhere.
>>
>> https://github.com/apache/iceberg/pull/17791
>>
>> The property is not in the spec. The only normative-ish text is one line
>> in docs/docs/configuration.md:
>>
>> > gc.enabled | true | Allows garbage collection operations such as
>> > expiring snapshots and removing orphan files
>>
>> That wording arguably supports the strictest reading ("expiry is a GC
>> operation, so it is blocked"), which is why I don't think of this as a
>> bug fix -- it's a semantic decision the community should make.
>>
>> Java implements at least three different meanings today:
>>
>> 1. `CatalogUtil.dropTableData` proceeds when `gc.enabled=false`: it
>> skips data files but still deletes manifests, manifest lists,
>> statistics, previous metadata and the metadata file. No error. That
>> is exactly the `METADATA_ONLY` semantic we just added to
>> `ExpireSnapshots.CleanupLevel`.
>>
>> 2. `RemoveSnapshots` (before this PR) hard-failed in its constructor,
>> even for an expiry configured to delete nothing at all.
>>
>> 3. `SparkCatalog.purgeTable`, `ExpireSnapshotsSparkAction`,
>> `DeleteOrphanFilesSparkAction` and `DeleteReachableFilesSparkAction`
>> all hard-fail on construction.
>>
>> 4. `HMSTablePropertyHelper` maps `gc.enabled` to Hive's
>> `external.table.purge`, i.e. treats it as a drop-purge flag. Its own
>> javadoc already concedes the two flags can disagree and produce
>> surprising behaviour.
>>
>> So within one repository the same flag means "skip data files, delete
>> metadata", "refuse the whole operation", and "control DROP TABLE purge".
>>
>> The clients have inherited the ambiguity, not resolved it:
>>
>> - iceberg-rust rejects expiry at commit time in
>> crates/iceberg/src/transaction/expire_snapshots.rs, with the comment
>> "Expiring metadata defeats a user's explicit decision to disable GC
>> (Java refuses too)" -- citing Java as the authority. Meanwhile
>> table_properties.rs documents the same key as "enabling garbage
>> collection on drop... data files will not be deleted when a table is
>> dropped". Two definitions in one crate.
>>
>> - iceberg-go documents it as "Snapshot expiration is rejected when
>> false, and data-file cleanup skips deletion."
>>
>> - iceberg-cpp rejects expiry in the `ExpireSnapshots` constructor, with
>> the identical error string to Java's pre-PR check ("Cannot expire
>> snapshots: GC is disabled (deleting files may corrupt other tables)")
>> in the same construction-time position. It is a direct port of the
>> behaviour this PR removes.
>> src/iceberg/update/expire_snapshots.cc
>>
>> - iceberg-python has a full `expire_snapshots` implementation and no
>> `gc.enabled` check that I can find under any spelling, so it already
>> permits what Rust, Go and C++ reject.
>>
>> So two clients have hard-coded Java's current behaviour -- one citing
>> Java in a comment, the other copying its error message verbatim -- while
>> a third ignores the property for this operation entirely. Whichever
>> definition we pick, the worst outcome for those implementations is Java
>> changing quietly, which is why I'd rather settle it here than in a Java
>> PR.
>>
>> Proposal. Define `gc.enabled=false` as:
>>
>> > Physical file deletion is disabled for this table. No client may
>> > delete a file from the table's storage, whether or not the table's
>> > metadata still references it. Operations that only rewrite table
>> > metadata are unaffected.
>>
>> Consequences:
>>
>> - Snapshot expiry that deletes no files is permitted; expiry that would
>> delete files is rejected.
>>
>> - `remove_orphan_files` and drop-with-purge stay blocked, as today.
>>
>> - `dropTableData` currently proceeds under `gc.enabled=false` and
>> deletes manifests, manifest lists, statistics and metadata files,
>> skipping only data files. Under this definition that becomes
>> non-conformant. As far as I can tell nothing in-tree depends on it --
>> Nessie ignores `purge` entirely -- but it is a behaviour change well
>> outside the scope of the PR above and would need to be its own
>> discussion.
>>
>> - The rule is stated once, in the spec's table-property appendix, so
>> clients implement one thing.
>>
>> The alternative, raised by Amogh in review and worth taking seriously:
>> keep `gc.enabled` as a blunt "no snapshot removal at all" switch, on the
>> grounds that catalogs and platform operators may already rely on it that
>> way, and introduce a separate, finer-grained property for cleanup
>> control. I lean against adding a property when `CleanupLevel` already
>> expresses the granularity, but I don't think the concern about existing
>> opinionated deployments is wrong.
>>
>> Questions to answer:
>>
>> 1. Which definition do we want -- "no physical deletion" or "no snapshot
>> removal"?
>>
>> 2. Should this land in the spec (table-property appendix) or stay
>> documentation-only?
>>
>> 3. Error or silent skip when a cleanup operation is blocked? Java
>> currently does both.
>>
>> 4. Should the strict reading extend to metadata files on drop, making
>> `dropTableData` skip manifests and metadata too, or do we grandfather
>> its current behaviour?
>>
>> 5. Does anything need saying about REST catalogs, which can reject the
>> remove-snapshot update server-side regardless of what clients do?
>>
>> Happy to write the spec PR and open tracking issues in the Python, Rust,
>> Go and C++ repos once there's a direction.
>>
>> Thanks,
>> Xander
>>
>
Re: [DISCUSS] Defining gc.enabled semantics across engines and clients
Posted by Russell Spitzer <ru...@gmail.com>.
We added this when we started snapshotting existing data: an Iceberg table
that only represents files owned by another system. The metadata would be
all new and owned by the Iceberg Table but the data files would still be
owned by another system. We did not want Iceberg maintenance to touch those
files. I have no problem writing that down. I don't think it matters much
how an implementation enforces it, as long as it does not delete data
files. We could put this in the spec appendix; when we added the property
we simply were not putting table properties in the spec.
I don't really see this as ambiguous in the Java Repo. The operations that
fail (or skip work) under gc.enabled=false are the ones *that might delete
a data file*. How that is enforced differs but that doesn't really bother
me. It is true that in some cases you could run the same API without any
data-file risk if we checked the cleanup mode instead of failing in the
constructor but we just haven't done that yet.
So following for q4, the behavior is fine
And Q5 - It would be too late, if a client already removed the files, it
doesn't matter if the catalog rejects the commit removing those snapshots.
If the Catalog is removing data files (through a purge or it's own
maintenance) then it should respect the GC flag.
TL;DR - The invariant here is "*Don't remove this table's data files"* and
I don't think anything needs to change about that.
On Fri, Sep 11, 2026 at 5:20 AM Alexander Bailey <za...@gmail.com>
wrote:
> Hi all,
>
> I have a PR open that relaxes `gc.enabled` for metadata-only snapshot
> expiry, and review feedback has convinced me the underlying problem is
> that we've never defined what `gc.enabled` means in full. I'd like to try
> and settle the
> definition on-list before the behaviour change goes anywhere.
>
> https://github.com/apache/iceberg/pull/17791
>
> The property is not in the spec. The only normative-ish text is one line
> in docs/docs/configuration.md:
>
> > gc.enabled | true | Allows garbage collection operations such as
> > expiring snapshots and removing orphan files
>
> That wording arguably supports the strictest reading ("expiry is a GC
> operation, so it is blocked"), which is why I don't think of this as a
> bug fix -- it's a semantic decision the community should make.
>
> Java implements at least three different meanings today:
>
> 1. `CatalogUtil.dropTableData` proceeds when `gc.enabled=false`: it
> skips data files but still deletes manifests, manifest lists,
> statistics, previous metadata and the metadata file. No error. That
> is exactly the `METADATA_ONLY` semantic we just added to
> `ExpireSnapshots.CleanupLevel`.
>
> 2. `RemoveSnapshots` (before this PR) hard-failed in its constructor,
> even for an expiry configured to delete nothing at all.
>
> 3. `SparkCatalog.purgeTable`, `ExpireSnapshotsSparkAction`,
> `DeleteOrphanFilesSparkAction` and `DeleteReachableFilesSparkAction`
> all hard-fail on construction.
>
> 4. `HMSTablePropertyHelper` maps `gc.enabled` to Hive's
> `external.table.purge`, i.e. treats it as a drop-purge flag. Its own
> javadoc already concedes the two flags can disagree and produce
> surprising behaviour.
>
> So within one repository the same flag means "skip data files, delete
> metadata", "refuse the whole operation", and "control DROP TABLE purge".
>
> The clients have inherited the ambiguity, not resolved it:
>
> - iceberg-rust rejects expiry at commit time in
> crates/iceberg/src/transaction/expire_snapshots.rs, with the comment
> "Expiring metadata defeats a user's explicit decision to disable GC
> (Java refuses too)" -- citing Java as the authority. Meanwhile
> table_properties.rs documents the same key as "enabling garbage
> collection on drop... data files will not be deleted when a table is
> dropped". Two definitions in one crate.
>
> - iceberg-go documents it as "Snapshot expiration is rejected when
> false, and data-file cleanup skips deletion."
>
> - iceberg-cpp rejects expiry in the `ExpireSnapshots` constructor, with
> the identical error string to Java's pre-PR check ("Cannot expire
> snapshots: GC is disabled (deleting files may corrupt other tables)")
> in the same construction-time position. It is a direct port of the
> behaviour this PR removes.
> src/iceberg/update/expire_snapshots.cc
>
> - iceberg-python has a full `expire_snapshots` implementation and no
> `gc.enabled` check that I can find under any spelling, so it already
> permits what Rust, Go and C++ reject.
>
> So two clients have hard-coded Java's current behaviour -- one citing
> Java in a comment, the other copying its error message verbatim -- while
> a third ignores the property for this operation entirely. Whichever
> definition we pick, the worst outcome for those implementations is Java
> changing quietly, which is why I'd rather settle it here than in a Java
> PR.
>
> Proposal. Define `gc.enabled=false` as:
>
> > Physical file deletion is disabled for this table. No client may
> > delete a file from the table's storage, whether or not the table's
> > metadata still references it. Operations that only rewrite table
> > metadata are unaffected.
>
> Consequences:
>
> - Snapshot expiry that deletes no files is permitted; expiry that would
> delete files is rejected.
>
> - `remove_orphan_files` and drop-with-purge stay blocked, as today.
>
> - `dropTableData` currently proceeds under `gc.enabled=false` and
> deletes manifests, manifest lists, statistics and metadata files,
> skipping only data files. Under this definition that becomes
> non-conformant. As far as I can tell nothing in-tree depends on it --
> Nessie ignores `purge` entirely -- but it is a behaviour change well
> outside the scope of the PR above and would need to be its own
> discussion.
>
> - The rule is stated once, in the spec's table-property appendix, so
> clients implement one thing.
>
> The alternative, raised by Amogh in review and worth taking seriously:
> keep `gc.enabled` as a blunt "no snapshot removal at all" switch, on the
> grounds that catalogs and platform operators may already rely on it that
> way, and introduce a separate, finer-grained property for cleanup
> control. I lean against adding a property when `CleanupLevel` already
> expresses the granularity, but I don't think the concern about existing
> opinionated deployments is wrong.
>
> Questions to answer:
>
> 1. Which definition do we want -- "no physical deletion" or "no snapshot
> removal"?
>
> 2. Should this land in the spec (table-property appendix) or stay
> documentation-only?
>
> 3. Error or silent skip when a cleanup operation is blocked? Java
> currently does both.
>
> 4. Should the strict reading extend to metadata files on drop, making
> `dropTableData` skip manifests and metadata too, or do we grandfather
> its current behaviour?
>
> 5. Does anything need saying about REST catalogs, which can reject the
> remove-snapshot update server-side regardless of what clients do?
>
> Happy to write the spec PR and open tracking issues in the Python, Rust,
> Go and C++ repos once there's a direction.
>
> Thanks,
> Xander
>