From 79a64a8f85f7171e9d15f03375b500b26a6f2654 Mon Sep 17 00:00:00 2001 From: Ahmed Ali Date: Thu, 26 Feb 2026 00:12:58 +0100 Subject: [PATCH] fix(postgrest): handle maybe_single zero-row responses correctly --- .../src/postgrest/_async/request_builder.py | 13 ++++- .../src/postgrest/_sync/request_builder.py | 13 ++++- src/postgrest/tests/_async/test_client.py | 48 +++++++++++++++++++ src/postgrest/tests/_sync/test_client.py | 46 ++++++++++++++++++ 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/postgrest/src/postgrest/_async/request_builder.py b/src/postgrest/src/postgrest/_async/request_builder.py index 3c996922..d8721278 100644 --- a/src/postgrest/src/postgrest/_async/request_builder.py +++ b/src/postgrest/src/postgrest/_async/request_builder.py @@ -26,6 +26,13 @@ from ..utils import model_validate_json ReqConfig = RequestConfig[AsyncClient] +_MAYBE_SINGLE_NO_ROWS_DETAILS = "The result contains 0 rows" + + +def _is_maybe_single_no_rows_error(error: APIError) -> bool: + return str(error.code) == "204" or ( + error.details is not None and _MAYBE_SINGLE_NO_ROWS_DETAILS in error.details + ) class AsyncQueryRequestBuilder: @@ -109,8 +116,8 @@ async def execute(self) -> Optional[SingleAPIResponse]: try: r = await AsyncSingleRequestBuilder(self.request).execute() except APIError as e: - if e.details and "The result contains 0 rows" in e.details: - return None + if _is_maybe_single_no_rows_error(e): + return SingleAPIResponse(data=None) if not r: raise APIError( { @@ -120,6 +127,8 @@ async def execute(self) -> Optional[SingleAPIResponse]: "details": "Postgrest couldn't retrieve response, please check traceback of the code. Please create an issue in `supabase-community/postgrest-py` if needed.", } ) + if r.data == []: + return SingleAPIResponse(data=None, count=r.count) return r diff --git a/src/postgrest/src/postgrest/_sync/request_builder.py b/src/postgrest/src/postgrest/_sync/request_builder.py index a5340403..66463d78 100644 --- a/src/postgrest/src/postgrest/_sync/request_builder.py +++ b/src/postgrest/src/postgrest/_sync/request_builder.py @@ -26,6 +26,13 @@ from ..utils import model_validate_json ReqConfig = RequestConfig[Client] +_MAYBE_SINGLE_NO_ROWS_DETAILS = "The result contains 0 rows" + + +def _is_maybe_single_no_rows_error(error: APIError) -> bool: + return str(error.code) == "204" or ( + error.details is not None and _MAYBE_SINGLE_NO_ROWS_DETAILS in error.details + ) class SyncQueryRequestBuilder: @@ -109,8 +116,8 @@ def execute(self) -> Optional[SingleAPIResponse]: try: r = SyncSingleRequestBuilder(self.request).execute() except APIError as e: - if e.details and "The result contains 0 rows" in e.details: - return None + if _is_maybe_single_no_rows_error(e): + return SingleAPIResponse(data=None) if not r: raise APIError( { @@ -120,6 +127,8 @@ def execute(self) -> Optional[SingleAPIResponse]: "details": "Postgrest couldn't retrieve response, please check traceback of the code. Please create an issue in `supabase-community/postgrest-py` if needed.", } ) + if r.data == []: + return SingleAPIResponse(data=None, count=r.count) return r diff --git a/src/postgrest/tests/_async/test_client.py b/src/postgrest/tests/_async/test_client.py index 18a95627..4447eacf 100644 --- a/src/postgrest/tests/_async/test_client.py +++ b/src/postgrest/tests/_async/test_client.py @@ -163,6 +163,54 @@ async def test_response_maybe_single(postgrest_client: AsyncPostgrestClient): assert "code" in exc_response and int(exc_response["code"]) == 204 +@pytest.mark.asyncio +async def test_maybe_single_returns_none_on_no_rows_204( + postgrest_client: AsyncPostgrestClient, +): + with patch( + "httpx._client.AsyncClient.request", + return_value=Response( + status_code=204, + request=Request(method="GET", url="http://example.com"), + ), + ): + response = await ( + postgrest_client.from_("test") + .select("a", "b") + .eq("c", "d") + .maybe_single() + .execute() + ) + + assert response is not None + assert response.data is None + + +@pytest.mark.asyncio +async def test_single_raises_on_no_rows(postgrest_client: AsyncPostgrestClient): + with patch( + "httpx._client.AsyncClient.request", + return_value=Response( + status_code=406, + json={ + "message": "JSON object requested, multiple (or no) rows returned", + "code": "PGRST116", + "hint": None, + "details": "The result contains 0 rows", + }, + request=Request(method="GET", url="http://example.com"), + ), + ): + with pytest.raises(APIError): + await ( + postgrest_client.from_("test") + .select("a", "b") + .eq("c", "d") + .single() + .execute() + ) + + # https://github.com/supabase/postgrest-py/issues/595 @pytest.mark.asyncio async def test_response_client_invalid_response_but_valid_json( diff --git a/src/postgrest/tests/_sync/test_client.py b/src/postgrest/tests/_sync/test_client.py index 7f7c7610..58d95656 100644 --- a/src/postgrest/tests/_sync/test_client.py +++ b/src/postgrest/tests/_sync/test_client.py @@ -158,6 +158,52 @@ def test_response_maybe_single(postgrest_client: SyncPostgrestClient): assert "code" in exc_response and int(exc_response["code"]) == 204 +def test_maybe_single_returns_none_on_no_rows_204( + postgrest_client: SyncPostgrestClient, +): + with patch( + "httpx._client.Client.request", + return_value=Response( + status_code=204, + request=Request(method="GET", url="http://example.com"), + ), + ): + response = ( + postgrest_client.from_("test") + .select("a", "b") + .eq("c", "d") + .maybe_single() + .execute() + ) + + assert response is not None + assert response.data is None + + +def test_single_raises_on_no_rows(postgrest_client: SyncPostgrestClient): + with patch( + "httpx._client.Client.request", + return_value=Response( + status_code=406, + json={ + "message": "JSON object requested, multiple (or no) rows returned", + "code": "PGRST116", + "hint": None, + "details": "The result contains 0 rows", + }, + request=Request(method="GET", url="http://example.com"), + ), + ): + with pytest.raises(APIError): + ( + postgrest_client.from_("test") + .select("a", "b") + .eq("c", "d") + .single() + .execute() + ) + + # https://github.com/supabase/postgrest-py/issues/595