Skip to content

[wip][core][spark][flink] Support TRUNCATE TABLE on format tables - #9330

Draft
sundapeng wants to merge 2 commits into
apache:masterfrom
sundapeng:feat/format-table-truncate
Draft

[wip][core][spark][flink] Support TRUNCATE TABLE on format tables#9330
sundapeng wants to merge 2 commits into
apache:masterfrom
sundapeng:feat/format-table-truncate

Conversation

@sundapeng

@sundapeng sundapeng commented Aug 20, 2026

Copy link
Copy Markdown
Member

Stacked on #9296. Its commit shows up here too until it merges. Review only the last commit.

Purpose

TRUNCATE TABLE did not work on a Format Table, and the two statements failed differently.

TRUNCATE TABLE t was rejected in the planner: PaimonFormatTable does not implement
TruncatableTable, so DataSourceV2Strategy failed evaluating asTruncatable with
Table does not support truncates: <table>.

TRUNCATE TABLE t PARTITION (...) got further, into PaimonPartitionManagement, which serves only
FileStoreTable and failed there with Only FileStoreTable supports partitions. — a message about
an internal type, on a table that has partitions and lists them under SHOW PARTITIONS.

Underneath both, FormatTableCommit.truncateTable() and truncatePartitions() threw an empty
UnsupportedOperationException, and Flink's FlinkFormatTableSink did not implement
SupportsTruncate either.

The workaround was INSERT OVERWRITE ... PARTITION (...) SELECT ... WHERE false, which does not
extend to the whole table: without a static partition, an overwrite deletes only the directories it
actually wrote a file into, so an empty result clears nothing.

Approach

FormatTableCommit deletes the data files, reusing deletePreviousDataFile, the primitive a static
INSERT OVERWRITE already clears partition directories with. Only data files go: the partition
directories stay, and so do their catalog registrations, so SHOW PARTITIONS returns what it
returned before (SPARK-34418). Staging trees of concurrent writers are left alone, on the same
judgement FormatTableScan reads with.

What the emptied partitions hold is reported to the catalog as an exact zero, dated to the
truncation and carried with replaceStatistics, so a truncated partition stops describing files
that are gone. The two operations differ in what they claim: an overwrite reports only the files it
removed itself, so that concurrent writers do not each claim the whole subtree, while truncation
states that the partition holds nothing whoever deleted the files — a partition that was already
empty reports zero as well.

Which partitions the table has is answered by whatever the table reads its partitions from. For a
catalog-managed table that is the catalog, so truncating the whole table empties the registered
partitions and leaves a directory still awaiting MSCK REPAIR TABLE alone — matching what
TruncateTableCommand does for a v1 partitioned table. Under filesystem partition discovery the
directory is the answer, and truncating clears the partition levels below the table root.

On the Spark side PaimonFormatTable implements TruncatableTable and overrides
SupportsAtomicPartitionManagement.truncatePartition(s) rather than taking the statement over in
PaimonStrategy: TruncatePartitionExec already expands a partial spec and refreshes the cache,
and managed tables reach truncation through the same interface. Truncating a named partition the
table does not have is an error, reported per entry point as Spark's TruncateTableSuiteBase
expects.

Flink has no TRUNCATE TABLE ... PARTITION, so FlinkFormatTableSink implements only whole-table
truncation.

Tests

  • FormatTableCommitTest: whole-table truncate under filesystem discovery and on a catalog-managed
    table (registered partitions emptied, an unregistered directory untouched), an unpartitioned
    table, named partitions, a leading-prefix spec, and the value-only default partition; staging
    trees survive throughout.
  • FormatTableCommitStatisticsTest: named partitions report an exact zero as a total, a partition
    that was already empty reports zero too, a prefix reports the partitions underneath it, and
    truncating the table reports every registered partition and no unregistered one.
  • FormatTablePartitionManagementTest, CatalogManagedPartitionTest: registrations survive,
    unregistered partitions are refused, a .. value is rejected before anything is deleted, and an
    end-to-end truncate through the REST catalog.
  • FormatTableTestBase: TRUNCATE TABLE, TRUNCATE TABLE ... PARTITION with a full and a partial
    spec, and a partition the table does not have.
  • FormatTableITCase: TRUNCATE TABLE on a partitioned and an unpartitioned Format Table in Flink.

API and Format

No format change. PaimonFormatTable gains TruncatableTable and FlinkFormatTableSink gains
SupportsTruncate — both engine-side interfaces already implemented for managed tables.
format-table.implementation = engine is unchanged and still does not support truncation.

Documentation

docs/docs/spark/sql-write.md and docs/docs/flink/sql-write.mdx.

@sundapeng
sundapeng marked this pull request as draft August 20, 2026 23:24
@sundapeng sundapeng changed the title [core][spark][flink] Support TRUNCATE TABLE on format tables [wip][core][spark][flink] Support TRUNCATE TABLE on format tables Aug 20, 2026
The writer already counted the rows and the bytes, the commit already knows which
partitions it wrote, and the catalog now has somewhere to put both. This connects
them, behind format-table.commit.report-partition-statistics.

Whether a commit replaces or adds follows from what it did to the partition. An
appending commit saw only its own files, so it reports an increment: a Flink sink
commits once per writer subtask, and N increments over one partition add up to
what the job wrote. An overwriting commit replaced everything the partitions held,
so what it wrote is the total and it reports that.

Static prefix overwrite is why a pure increment cannot express this. Clearing a
prefix empties every partition beneath it, including ones this commit writes
nothing to; their old data is gone and no increment says so. Those partitions
report zero — an exact zero, they really are empty — dated to this commit, since a
time reported as unknown would leave the stored one describing files that are
gone. They ride in the same create request as the written ones, since a statistic
can only be reported for a partition its own request registers. Nothing here
unregisters a partition, whatever the numbers say. Only the files this commit
actually deleted count as emptying a partition: one another writer removed first
is not this commit's doing, and counting it as such would have every concurrent
writer report the whole subtree.

What a commit wrote into a partition is folded the way PartitionEntry already
folds the same five fields: an immutable PartitionStatistics per file, merged into
a map by spec. A count nobody took leaves that field unknown for the whole
partition rather than reporting the sum of the rest as exact.

Reporting is unconditional, the way a Paimon table's commit reports through
commitSnapshot. An increment can drift, from a job that retries or a writer that
is not Paimon, and a field cannot be written back to unknown once it is set; what
converges it is a later full measurement over the same partition.

Tests: FormatTableCommitStatisticsTest covers append, dynamic overwrite, static
prefix overwrite of a partition this commit does not write, a directory that is no
partition of this table being left alone rather than failing the commit, the
summation of the independent increments of concurrent writers of one partition, a
listing that answers under another scheme, an uncounted file between two counted
ones so that unknown has to stay unknown, a report the catalog refuses taking the
commit down with it and the written file with it, the same for an overwrite that
has already deleted what the partition held, and the numbers reaching the catalog
through the write builder.
TRUNCATE TABLE on a Format Table was rejected in the planner, because
PaimonFormatTable did not implement TruncatableTable, and TRUNCATE TABLE
... PARTITION reached PaimonPartitionManagement, which serves only
FileStoreTable and failed there with "Only FileStoreTable supports
partitions." Both FormatTableCommit entry points threw an empty
UnsupportedOperationException, and Flink's FlinkFormatTableSink did not
implement SupportsTruncate either.

FormatTableCommit now removes the data files, reusing the primitive that
INSERT OVERWRITE already clears partition directories with. Only data
files go: the partition directories stay, and so do their catalog
registrations, so SHOW PARTITIONS returns what it returned before
(SPARK-34418). What the emptied partitions hold is reported to the
catalog as an exact zero, replacing the statistics rather than adjusting
them, so a truncated partition stops describing files that are gone.
Truncation states that a partition holds nothing whoever deleted the
files, so one that was already empty reports zero as well - unlike an
overwrite, which reports only what it removed itself so that concurrent
writers do not each claim the whole subtree.

Which partitions the table has is answered by whatever the table reads
its partitions from. For a catalog-managed table that is the catalog, so
truncating the whole table empties the registered partitions and leaves a
directory still awaiting MSCK REPAIR TABLE alone; for filesystem
partition discovery the directory is the answer, and truncating clears
the partition levels below the table root. Truncating a named partition
the table does not have is an error, reported as Spark expects it per
entry point.

Flink has no TRUNCATE TABLE ... PARTITION, so FlinkFormatTableSink
implements only whole-table truncation.
@sundapeng
sundapeng force-pushed the feat/format-table-truncate branch from f710014 to 94ed7ad Compare August 20, 2026 23:59
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