From 08dba18a5a281926fd261b259a1242a51b8913d5 Mon Sep 17 00:00:00 2001 From: Tushar Pagar <240662211+tushardev-365@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:03:25 +0000 Subject: [PATCH] feat(postgrest): support single()/maybe_single() after insert/upsert select insert() and upsert() return an *QueryRequestBuilder, whose select() returns self, so single()/maybe_single() were unreachable and the JS client chain insert(...).select().single() had no Python equivalent (GH-1553). Move single()/maybe_single() up to *QueryRequestBuilder so they are available on every builder that select() can return, matching the JS client. Sync code and tests are the unasync-generated mirror. Closes #1553 --- .../src/postgrest/_async/request_builder.py | 26 +++++++++---------- .../src/postgrest/_sync/request_builder.py | 26 +++++++++---------- .../_async/test_query_request_builder.py | 22 +++++++++++++++- .../tests/_sync/test_query_request_builder.py | 22 +++++++++++++++- 4 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/postgrest/src/postgrest/_async/request_builder.py b/src/postgrest/src/postgrest/_async/request_builder.py index 9523d2ca..4e96e690 100644 --- a/src/postgrest/src/postgrest/_async/request_builder.py +++ b/src/postgrest/src/postgrest/_async/request_builder.py @@ -75,6 +75,19 @@ def retry(self, enabled: bool) -> Self: self.request.retry_enabled = enabled return self + def single(self) -> AsyncSingleRequestBuilder: + """Specify that the query will only return a single row in response. + + .. caution:: + The API will raise an error if the query returned more than one row. + """ + self.request.headers["Accept"] = "application/vnd.pgrst.object+json" + return AsyncSingleRequestBuilder(self.request) + + def maybe_single(self) -> AsyncMaybeSingleRequestBuilder: + """Retrieves at most one row from the result. Result must be at most one row (e.g. using `eq` on a UNIQUE column), otherwise this will result in an error.""" + return AsyncMaybeSingleRequestBuilder(self.request) + async def execute(self) -> APIResponse: """Execute the query. @@ -205,19 +218,6 @@ def __init__(self, request: ReqConfig) -> None: BaseSelectRequestBuilder.__init__(self, request) AsyncQueryRequestBuilder.__init__(self, request) - def single(self) -> AsyncSingleRequestBuilder: - """Specify that the query will only return a single row in response. - - .. caution:: - The API will raise an error if the query returned more than one row. - """ - self.request.headers["Accept"] = "application/vnd.pgrst.object+json" - return AsyncSingleRequestBuilder(self.request) - - def maybe_single(self) -> AsyncMaybeSingleRequestBuilder: - """Retrieves at most one row from the result. Result must be at most one row (e.g. using `eq` on a UNIQUE column), otherwise this will result in an error.""" - return AsyncMaybeSingleRequestBuilder(self.request) - def text_search( self, column: str, query: str, options: dict[str, Any] = {} ) -> AsyncQueryRequestBuilder: diff --git a/src/postgrest/src/postgrest/_sync/request_builder.py b/src/postgrest/src/postgrest/_sync/request_builder.py index b6891f56..108d14a6 100644 --- a/src/postgrest/src/postgrest/_sync/request_builder.py +++ b/src/postgrest/src/postgrest/_sync/request_builder.py @@ -75,6 +75,19 @@ def retry(self, enabled: bool) -> Self: self.request.retry_enabled = enabled return self + def single(self) -> SyncSingleRequestBuilder: + """Specify that the query will only return a single row in response. + + .. caution:: + The API will raise an error if the query returned more than one row. + """ + self.request.headers["Accept"] = "application/vnd.pgrst.object+json" + return SyncSingleRequestBuilder(self.request) + + def maybe_single(self) -> SyncMaybeSingleRequestBuilder: + """Retrieves at most one row from the result. Result must be at most one row (e.g. using `eq` on a UNIQUE column), otherwise this will result in an error.""" + return SyncMaybeSingleRequestBuilder(self.request) + def execute(self) -> APIResponse: """Execute the query. @@ -205,19 +218,6 @@ def __init__(self, request: ReqConfig) -> None: BaseSelectRequestBuilder.__init__(self, request) SyncQueryRequestBuilder.__init__(self, request) - def single(self) -> SyncSingleRequestBuilder: - """Specify that the query will only return a single row in response. - - .. caution:: - The API will raise an error if the query returned more than one row. - """ - self.request.headers["Accept"] = "application/vnd.pgrst.object+json" - return SyncSingleRequestBuilder(self.request) - - def maybe_single(self) -> SyncMaybeSingleRequestBuilder: - """Retrieves at most one row from the result. Result must be at most one row (e.g. using `eq` on a UNIQUE column), otherwise this will result in an error.""" - return SyncMaybeSingleRequestBuilder(self.request) - def text_search( self, column: str, query: str, options: dict[str, Any] = {} ) -> SyncQueryRequestBuilder: diff --git a/src/postgrest/tests/_async/test_query_request_builder.py b/src/postgrest/tests/_async/test_query_request_builder.py index 78edc2da..9c2b4204 100644 --- a/src/postgrest/tests/_async/test_query_request_builder.py +++ b/src/postgrest/tests/_async/test_query_request_builder.py @@ -5,7 +5,11 @@ from yarl import URL from postgrest import AsyncQueryRequestBuilder -from postgrest._async.request_builder import RequestConfig +from postgrest._async.request_builder import ( + AsyncMaybeSingleRequestBuilder, + AsyncSingleRequestBuilder, + RequestConfig, +) @pytest.fixture @@ -25,3 +29,19 @@ def test_constructor(query_request_builder: AsyncQueryRequestBuilder): assert len(builder.request.params) == 0 assert builder.request.http_method == "GET" assert builder.request.json is None + + +def test_select_single(query_request_builder: AsyncQueryRequestBuilder): + # insert()/upsert() return an AsyncQueryRequestBuilder, so single() must be + # reachable after select() to match the JS client's + # insert(...).select().single() chain. See GH-1553. + builder = query_request_builder.select("*").single() + + assert isinstance(builder, AsyncSingleRequestBuilder) + assert builder.request.headers["Accept"] == "application/vnd.pgrst.object+json" + + +def test_select_maybe_single(query_request_builder: AsyncQueryRequestBuilder): + builder = query_request_builder.select("*").maybe_single() + + assert isinstance(builder, AsyncMaybeSingleRequestBuilder) diff --git a/src/postgrest/tests/_sync/test_query_request_builder.py b/src/postgrest/tests/_sync/test_query_request_builder.py index 58fbf2fc..723c85e1 100644 --- a/src/postgrest/tests/_sync/test_query_request_builder.py +++ b/src/postgrest/tests/_sync/test_query_request_builder.py @@ -5,7 +5,11 @@ from yarl import URL from postgrest import SyncQueryRequestBuilder -from postgrest._sync.request_builder import RequestConfig +from postgrest._sync.request_builder import ( + RequestConfig, + SyncMaybeSingleRequestBuilder, + SyncSingleRequestBuilder, +) @pytest.fixture @@ -25,3 +29,19 @@ def test_constructor(query_request_builder: SyncQueryRequestBuilder): assert len(builder.request.params) == 0 assert builder.request.http_method == "GET" assert builder.request.json is None + + +def test_select_single(query_request_builder: SyncQueryRequestBuilder): + # insert()/upsert() return a SyncQueryRequestBuilder, so single() must be + # reachable after select() to match the JS client's + # insert(...).select().single() chain. See GH-1553. + builder = query_request_builder.select("*").single() + + assert isinstance(builder, SyncSingleRequestBuilder) + assert builder.request.headers["Accept"] == "application/vnd.pgrst.object+json" + + +def test_select_maybe_single(query_request_builder: SyncQueryRequestBuilder): + builder = query_request_builder.select("*").maybe_single() + + assert isinstance(builder, SyncMaybeSingleRequestBuilder)