| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| """Branch commands for the Paimon CLI. | |
| Adds the top-level ``branch {create,list,delete,rename,fast-forward}`` | |
| command. All operations go through the Catalog layer so they get typed | |
| exceptions and work for both filesystem and REST catalogs. Command and | |
| argument names mirror the Java branch procedures (``create_branch`` / | |
| ``delete_branch`` / ``rename_branch`` / ``fast_forward``). | |
| """ | |
| import json | |
| import sys | |
| from pypaimon.catalog.catalog_exception import (BranchAlreadyExistException, | |
| BranchNotExistException, | |
| TableNotExistException, | |
| TagNotExistException) | |
| def _open_catalog(args): | |
| """Load config, build the catalog and validate the ``database.table`` id. | |
| Returns ``(catalog, identifier)``. On any failure, prints to stderr and | |
| exits with a non-zero status (matching the other CLI commands). | |
| """ | |
| from pypaimon.cli.cli import create_catalog, load_catalog_config | |
| identifier = args.table | |
| if len(identifier.split('.')) != 2: | |
| print("Error: Invalid table identifier '{}'. Expected format: " | |
| "'database.table'".format(identifier), file=sys.stderr) | |
| sys.exit(1) | |
| try: | |
| catalog = create_catalog(load_catalog_config(args.config)) | |
| except Exception as e: | |
| print("Error: {}".format(e), file=sys.stderr) | |
| sys.exit(1) | |
| return catalog, identifier | |
| def cmd_branch_create(args): | |
| """Execute ``branch create``.""" | |
| catalog, identifier = _open_catalog(args) | |
| try: | |
| catalog.create_branch(identifier, args.branch_name, tag_name=args.tag) | |
| except TableNotExistException: | |
| print("Error: Table '{}' does not exist.".format(identifier), | |
| file=sys.stderr) | |
| sys.exit(1) | |
| except BranchAlreadyExistException: | |
| print("Error: Branch '{}' already exists.".format(args.branch_name), | |
| file=sys.stderr) | |
| sys.exit(1) | |
| except TagNotExistException: | |
| print("Error: Tag '{}' does not exist.".format(args.tag), | |
| file=sys.stderr) | |
| sys.exit(1) | |
| except Exception as e: | |
| print("Error: Failed to create branch: {}".format(e), file=sys.stderr) | |
| sys.exit(1) | |
| if args.tag is not None: | |
| print("Branch '{}' created from tag '{}' on table '{}'.".format( | |
| args.branch_name, args.tag, identifier)) | |
| else: | |
| print("Branch '{}' created on table '{}'.".format( | |
| args.branch_name, identifier)) | |
| def cmd_branch_delete(args): | |
| """Execute ``branch delete``.""" | |
| catalog, identifier = _open_catalog(args) | |
| try: | |
| catalog.drop_branch(identifier, args.branch_name) | |
| except TableNotExistException: | |
| print("Error: Table '{}' does not exist.".format(identifier), | |
| file=sys.stderr) | |
| sys.exit(1) | |
| except BranchNotExistException: | |
| print("Error: Branch '{}' does not exist.".format(args.branch_name), | |
| file=sys.stderr) | |
| sys.exit(1) | |
| except Exception as e: | |
| print("Error: Failed to delete branch: {}".format(e), file=sys.stderr) | |
| sys.exit(1) | |
| print("Branch '{}' deleted from table '{}'.".format( | |
| args.branch_name, identifier)) | |
| def cmd_branch_list(args): | |
| """Execute ``branch list``.""" | |
| catalog, identifier = _open_catalog(args) | |
| try: | |
| branches = catalog.list_branches(identifier) | |
| except TableNotExistException: | |
| print("Error: Table '{}' does not exist.".format(identifier), | |
| file=sys.stderr) | |
| sys.exit(1) | |
| except Exception as e: | |
| print("Error: Failed to list branches: {}".format(e), file=sys.stderr) | |
| sys.exit(1) | |
| if args.format == 'json': | |
| print(json.dumps(branches, ensure_ascii=False)) | |
| elif not branches: | |
| print("No branches found.") | |
| else: | |
| for branch in branches: | |
| print(branch) | |
| def cmd_branch_rename(args): | |
| """Execute ``branch rename``.""" | |
| catalog, identifier = _open_catalog(args) | |
| try: | |
| catalog.rename_branch(identifier, args.from_branch, args.to_branch) | |
| except TableNotExistException: | |
| print("Error: Table '{}' does not exist.".format(identifier), | |
| file=sys.stderr) | |
| sys.exit(1) | |
| except BranchNotExistException: | |
| print("Error: Branch '{}' does not exist.".format(args.from_branch), | |
| file=sys.stderr) | |
| sys.exit(1) | |
| except BranchAlreadyExistException: | |
| print("Error: Branch '{}' already exists.".format(args.to_branch), | |
| file=sys.stderr) | |
| sys.exit(1) | |
| except Exception as e: | |
| print("Error: Failed to rename branch: {}".format(e), file=sys.stderr) | |
| sys.exit(1) | |
| print("Branch '{}' renamed to '{}' on table '{}'.".format( | |
| args.from_branch, args.to_branch, identifier)) | |
| def cmd_branch_fast_forward(args): | |
| """Execute ``branch fast-forward``.""" | |
| catalog, identifier = _open_catalog(args) | |
| try: | |
| catalog.fast_forward(identifier, args.branch_name) | |
| except TableNotExistException: | |
| print("Error: Table '{}' does not exist.".format(identifier), | |
| file=sys.stderr) | |
| sys.exit(1) | |
| except BranchNotExistException: | |
| print("Error: Branch '{}' does not exist.".format(args.branch_name), | |
| file=sys.stderr) | |
| sys.exit(1) | |
| except Exception as e: | |
| print("Error: Failed to fast-forward: {}".format(e), file=sys.stderr) | |
| sys.exit(1) | |
| print("Fast-forwarded table '{}' to branch '{}'.".format( | |
| identifier, args.branch_name)) | |
| def add_branch_subcommands(subparsers): | |
| """Register the top-level ``branch <command>`` command. | |
| Args: | |
| subparsers: The subparsers object from the main argument parser. | |
| """ | |
| branch_parser = subparsers.add_parser('branch', help='Branch operations') | |
| branch_subparsers = branch_parser.add_subparsers( | |
| dest='branch_command', help='Branch commands') | |
| # branch create | |
| create_parser = branch_subparsers.add_parser( | |
| 'create', help='Create a branch on a table') | |
| create_parser.add_argument( | |
| 'table', help='Table identifier in format: database.table') | |
| create_parser.add_argument( | |
| 'branch_name', help='Name of the branch to create') | |
| create_parser.add_argument( | |
| '--tag', '-t', default=None, | |
| help='Create the branch from this tag (default: current state)') | |
| create_parser.set_defaults(func=cmd_branch_create) | |
| # branch list | |
| list_parser = branch_subparsers.add_parser( | |
| 'list', help='List branches of a table') | |
| list_parser.add_argument( | |
| 'table', help='Table identifier in format: database.table') | |
| list_parser.add_argument( | |
| '--format', '-f', choices=['table', 'json'], default='table', | |
| help='Output format: table (default) or json') | |
| list_parser.set_defaults(func=cmd_branch_list) | |
| # branch delete | |
| delete_parser = branch_subparsers.add_parser( | |
| 'delete', help='Delete a branch from a table') | |
| delete_parser.add_argument( | |
| 'table', help='Table identifier in format: database.table') | |
| delete_parser.add_argument( | |
| 'branch_name', help='Name of the branch to delete') | |
| delete_parser.set_defaults(func=cmd_branch_delete) | |
| # branch rename | |
| rename_parser = branch_subparsers.add_parser( | |
| 'rename', help='Rename a branch') | |
| rename_parser.add_argument( | |
| 'table', help='Table identifier in format: database.table') | |
| rename_parser.add_argument('from_branch', help='Current branch name') | |
| rename_parser.add_argument('to_branch', help='New branch name') | |
| rename_parser.set_defaults(func=cmd_branch_rename) | |
| # branch fast-forward | |
| ff_parser = branch_subparsers.add_parser( | |
| 'fast-forward', help='Fast-forward main to a branch') | |
| ff_parser.add_argument( | |
| 'table', help='Table identifier in format: database.table') | |
| ff_parser.add_argument( | |
| 'branch_name', help='Name of the branch to fast-forward to') | |
| ff_parser.set_defaults(func=cmd_branch_fast_forward) |