From b3089a90c7d6bb030b5aad7c1e14bc1508283ce0 Mon Sep 17 00:00:00 2001 From: Oleg Ovcharuk Date: Wed, 29 Jul 2026 10:14:32 +0300 Subject: [PATCH 1/3] Add describe_system_view to table client Implement TableClient.describe_system_view (sync and async), a stateless TableService RPC that returns a new SystemViewSchemeEntry with the system view id, name, columns, primary key and attributes. --- CHANGELOG.md | 2 ++ tests/aio/test_table_client.py | 16 +++++++++ tests/table/test_table_client.py | 15 +++++++++ ydb/_apis.py | 1 + ydb/_session_impl.py | 21 ++++++++++++ ydb/aio/table.py | 3 ++ ydb/table.py | 57 ++++++++++++++++++++++++++++++++ ydb/table_test.py | 50 +++++++++++++++++++++++++++- 8 files changed, 164 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c05c665..321c73f24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +* Add `TableClient.describe_system_view` (sync and async) returning a dedicated `SystemViewSchemeEntry` with the system view id, name, columns, primary key and attributes — a full description of system view objects that `describe_table` does not provide + ## 3.31.1 ## * Support the new `SECRET` scheme entry type: `SchemeEntryType.SECRET` and `SchemeEntry.is_secret()` now recognise secret entries returned by `list_directory`/`describe_path` diff --git a/tests/aio/test_table_client.py b/tests/aio/test_table_client.py index 7316dbdf0..860604ce9 100644 --- a/tests/aio/test_table_client.py +++ b/tests/aio/test_table_client.py @@ -128,3 +128,19 @@ async def test_rename_index(self, driver: ydb.aio.Driver): description = await client.describe_table(table_name) assert len(description.indexes) == 1 assert description.indexes[0].name == "index2" + + @pytest.mark.asyncio + async def test_describe_system_view(self, driver: ydb.aio.Driver): + client = driver.table_client + + with pytest.raises(ydb.SchemeError): + await client.describe_system_view("/local/.sys/does_not_exist") + + entry = await client.describe_system_view("/local/.sys/nodes") + + assert isinstance(entry, ydb.SystemViewSchemeEntry) + assert entry.is_sysview() + assert entry.sys_view_name == "nodes" + assert entry.sys_view_id > 0 + assert entry.primary_key == ["NodeId"] + assert "NodeId" in [column.name for column in entry.columns] diff --git a/tests/table/test_table_client.py b/tests/table/test_table_client.py index 3fc9e0631..9f9a6fe3a 100644 --- a/tests/table/test_table_client.py +++ b/tests/table/test_table_client.py @@ -168,3 +168,18 @@ def test_rename_index(self, driver_sync: ydb.Driver): description = client.describe_table(table_name) assert len(description.indexes) == 1 assert description.indexes[0].name == "index2" + + def test_describe_system_view(self, driver_sync: ydb.Driver): + client = driver_sync.table_client + + with pytest.raises(ydb.SchemeError): + client.describe_system_view("/local/.sys/does_not_exist") + + entry = client.describe_system_view("/local/.sys/nodes") + + assert isinstance(entry, ydb.SystemViewSchemeEntry) + assert entry.is_sysview() + assert entry.sys_view_name == "nodes" + assert entry.sys_view_id > 0 + assert entry.primary_key == ["NodeId"] + assert "NodeId" in [column.name for column in entry.columns] diff --git a/ydb/_apis.py b/ydb/_apis.py index 638e2c020..f4b6dd537 100644 --- a/ydb/_apis.py +++ b/ydb/_apis.py @@ -99,6 +99,7 @@ class TableService(object): CopyTables = "CopyTables" RenameTables = "RenameTables" DescribeTable = "DescribeTable" + DescribeSystemView = "DescribeSystemView" CreateSession = "CreateSession" DeleteSession = "DeleteSession" ExecuteSchemeQuery = "ExecuteSchemeQuery" diff --git a/ydb/_session_impl.py b/ydb/_session_impl.py index 4fcd23314..9d5f77703 100644 --- a/ydb/_session_impl.py +++ b/ydb/_session_impl.py @@ -125,6 +125,21 @@ def wrap_describe_table_response(rpc_state, response_pb, sesssion_state, scheme_ ) +def wrap_describe_system_view_response(rpc_state, response_pb, scheme_entry_cls): + issues._process_response(response_pb.operation) + message = _apis.ydb_table.DescribeSystemViewResult() + response_pb.operation.result.Unpack(message) + return scheme._wrap_scheme_entry( + message.self, + scheme_entry_cls, + message.sys_view_id, + message.sys_view_name, + message.columns, + message.primary_key, + message.attributes, + ) + + def explicit_partitions_factory(primary_key, columns, split_points): column_types = {} pk = set(primary_key) @@ -251,6 +266,12 @@ def describe_table_request_factory(session_state, path, settings=None): return request +def describe_system_view_request_factory(path, settings=None): + request = _apis.ydb_table.DescribeSystemViewRequest() + request.path = path + return request + + def alter_table_request_factory( session_state, path, diff --git a/ydb/aio/table.py b/ydb/aio/table.py index 8d5e02c18..79f900628 100644 --- a/ydb/aio/table.py +++ b/ydb/aio/table.py @@ -172,6 +172,9 @@ def session(self): async def bulk_upsert(self, *args, **kwargs): # pylint: disable=W0236 return await super().bulk_upsert(*args, **kwargs) + async def describe_system_view(self, path, settings=None): # pylint: disable=W0236 + return await super().describe_system_view(path, settings) + async def scan_query(self, query, parameters=None, settings=None): # pylint: disable=W0236 request = _scan_query_request_factory(query, parameters, settings) response = await self._driver( diff --git a/ydb/table.py b/ydb/table.py index fb7fadc42..fbf2b5a71 100644 --- a/ydb/table.py +++ b/ydb/table.py @@ -1239,6 +1239,25 @@ def bulk_upsert(self, table_path, rows, column_types, settings=None): (), ) + def describe_system_view(self, path, settings=None): + # type: (str, ydb.BaseRequestSettings) -> Any + """ + Returns a full description of a system view by the provided path. + + :param path: A system view path + :param settings: A request settings + + :return: SystemViewSchemeEntry describing the system view + """ + return self._driver( + _session_impl.describe_system_view_request_factory(path, settings), + _apis.TableService.Stub, + _apis.TableService.DescribeSystemView, + _session_impl.wrap_describe_system_view_response, + settings, + (SystemViewSchemeEntry,), + ) + class TableClient(BaseTableClient["SyncDriver"]): def __init__(self, driver: "SyncDriver", table_client_settings: Optional[TableClientSettings] = None) -> None: @@ -1274,6 +1293,17 @@ def async_bulk_upsert(self, table_path, rows, column_types, settings=None): (), ) + @_utilities.wrap_async_call_exceptions + def async_describe_system_view(self, path, settings=None): + return self._driver.future( + _session_impl.describe_system_view_request_factory(path, settings), + _apis.TableService.Stub, + _apis.TableService.DescribeSystemView, + _session_impl.wrap_describe_system_view_response, + settings, + (SystemViewSchemeEntry,), + ) + def _init_pool_if_needed(self) -> None: if self._pool is None: self._pool = SessionPool(self._driver, 10) @@ -1650,6 +1680,33 @@ def __init__( self.attributes = attributes +class SystemViewSchemeEntry(scheme.SchemeEntry): + def __init__( + self, + name, + owner, + type, + effective_permissions, + permissions, + size_bytes, + sys_view_id, + sys_view_name, + columns, + primary_key, + attributes, + *args, + **kwargs + ): + super(SystemViewSchemeEntry, self).__init__( + name, owner, type, effective_permissions, permissions, size_bytes, *args, **kwargs + ) + self.sys_view_id = sys_view_id + self.sys_view_name = sys_view_name + self.columns = [Column(column.name, convert.type_to_native(column.type), column.family) for column in columns] + self.primary_key = [pk for pk in primary_key] + self.attributes = dict(attributes) + + class RenameItem: def __init__(self, source_path, destination_path, replace_destination=False): self._source_path = source_path diff --git a/ydb/table_test.py b/ydb/table_test.py index 4eaa5b882..c23a2b957 100644 --- a/ydb/table_test.py +++ b/ydb/table_test.py @@ -3,7 +3,8 @@ import pytest from unittest import mock -from . import issues, convert, types, _apis +from . import issues, convert, types, _apis, scheme, _session_impl +from .table import SystemViewSchemeEntry from .retries import ( retry_operation_impl, @@ -272,3 +273,50 @@ def check_retriable_error(err_type, backoff): check_unretriable_error(TestException, False) with mock.patch.object(retry_once_settings, "idempotent", True): check_unretriable_error(TestException, False) + + +def _build_describe_system_view_response(): + result = _apis.ydb_table.DescribeSystemViewResult() + result.self.name = "partition_stats" + result.self.type = scheme.SchemeEntryType.SYS_VIEW + result.sys_view_id = 42 + result.sys_view_name = "partition_stats" + column = result.columns.add() + column.name = "OwnerId" + column.type.type_id = types.PrimitiveType.Uint64._idn_ + result.primary_key.append("OwnerId") + result.attributes["origin"] = "test" + + response = _apis.ydb_table.DescribeSystemViewResponse() + response.operation.status = _apis.StatusIds.SUCCESS + response.operation.result.Pack(result) + return response + + +def test_describe_system_view_request_factory(): + request = _session_impl.describe_system_view_request_factory("/local/.sys/partition_stats") + assert request.path == "/local/.sys/partition_stats" + # DescribeSystemView is stateless: the request carries no session id. + assert not hasattr(request, "session_id") + + +def test_wrap_describe_system_view_response(): + entry = _session_impl.wrap_describe_system_view_response( + None, _build_describe_system_view_response(), SystemViewSchemeEntry + ) + + assert isinstance(entry, SystemViewSchemeEntry) + assert entry.name == "partition_stats" + assert entry.is_sysview() + assert entry.sys_view_id == 42 + assert entry.sys_view_name == "partition_stats" + assert entry.primary_key == ["OwnerId"] + assert [column.name for column in entry.columns] == ["OwnerId"] + assert entry.attributes == {"origin": "test"} + + +def test_wrap_describe_system_view_response_raises_on_error(): + response = _apis.ydb_table.DescribeSystemViewResponse() + response.operation.status = _apis.StatusIds.SCHEME_ERROR + with pytest.raises(issues.SchemeError): + _session_impl.wrap_describe_system_view_response(None, response, SystemViewSchemeEntry) From a2d20e407d2d12ecc4aab2e4b774033eed7e8eaf Mon Sep 17 00:00:00 2001 From: Oleg Ovcharuk Date: Wed, 29 Jul 2026 10:30:22 +0300 Subject: [PATCH 2/3] Address review: keep raw attributes map, document describe_system_view Store SystemViewSchemeEntry.attributes as the protobuf map like TableSchemeEntry (consistency), and document describe_system_view / SystemViewSchemeEntry in docs/table.rst and docs/apireference.rst. --- docs/apireference.rst | 7 +++++++ docs/table.rst | 22 ++++++++++++++++++++++ ydb/table.py | 2 +- ydb/table_test.py | 2 +- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/docs/apireference.rst b/docs/apireference.rst index e8f66f74e..5dc3144ca 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -372,6 +372,13 @@ TableSchemeEntry :members: :undoc-members: +SystemViewSchemeEntry +^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: ydb.SystemViewSchemeEntry + :members: + :undoc-members: + DescribeTableSettings ^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/table.rst b/docs/table.rst index 952d29e3b..6fff47d77 100644 --- a/docs/table.rst +++ b/docs/table.rst @@ -77,6 +77,24 @@ Pass :class:`~ydb.DescribeTableSettings` to request additional detail: entry = driver.table_client.describe_table("/local/users", settings) +Describing a System View +^^^^^^^^^^^^^^^^^^^^^^^^ + +System views (the ``.sys`` schema objects) are described with ``describe_system_view``, +which returns a :class:`~ydb.SystemViewSchemeEntry` with the system view id and name, +its columns, primary key, and attributes. Unlike ``describe_table`` this is a dedicated +description that omits the table-only fields that do not apply to system views: + +.. code-block:: python + + entry = driver.table_client.describe_system_view("/local/.sys/nodes") + + print(entry.sys_view_name, entry.sys_view_id) + for column in entry.columns: + print(column.name, column.type) + print(entry.primary_key) + + Altering a Table ^^^^^^^^^^^^^^^^ @@ -556,6 +574,10 @@ on an async driver. All I/O methods become coroutines: for col in entry.columns: print(col.name, col.type) + # Describe a system view + sys_view = await driver.table_client.describe_system_view("/local/.sys/nodes") + print(sys_view.sys_view_name, sys_view.primary_key) + await driver.table_client.drop_table("/local/users") asyncio.run(main()) diff --git a/ydb/table.py b/ydb/table.py index fbf2b5a71..765570cbe 100644 --- a/ydb/table.py +++ b/ydb/table.py @@ -1704,7 +1704,7 @@ def __init__( self.sys_view_name = sys_view_name self.columns = [Column(column.name, convert.type_to_native(column.type), column.family) for column in columns] self.primary_key = [pk for pk in primary_key] - self.attributes = dict(attributes) + self.attributes = attributes class RenameItem: diff --git a/ydb/table_test.py b/ydb/table_test.py index c23a2b957..3325418ad 100644 --- a/ydb/table_test.py +++ b/ydb/table_test.py @@ -312,7 +312,7 @@ def test_wrap_describe_system_view_response(): assert entry.sys_view_name == "partition_stats" assert entry.primary_key == ["OwnerId"] assert [column.name for column in entry.columns] == ["OwnerId"] - assert entry.attributes == {"origin": "test"} + assert dict(entry.attributes) == {"origin": "test"} def test_wrap_describe_system_view_response_raises_on_error(): From f1f52973143461783de2fb40ccaa9f5a69e536b9 Mon Sep 17 00:00:00 2001 From: Oleg Ovcharuk Date: Wed, 29 Jul 2026 10:37:14 +0300 Subject: [PATCH 3/3] Cover async_describe_system_view with a unit test The sync client's async_describe_system_view was the one patch line left uncovered; exercise it with a fake driver. --- ydb/table_test.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ydb/table_test.py b/ydb/table_test.py index 3325418ad..ad158260b 100644 --- a/ydb/table_test.py +++ b/ydb/table_test.py @@ -4,7 +4,7 @@ from unittest import mock from . import issues, convert, types, _apis, scheme, _session_impl -from .table import SystemViewSchemeEntry +from .table import SystemViewSchemeEntry, TableClient from .retries import ( retry_operation_impl, @@ -320,3 +320,19 @@ def test_wrap_describe_system_view_response_raises_on_error(): response.operation.status = _apis.StatusIds.SCHEME_ERROR with pytest.raises(issues.SchemeError): _session_impl.wrap_describe_system_view_response(None, response, SystemViewSchemeEntry) + + +def test_async_describe_system_view(): + class _FakeSyncDriver: + def future(self, request, stub, method, wrap_fn, settings, wrap_args, *rest): + self.request = request + self.method = method + return wrap_fn(None, _build_describe_system_view_response(), *wrap_args) + + driver = _FakeSyncDriver() + entry = TableClient(driver).async_describe_system_view("/local/.sys/partition_stats") + + assert driver.method == _apis.TableService.DescribeSystemView + assert driver.request.path == "/local/.sys/partition_stats" + assert isinstance(entry, SystemViewSchemeEntry) + assert entry.sys_view_name == "partition_stats"