Skip to content

[python] Reject changing immutable options once a table has snapshots#8565

Open
plusplusjiajia wants to merge 1 commit into
apache:masterfrom
plusplusjiajia:python-immutable-options
Open

[python] Reject changing immutable options once a table has snapshots#8565
plusplusjiajia wants to merge 1 commit into
apache:masterfrom
plusplusjiajia:python-immutable-options

Conversation

@plusplusjiajia

Copy link
Copy Markdown
Member

Purpose

Java's SchemaManager rejects altering structural options (IMMUTABLE_OPTIONS: primary-key, partition, merge-engine, bucket-key, type, ...) once a table has snapshots, but pypaimon applied
SetOption/RemoveOption with no guard at all — through the filesystem/jdbc catalogs one could alter e.g. merge-engine or type on a table with data, silently changing read semantics or making the table
unreadable (the REST path is safe: the server enforces it).

This ports the guard: CoreOptions.IMMUTABLE_OPTIONS mirrors the Java @Immutable options; commit_changes rejects SET (when the value actually changes) and RESET of those keys once a snapshot exists, with
the Java-identical error message. The empty-table carve-out is kept.

test_time_travel_before_row_tracking_raises relied on enabling row-tracking.enabled on a table that already had a snapshot — a state the Java guard forbids constructing — so it now asserts the alter itself
is rejected.

Tests

New test_alter_immutable_options (empty-table allowed; with snapshots SET/RESET rejected; mutable options unaffected). 229 tests across the alter-related files pass.

@plusplusjiajia plusplusjiajia force-pushed the python-immutable-options branch 2 times, most recently from 78499e2 to 06382a9 Compare July 11, 2026 18:08
The Java SchemaManager rejects altering structural options
(IMMUTABLE_OPTIONS: primary-key, partition, merge-engine, bucket-key,
type, ...) once a table has snapshots, but pypaimon's
SchemaManager._generate_table_schema applied SetOption / RemoveOption
with no guard at all. Through the filesystem/jdbc catalogs this allowed
altering e.g. merge-engine or type on a table with data, silently
changing read semantics or making the table unreadable (the REST
catalog path was safe because the server enforces the guard).

Port the guard: CoreOptions.IMMUTABLE_OPTIONS mirrors the @immutable
annotated options in the Java CoreOptions (canonical key strings), and
commit_changes rejects SET (when the value actually changes) and RESET
of those keys once the table has a snapshot, with the same error
message as Java. The Java empty-table carve-out is kept, and semantic
no-ops are recognized as unchanged, mirroring Java's
isUnchangedNormalizedKey: replaying the actual primary/partition keys
(stored in the schema fields, not the options map) or setting the
default type explicitly passes without inserting a duplicate option.
No-op detection compares against the accumulated options so repeated
changes to the same key in one batch still apply in order.

The 'type' no-op is compared against CoreOptions.TYPE.default_value()
rather than a hard-coded string, and that default is aligned with
Java's TableType.TABLE ('table', not the previous 'primary-key' which
is not even a valid table type and is rejected at create time by the
filesystem/jdbc catalogs). So an implicit-type table now resolves its
effective type to 'table', and 'type'='table' is a true no-op instead
of silently reporting success while the reloaded type differed. Like
the Java side, 'type' is rejected even without snapshots: table kinds
like format tables never create Paimon snapshots but can still hold
data.

The snapshot-presence check must not fail open, so
_get_latest_snapshot_from_filesystem treats the LATEST file as the hint
it is and reconciles with a scan of the snapshot files, like the Java
SnapshotManager.findLatest: it trusts the hint only when it points to
an existing snapshot and no newer snapshot exists (a lagging
_commit_latest_hint leaves it stale), and when the hint indicates the
table has snapshots but none can be resolved -- an unreadable LATEST
plus a listing an IO error truncated to empty -- it raises rather than
reporting the populated table as empty.

test_time_travel_before_row_tracking_raises relied on enabling
row-tracking.enabled on a table that already had a snapshot -- a state
the Java guard forbids constructing -- so it now asserts the alter
itself is rejected.
@plusplusjiajia plusplusjiajia force-pushed the python-immutable-options branch from 06382a9 to f60d646 Compare July 12, 2026 01:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant