blob: 736a4c9ab9d1ac0e2ae42ca95170084bf7f4de05 [file]
# 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