diff --git a/CHANGELOG.md b/CHANGELOG.md index c18e547a..c5346578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +* Deprecated the table client scan query methods — `TableClient.scan_query`, `TableClient.async_scan_query` and the async `ydb.aio.TableClient.scan_query`: they now emit a `DeprecationWarning` and keep working as before, use QueryService (`ydb.QuerySessionPool` / `ydb.aio.QuerySessionPool`) instead * Mark the package as typed so type checkers use the SDK's inline annotations ## 3.32.0 ## diff --git a/docs/index.rst b/docs/index.rst index 66970c20..546ee710 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -91,7 +91,7 @@ The :doc:`table` page covers ``driver.table_client`` — the lower-level API for operations that cannot be expressed in YQL: creating tables with custom partitioning, TTL, secondary indexes, and column families; bulk loading data with ``bulk_upsert``; point reads by primary key with ``read_rows``; and streaming full-table reads with -``read_table`` or ``scan_query``. Use this +``read_table``. Use this alongside the Query service when you need fine-grained schema or data-loading control. diff --git a/docs/table.rst b/docs/table.rst index 7f92cdc3..0b85c407 100644 --- a/docs/table.rst +++ b/docs/table.rst @@ -543,6 +543,13 @@ Pass ``None`` to ``from_bound`` or ``to_bound`` of :class:`~ydb.KeyRange` to mea scan_query ^^^^^^^^^^ +The table-client ``scan_query`` methods are deprecated and emit a +``DeprecationWarning``. Use :class:`~ydb.QuerySessionPool` (or +:class:`~ydb.aio.QuerySessionPool`) and stream results with ``session.execute()`` +for new code; see :doc:`query`. + +The examples below show legacy usage for applications that have not migrated yet. + ``scan_query`` executes a YQL query in streaming mode — the server sends result chunks as they are produced without buffering the entire result set: diff --git a/ydb/aio/table.py b/ydb/aio/table.py index ef9c53a6..136c0200 100644 --- a/ydb/aio/table.py +++ b/ydb/aio/table.py @@ -2,6 +2,7 @@ import logging import time import typing +import warnings from typing import ( Any, @@ -19,6 +20,7 @@ from ydb.table import ( BaseSession, BaseTableClient, + _SCAN_QUERY_DEPRECATION_MESSAGE, _scan_query_request_factory, _wrap_scan_query_response, BaseTxContext, @@ -181,6 +183,14 @@ async def describe_system_view(self, path, settings=None): # pylint: disable=W0 return await super().describe_system_view(path, settings) async def scan_query(self, query, parameters=None, settings=None): # pylint: disable=W0236 + """ + Deprecated: use QueryService (:class:`ydb.aio.QuerySessionPool`) instead. + """ + warnings.warn( + _SCAN_QUERY_DEPRECATION_MESSAGE.format(method="scan_query", pool="ydb.aio.QuerySessionPool"), + DeprecationWarning, + stacklevel=2, + ) request = _scan_query_request_factory(query, parameters, settings) response = await self._driver( request, diff --git a/ydb/table.py b/ydb/table.py index 966bae82..0346e8e0 100644 --- a/ydb/table.py +++ b/ydb/table.py @@ -7,6 +7,7 @@ import logging import enum import typing +import warnings from typing import ( Any, @@ -988,6 +989,11 @@ def __init__(self, yql_text, parameters_types): self.parameters_types = parameters_types +_SCAN_QUERY_DEPRECATION_MESSAGE = ( + "{method} is deprecated and will be removed in a future release, use QueryService ({pool}) instead" +) + + def _wrap_scan_query_response(response, table_client_settings): issues._process_response(response) return ScanQueryResult(response.result, table_client_settings) @@ -1184,6 +1190,9 @@ def session(self): @abstractmethod def scan_query(self, query, parameters=None, settings=None): + """ + Deprecated: use QueryService (:class:`ydb.QuerySessionPool`) instead. + """ pass @abstractmethod @@ -1226,6 +1235,14 @@ def session(self): def scan_query(self, query, parameters=None, settings=None): # type: (Union[str, ydb.ScanQuery], Optional[Mapping[str, Any]], Optional[ydb.BaseRequestSettings]) -> _utilities.SyncResponseIterator + """ + Deprecated: use QueryService (:class:`ydb.QuerySessionPool`) instead. + """ + warnings.warn( + _SCAN_QUERY_DEPRECATION_MESSAGE.format(method="scan_query", pool="ydb.QuerySessionPool"), + DeprecationWarning, + stacklevel=2, + ) request = _scan_query_request_factory(query, parameters, settings) stream_it = self._driver( request, @@ -1309,6 +1326,14 @@ def __del__(self): def async_scan_query(self, query, parameters=None, settings=None): # type: (Union[str, ydb.ScanQuery], Optional[Mapping[str, Any]], Optional[ydb.BaseRequestSettings]) -> _utilities.AsyncResponseIterator + """ + Deprecated: use QueryService (:class:`ydb.QuerySessionPool`) instead. + """ + warnings.warn( + _SCAN_QUERY_DEPRECATION_MESSAGE.format(method="async_scan_query", pool="ydb.QuerySessionPool"), + DeprecationWarning, + stacklevel=2, + ) request = _scan_query_request_factory(query, parameters, settings) stream_it = self._driver( request, diff --git a/ydb/table_test.py b/ydb/table_test.py index e6a33af6..dc64e025 100644 --- a/ydb/table_test.py +++ b/ydb/table_test.py @@ -3,7 +3,9 @@ import pytest from unittest import mock -from . import issues, convert, types, _apis, scheme, _session_impl +from . import issues, convert, types, _apis, scheme, _session_impl, _utilities +from .aio import _utilities as _aio_utilities +from .aio.table import TableClient as AioTableClient from .table import SystemViewSchemeEntry, TableClient, TableClientSettings from .retries import ( @@ -338,6 +340,72 @@ def future(self, request, stub, method, wrap_fn, settings, wrap_args, *rest): assert entry.sys_view_name == "partition_stats" +class _FakeScanQueryDriver: + def __call__(self, request, stub, method, settings=None): + self.request = request + self.method = method + return iter(()) + + +class _FakeAsyncScanQueryDriver: + async def __call__(self, request, stub, method, settings=None): + self.request = request + self.method = method + return _EmptyAsyncStream() + + +class _EmptyAsyncStream: + def __aiter__(self): + return self + + async def __anext__(self): + raise StopAsyncIteration + + +def test_scan_query_warns_and_still_returns_iterator(): + driver = _FakeScanQueryDriver() + + with pytest.warns(DeprecationWarning) as record: + stream = TableClient(driver).scan_query("SELECT 1") + + assert str(record[0].message) == ( + "scan_query is deprecated and will be removed in a future release, " + "use QueryService (ydb.QuerySessionPool) instead" + ) + assert driver.method == _apis.TableService.StreamExecuteScanQuery + assert isinstance(stream, _utilities.SyncResponseIterator) + + +def test_async_scan_query_warns_and_still_returns_iterator(): + driver = _FakeScanQueryDriver() + + with pytest.warns(DeprecationWarning) as record: + stream = TableClient(driver).async_scan_query("SELECT 1") + + assert str(record[0].message) == ( + "async_scan_query is deprecated and will be removed in a future release, " + "use QueryService (ydb.QuerySessionPool) instead" + ) + assert driver.method == _apis.TableService.StreamExecuteScanQuery + assert isinstance(stream, _utilities.AsyncResponseIterator) + + +@pytest.mark.asyncio +async def test_aio_scan_query_warns_and_points_to_async_pool(): + driver = _FakeAsyncScanQueryDriver() + + with pytest.warns(DeprecationWarning) as record: + stream = await AioTableClient(driver).scan_query("SELECT 1") + + # The async client must not send users to the sync session pool. + assert str(record[0].message) == ( + "scan_query is deprecated and will be removed in a future release, " + "use QueryService (ydb.aio.QuerySessionPool) instead" + ) + assert driver.method == _apis.TableService.StreamExecuteScanQuery + assert isinstance(stream, _aio_utilities.AsyncResponseIterator) + + def _read_rows_key_types(): return types.BulkUpsertColumns().add_column("id", types.PrimitiveType.Uint64)