From 7cfc474d45ddf84ee11fa478dc0ed0d7d8467196 Mon Sep 17 00:00:00 2001 From: Sueun Cho Date: Wed, 3 Jun 2026 16:31:40 -0400 Subject: [PATCH] Validate dry_run update mode --- pinecone/async_client/async_index.py | 5 ++++- pinecone/grpc/__init__.py | 5 ++++- pinecone/index/__init__.py | 2 ++ tests/unit/test_async_index_operations.py | 6 ++++++ tests/unit/test_grpc_index.py | 4 ++++ tests/unit/test_index_update.py | 5 +++++ 6 files changed, 25 insertions(+), 2 deletions(-) diff --git a/pinecone/async_client/async_index.py b/pinecone/async_client/async_index.py index 664a88d28..d96d10321 100644 --- a/pinecone/async_client/async_index.py +++ b/pinecone/async_client/async_index.py @@ -910,7 +910,8 @@ async def update( namespace (str): Namespace to target. Defaults to the default namespace. filter (dict[str, Any] | None): Metadata filter expression selecting vectors to update. dry_run (bool): If True, return the count of records that would be - affected without applying changes. + affected without applying changes. Only applies to filter-based + updates. Returns: :class:`UpdateResponse` with matched_records count (when available). @@ -942,6 +943,8 @@ async def update( raise ValidationError("Exactly one of id or filter must be provided, not both") if not has_id and not has_filter: raise ValidationError("Exactly one of id or filter must be provided, got neither") + if dry_run and has_id: + raise ValidationError("dry_run is only supported for filter-based updates") body: dict[str, Any] = {"namespace": namespace} if id is not None: diff --git a/pinecone/grpc/__init__.py b/pinecone/grpc/__init__.py index 964ef4228..febdf8885 100644 --- a/pinecone/grpc/__init__.py +++ b/pinecone/grpc/__init__.py @@ -765,7 +765,8 @@ def update( namespace (str): Namespace to target. Defaults to the default namespace. filter (dict[str, Any] | None): Metadata filter expression selecting vectors to update. dry_run (bool): If True, return the count of records that would be - affected without applying changes. + affected without applying changes. Only applies to filter-based + updates. timeout (float | None): Per-call timeout in seconds. None uses the client-level default. Returns: @@ -795,6 +796,8 @@ def update( raise ValidationError("Exactly one of id or filter must be provided, not both") if not has_id and not has_filter: raise ValidationError("Exactly one of id or filter must be provided, got neither") + if dry_run and has_id: + raise ValidationError("dry_run is only supported for filter-based updates") # Convert SparseValues model to dict for GrpcChannel sv_dict: Mapping[str, Any] | None = None diff --git a/pinecone/index/__init__.py b/pinecone/index/__init__.py index 07b15bc67..8f06c4e34 100644 --- a/pinecone/index/__init__.py +++ b/pinecone/index/__init__.py @@ -1018,6 +1018,8 @@ def update( raise ValidationError("Exactly one of id or filter must be provided, not both") if not has_id and not has_filter: raise ValidationError("Exactly one of id or filter must be provided, got neither") + if dry_run and has_id: + raise ValidationError("dry_run is only supported for filter-based updates") body: dict[str, Any] = {"namespace": namespace} if id is not None: diff --git a/tests/unit/test_async_index_operations.py b/tests/unit/test_async_index_operations.py index b1e00bd9c..62e7fa031 100644 --- a/tests/unit/test_async_index_operations.py +++ b/tests/unit/test_async_index_operations.py @@ -426,6 +426,12 @@ async def test_update_both_id_and_filter_raises(self) -> None: with pytest.raises(ValidationError, match="not both"): await idx.update(id="vec1", filter={"x": 1}) + @pytest.mark.asyncio + async def test_update_dry_run_with_id_raises(self) -> None: + idx = _make_async_index() + with pytest.raises(ValidationError, match="dry_run is only supported"): + await idx.update(id="vec1", set_metadata={"genre": "comedy"}, dry_run=True) + @respx.mock @pytest.mark.asyncio async def test_update_dry_run(self) -> None: diff --git a/tests/unit/test_grpc_index.py b/tests/unit/test_grpc_index.py index 37ebcf837..d8fb7bf6f 100644 --- a/tests/unit/test_grpc_index.py +++ b/tests/unit/test_grpc_index.py @@ -500,6 +500,10 @@ def test_update_validates_neither_id_nor_filter(self, grpc_index: GrpcIndex) -> with pytest.raises(ValidationError, match="got neither"): grpc_index.update(values=[0.1]) + def test_update_validates_dry_run_with_id(self, grpc_index: GrpcIndex) -> None: + with pytest.raises(ValidationError, match="dry_run is only supported"): + grpc_index.update(id="v1", set_metadata={"key": "new_val"}, dry_run=True) + def test_update_by_filter_with_dry_run( self, grpc_index: GrpcIndex, mock_channel: MagicMock ) -> None: diff --git a/tests/unit/test_index_update.py b/tests/unit/test_index_update.py index 1c5031558..1ec26eb05 100644 --- a/tests/unit/test_index_update.py +++ b/tests/unit/test_index_update.py @@ -168,6 +168,11 @@ def test_neither_id_nor_filter_raises(self) -> None: with pytest.raises(ValidationError, match="got neither"): idx.update(values=[0.1]) + def test_dry_run_with_id_raises(self) -> None: + idx = _make_index() + with pytest.raises(ValidationError, match="dry_run is only supported"): + idx.update(id="vec1", set_metadata={"genre": "comedy"}, dry_run=True) + @respx.mock def test_dry_run_not_in_body_when_false(self) -> None: route = respx.post(UPDATE_URL).mock(