Repository navigation
feat(catalog): support writing and committing to a branch - #357
Merged
Merged
Conversation
SteNicholas
force-pushed
the
PAIMON-351
branch
4 times, most recently
from
September 16, 2026 05:28
e0355d1 to
c3af674
Compare
SteNicholas
requested review from
lucasfang and
lxy-9602
and removed request for
lxy-9602
September 16, 2026 12:55
lxy-9602
reviewed
Sep 17, 2026
SteNicholas
force-pushed
the
PAIMON-351
branch
from
September 17, 2026 06:16
c3af674 to
35bd1e7
Compare
A commit was hard-wired to the main branch: `FileStoreCommit::Create()` built its schema and snapshot managers without a branch, so a commit configured with `branch=dev` read the main schema and published its snapshot under the main branch's directory. A write through a catalog refused a branch outright. Resolve the branch an operation is aimed at from every source that may name it - the table identifier, the `branch` option and, where a builder has one, `WithBranch()` - and refuse names that disagree rather than choosing between them. A read resolves it the same way, so the `branch` option now names a read's branch as `WithBranch()` already did. A catalog is told which branch to answer for by the `<table>$branch_<branch>` object name that `Identifier(db, table, branch)` builds and `CatalogUtils::BranchIdentifier()` rebuilds for every catalog call, as the Java client does. `SnapshotCommit::Commit()` now takes that branch, so a renaming commit writes through a manager of the branch and a catalog commit addresses the branch's object name. `UseRESTCatalogCommit()` still builds a body naming no branch, so the caller sends it to the branch's own URL. A branch keeps its schema, snapshots and real-time offsets under `branch/branch-<name>`, while data and manifests stay shared under the table path. A write or a commit aimed at a branch reads the schema published there rather than the catalog's, as `AbstractFileStoreTable.switchToBranch()` does, so a catalog serving only the table's own current schema still takes the branch's snapshots; only the main branch reloads the catalog's schema id for a new snapshot. Refuse the two kinds of name a catalog cannot tell apart from another object: a branch differing from `main` by case alone, whose snapshots would go to a directory of their own, and a branch holding a `$`, which reads back as a system table of another branch. Expiration and snapshot loading follow the commit's branch. Expiration reads the retained snapshots of that branch alone, as Java does, while every branch shares the table's data files, so a file that only another branch still refers to is deleted, which the public header and the catalog guide both state.
SteNicholas
force-pushed
the
PAIMON-351
branch
from
September 17, 2026 06:20
35bd1e7 to
eb06e6e
Compare
Expiration reads the retained snapshots of its own branch alone, while every branch of a table shares its data files, so it could delete a file that another branch still refers to. Return `NotImplemented` from `Expire()` before reading any snapshot or deleting any file when it runs on a branch other than main, or when it runs on main and finds another branch under `branch/branch-<name>` of the table path; the latter error lists the branches found. A branch held only by a catalog, or created while expiration runs, is not found, so the public header and the guides ask callers not to expire such a table and to serialize branch creation and expiration. Fail a `RenamingSnapshotCommit` aimed at a branch other than the one of its snapshot manager instead of redirecting it. `FileStoreCommit::Create()` hands the same manager to the commit and to `FileStoreCommitImpl`, which passes that manager's branch down, so a mismatch is a caller bug. Stop the catalog guide from claiming that a branch keeps its latest snapshot under its own directory: a version-managed catalog can hold it alone, as it can for the main branch. Tests: - `FileStoreCommitImplTest.TestExpireOnBranchIsNotSupported` and `TestExpireOnMainOfTableWithBranchesIsNotSupported` of the same suite cover each refusal and check that no snapshot, manifest or data file is deleted. - `RenamingSnapshotCommitTest.TestBranchHasToMatchSnapshotManager` covers the refusal of a mismatched branch. - `PkCompactionInteTest.WriteAndCompactWithBranch` checks the snapshot of every split of the branch and reads all of them instead of picking the compacted one. - `FileStoreCommitImplTest.TestBranchCommitNamesTheBranchWhenItFails` uses `ASSERT_NOK_WITH_MSG`.
SteNicholas
force-pushed
the
PAIMON-351
branch
from
September 17, 2026 06:24
eb06e6e to
0af0eea
Compare
lucasfang
approved these changes
Sep 17, 2026
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Linked issue: close #351
Java Paimon addresses a branch by the
<table>$branch_<branch>object name and hands that branch down toSnapshotCommit#commit(baseSnapshotUuid, snapshot, branch, statistics), whileAbstractFileStoreTable#switchToBranchreads the branch's schema frombranch/branch-<name>through aFileSystemSchemaManager. Paimon C++ had none of this on the commit path:FileStoreCommit::Create()built itsSchemaManagerandSnapshotManagerwithout a branch, so a commit configured withbranch=devread the main schema and published its snapshot under the main branch's directory, and a write through a catalog refused a branch outright witha write through a catalog requires the main branch.This PR makes a branch a first-class target of the write and commit path, for a file-system commit, a catalog commit and a request-only commit alike.
Resolving the branch
BranchManager::ResolveBranch()resolves the branch an operation is aimed at from every source that may name it: the table identifier, thebranchoption and, where a builder has one,WithBranch(). Names that disagree are refused rather than chosen between, andtbl$branch_main,branch=main, an empty and a blank name all mean the main branch. A read resolves it the same way, so thebranchoption now names a read's branch asWithBranch()already did.WithCatalog()names the bare table is refused: the catalog would answer for the table while the files were written for the branch.Addressing the branch on a catalog
Identifier(database, table, branch)builds the<table>$branch_<branch>object name, folding every spelling ofmaininto the bare table name as the Java client does.CatalogUtils::BranchIdentifier()rebuilds it for every catalog call, so what is read and what is committed name one object;RestCatalognow goes through the same builder instead of splicing the name itself.SnapshotCommit::Commit()takes the branch.CatalogSnapshotCommitcommits to the branch's object name, andRenamingSnapshotCommitwrites the snapshot through a manager of that branch.UseRESTCatalogCommit()still builds a body that names no branch, so the caller sends it to the branch's own URL: the commit endpoint oftbl$branch_dev, not the one oftbl.mainby case alone, whose snapshots would go to a directory of their own, and a branch holding a$, which reads back as a system table of another branch.Schema of a branch
branch/branch-<name>, while data and manifests stay shared under the table path. A write or a commit aimed at a branch reads the schema published there rather than the catalog's, asswitchToBranchdoes, so a catalog serving only the table's own current schema still takes the branch's snapshots. Only the main branch reloads the catalog's schema id for a new snapshot.branch/branch-<name>, before a write or a commit can be aimed at it. Catalog branch management (creating, deleting and merging a branch) remains unimplemented.Expiration
Expire()and snapshot loading follow the commit's branch, so expiring on a branch manages that branch's snapshots and itsEARLIESThint. As in Java'sExpireSnapshotsImpl, only the retained snapshots of that branch are read while every branch shares the table's data files, so a file that only another branch still refers to is deleted. This is stated ininclude/paimon/file_store_commit.hand called out as a warning in the catalog guide; cross-branch reachability belongs to orphan-file cleaning, as it does upstream.Tests
New and extended UT:
identifier_test.cpp:BranchConstructor(object name, round trip, and every spelling ofmainfolding to the bare table); a blank branch added toInvalidEmptySystemTableNameParts.branch_manager_test.cpp:TestResolveBranch(each source, agreement, disagreement, names that would leave the table root),TestCheckCatalogAddressableBranch.catalog_snapshot_commit_test.cpp:TestCommitToBranch(branch object name per attempt, the branch of the attempt winning over the one the identifier was built for,MAINrefused);TestBuildRequestWithoutCatalogextended so a refused attempt leaves no request behind.renaming_snapshot_commit_test.cpp:TestCommitToBranch(snapshot andLATESTunder the branch, main untouched, and a commit aimed back at main from a branch manager).commit_context_test.cpp/write_context_test.cpp:TestCatalogAddressesBranchByIdentifier,TestBranch.read_context_test.cpp:TestBranch.file_store_commit_impl_test.cpp:TestCommitToBranch,TestRequestOnlyCommitToBranch,TestCatalogCommitToBranchTakesTheBranchSnapshot,TestBranchCommitReadsTheSchemaFromTheBranch,TestBranchCommitNamesTheBranchWhenItFails,TestCatalogCommitCanonicalizesTheMainBranchIdentifier,TestBranchExpireLeavesMainSnapshotMetadataAlone.file_store_write_test.cpp:TestCatalogWriteAsksTheCatalogOnlyForTheMainBranchSchema.rest_catalog_test.cpp:ListSnapshotsextended with a blank branch, which is the bare table asmainandMAINare.New and extended IT:
write_inte_test.cpp:TestBranchCommitAndReadwrites and commits ondevand on main, then reads each back.pk_compaction_inte_test.cpp:WriteAndCompactWithBranchnow commits the compaction tobranch-rtand scans the branch for the compacted file, instead of verifying the commit messages only.API and Format
Yes.
include/paimon/catalog/identifier.h: newIdentifier(database, table, branch)constructor.include/paimon/file_store_commit.h:GetLastCommitTableRequest()now clears the request of the previous attempt, so an attempt that failed before building one returns an error rather than the request before it.Expire()documents that only the retained snapshots of its own branch are read.include/paimon/commit_context.h,include/paimon/write_context.h,include/paimon/read_context.h:WithCatalog(),WithBranch()and thebranchoption document how a branch is named and how disagreeing names are refused. A commit or a catalog write aimed at a branch is accepted where it was refused before.<table>$branch_<branch>andbranch/branch-<name>are the existing Java spellings.Documentation
Yes.
docs/source/user_guide/catalog.rst: a new Committing to a branch section with Naming the branch, What a branch keeps and what it shares and Addressing a branch without a catalog client; the expiration warning covering data files shared by every branch; and a note that branch management covers creating, deleting and merging a branch while committing to an existing one is supported.docs/source/user_guide/write.rst: the main branch loads its current schema from the catalog while a branch reads the schema published underbranch/branch-<name>, and a request-only commit aimed at a branch has to be sent to that branch's URL.Generative AI tooling
Generated-by: Claude Code (claude-opus-5[1m])
🤖 Generated with Claude Code