This is an automated email from the ASF dual-hosted git repository.
mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 492cabc99f [#11008] feat(client-python): add tests and update
documentation of view CRUD support for relational catalog (#14637)
492cabc99f is described below
commit 492cabc99fce6b034c7df476bae4c64718a87337
Author: PiotrAndrzejewsky <[email protected]>
AuthorDate: Fri Nov 6 15:16:15 2026 +0100
[#11008] feat(client-python): add tests and update documentation of view
CRUD support for relational catalog (#14637)
### What changes were proposed in this pull request?
This PR adds unit and integration tests and user
documentation for View CRUD operations in the python relational catalog:
- **Documentation**:
- Updated `docs/manage-view-metadata-using-gravitino.md` with examples
for creating, loading, altering, dropping, and listing views.
- Updated `docs/how-to-use-python-client.md` to reference the view
management capabilities and link to the guide.
- **Unit Tests**:
- Added unit test coverage in
`tests/unittests/test_relational_catalog.py` matching
`TestRelationalCatalogView.java` (create, load, list, drop, and alter
operations including rename, set property, remove property, and replace
view).
- **Integration Tests**:
- Added integration tests in
`tests/integration/test_relational_catalog.py` for view rename and
replace operations.
- **Client Validation**:
- Added `_check_full_schema_namespace` in `RelationalCatalog` to
validate 3-level schema namespaces (`metalake.catalog.schema`) when
formatting table and view REST request paths. This check is already
present in java-client
### Why are the changes needed?
To increase test coverage and add missing documentation after
implementing the core functionality here #11947
Fixes #11008
### Does this PR introduce _any_ user-facing change?
No API change, only changes in documentation, to reflect actual code
### How was this patch tested?
- **Unit tests**: Passed all Python client unit tests (1,429 tests):
` ./gradlew :clients:client-python:test -PskipITs`
- Integration tests: Passed relational catalog integration tests against
a live Gravitino server and Hive container:
`./gradlew :clients:client-python:integrationTest
-PskipDockerTests=false`
---------
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
---
.../gravitino/client/relational_catalog.py | 13 +
.../tests/integration/test_relational_catalog.py | 70 ++++
.../tests/unittests/test_relational_catalog.py | 365 +++++++++++++++++----
docs/how-to-use-python-client.md | 7 +
docs/manage-view-metadata-using-gravitino.md | 94 +++++-
5 files changed, 479 insertions(+), 70 deletions(-)
diff --git a/clients/client-python/gravitino/client/relational_catalog.py
b/clients/client-python/gravitino/client/relational_catalog.py
index 427046d901..7ca4d053e2 100644
--- a/clients/client-python/gravitino/client/relational_catalog.py
+++ b/clients/client-python/gravitino/client/relational_catalog.py
@@ -188,7 +188,19 @@ class RelationalCatalog(
f"Table namespace must be non-null and have 1 level, the input
namespace is {namespace}",
)
+ def _check_full_schema_namespace(
+ self, namespace: Namespace, entity_type: str
+ ) -> None:
+ Namespace.check(
+ namespace is not None and namespace.length() == 3,
+ (
+ f"{entity_type} full namespace must be non-null and have 3
levels "
+ f'in the format "metalake.catalog.schema", the input namespace
is {namespace}'
+ ),
+ )
+
def _format_table_request_path(self, ns: Namespace) -> str:
+ self._check_full_schema_namespace(ns, "Table")
schema_ns = Namespace.of(ns.level(0), ns.level(1))
return (
f"{BaseSchemaCatalog.format_schema_request_path(schema_ns)}"
@@ -228,6 +240,7 @@ class RelationalCatalog(
)
def _format_view_request_path(self, ns: Namespace) -> str:
+ self._check_full_schema_namespace(ns, "View")
schema_ns = Namespace.of(ns.level(0), ns.level(1))
return (
f"{BaseSchemaCatalog.format_schema_request_path(schema_ns)}"
diff --git a/clients/client-python/tests/integration/test_relational_catalog.py
b/clients/client-python/tests/integration/test_relational_catalog.py
index dae7fa8f04..e5e7439721 100644
--- a/clients/client-python/tests/integration/test_relational_catalog.py
+++ b/clients/client-python/tests/integration/test_relational_catalog.py
@@ -484,3 +484,73 @@ class TestRelationalCatalog(IntegrationTestEnv):
with self.assertRaises(NoSuchViewException):
view_catalog.alter_view(ident, ViewChange.set_property("property",
"value"))
+
+ def _assert_view_renamed(
+ self,
+ view_catalog,
+ altered_view,
+ old_view_ident,
+ new_view_ident,
+ new_view_name,
+ ):
+ self.assertEqual(altered_view.name(), new_view_name)
+ self.assertTrue(view_catalog.view_exists(new_view_ident))
+ self.assertFalse(view_catalog.view_exists(old_view_ident))
+
+ def test_relational_catalog_alter_view_rename(self):
+ """Test altering view name in the relational catalog."""
+ self._create_test_table()
+ self._create_test_view()
+ view_catalog = self.catalog.as_view_catalog()
+
+ new_view_name = f"{self.view_name}_renamed"
+ new_view_ident = NameIdentifier.of(
+ TestRelationalCatalog.SCHEMA_NAME, new_view_name
+ )
+
+ altered_view = view_catalog.alter_view(
+ self.view_ident, ViewChange.rename(new_view_name)
+ )
+ self._assert_view_renamed(
+ view_catalog,
+ altered_view,
+ self.view_ident,
+ new_view_ident,
+ new_view_name,
+ )
+
+ def test_relational_catalog_alter_view_replace(self):
+ """Test replacing a view definition in the relational catalog."""
+ self._create_test_table()
+ self._create_test_view()
+ view_catalog = self.catalog.as_view_catalog()
+
+ new_comment = "Updated view comment"
+ new_columns = [
+ Column.of("id", Types.LongType.get(), "Primary key"),
+ ]
+ new_representations = [
+ SQLRepresentation(
+ Dialects.HIVE,
+ f"SELECT id FROM {TestRelationalCatalog.TABLE_NAME}",
+ )
+ ]
+
+ altered_view = view_catalog.alter_view(
+ self.view_ident,
+ ViewChange.replace_view(
+ new_columns,
+ new_representations,
+ None,
+ None,
+ new_comment,
+ ),
+ )
+
+ self.assertEqual(altered_view.comment(), new_comment)
+ self.assertEqual(len(altered_view.columns()), 1)
+ self.assertEqual(altered_view.columns()[0].name(), "id")
+ self.assertEqual(
+ altered_view.sql_for(Dialects.HIVE).sql(),
+ f"SELECT id FROM {TestRelationalCatalog.TABLE_NAME}",
+ )
diff --git a/clients/client-python/tests/unittests/test_relational_catalog.py
b/clients/client-python/tests/unittests/test_relational_catalog.py
index bdc093cc7e..2c352ccb85 100644
--- a/clients/client-python/tests/unittests/test_relational_catalog.py
+++ b/clients/client-python/tests/unittests/test_relational_catalog.py
@@ -18,12 +18,14 @@
import json
import unittest
from http.client import HTTPResponse
+from typing import Optional
from unittest.mock import Mock, patch
from gravitino.api.authorization.privileges import Privilege
from gravitino.api.rel.column import Column
from gravitino.api.rel.dialects import Dialects
from gravitino.api.rel.indexes.index import Index
+from gravitino.api.rel.representation import Representation
from gravitino.api.rel.sql_representation import SQLRepresentation
from gravitino.api.rel.table_change import TableChange
from gravitino.api.rel.types.types import Types
@@ -35,7 +37,11 @@ from gravitino.dto.rel.distribution_dto import
DistributionDTO
from gravitino.dto.rel.sql_representation_dto import SQLRepresentationDTO
from gravitino.dto.rel.table_dto import TableDTO
from gravitino.dto.rel.view_dto import ViewDTO
-from gravitino.dto.requests.view_update_request import ViewUpdateRequest
+from gravitino.dto.requests.view_update_request import (
+ ViewUpdateRequest,
+ ViewUpdateRequestBase,
+)
+from gravitino.dto.requests.view_updates_request import ViewUpdatesRequest
from gravitino.dto.responses.drop_response import DropResponse
from gravitino.dto.responses.entity_list_response import EntityListResponse
from gravitino.dto.responses.table_response import TableResponse
@@ -43,6 +49,8 @@ from gravitino.dto.responses.view_response import ViewResponse
from gravitino.dto.util.dto_converters import DTOConverters
from gravitino.exceptions.base import (
IllegalArgumentException,
+ IllegalNameIdentifierException,
+ IllegalNamespaceException,
NoSuchSchemaException,
NoSuchTableException,
NoSuchViewException,
@@ -58,6 +66,69 @@ from tests.unittests.fixtures.table_fixtures import (
class TestRelationalCatalog(unittest.TestCase):
+ @classmethod
+ def _default_columns(cls) -> list[ColumnDTO]:
+ return [
+ ColumnDTO(
+ _name="id",
+ _data_type=Types.IntegerType.get(),
+ _comment="id column",
+ _nullable=False,
+ )
+ ]
+
+ @classmethod
+ def _default_representations(cls) -> list[SQLRepresentationDTO]:
+ return [
+ SQLRepresentationDTO(
+ _dialect=Dialects.TRINO,
+ _sql="SELECT id FROM test_table",
+ )
+ ]
+
+ @classmethod
+ def _default_api_columns(cls) -> list[Column]:
+ return [
+ Column.of(
+ "id",
+ Types.IntegerType.get(),
+ comment="id column",
+ nullable=False,
+ )
+ ]
+
+ @classmethod
+ def _default_api_representations(cls) -> list[Representation]:
+ return [SQLRepresentation(Dialects.TRINO, "SELECT id FROM test_table")]
+
+ @classmethod
+ def _new_view_dto(
+ cls,
+ name: str,
+ comment: str = "test view comment",
+ columns: Optional[list[ColumnDTO]] = None,
+ representations: Optional[list[SQLRepresentationDTO]] = None,
+ properties: Optional[dict[str, str]] = None,
+ default_catalog: Optional[str] = "test_catalog",
+ default_schema: Optional[str] = "test_schema",
+ ) -> ViewDTO:
+ return ViewDTO(
+ _name=name,
+ _columns=columns if columns is not None else
cls._default_columns(),
+ _representations=(
+ representations
+ if representations is not None
+ else cls._default_representations()
+ ),
+ _comment=comment,
+ _default_catalog=default_catalog,
+ _default_schema=default_schema,
+ _properties=properties if properties is not None else {},
+ _audit=AuditDTO(
+ "creator", "2022-01-01T00:00:00Z", "modifier",
"2022-01-01T00:00:00Z"
+ ),
+ )
+
@classmethod
def setUpClass(cls) -> None:
cls.metalake_name = "test_metalake"
@@ -79,30 +150,7 @@ class TestRelationalCatalog(unittest.TestCase):
)
cls.TABLE_DTO_JSON_STRING =
TABLE_DTO_JSON_STRING_WITH_STARTING_DATE_SORT
cls.table_dto = TableDTO.from_json(cls.TABLE_DTO_JSON_STRING)
- cls.view_dto = ViewDTO(
- _name=cls.view_name,
- _columns=[
- ColumnDTO(
- _name="id",
- _data_type=Types.IntegerType.get(),
- _comment="id column",
- _nullable=False,
- )
- ],
- _representations=[
- SQLRepresentationDTO(
- _dialect=Dialects.TRINO,
- _sql="SELECT id FROM test_table",
- )
- ],
- _comment="test view comment",
- _default_catalog="test_catalog",
- _default_schema="test_schema",
- _properties={"k1": "v1"},
- _audit=AuditDTO(
- "creator", "2022-01-01T00:00:00Z", "modifier",
"2022-01-01T00:00:00Z"
- ),
- )
+ cls.view_dto = cls._new_view_dto(cls.view_name, properties={"k1":
"v1"})
def _get_mock_http_resp(self, json_str: str, return_code: int = 200):
mock_http_resp = Mock(HTTPResponse)
@@ -458,6 +506,42 @@ class TestRelationalCatalog(unittest.TestCase):
Namespace.of("invalid_schema")
)
+ def test_list_views_empty(self):
+ resp_body = EntityListResponse(0, [])
+ mock_resp = self._get_mock_http_resp(resp_body.to_json())
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.get",
+ return_value=mock_resp,
+ ):
+ views = self.catalog.as_view_catalog().list_views(
+ Namespace.of(self.schema_name)
+ )
+ self.assertEqual(0, len(views))
+
+ def test_list_views_server_internal_error(self):
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.get",
+ side_effect=RuntimeError("internal error"),
+ ):
+ with self.assertRaises(RuntimeError):
+ self.catalog.as_view_catalog().list_views(
+ Namespace.of(self.schema_name)
+ )
+
+ def test_list_views_invalid_namespace_length(self):
+ with self.assertRaisesRegex(
+ IllegalNamespaceException,
+ "View namespace must be non-null and have 1 level",
+ ):
+ self.catalog.as_view_catalog().list_views(Namespace.of("s1", "s2"))
+
+ with self.assertRaisesRegex(
+ IllegalNamespaceException,
+ "View namespace must be non-null and have 1 level",
+ ):
+ self.catalog.as_view_catalog().list_views(Namespace.empty())
+
def test_load_view(self):
resp_body = ViewResponse(0, self.view_dto)
mock_resp = self._get_mock_http_resp(resp_body.to_json())
@@ -484,6 +568,20 @@ class TestRelationalCatalog(unittest.TestCase):
with self.assertRaises(NoSuchViewException):
self.catalog.as_view_catalog().load_view(self.view_identifier)
+ def test_load_view_invalid_identifier(self):
+ with self.assertRaisesRegex(
+ IllegalNameIdentifierException, "NameIdentifier must not be null"
+ ):
+ self.catalog.as_view_catalog().load_view(None)
+
+ with self.assertRaisesRegex(
+ IllegalNamespaceException,
+ "View namespace must be non-null and have 1 level",
+ ):
+ self.catalog.as_view_catalog().load_view(
+ NameIdentifier.of("s1", "s2", "view1")
+ )
+
def test_create_view(self):
resp_body = ViewResponse(0, self.view_dto)
mock_resp = self._get_mock_http_resp(resp_body.to_json())
@@ -494,8 +592,8 @@ class TestRelationalCatalog(unittest.TestCase):
):
view = self.catalog.as_view_catalog().create_view(
self.view_identifier,
- [Column.of("id", Types.IntegerType.get(), nullable=False)],
- [SQLRepresentation(Dialects.TRINO, "SELECT id FROM
test_table")],
+ self._default_api_columns(),
+ self._default_api_representations(),
comment="test view comment",
default_catalog="test_catalog",
default_schema="test_schema",
@@ -512,47 +610,96 @@ class TestRelationalCatalog(unittest.TestCase):
with self.assertRaises(ViewAlreadyExistsException):
self.catalog.as_view_catalog().create_view(
self.view_identifier,
- [Column.of("id", Types.IntegerType.get())],
- [SQLRepresentation(Dialects.TRINO, "SELECT id FROM
test_table")],
+ self._default_api_columns(),
+ self._default_api_representations(),
)
- def test_alter_view(self):
- updated_view_dto = ViewDTO(
- _name="new_view",
- _columns=[
- ColumnDTO(
- _name="id",
- _data_type=Types.IntegerType.get(),
- _comment="id column",
- _nullable=False,
- )
- ],
- _representations=[
- SQLRepresentationDTO(
- _dialect=Dialects.TRINO,
- _sql="SELECT id FROM test_table",
+ def test_create_view_schema_not_found(self):
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.post",
+ side_effect=NoSuchSchemaException("Schema not found"),
+ ):
+ with self.assertRaises(NoSuchSchemaException):
+ self.catalog.as_view_catalog().create_view(
+ self.view_identifier,
+ self._default_api_columns(),
+ self._default_api_representations(),
)
- ],
- _comment="test view comment",
- _default_catalog="test_catalog",
- _default_schema="test_schema",
- _properties={"k1": "v1"},
- _audit=AuditDTO(
- "creator", "2022-01-01T00:00:00Z", "modifier",
"2022-01-01T00:00:00Z"
- ),
+
+ def test_create_view_rejects_empty_representations(self):
+ with self.assertRaisesRegex(
+ IllegalArgumentException,
+ '"representations" field is required and cannot be empty',
+ ):
+ self.catalog.as_view_catalog().create_view(
+ self.view_identifier,
+ self._default_api_columns(),
+ [],
+ )
+
+ with self.assertRaisesRegex(
+ IllegalArgumentException,
+ '"representations" field is required and cannot be empty',
+ ):
+ self.catalog.as_view_catalog().create_view(
+ self.view_identifier,
+ self._default_api_columns(),
+ None,
+ )
+
+ def test_create_view_invalid_identifier(self):
+ with self.assertRaises(IllegalNamespaceException):
+ self.catalog.as_view_catalog().create_view(
+ NameIdentifier.of("s1", "s2", "v1"),
+ self._default_api_columns(),
+ self._default_api_representations(),
+ )
+
+ def _run_alter_view(
+ self,
+ ident: NameIdentifier,
+ req: ViewUpdateRequestBase,
+ updated: ViewDTO,
+ change: ViewChange,
+ ):
+ full_namespace = Namespace.of(
+ self.metalake_name, self.catalog_name, ident.namespace().level(0)
)
- resp_body = ViewResponse(0, updated_view_dto)
+ # pylint: disable=protected-access
+ view_path = (
+
f"{self.catalog._format_view_request_path(full_namespace)}/{ident.name()}"
+ )
+ updates_request = ViewUpdatesRequest(_updates=[req])
+ resp_body = ViewResponse(0, updated)
mock_resp = self._get_mock_http_resp(resp_body.to_json())
with patch(
"gravitino.utils.http_client.HTTPClient.put",
return_value=mock_resp,
- ):
- view = self.catalog.as_view_catalog().alter_view(
- self.view_identifier,
- ViewChange.rename("new_view"),
+ ) as mock_put:
+ altered = self.catalog.as_view_catalog().alter_view(ident, change)
+ self.assertEqual(mock_put.call_args[0][0], view_path)
+ self.assertEqual(mock_put.call_args[1]["json"], updates_request)
+ self.assertEqual(updated.name(), altered.name())
+ self.assertEqual(updated.comment(), altered.comment())
+ self.assertEqual(updated.properties(), altered.properties())
+ self.assertEqual(
+ len(updated.representations()), len(altered.representations())
)
- self.assertEqual("new_view", view.name())
+ return altered
+
+ def test_alter_view_rename(self):
+ ident = NameIdentifier.of(self.schema_name, "view1")
+ updated = self._new_view_dto(
+ "view2",
+ "comment",
+ self._default_columns(),
+ self._default_representations(),
+ )
+ req = ViewUpdateRequest.RenameViewRequest(_new_name="view2")
+ self._run_alter_view(ident, req, updated, ViewChange.rename("view2"))
+
+ test_alter_view = test_alter_view_rename
def test_alter_view_not_exists(self):
with patch(
@@ -565,6 +712,76 @@ class TestRelationalCatalog(unittest.TestCase):
ViewChange.rename("new_view"),
)
+ def test_alter_view_set_property(self):
+ ident = NameIdentifier.of(self.schema_name, "view1")
+ updated = self._new_view_dto(
+ "view1",
+ "comment",
+ self._default_columns(),
+ self._default_representations(),
+ properties={"k1": "v1"},
+ )
+ req = ViewUpdateRequest.SetViewPropertyRequest(_property="k1",
_value="v1")
+ self._run_alter_view(ident, req, updated,
ViewChange.set_property("k1", "v1"))
+
+ def test_alter_view_remove_property(self):
+ ident = NameIdentifier.of(self.schema_name, "view1")
+ updated = self._new_view_dto(
+ "view1",
+ "comment",
+ self._default_columns(),
+ self._default_representations(),
+ )
+ req = ViewUpdateRequest.RemoveViewPropertyRequest(_property="k1")
+ self._run_alter_view(ident, req, updated,
ViewChange.remove_property("k1"))
+
+ def test_alter_view_replace(self):
+ ident = NameIdentifier.of(self.schema_name, "view1")
+ new_rep = SQLRepresentationDTO(
+ _dialect=Dialects.TRINO,
+ _sql="SELECT 3",
+ )
+ new_columns = self._default_columns()
+ new_reps = [new_rep]
+ updated = self._new_view_dto(
+ "view1",
+ "new comment",
+ new_columns,
+ new_reps,
+ default_catalog=None,
+ default_schema=None,
+ )
+
+ req = ViewUpdateRequest.ReplaceViewRequest(
+ _columns=new_columns,
+ _representations=new_reps,
+ _default_catalog=None,
+ _default_schema=None,
+ _comment="new comment",
+ )
+ new_rep_api = SQLRepresentation(Dialects.TRINO, "SELECT 3")
+ altered = self._run_alter_view(
+ ident,
+ req,
+ updated,
+ ViewChange.replace_view(
+ self._default_api_columns(),
+ [new_rep_api],
+ None,
+ None,
+ "new comment",
+ ),
+ )
+ self.assertIsNone(altered.default_catalog())
+ self.assertIsNone(altered.default_schema())
+
+ def test_alter_view_invalid_identifier(self):
+ with self.assertRaises(IllegalNamespaceException):
+ self.catalog.as_view_catalog().alter_view(
+ NameIdentifier.of("s1", "s2", "v1"),
+ ViewChange.rename("new_name"),
+ )
+
def test_drop_view(self):
resp_body = DropResponse(0, True)
mock_resp = self._get_mock_http_resp(resp_body.to_json())
@@ -587,6 +804,38 @@ class TestRelationalCatalog(unittest.TestCase):
is_dropped =
self.catalog.as_view_catalog().drop_view(self.view_identifier)
self.assertFalse(is_dropped)
+ def test_drop_view_server_internal_error(self):
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.delete",
+ side_effect=RuntimeError("internal error"),
+ ):
+ with self.assertRaises(RuntimeError):
+ self.catalog.as_view_catalog().drop_view(self.view_identifier)
+
+ def test_drop_view_invalid_identifier(self):
+ with self.assertRaises(IllegalNamespaceException):
+ self.catalog.as_view_catalog().drop_view(
+ NameIdentifier.of("s1", "s2", "v1")
+ )
+
+ def test_format_table_request_path_invalid_namespace(self):
+ with self.assertRaisesRegex(
+ IllegalNamespaceException,
+ "Table full namespace must be non-null and have 3 levels in the
format",
+ ):
+ self.catalog._format_table_request_path( # pylint:
disable=protected-access
+ Namespace.of("schema1")
+ )
+
+ def test_format_view_request_path_invalid_namespace(self):
+ with self.assertRaisesRegex(
+ IllegalNamespaceException,
+ "View full namespace must be non-null and have 3 levels in the
format",
+ ):
+ self.catalog._format_view_request_path( # pylint:
disable=protected-access
+ Namespace.of("schema1")
+ )
+
def test_to_view_update_request(self):
to_request = (
RelationalCatalog._to_view_update_request # pylint:
disable=protected-access
diff --git a/docs/how-to-use-python-client.md b/docs/how-to-use-python-client.md
index 6618439da0..695f6558bf 100644
--- a/docs/how-to-use-python-client.md
+++ b/docs/how-to-use-python-client.md
@@ -54,6 +54,13 @@ extras.
1. [Manage metalake using Gravitino Python
API](./manage-metalake-using-gravitino.md?language=python)
2. [Manage fileset metadata using Gravitino Python
API](./manage-fileset-metadata-using-gravitino.md?language=python)
3. [Manage Semantic Model metadata using Gravitino Python
API](./manage-semantic-model-metadata-using-gravitino.md?language=python)
+4. [Manage view metadata using Gravitino Python
API](./manage-view-metadata-using-gravitino.md?language=python)
+
+### View Operations
+
+Use `as_view_catalog()` on a loaded relational catalog to manage views. The
+[View guide](./manage-view-metadata-using-gravitino.md?language=python)
includes Python examples for view
+creation, list/load, alteration (rename, set/remove property, and replace),
and drop operations.
### HTTP Error Handling
diff --git a/docs/manage-view-metadata-using-gravitino.md
b/docs/manage-view-metadata-using-gravitino.md
index 5118040e51..3d24e5bae7 100644
--- a/docs/manage-view-metadata-using-gravitino.md
+++ b/docs/manage-view-metadata-using-gravitino.md
@@ -18,6 +18,8 @@ Views are supported by the Hive, Iceberg, and Paimon catalogs.
## View Operations
+For client setup and configuration, see [Client
Setup](./how-to-use-gravitino-client.md) and [Python
Client](./how-to-use-python-client.md).
+
### Create a View
A view carries its columns and one or more representations, each holding a
query and the dialect it
@@ -32,13 +34,20 @@ curl -X POST -H "Accept: application/vnd.gravitino.v1+json"
\
-H "Content-Type: application/json" -d '{
"name": "active_customers",
"comment": "Customers with orders in the last year",
- "query": "SELECT * FROM customers WHERE last_order_at > current_date -
interval 365 day",
- "dialect": "spark",
"columns": [
{"name": "id", "type": "integer", "nullable": false},
{"name": "name", "type": "varchar(500)", "nullable": true}
],
- "properties": {}
+ "representations": [
+ {
+ "type": "sql",
+ "sql": "SELECT * FROM customers WHERE last_order_at > current_date -
interval 365 day",
+ "dialect": "spark"
+ }
+ ],
+ "properties": {
+ "spark.sql.create.version": "3.5.0"
+ }
}'
http://localhost:8090/api/metalakes/example/catalogs/sales/schemas/public/views
```
@@ -69,7 +78,37 @@ View view = views.createView(
representations,
"sales",
"public",
- ImmutableMap.of());
+ ImmutableMap.of("spark.sql.create.version", "3.5.0"));
+```
+
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+catalog = client.load_catalog("sales")
+views = catalog.as_view_catalog()
+
+columns = [
+ Column.of("id", Types.IntegerType.get(), nullable=False),
+ Column.of("name", Types.VarCharType.of(500)),
+]
+
+representations = [
+ SQLRepresentation(
+ Dialects.SPARK,
+ "SELECT * FROM customers WHERE last_order_at > current_date - interval
365 day",
+ )
+]
+
+view = views.create_view(
+ NameIdentifier.of("public", "active_customers"),
+ columns=columns,
+ representations=representations,
+ comment="Customers with orders in the last year",
+ default_catalog="sales",
+ default_schema="public",
+ properties={"spark.sql.create.version": "3.5.0"},
+)
```
</TabItem>
@@ -95,17 +134,24 @@ curl -X GET -H "Accept: application/vnd.gravitino.v1+json"
\
View view = views.loadView(NameIdentifier.of("public", "active_customers"));
```
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+view = views.load_view(NameIdentifier.of("public", "active_customers"))
+```
+
</TabItem>
</Tabs>
### Alter a View
-| Change | JSON
| Java |
-|--------------------|--------------------------------------------------------------|--------------------------------------------|
-| Rename | `{"@type":"rename","newName":"view_renamed"}`
| `ViewChange.rename("view_renamed")` |
-| Update the comment | `{"@type":"updateComment","newComment":"new_comment"}`
| `ViewChange.updateComment("new_comment")` |
-| Set a property |
`{"@type":"setProperty","property":"key1","value":"value1"}` |
`ViewChange.setProperty("key1", "value1")` |
-| Remove a property | `{"@type":"removeProperty","property":"key1"}`
| `ViewChange.removeProperty("key1")` |
+| Change | JSON
| Java | Python
|
+|--------------------|--------------------------------------------------------------|--------------------------------------------|--------------------------------------------|
+| Rename | `{"@type":"rename","newName":"view_renamed"}`
| `ViewChange.rename("view_renamed")` |
`ViewChange.rename("view_renamed")` |
+| Set a property |
`{"@type":"setProperty","property":"key1","value":"value1"}` |
`ViewChange.setProperty("key1", "value1")` | `ViewChange.set_property("key1",
"value1")` |
+| Remove a property | `{"@type":"removeProperty","property":"key1"}`
| `ViewChange.removeProperty("key1")` |
`ViewChange.remove_property("key1")` |
+| Replace a view | `{"@type":"replaceView",...}`
| `ViewChange.replaceView(...)` |
`ViewChange.replace_view(...)` |
<Tabs groupId='language' queryString>
<TabItem value="shell" label="REST">
@@ -114,7 +160,7 @@ View view = views.loadView(NameIdentifier.of("public",
"active_customers"));
curl -X PUT -H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" -d '{
"updates": [
- {"@type": "updateComment", "newComment": "Customers active in the last
year"}
+ {"@type": "setProperty", "property": "key1", "value": "value1"}
]
}'
http://localhost:8090/api/metalakes/example/catalogs/sales/schemas/public/views/active_customers
```
@@ -125,7 +171,17 @@ curl -X PUT -H "Accept: application/vnd.gravitino.v1+json"
\
```java
View view = views.alterView(
NameIdentifier.of("public", "active_customers"),
- ViewChange.updateComment("Customers active in the last year"));
+ ViewChange.setProperty("key1", "value1"));
+```
+
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+view = views.alter_view(
+ NameIdentifier.of("public", "active_customers"),
+ ViewChange.set_property("key1", "value1"),
+)
```
</TabItem>
@@ -150,6 +206,13 @@ curl -X DELETE -H "Accept:
application/vnd.gravitino.v1+json" \
boolean dropped = views.dropView(NameIdentifier.of("public",
"active_customers"));
```
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+dropped = views.drop_view(NameIdentifier.of("public", "active_customers"))
+```
+
</TabItem>
</Tabs>
@@ -170,5 +233,12 @@ curl -X GET -H "Accept: application/vnd.gravitino.v1+json"
\
NameIdentifier[] identifiers = views.listViews(Namespace.of("public"));
```
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+identifiers = views.list_views(Namespace.of("public"))
+```
+
</TabItem>
</Tabs>