Skip to content

[core][python] Reject case variants of main as branch names and blank branches in identifiers - #10915

Draft
plusplusjiajia wants to merge 1 commit into
apache:masterfrom
plusplusjiajia:fix/reject-main-variant-and-blank-branches
Draft

plusplusjiajia wants to merge 1 commit into
apache:masterfrom
plusplusjiajia:fix/reject-main-variant-and-blank-branches

Conversation

@plusplusjiajia

@plusplusjiajia plusplusjiajia commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Purpose

Identifier treats any case of main as the default branch and drops it from the encoded name, but branch creation only rejected the exact name main:

  • a branch named MAIN could be created, and identifiers rebuilt from its parts, such as the base table of db.t$branch_MAIN$options, lost the branch and resolved to main;
  • an encoded identifier with a blank branch, such as db.t$branch_, resolved to the main branch wherever it was used, so getTable, alterTable and the Hive partition operations acted on main through an invalid branch identifier.

In Java and PyPaimon:

  • creating a branch, or renaming one, to a name that equals main ignoring case is rejected with the existing "is the default branch" message. PyPaimon compares names as Java String.equalsIgnoreCase does, which also takes the dotted and dotless i (maİn, maın) for i, and its identifiers drop such a branch from the encoded name as Java ones do;
  • Identifier rejects an encoded name with a blank branch when it is parsed, like other invalid object names, so every operation fails with IllegalArgumentException (ValueError in PyPaimon) before it resolves the branch, without a check in each catalog method. The PyPaimon REST client parses an identifier before it sends a request. Identifier(db, t, "") is the main table, as a blank branch option already is, so its encoded name stays valid.

Java and PyPaimon decide whether a branch name is blank alike, as Character.isWhitespace does since Java 9: Java 8 also takes U+180E for whitespace, and str.strip() also strips the no-break spaces. A branch named U+180E therefore resolved to main on Java 8, and a branch Java allows, such as U+00A0, resolved to main in PyPaimon branch and consumer paths and could not be created, renamed, dropped or fast-forwarded there. Identifier.isBlankBranch, used by Identifier, the branch and consumer managers and the PyPaimon REST branch requests, is now the one check.

Tests

  • IdentifierTest / identifier_test.py: encoded names with a blank branch are rejected when parsed, also on a second access; a blank branch argument gives the main table.
  • FileSystemBranchManagerTest, branch_manager_test.py, filesystem_catalog_branch_test.py: MAIN, Main, maın and maİn are rejected on create and rename in both languages.
  • CatalogTestBase#testGetTable, filesystem_catalog_branch_test.py, jdbc_catalog_test.py, rest_catalog_test.py: getTable fails for t$branch_; the PyPaimon REST catalog fails before sending a request.
  • FileSystemCatalogTest#testAlterTableRejectsBlankBranch: alterTable through a blank branch fails, also with ignoreIfNotExists, and leaves the main schema unchanged.
  • HiveCatalogTest#testPartitionOperationsRejectBlankBranch: creating, dropping and altering partitions through a blank branch fail and leave the metastore partitions unchanged.
  • catalog_table_guard_test.py: creating a MAIN branch is now rejected.
  • identifier_test.py: MAIN, maİn and maın branch arguments encode as the main table, as in Java.
  • rest_branch_transport_test.py: creating, dropping, fast-forwarding and listing branches through table$branch_ fail without sending a request.
  • IdentifierTest, FileSystemBranchManagerTest, identifier_test.py, branch_manager_test.py: U+00A0, U+2007, U+202F, U+0085 and U+180E are not blank, on every JVM the build runs on; U+3000 is still blank.
  • consumer_test.py, filesystem_catalog_branch_test.py, rest_branch_test.py, rest_branch_transport_test.py: a branch named U+00A0 keeps its name in identifiers, branch and consumer paths and REST requests, reads its own data rather than main's, and can be renamed, dropped and fast-forwarded.
  • identifier_test.py: setting a blank branch on an identifier gives the main table, and the getter agrees with the encoded name.
  • The new tests fail without this change, except those for behavior that already held: the Java U+00A0, U+2007, U+202F and U+0085 cases, and the U+180E cases, which only Java 8 got wrong. IdentifierTest, RESTTokenFileIOTest, FileSystemBranchManagerTest, FileSystemCatalogTest, CachingCatalogTest, JdbcCatalogTest, MockRESTCatalogTest, ConsumerManagerTest and HiveCatalogTest (512 tests) and the related PyPaimon catalog, branch, consumer, REST, partition and system table tests (1182 passed, 256 skipped) pass locally; flake8 is clean.

@plusplusjiajia
plusplusjiajia force-pushed the fix/reject-main-variant-and-blank-branches branch from 6e23705 to b5669c6 Compare November 1, 2026 04:59
@plusplusjiajia plusplusjiajia changed the title [core][python] Reject case variants of main and blank branch identifiers [core][python] Reject case variants of main as branch names and blank branches in identifiers Nov 1, 2026
@plusplusjiajia
plusplusjiajia force-pushed the fix/reject-main-variant-and-blank-branches branch 4 times, most recently from 658c463 to 7d4e50f Compare November 1, 2026 13:19
… branches in identifiers

Identifier treats any case of main as the default branch and drops it
from the encoded name, but branch creation only rejected the exact name
main. A branch named MAIN could therefore be created, and identifiers
rebuilt from its parts, such as the base table of db.t$branch_MAIN$options,
lost the branch and resolved to main. An encoded identifier with a blank
branch, such as db.t$branch_, resolved to the main branch wherever it
was used, so getTable, alterTable and the Hive partition operations
acted on main through an invalid branch identifier.

In Java and PyPaimon:
- creating a branch, or renaming one, to a name that equals main
  ignoring case, as Java String.equalsIgnoreCase compares it, is
  rejected with the existing "is the default branch" message, and a
  PyPaimon identifier drops such a branch from its encoded name as a
  Java one does;
- Identifier rejects an encoded name with a blank branch when it is
  parsed, like other invalid object names, so every operation fails
  with IllegalArgumentException (ValueError in PyPaimon) before it
  resolves the branch; the PyPaimon REST client parses an identifier
  before it sends a request. Identifier(db, t, "") is the main table,
  as a blank branch option already is, so its encoded name stays valid.

Java and PyPaimon decide whether a branch name is blank alike, as
Character.isWhitespace does since Java 9: Java 8 also takes U+180E for
whitespace, and str.strip also strips the no-break spaces. A branch named
U+180E therefore resolved to main on Java 8, and a branch Java allows,
such as U+00A0, resolved to main in PyPaimon branch and consumer paths
and could not be created, renamed, dropped or fast-forwarded there.
Identifier, the branch and consumer managers and the PyPaimon REST
branch requests now share one check.
@plusplusjiajia
plusplusjiajia force-pushed the fix/reject-main-variant-and-blank-branches branch from 7d4e50f to 5c8f588 Compare November 2, 2026 01:50
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