Skip to content

[spark] Fix MSCK dropping the null partition of a value-only format table - #8878

Closed
sundapeng wants to merge 1 commit into
apache:masterfrom
sundapeng:fix/msck-default-partition-in-value-only-layout
Closed

[spark] Fix MSCK dropping the null partition of a value-only format table#8878
sundapeng wants to merge 1 commit into
apache:masterfrom
sundapeng:fix/msck-default-partition-in-value-only-layout

Conversation

@sundapeng

@sundapeng sundapeng commented Jul 28, 2026

Copy link
Copy Markdown
Member

Purpose

__DEFAULT_PARTITION__ is partition.default-name, the value Paimon substitutes when a dynamic partition column is null or empty. It is the same value whatever the path layout — what the layout decides is the directory name:

layout null partition directory first character
key=value (default) dt=__DEFAULT_PARTITION__ d — the hidden-directory rule never touches it
value-only (format-table.partition-path-only-value=true) __DEFAULT_PARTITION__ _ — the rule swallows it

PartitionPathUtils.isHiddenFile already knows about this and spares the directory whose name equals the table's default partition name — but only when it is told what that name is. FormatTablePartitionRepair called the five-argument searchPartSpecAndPaths, which passes defaultPartValue as null, so the rescue never applied and MSCK never saw the directory.

Two ways to lose data followed:

  • MSCK REPAIR TABLE ... ADD PARTITIONS never registered the null partition;
  • MSCK REPAIR TABLE ... SYNC PARTITIONS read it as registered but the directory is gone and unregistered a partition that still holds data. Later queries then skip it without a word.

listStatusRecursively applies the same rule while descending, so a null value on a non-leaf level hid the whole subtree rather than a single directory.

Tests

FormatTablePartitionRepairTest, 18 cases green. New cases cover:

case pins
repairAddsTheNullPartitionDirectoryInValueOnlyLayout ADD registers it (2 instead of 1 before the fix)
repairKeepsTheRegisteredNullPartitionInValueOnlyLayout SYNC is a no-op instead of unregistering live data
repairDescendsIntoANullPartitionSubtreeInValueOnlyLayout a null value on a non-leaf level no longer hides the subtree
repairReadsTheDefaultPartitionNameFromTableOptions the name comes from partition.default-name; hardcoding the literal passes every other case
repairKeepsAnUnderscoreValueInTheKeyValueLayout dt=_abc is unaffected — the underscore sits on the value, not on the first character of the directory

CatalogManagedPartitionMsckRepairTest (11 SQL-level cases) stays green.

API and Format

No API or format change. partitionFilter and partitionType stay null on purpose: repair diffs raw directory names, and handing it a partition type would reintroduce the cast that rewrites month=01 to 1 and no longer round-trips to the real directory. A comment now records that reason so the two nulls do not get "helpfully" filled in later.

Note on scope

This fixes the default partition name. A partition value that merely starts with _ or . (dt='_abc') is still dropped by SYNC in a value-only layout, because _ and . are not in CHAR_TO_ESCAPE and the directory is written bare. Widening the rule is a separate discussion: in a value-only layout a partition directory and a committer staging directory sit at the same level and cannot be told apart by name, so relaxing the rule would register _temporary/__magic trees as partitions. Happy to open a separate issue for that if you agree it needs a decision.

@sundapeng sundapeng changed the title [spark] Let MSCK see the null partition of a value-only format table [spark] Fix MSCK dropping the null partition of a value-only format table Jul 28, 2026
`__DEFAULT_PARTITION__` is `partition.default-name`, the value Paimon
substitutes when a dynamic partition column is null or empty. It is the same
value whatever the path layout. What the layout decides is the directory name:
with `key=value` the null partition lives in `dt=__DEFAULT_PARTITION__`, whose
first character is `d`; with a value-only layout the directory name is the value
itself, so it begins with `_` and the generic hidden-directory rule swallows it.

`PartitionPathUtils.isHiddenFile` already knows this and spares the directory
whose name equals the table's default partition name, but only when it is told
what that name is. `FormatTablePartitionRepair` called the five-argument
`searchPartSpecAndPaths`, which passes `defaultPartValue` as null, so the rescue
never applied and MSCK never saw the directory. Two ways to lose data followed:

- `MSCK REPAIR TABLE ... ADD PARTITIONS` never registered the null partition;
- `MSCK REPAIR TABLE ... SYNC PARTITIONS` read it as "registered but the
  directory is gone" and unregistered a partition that still holds data. Later
  queries then skip it without a word.

`listStatusRecursively` applies the same rule while descending, so a null value
on a non-leaf level hid the whole subtree rather than one directory.

The scan path already gets this right: `FileSystemSplitEnumerator` passes
`table.defaultPartName()` to the eight-argument overload. Repair now does the
same. `partitionFilter` and `partitionType` stay null on purpose — repair diffs
raw directory names, and handing it a partition type would reintroduce the cast
that rewrites `month=01` to `1` and no longer round-trips to the real directory.

The registered spec keeps the literal `__DEFAULT_PARTITION__`, which is what
`FormatTableCommit` writes through the same `extractPartitionSpecFromPathOnlyValue`
helper, so both sides of the diff agree and a second SYNC stays a no-op.

Tests cover the missed registration, the wrongful unregistration, a subtree
under a null non-leaf level, a table that overrides `partition.default-name`
(hardcoding the literal passes every other case), and a key=value layout whose
value starts with `_`, which is unaffected because the underscore sits on the
value rather than on the first character of the directory name.
@sundapeng
sundapeng force-pushed the fix/msck-default-partition-in-value-only-layout branch from 429c4ec to 1c1dce2 Compare July 28, 2026 00:09
@sundapeng

Copy link
Copy Markdown
Member Author

Closing this for now.

The failure is confined to the opt-in value-only layout (format-table.partition-path-only-value=true, default false). With the default key=value layout the null partition lives in dt=__DEFAULT_PARTITION__, whose first character is d, so the hidden-directory rule never touches it — and the rescue branch in isHiddenFile is itself guarded by onlyValueInPath, which makes this patch a no-op outside that layout.

It is also only half of the class. A partition value that merely starts with _ or . (dt='_abc') fails the same way in the same layout, because neither character is in CHAR_TO_ESCAPE and the directory is written bare. Fixing only the default partition name leaves the neighbouring case behaving differently for no reason a user could infer.

The root of both is that a value-only layout puts partition directories and committer staging directories on the same level with no way to tell them apart by name, so relaxing the rule would start registering _temporary/__magic trees as partitions. That needs a decision rather than a patch, and we would rather take it as a whole than land a partial fix that makes the remaining half harder to see.

Recording it as a known issue on our side. Happy to reopen or file an issue for the naming ambiguity if that is useful to anyone.

@sundapeng sundapeng closed this Jul 28, 2026
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