This is an automated email from the ASF dual-hosted git repository.
yuqi4733 pushed a commit to branch branch-1.0
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.0 by this push:
new 15aef291c6 [#8627] feat(client-python): add metadata objects (#8966)
15aef291c6 is described below
commit 15aef291c6f27eed4d8a4192e23c6618a1214e3a
Author: George T. C. Lai <[email protected]>
AuthorDate: Thu Oct 30 15:58:29 2025 +0800
[#8627] feat(client-python): add metadata objects (#8966)
<!--
1. Title: [#<issue>] <type>(<scope>): <subject>
Examples:
- "[#123] feat(operator): support xxx"
- "[#233] fix: check null before access result in xxx"
- "[MINOR] refactor: fix typo in variable name"
- "[MINOR] docs: fix typo in README"
- "[#255] test: fix flaky test NameOfTheTest"
Reference: https://www.conventionalcommits.org/en/v1.0.0/
2. If the PR is unfinished, please mark this PR as draft.
-->
### What changes were proposed in this pull request?
This PR is aimed at implementing the following classes corresponding to
the Java client.
MetadataObjects.java
A refactor of `MetadataObject` and of `MetadataObjectImpl` are included
as well to conform with the up-to-date implementation in Java client.
### Why are the changes needed?
We need to support python client for table operations.
#8627
### Does this PR introduce _any_ user-facing change?
No
### How was this patch tested?
Unit tests
---------
Signed-off-by: George T. C. Lai <[email protected]>
---
.../client-python/gravitino/api/metadata_object.py | 76 +++++-
.../gravitino/api/metadata_objects.py | 221 ++++++++++++++++++
.../gravitino/client/base_schema_catalog.py | 8 +-
.../gravitino/client/generic_fileset.py | 10 +-
.../tests/unittests/test_metadata_objects.py | 260 +++++++++++++++++++++
5 files changed, 556 insertions(+), 19 deletions(-)
diff --git a/clients/client-python/gravitino/api/metadata_object.py
b/clients/client-python/gravitino/api/metadata_object.py
index f0429893ed..20a498c0ad 100644
--- a/clients/client-python/gravitino/api/metadata_object.py
+++ b/clients/client-python/gravitino/api/metadata_object.py
@@ -17,6 +17,7 @@
from abc import ABC, abstractmethod
from enum import Enum
+from typing import Optional
class MetadataObject(ABC):
@@ -27,30 +28,85 @@ class MetadataObject(ABC):
class Type(Enum):
"""The type of object in the Gravitino system. Every type will map one
- kind of the entity of the underlying system."""
+ kind of the entity of the underlying system.
+ """
+
+ METALAKE = "metalake"
+ """A metalake is a concept of tenant. It means an organization. A
metalake
+ contains many data sources.
+ """
CATALOG = "catalog"
- """"Metadata Type for catalog."""
+ """A catalog is a collection of metadata from a specific metadata
source,
+ like Apache Hive catalog, Apache Iceberg catalog, JDBC catalog, etc.
+ """
+
+ SCHEMA = "schema"
+ """"A schema is a sub collection of the catalog. The schema can
contain filesets,
+ tables, topics, etc.
+ """
FILESET = "fileset"
- """Metadata Type for Fileset System (including HDFS, S3, etc.), like
path/to/file"""
+ """A fileset is mapped to a directory on a file system like HDFS, S3,
ADLS,
+ GCS, etc.
+ """
+
+ TABLE = "table"
+ """A table is mapped the table of relational data sources like Apache
Hive,
+ MySQL, etc.
+ """
+
+ TOPIC = "topic"
+ """A topic is mapped the topic of messaging data sources like Apache
Kafka,
+ Apache Pulsar, etc.
+ """
+
+ COLUMN = "column"
+ """A column is a sub-collection of the table that represents a group of
+ same type data.
+ """
+
+ ROLE = "role"
+ """A role is an object contains specific securable objects with
privileges."""
+
+ MODEL = "model"
+ """A model is mapped to the model artifact in ML."""
@abstractmethod
- def type(self) -> Type:
+ def parent(self) -> Optional[str]:
+ """The parent full name of the object.
+
+ If the object doesn't have parent, this method will return `None`.
+
+ Returns:
+ Optional[str]: The parent full name of the object.
"""
- The type of the object.
+
+ @abstractmethod
+ def type(self) -> Type:
+ """The type of the object.
Returns:
- The type of the object.
+ Type: The type of the object.
"""
- pass
@abstractmethod
def name(self) -> str:
+ """The name of the object.
+
+ Returns:
+ str: The name of the object.
"""
- The name of the object.
+
+ def full_name(self) -> str:
+ """The full name of the object.
+
+ Full name will be separated by "." to represent a string identifier of
the object,
+ like catalog, catalog.table, etc.
Returns:
- The name of the object.
+ str: The name of the object.
"""
- pass
+
+ parent = self.parent()
+ return ".".join([parent, self.name()]) if parent else self.name()
diff --git a/clients/client-python/gravitino/api/metadata_objects.py
b/clients/client-python/gravitino/api/metadata_objects.py
new file mode 100644
index 0000000000..8a29c612e1
--- /dev/null
+++ b/clients/client-python/gravitino/api/metadata_objects.py
@@ -0,0 +1,221 @@
+# 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 typing import ClassVar, Optional, Union, overload
+
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.exceptions.base import IllegalArgumentException
+from gravitino.utils.precondition import Precondition
+
+
+class MetadataObjects:
+ """The helper class for `MetadataObject`."""
+
+ METADATA_OBJECT_RESERVED_NAME: ClassVar[str] = "*"
+ """The reserved name for the metadata object."""
+
+ DOT_SPLITTER: ClassVar[str] = "."
+ DOT_JOINER: ClassVar[str] = "."
+
+ _NAMES_LEN_CONDS: ClassVar[dict[int, set[MetadataObject.Type]]] = {
+ 1: {
+ MetadataObject.Type.METALAKE,
+ MetadataObject.Type.CATALOG,
+ MetadataObject.Type.ROLE,
+ },
+ 2: {MetadataObject.Type.SCHEMA},
+ 3: {
+ MetadataObject.Type.FILESET,
+ MetadataObject.Type.TABLE,
+ MetadataObject.Type.TOPIC,
+ MetadataObject.Type.MODEL,
+ },
+ 4: {MetadataObject.Type.COLUMN},
+ }
+
+ @staticmethod
+ @overload
+ def of(name_or_names: list[str], type_: MetadataObject.Type) ->
MetadataObject: ...
+
+ @staticmethod
+ @overload
+ def of(
+ name_or_names: str, type_: MetadataObject.Type, parent: Optional[str]
= None
+ ) -> MetadataObject: ...
+
+ @staticmethod
+ def of(
+ name_or_names: Union[str, list[str]],
+ type_: MetadataObject.Type,
+ parent: Optional[str] = None,
+ ) -> MetadataObject:
+ if isinstance(name_or_names, str):
+ name = name_or_names
+ full_name = (
+ MetadataObjects.DOT_JOINER.join([parent, name]) if parent else
name
+ )
+ return MetadataObjects.parse(full_name, type_)
+ names_len = len(name_or_names)
+ Precondition.check_argument(
+ names_len > 0,
+ "Cannot create a metadata object with no names",
+ )
+ Precondition.check_argument(
+ names_len <= 4,
+ "Cannot create a metadata object with the name length which is
greater than 4",
+ )
+ Precondition.check_argument(
+ names_len != 1 or type_ in MetadataObjects._NAMES_LEN_CONDS[1],
+ "If the length of names is 1, it must be the CATALOG, METALAKE, or
ROLE type",
+ )
+ Precondition.check_argument(
+ names_len != 2 or type_ in MetadataObjects._NAMES_LEN_CONDS[2],
+ "If the length of names is 2, it must be the SCHEMA type",
+ )
+ Precondition.check_argument(
+ names_len != 3 or type_ in MetadataObjects._NAMES_LEN_CONDS[3],
+ "If the length of names is 3, it must be FILESET, TABLE, TOPIC or
MODEL",
+ )
+ Precondition.check_argument(
+ names_len != 4 or type_ in MetadataObjects._NAMES_LEN_CONDS[4],
+ "If the length of names is 4, it must be COLUMN",
+ )
+ names = name_or_names
+ for name in names:
+ MetadataObjects.check_name(name)
+ return MetadataObjects.MetadataObjectImpl(
+ MetadataObjects.get_last_name(names),
+ type_,
+ MetadataObjects.get_parent_full_name(names),
+ )
+
+ @staticmethod
+ def parent(object_: MetadataObject) -> Optional[MetadataObject]:
+ """Get the parent metadata object of the given metadata object.
+
+ Args:
+ object_ (MetadataObject): The metadata object
+
+ Returns:
+ Optional[MetadataObject]:
+ The parent metadata object if it exists, otherwise `None`
+ """
+ if object_ is None:
+ return None
+
+ object_type = object_.type()
+ # Return None if the object is the root object
+ if object_type in {
+ MetadataObject.Type.METALAKE,
+ MetadataObject.Type.CATALOG,
+ MetadataObject.Type.ROLE,
+ }:
+ return None
+
+ parent_type = None
+ if object_type is MetadataObject.Type.COLUMN:
+ parent_type = MetadataObject.Type.TABLE
+ elif object_type in {
+ MetadataObject.Type.TABLE,
+ MetadataObject.Type.FILESET,
+ MetadataObject.Type.TOPIC,
+ MetadataObject.Type.MODEL,
+ }:
+ parent_type = MetadataObject.Type.SCHEMA
+ elif object_type is MetadataObject.Type.SCHEMA:
+ parent_type = MetadataObject.Type.CATALOG
+ else:
+ raise IllegalArgumentException(
+ f"Unexpected to reach here for metadata object type:
{object_type.value}"
+ )
+
+ return MetadataObjects.parse(object_.parent(), parent_type)
+
+ @staticmethod
+ def get_parent_full_name(names: list[str]) -> Optional[str]:
+ """Get the parent full name of the given full name.
+
+ Args:
+ names (list[str]): The names of the metadata object
+
+ Returns:
+ Optional[str]: The parent full name if it exists, otherwise `None`.
+ """
+ names_len = len(names)
+ if names_len <= 1:
+ return None
+ return MetadataObjects.DOT_JOINER.join(names[:-1])
+
+ @staticmethod
+ def get_last_name(names: list[str]) -> str:
+ return names[-1]
+
+ @staticmethod
+ def check_name(name: str) -> None:
+ Precondition.check_argument(
+ name is not None,
+ "Cannot create a metadata object with null name",
+ )
+ Precondition.check_argument(
+ name != MetadataObjects.METADATA_OBJECT_RESERVED_NAME,
+ "Cannot create a metadata object with `*` name.",
+ )
+
+ @staticmethod
+ def parse(full_name: str, type_: MetadataObject.Type) -> MetadataObject:
+ Precondition.check_argument(
+ full_name is not None and len(full_name.strip()) > 0,
+ "Metadata object full name cannot be blank",
+ )
+
+ parts = full_name.split(MetadataObjects.DOT_SPLITTER)
+ if type_ is MetadataObject.Type.ROLE:
+ return MetadataObjects.of([full_name], MetadataObject.Type.ROLE)
+
+ return MetadataObjects.of(parts, type_)
+
+ class MetadataObjectImpl(MetadataObject):
+ def __init__(
+ self, name: str, type_: MetadataObject.Type, parent: Optional[str]
= None
+ ) -> None:
+ self._name = name
+ self._type = type_
+ self._parent = parent
+
+ def name(self) -> str:
+ return self._name
+
+ def type(self) -> MetadataObject.Type:
+ return self._type
+
+ def parent(self) -> Optional[str]:
+ return self._parent
+
+ def __eq__(self, value: object) -> bool:
+ if not isinstance(value, MetadataObjects.MetadataObjectImpl):
+ return False
+ return (
+ self._name == value.name()
+ and self._type == value.type()
+ and self._parent == value.parent()
+ )
+
+ def __hash__(self) -> int:
+ return hash((self._name, self._parent, self._type))
+
+ def __str__(self) -> str:
+ return f"MetadataObject: [fullName={self.full_name()}],
[type={self.type().value}]"
diff --git a/clients/client-python/gravitino/client/base_schema_catalog.py
b/clients/client-python/gravitino/client/base_schema_catalog.py
index 1555b8e0cb..74dc5e8911 100644
--- a/clients/client-python/gravitino/client/base_schema_catalog.py
+++ b/clients/client-python/gravitino/client/base_schema_catalog.py
@@ -20,13 +20,13 @@ from typing import Dict, List
from gravitino.api.catalog import Catalog
from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.metadata_objects import MetadataObjects
from gravitino.api.schema import Schema
from gravitino.api.schema_change import SchemaChange
from gravitino.api.supports_schemas import SupportsSchemas
from gravitino.client.metadata_object_credential_operations import (
MetadataObjectCredentialOperations,
)
-from gravitino.client.metadata_object_impl import MetadataObjectImpl
from gravitino.dto.audit_dto import AuditDTO
from gravitino.dto.catalog_dto import CatalogDTO
from gravitino.dto.requests.schema_create_request import SchemaCreateRequest
@@ -35,11 +35,11 @@ from gravitino.dto.requests.schema_updates_request import
SchemaUpdatesRequest
from gravitino.dto.responses.drop_response import DropResponse
from gravitino.dto.responses.entity_list_response import EntityListResponse
from gravitino.dto.responses.schema_response import SchemaResponse
+from gravitino.exceptions.base import IllegalArgumentException
from gravitino.exceptions.handlers.schema_error_handler import
SCHEMA_ERROR_HANDLER
from gravitino.namespace import Namespace
-from gravitino.utils import HTTPClient
from gravitino.rest.rest_utils import encode_string
-from gravitino.exceptions.base import IllegalArgumentException
+from gravitino.utils import HTTPClient
logger = logging.getLogger(__name__)
@@ -82,7 +82,7 @@ class BaseSchemaCatalog(CatalogDTO, SupportsSchemas):
self.rest_client = rest_client
self._catalog_namespace = catalog_namespace
- metadata_object = MetadataObjectImpl([name],
MetadataObject.Type.CATALOG)
+ metadata_object = MetadataObjects.of([name],
MetadataObject.Type.CATALOG)
self._object_credential_operations =
MetadataObjectCredentialOperations(
catalog_namespace.level(0), metadata_object, rest_client
)
diff --git a/clients/client-python/gravitino/client/generic_fileset.py
b/clients/client-python/gravitino/client/generic_fileset.py
index ccadb26335..b2adb21cc4 100644
--- a/clients/client-python/gravitino/client/generic_fileset.py
+++ b/clients/client-python/gravitino/client/generic_fileset.py
@@ -14,16 +14,16 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-from typing import Optional, Dict, List
+from typing import Dict, List, Optional
+from gravitino.api.credential.credential import Credential
+from gravitino.api.credential.supports_credentials import SupportsCredentials
from gravitino.api.file.fileset import Fileset
from gravitino.api.metadata_object import MetadataObject
-from gravitino.api.credential.supports_credentials import SupportsCredentials
-from gravitino.api.credential.credential import Credential
+from gravitino.api.metadata_objects import MetadataObjects
from gravitino.client.metadata_object_credential_operations import (
MetadataObjectCredentialOperations,
)
-from gravitino.client.metadata_object_impl import MetadataObjectImpl
from gravitino.dto.audit_dto import AuditDTO
from gravitino.dto.fileset_dto import FilesetDTO
from gravitino.namespace import Namespace
@@ -41,7 +41,7 @@ class GenericFileset(Fileset, SupportsCredentials):
self, fileset: FilesetDTO, rest_client: HTTPClient, full_namespace:
Namespace
):
self._fileset = fileset
- metadata_object = MetadataObjectImpl(
+ metadata_object = MetadataObjects.of(
[full_namespace.level(1), full_namespace.level(2), fileset.name()],
MetadataObject.Type.FILESET,
)
diff --git a/clients/client-python/tests/unittests/test_metadata_objects.py
b/clients/client-python/tests/unittests/test_metadata_objects.py
new file mode 100644
index 0000000000..412f4e02b3
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_metadata_objects.py
@@ -0,0 +1,260 @@
+# 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.
+
+import unittest
+
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.metadata_objects import MetadataObjects
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+class TestMetadataObjects(unittest.TestCase):
+ def test_of_with_string_name(self):
+ obj = MetadataObjects.of("catalog1", MetadataObject.Type.CATALOG)
+ self.assertEqual("catalog1", obj.name())
+ self.assertEqual(MetadataObject.Type.CATALOG, obj.type())
+ self.assertIsNone(obj.parent())
+
+ def test_of_with_string_name_and_parent(self):
+ obj = MetadataObjects.of("schema1", MetadataObject.Type.SCHEMA,
"catalog1")
+ self.assertEqual("schema1", obj.name())
+ self.assertEqual(MetadataObject.Type.SCHEMA, obj.type())
+ self.assertEqual("catalog1", obj.parent())
+
+ def test_of_with_list_names_metalake(self):
+ obj = MetadataObjects.of(["metalake1"], MetadataObject.Type.METALAKE)
+ self.assertEqual("metalake1", obj.name())
+ self.assertEqual(MetadataObject.Type.METALAKE, obj.type())
+ self.assertIsNone(obj.parent())
+
+ def test_of_with_list_names_catalog(self):
+ obj = MetadataObjects.of(["catalog1"], MetadataObject.Type.CATALOG)
+ self.assertEqual("catalog1", obj.name())
+ self.assertEqual(MetadataObject.Type.CATALOG, obj.type())
+ self.assertIsNone(obj.parent())
+
+ def test_of_with_list_names_role(self):
+ obj = MetadataObjects.of(["role1"], MetadataObject.Type.ROLE)
+ self.assertEqual("role1", obj.name())
+ self.assertEqual(MetadataObject.Type.ROLE, obj.type())
+ self.assertIsNone(obj.parent())
+
+ def test_of_with_list_names_schema(self):
+ obj = MetadataObjects.of(["catalog1", "schema1"],
MetadataObject.Type.SCHEMA)
+ self.assertEqual("schema1", obj.name())
+ self.assertEqual(MetadataObject.Type.SCHEMA, obj.type())
+ self.assertEqual("catalog1", obj.parent())
+
+ def test_of_with_list_names_table(self):
+ obj = MetadataObjects.of(
+ ["catalog1", "schema1", "table1"], MetadataObject.Type.TABLE
+ )
+ self.assertEqual("table1", obj.name())
+ self.assertEqual(MetadataObject.Type.TABLE, obj.type())
+ self.assertEqual("catalog1.schema1", obj.parent())
+
+ def test_of_with_list_names_column(self):
+ obj = MetadataObjects.of(
+ ["catalog1", "schema1", "table1", "col1"],
MetadataObject.Type.COLUMN
+ )
+ self.assertEqual("col1", obj.name())
+ self.assertEqual(MetadataObject.Type.COLUMN, obj.type())
+ self.assertEqual("catalog1.schema1.table1", obj.parent())
+
+ def test_of_invalid_names(self):
+ with self.assertRaisesRegex(
+ IllegalArgumentException, "Cannot create a metadata object with no
names"
+ ):
+ MetadataObjects.of([], MetadataObject.Type.CATALOG)
+ with self.assertRaisesRegex(
+ IllegalArgumentException,
+ "Cannot create a metadata object with the name length which is
greater than 4",
+ ):
+ MetadataObjects.of(["a", "b", "c", "d", "e"],
MetadataObject.Type.CATALOG)
+
+ def test_of_invalid_name_length_type_mismatch(self):
+ with self.assertRaisesRegex(
+ IllegalArgumentException,
+ "If the length of names is 1, it must be the CATALOG, METALAKE, or
ROLE type",
+ ):
+ MetadataObjects.of(["catalog1"], MetadataObject.Type.SCHEMA)
+
+ with self.assertRaisesRegex(
+ IllegalArgumentException,
+ "If the length of names is 2, it must be the SCHEMA type",
+ ):
+ MetadataObjects.of(["catalog1", "schema1"],
MetadataObject.Type.CATALOG)
+
+ with self.assertRaisesRegex(
+ IllegalArgumentException,
+ "If the length of names is 3, it must be FILESET, TABLE, TOPIC or
MODEL",
+ ):
+ MetadataObjects.of(
+ ["catalog1", "schema1", "table1"], MetadataObject.Type.COLUMN
+ )
+
+ with self.assertRaisesRegex(
+ IllegalArgumentException, "If the length of names is 4, it must be
COLUMN"
+ ):
+ MetadataObjects.of(
+ ["catalog1", "schema1", "table1", "column1"],
MetadataObject.Type.TABLE
+ )
+
+ def test_of_invalid_reserved_name(self):
+ with self.assertRaises(IllegalArgumentException):
+ MetadataObjects.of(["*"], MetadataObject.Type.CATALOG)
+
+ def test_parent_root_object(self):
+ obj = MetadataObjects.of(["metalake1"], MetadataObject.Type.METALAKE)
+ self.assertIsNone(MetadataObjects.parent(obj))
+
+ obj = MetadataObjects.of(["catalog1"], MetadataObject.Type.CATALOG)
+ self.assertIsNone(MetadataObjects.parent(obj))
+
+ obj = MetadataObjects.of(["role1"], MetadataObject.Type.ROLE)
+ self.assertIsNone(MetadataObjects.parent(obj))
+
+ def test_parent_schema(self):
+ obj = MetadataObjects.of(["catalog1", "schema1"],
MetadataObject.Type.SCHEMA)
+ parent = MetadataObjects.parent(obj)
+ self.assertIsNotNone(parent)
+ self.assertEqual("catalog1", parent.name())
+ self.assertEqual(MetadataObject.Type.CATALOG, parent.type())
+
+ def test_parent_table(self):
+ obj = MetadataObjects.of(
+ ["catalog1", "schema1", "table1"], MetadataObject.Type.TABLE
+ )
+ parent = MetadataObjects.parent(obj)
+ self.assertIsNotNone(parent)
+ self.assertEqual("schema1", parent.name())
+ self.assertEqual(MetadataObject.Type.SCHEMA, parent.type())
+ self.assertEqual("catalog1", parent.parent())
+
+ def test_parent_column(self):
+ obj = MetadataObjects.of(
+ ["catalog1", "schema1", "table1", "col1"],
MetadataObject.Type.COLUMN
+ )
+ parent = MetadataObjects.parent(obj)
+ self.assertIsNotNone(parent)
+ self.assertEqual("table1", parent.name())
+ self.assertEqual(MetadataObject.Type.TABLE, parent.type())
+ self.assertEqual("catalog1.schema1", parent.parent())
+
+ def test_parent_none_input(self):
+ self.assertIsNone(MetadataObjects.parent(None))
+
+ def test_get_parent_full_name(self):
+ self.assertIsNone(MetadataObjects.get_parent_full_name(["single"]))
+ self.assertEqual(
+ "catalog1", MetadataObjects.get_parent_full_name(["catalog1",
"schema1"])
+ )
+ self.assertEqual(
+ "catalog1.schema1",
+ MetadataObjects.get_parent_full_name(["catalog1", "schema1",
"table1"]),
+ )
+ self.assertEqual(
+ "catalog1.schema1.table1",
+ MetadataObjects.get_parent_full_name(
+ ["catalog1", "schema1", "table1", "col1"]
+ ),
+ )
+
+ def test_get_last_name(self):
+ self.assertEqual("single", MetadataObjects.get_last_name(["single"]))
+ self.assertEqual(
+ "schema1", MetadataObjects.get_last_name(["catalog1", "schema1"])
+ )
+ self.assertEqual(
+ "table1", MetadataObjects.get_last_name(["catalog1", "schema1",
"table1"])
+ )
+
+ # def test_check_name_valid(self):
+ # MetadataObjects.check_name("valid_name")
+ # MetadataObjects.check_name("catalog1")
+
+ # def test_check_name_invalid_none(self):
+ # with self.assertRaises(IllegalArgumentException):
+ # MetadataObjects.check_name(None)
+
+ # def test_check_name_invalid_reserved(self):
+ # with self.assertRaises(IllegalArgumentException):
+ # MetadataObjects.check_name("*")
+
+ def test_parse_root_object(self):
+ obj = MetadataObjects.parse("metalake1", MetadataObject.Type.METALAKE)
+ self.assertEqual("metalake1", obj.name())
+ self.assertEqual(MetadataObject.Type.METALAKE, obj.type())
+ self.assertIsNone(obj.parent())
+
+ obj = MetadataObjects.parse("catalog1", MetadataObject.Type.CATALOG)
+ self.assertEqual("catalog1", obj.name())
+ self.assertEqual(MetadataObject.Type.CATALOG, obj.type())
+ self.assertIsNone(obj.parent())
+
+ obj = MetadataObjects.parse("role.with.dots", MetadataObject.Type.ROLE)
+ self.assertEqual("role.with.dots", obj.name())
+ self.assertEqual(MetadataObject.Type.ROLE, obj.type())
+ self.assertIsNone(obj.parent())
+
+ def test_parse_schema(self):
+ obj = MetadataObjects.parse("catalog1.schema1",
MetadataObject.Type.SCHEMA)
+ self.assertEqual("schema1", obj.name())
+ self.assertEqual(MetadataObject.Type.SCHEMA, obj.type())
+ self.assertEqual("catalog1", obj.parent())
+
+ def test_parse_table(self):
+ obj = MetadataObjects.parse(
+ "catalog1.schema1.table1", MetadataObject.Type.TABLE
+ )
+ self.assertEqual("table1", obj.name())
+ self.assertEqual(MetadataObject.Type.TABLE, obj.type())
+ self.assertEqual("catalog1.schema1", obj.parent())
+
+ def test_parse_invalid_blank_name(self):
+ with self.assertRaisesRegex(
+ IllegalArgumentException, "Metadata object full name cannot be
blank"
+ ):
+ MetadataObjects.parse("", MetadataObject.Type.CATALOG)
+
+ with self.assertRaisesRegex(
+ IllegalArgumentException, "Metadata object full name cannot be
blank"
+ ):
+ MetadataObjects.parse(" ", MetadataObject.Type.CATALOG)
+
+ with self.assertRaisesRegex(
+ IllegalArgumentException, "Metadata object full name cannot be
blank"
+ ):
+ MetadataObjects.parse(None, MetadataObject.Type.CATALOG)
+
+ def test_metadata_object_impl_equal_and_hash(self):
+ obj1 = MetadataObjects.of(["catalog1"], MetadataObject.Type.CATALOG)
+ obj2 = MetadataObjects.of(["catalog1"], MetadataObject.Type.CATALOG)
+ obj3 = MetadataObjects.of(["catalog2"], MetadataObject.Type.CATALOG)
+
+ self.assertEqual(obj1, obj2)
+ self.assertNotEqual(obj1, obj3)
+ self.assertNotEqual(obj1, "not_a_metadata_object")
+ self.assertEqual(hash(obj1), hash(obj2))
+ self.assertNotEqual(hash(obj1), hash(obj3))
+
+ def test_metadata_object_impl_str(self):
+ obj = MetadataObjects.of(["catalog1", "schema1"],
MetadataObject.Type.SCHEMA)
+ expected_str = (
+ f"MetadataObject: [fullName={obj.full_name()}],
[type={obj.type().value}]"
+ )
+ self.assertEqual(expected_str, str(obj))