| # 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. | |
| from dataclasses import dataclass | |
| from typing import Optional | |
| from pypaimon.common.json_util import json_field | |
| SYSTEM_TABLE_SPLITTER = '$' | |
| SYSTEM_BRANCH_PREFIX = 'branch_' | |
| DEFAULT_MAIN_BRANCH = 'main' | |
| UNKNOWN_DATABASE = 'unknown' | |
| @dataclass(init=False) | |
| class Identifier: | |
| """Identifies a database object (table, view, etc.). | |
| 1:1 port of ``org.apache.paimon.catalog.Identifier``: the on-the-wire | |
| shape is exactly two fields, ``database`` and ``object``. Any branch / | |
| system-table portion is encoded into the ``object`` field using the | |
| ``$`` separator and the ``branch_`` prefix, so JSON written by Python | |
| is round-trippable through the Java REST server (and vice versa). | |
| Mirrors Java's three public constructors via a single signature: | |
| * ``Identifier(database, object)`` — JSON-create form. ``object`` | |
| is the final, possibly-encoded string. | |
| * ``Identifier(database, table, branch=...)`` — encodes ``branch`` | |
| into ``object``. | |
| * ``Identifier(database, table, branch=..., system_table=...)`` — | |
| encodes both. | |
| ``branch == "main"`` (case-insensitive) is treated as the default | |
| branch and is not encoded into the object name, matching Java. | |
| """ | |
| database: str = json_field("database", default=None) | |
| object: str = json_field("object", default=None) | |
| def __init__(self, database: str, object: Optional[str] = None, | |
| branch: Optional[str] = None, | |
| system_table: Optional[str] = None): | |
| self.database = database | |
| if branch is None and system_table is None: | |
| # @JsonCreator form: ``object`` is already the final, encoded | |
| # string. Components are decoded lazily by _split_object_name(). | |
| self.object = object | |
| self._table: Optional[str] = None | |
| self._branch: Optional[str] = None | |
| self._system_table: Optional[str] = None | |
| else: | |
| # Encoding form: ``object`` is the bare table name; encode | |
| # branch / system_table into the on-wire ``object``. | |
| builder = object | |
| if branch is not None and branch.lower() != DEFAULT_MAIN_BRANCH: | |
| builder = (builder + SYSTEM_TABLE_SPLITTER | |
| + SYSTEM_BRANCH_PREFIX + branch) | |
| if system_table is not None: | |
| builder = builder + SYSTEM_TABLE_SPLITTER + system_table | |
| self.object = builder | |
| self._table = object | |
| self._branch = branch | |
| self._system_table = system_table | |
| @classmethod | |
| def create(cls, database: str, table: str, | |
| branch: Optional[str] = None, | |
| system_table: Optional[str] = None) -> "Identifier": | |
| """Create an Identifier. | |
| Two-arg form ``create(database, object)`` mirrors Java's | |
| ``Identifier.create``: the second argument is treated as the final | |
| ``object`` string (may already carry encoded branch / system_table | |
| segments). | |
| Multi-arg form ``create(database, table, branch=..., system_table=...)`` | |
| is a Python convenience that encodes the components into ``object`` | |
| for you, equivalent to ``Identifier(database, table, branch=..., | |
| system_table=...)``. | |
| """ | |
| return cls(database, table, branch=branch, system_table=system_table) | |
| @classmethod | |
| def from_string(cls, full_name: str) -> "Identifier": | |
| """Parse a ``database.object`` identifier, with optional backtick quoting.""" | |
| if not full_name or not full_name.strip(): | |
| raise ValueError("fullName cannot be null or empty") | |
| if '`' in full_name: | |
| return cls._parse_with_backticks(full_name) | |
| parts = full_name.split(".", 1) | |
| if len(parts) != 2: | |
| raise ValueError( | |
| "Cannot get splits from '{}' to get database and object".format(full_name) | |
| ) | |
| return cls(parts[0], parts[1]) | |
| @classmethod | |
| def _parse_with_backticks(cls, full_name: str) -> "Identifier": | |
| parts = [] | |
| current = "" | |
| in_backticks = False | |
| for char in full_name: | |
| if char == '`': | |
| in_backticks = not in_backticks | |
| elif char == '.' and not in_backticks: | |
| parts.append(current) | |
| current = "" | |
| else: | |
| current += char | |
| if current: | |
| parts.append(current) | |
| if in_backticks: | |
| raise ValueError("Unclosed backtick in identifier: {}".format(full_name)) | |
| if len(parts) != 2: | |
| raise ValueError("Invalid identifier format: {}".format(full_name)) | |
| return cls(parts[0], parts[1]) | |
| def _split_object_name(self) -> None: | |
| if self._table is not None: | |
| return | |
| splits = self.object.split(SYSTEM_TABLE_SPLITTER) | |
| if len(splits) == 1: | |
| self._table = self.object | |
| self._branch = None | |
| self._system_table = None | |
| elif len(splits) == 2: | |
| self._table = splits[0] | |
| if splits[1].startswith(SYSTEM_BRANCH_PREFIX): | |
| self._branch = splits[1][len(SYSTEM_BRANCH_PREFIX):] | |
| self._system_table = None | |
| else: | |
| self._branch = None | |
| self._system_table = splits[1] | |
| elif len(splits) == 3: | |
| if not splits[1].startswith(SYSTEM_BRANCH_PREFIX): | |
| raise ValueError( | |
| "System table can only contain one '$' separator, " | |
| "but this is: " + self.object | |
| ) | |
| self._table = splits[0] | |
| self._branch = splits[1][len(SYSTEM_BRANCH_PREFIX):] | |
| self._system_table = splits[2] | |
| else: | |
| raise ValueError("Invalid object name: " + self.object) | |
| def get_full_name(self) -> str: | |
| # Match Java: tables created without an explicit database (e.g. some | |
| # ad-hoc query paths) land in the special "unknown" database, in which | |
| # case the database segment is dropped from the rendered name. | |
| if UNKNOWN_DATABASE == self.database: | |
| return self.object | |
| return "{}.{}".format(self.database, self.object) | |
| def get_database_name(self) -> str: | |
| return self.database | |
| def get_table_name(self) -> str: | |
| self._split_object_name() | |
| return self._table | |
| def get_object_name(self) -> str: | |
| return self.object | |
| def get_branch_name(self) -> Optional[str]: | |
| self._split_object_name() | |
| return self._branch | |
| def get_branch_name_or_default(self) -> str: | |
| """Get branch name or return ``DEFAULT_MAIN_BRANCH`` if no branch is encoded.""" | |
| branch = self.get_branch_name() | |
| return branch if branch is not None else DEFAULT_MAIN_BRANCH | |
| def get_system_table_name(self) -> Optional[str]: | |
| self._split_object_name() | |
| return self._system_table | |
| def is_system_table(self) -> bool: | |
| return self.get_system_table_name() is not None | |
| @property | |
| def branch(self) -> Optional[str]: | |
| # Read/write alias for callers that previously accessed the | |
| # ``Identifier.branch`` dataclass field directly. Java's | |
| # ``branch`` is transient/private and not exposed; Python kept | |
| # it public, so this property tides external code over. | |
| return self.get_branch_name() | |
| @branch.setter | |
| def branch(self, value: Optional[str]) -> None: | |
| # Re-encode ``object`` so the wire shape stays consistent with | |
| # the new value (equivalent to Identifier(db, table, branch=value, | |
| # system_table=current_system_table)). | |
| table = self.get_table_name() | |
| system_table = self.get_system_table_name() | |
| rebuilt = Identifier( | |
| self.database, table, branch=value, system_table=system_table | |
| ) | |
| self.object = rebuilt.object | |
| self._table = table | |
| self._branch = value | |
| self._system_table = system_table | |
| def __hash__(self): | |
| return hash((self.database, self.object)) | |
| def __eq__(self, other): | |
| if not isinstance(other, Identifier): | |
| return NotImplemented | |
| return self.database == other.database and self.object == other.object |