Skip to content

Commit 58cc7ac

Browse files
committed
feat(postgrest): return a select builder from insert, upsert, update and delete
`insert()` and `upsert()` returned a query builder and `update()` and `delete()` returned a filter builder. Neither exposes `single()`, `maybe_single()`, `csv()`, `explain()`, `order()`, `limit()` or `range()`, so a chain that reads naturally in the JS client has no equivalent in Python: client.from_("users").insert({...}).select().single().execute() `select()` on those builders returns `self`, so the chain stops at a builder that never had `single()` to begin with. In supabase-js all four of these methods return a `PostgrestFilterBuilder`, which carries the transform methods. Return `SelectRequestBuilder` from all four instead. It already derives from the query builder and from the filter builder, so this only widens the surface: every method that worked before still works, on the same object, with the same request state. Filters after `update()` and `delete()` are unaffected. The sync package is edited by hand rather than through `make build-sync`. The generated output currently differs from the committed `_sync` in ways unrelated to this change (`asyncio.sleep` in place of `time.sleep` in `send_with_retry`, among others), so regenerating would have pulled unrelated breakage into this PR. Closes #1553
1 parent 3c98900 commit 58cc7ac

4 files changed

Lines changed: 134 additions & 26 deletions

File tree

src/postgrest/src/postgrest/_async/request_builder.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ def insert(
332332
returning: ReturnMethod = ReturnMethod.representation,
333333
upsert: bool = False,
334334
default_to_null: bool = True,
335-
) -> AsyncQueryRequestBuilder:
335+
) -> AsyncSelectRequestBuilder:
336336
"""Run an INSERT query.
337337
338338
Args:
@@ -344,7 +344,7 @@ def insert(
344344
Otherwise, use the default value for the column.
345345
Only applies for bulk inserts.
346346
Returns:
347-
:class:`AsyncQueryRequestBuilder`
347+
:class:`AsyncSelectRequestBuilder`
348348
"""
349349
method, params, headers, json = pre_insert(
350350
json,
@@ -363,7 +363,7 @@ def insert(
363363
headers=headers,
364364
json=json,
365365
)
366-
return AsyncQueryRequestBuilder(request)
366+
return AsyncSelectRequestBuilder(request)
367367

368368
def upsert(
369369
self,
@@ -374,7 +374,7 @@ def upsert(
374374
ignore_duplicates: bool = False,
375375
on_conflict: str = "",
376376
default_to_null: bool = True,
377-
) -> AsyncQueryRequestBuilder:
377+
) -> AsyncSelectRequestBuilder:
378378
"""Run an upsert (INSERT ... ON CONFLICT DO UPDATE) query.
379379
380380
Args:
@@ -388,7 +388,7 @@ def upsert(
388388
not when merging with existing rows under `ignoreDuplicates: false`.
389389
This also only applies when doing bulk upserts.
390390
Returns:
391-
:class:`AsyncQueryRequestBuilder`
391+
:class:`AsyncSelectRequestBuilder`
392392
"""
393393
method, params, headers, json = pre_upsert(
394394
json,
@@ -408,23 +408,23 @@ def upsert(
408408
headers=headers,
409409
json=json,
410410
)
411-
return AsyncQueryRequestBuilder(request)
411+
return AsyncSelectRequestBuilder(request)
412412

413413
def update(
414414
self,
415415
json: JSON,
416416
*,
417417
count: Optional[CountMethod] = None,
418418
returning: ReturnMethod = ReturnMethod.representation,
419-
) -> AsyncFilterRequestBuilder:
419+
) -> AsyncSelectRequestBuilder:
420420
"""Run an UPDATE query.
421421
422422
Args:
423423
json: The updated fields.
424424
count: The method to use to get the count of rows returned.
425425
returning: Either 'minimal' or 'representation'
426426
Returns:
427-
:class:`AsyncFilterRequestBuilder`
427+
:class:`AsyncSelectRequestBuilder`
428428
"""
429429
method, params, headers, json = pre_update(
430430
json,
@@ -441,21 +441,21 @@ def update(
441441
headers=headers,
442442
json=json,
443443
)
444-
return AsyncFilterRequestBuilder(request)
444+
return AsyncSelectRequestBuilder(request)
445445

446446
def delete(
447447
self,
448448
*,
449449
count: Optional[CountMethod] = None,
450450
returning: ReturnMethod = ReturnMethod.representation,
451-
) -> AsyncFilterRequestBuilder:
451+
) -> AsyncSelectRequestBuilder:
452452
"""Run a DELETE query.
453453
454454
Args:
455455
count: The method to use to get the count of rows returned.
456456
returning: Either 'minimal' or 'representation'
457457
Returns:
458-
:class:`AsyncFilterRequestBuilder`
458+
:class:`AsyncSelectRequestBuilder`
459459
"""
460460
method, params, headers, json = pre_delete(
461461
count=count,
@@ -471,4 +471,4 @@ def delete(
471471
headers=headers,
472472
json=json,
473473
)
474-
return AsyncFilterRequestBuilder(request)
474+
return AsyncSelectRequestBuilder(request)

src/postgrest/src/postgrest/_sync/request_builder.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ def insert(
332332
returning: ReturnMethod = ReturnMethod.representation,
333333
upsert: bool = False,
334334
default_to_null: bool = True,
335-
) -> SyncQueryRequestBuilder:
335+
) -> SyncSelectRequestBuilder:
336336
"""Run an INSERT query.
337337
338338
Args:
@@ -344,7 +344,7 @@ def insert(
344344
Otherwise, use the default value for the column.
345345
Only applies for bulk inserts.
346346
Returns:
347-
:class:`SyncQueryRequestBuilder`
347+
:class:`SyncSelectRequestBuilder`
348348
"""
349349
method, params, headers, json = pre_insert(
350350
json,
@@ -363,7 +363,7 @@ def insert(
363363
headers=headers,
364364
json=json,
365365
)
366-
return SyncQueryRequestBuilder(request)
366+
return SyncSelectRequestBuilder(request)
367367

368368
def upsert(
369369
self,
@@ -374,7 +374,7 @@ def upsert(
374374
ignore_duplicates: bool = False,
375375
on_conflict: str = "",
376376
default_to_null: bool = True,
377-
) -> SyncQueryRequestBuilder:
377+
) -> SyncSelectRequestBuilder:
378378
"""Run an upsert (INSERT ... ON CONFLICT DO UPDATE) query.
379379
380380
Args:
@@ -388,7 +388,7 @@ def upsert(
388388
not when merging with existing rows under `ignoreDuplicates: false`.
389389
This also only applies when doing bulk upserts.
390390
Returns:
391-
:class:`SyncQueryRequestBuilder`
391+
:class:`SyncSelectRequestBuilder`
392392
"""
393393
method, params, headers, json = pre_upsert(
394394
json,
@@ -408,23 +408,23 @@ def upsert(
408408
headers=headers,
409409
json=json,
410410
)
411-
return SyncQueryRequestBuilder(request)
411+
return SyncSelectRequestBuilder(request)
412412

413413
def update(
414414
self,
415415
json: JSON,
416416
*,
417417
count: Optional[CountMethod] = None,
418418
returning: ReturnMethod = ReturnMethod.representation,
419-
) -> SyncFilterRequestBuilder:
419+
) -> SyncSelectRequestBuilder:
420420
"""Run an UPDATE query.
421421
422422
Args:
423423
json: The updated fields.
424424
count: The method to use to get the count of rows returned.
425425
returning: Either 'minimal' or 'representation'
426426
Returns:
427-
:class:`SyncFilterRequestBuilder`
427+
:class:`SyncSelectRequestBuilder`
428428
"""
429429
method, params, headers, json = pre_update(
430430
json,
@@ -441,21 +441,21 @@ def update(
441441
headers=headers,
442442
json=json,
443443
)
444-
return SyncFilterRequestBuilder(request)
444+
return SyncSelectRequestBuilder(request)
445445

446446
def delete(
447447
self,
448448
*,
449449
count: Optional[CountMethod] = None,
450450
returning: ReturnMethod = ReturnMethod.representation,
451-
) -> SyncFilterRequestBuilder:
451+
) -> SyncSelectRequestBuilder:
452452
"""Run a DELETE query.
453453
454454
Args:
455455
count: The method to use to get the count of rows returned.
456456
returning: Either 'minimal' or 'representation'
457457
Returns:
458-
:class:`SyncFilterRequestBuilder`
458+
:class:`SyncSelectRequestBuilder`
459459
"""
460460
method, params, headers, json = pre_delete(
461461
count=count,
@@ -471,4 +471,4 @@ def delete(
471471
headers=headers,
472472
json=json,
473473
)
474-
return SyncFilterRequestBuilder(request)
474+
return SyncSelectRequestBuilder(request)

src/postgrest/tests/_async/test_request_builder.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from httpx import AsyncClient, Headers, QueryParams, Request, Response
55
from yarl import URL
66

7-
from postgrest import AsyncRequestBuilder, AsyncSingleRequestBuilder
7+
from postgrest import (
8+
AsyncMaybeSingleRequestBuilder,
9+
AsyncRequestBuilder,
10+
AsyncSingleRequestBuilder,
11+
)
812
from postgrest._async.request_builder import RequestConfig
913
from postgrest.base_request_builder import APIResponse, SingleAPIResponse
1014
from postgrest.types import JSON, CountMethod, ReturnMethod
@@ -140,6 +144,34 @@ def test_insert_with_select_forces_representation(
140144
"return=representation",
141145
]
142146

147+
def test_insert_with_select_single(self, request_builder: AsyncRequestBuilder):
148+
builder = request_builder.insert({"key1": "val1"}).select("id").single()
149+
150+
assert isinstance(builder, AsyncSingleRequestBuilder)
151+
assert builder.request.params["select"] == "id"
152+
assert builder.request.headers["accept"] == "application/vnd.pgrst.object+json"
153+
assert builder.request.headers.get_list("prefer", True) == [
154+
"return=representation"
155+
]
156+
assert builder.request.http_method == "POST"
157+
158+
def test_insert_with_select_maybe_single(
159+
self, request_builder: AsyncRequestBuilder
160+
):
161+
builder = request_builder.insert({"key1": "val1"}).select("id").maybe_single()
162+
163+
assert isinstance(builder, AsyncMaybeSingleRequestBuilder)
164+
assert builder.request.params["select"] == "id"
165+
assert builder.request.http_method == "POST"
166+
167+
def test_upsert_with_select_single(self, request_builder: AsyncRequestBuilder):
168+
builder = request_builder.upsert({"key1": "val1"}).select("id").single()
169+
170+
assert isinstance(builder, AsyncSingleRequestBuilder)
171+
assert builder.request.params["select"] == "id"
172+
assert builder.request.headers["accept"] == "application/vnd.pgrst.object+json"
173+
assert builder.request.http_method == "POST"
174+
143175
def test_bulk_upsert_with_default(self, request_builder: AsyncRequestBuilder):
144176
builder = request_builder.upsert(
145177
[{"key1": "val1", "key2": "val2"}, {"key3": "val3"}], default_to_null=False
@@ -194,6 +226,21 @@ def test_update_with_select(self, request_builder: AsyncRequestBuilder):
194226
assert builder.request.params["id"] == "eq.1"
195227
assert builder.request.params["select"] == "id"
196228

229+
def test_update_with_select_single(self, request_builder: AsyncRequestBuilder):
230+
builder = request_builder.update({"key1": "val1"}).eq("id", 1).select().single()
231+
232+
assert isinstance(builder, AsyncSingleRequestBuilder)
233+
assert builder.request.params["id"] == "eq.1"
234+
assert builder.request.headers["accept"] == "application/vnd.pgrst.object+json"
235+
assert builder.request.http_method == "PATCH"
236+
237+
def test_update_with_order_and_limit(self, request_builder: AsyncRequestBuilder):
238+
builder = request_builder.update({"key1": "val1"}).order("id").limit(1)
239+
240+
assert builder.request.params["order"] == "id.asc"
241+
assert builder.request.params["limit"] == "1"
242+
assert builder.request.http_method == "PATCH"
243+
197244

198245
class TestDelete:
199246
def test_delete(self, request_builder: AsyncRequestBuilder):
@@ -230,6 +277,14 @@ def test_delete_with_select(self, request_builder: AsyncRequestBuilder):
230277
assert builder.request.params["id"] == "eq.1"
231278
assert builder.request.params["select"] == "id"
232279

280+
def test_delete_with_select_single(self, request_builder: AsyncRequestBuilder):
281+
builder = request_builder.delete().eq("id", 1).select("id").single()
282+
283+
assert isinstance(builder, AsyncSingleRequestBuilder)
284+
assert builder.request.params["id"] == "eq.1"
285+
assert builder.request.headers["accept"] == "application/vnd.pgrst.object+json"
286+
assert builder.request.http_method == "DELETE"
287+
233288

234289
class TestTextSearch:
235290
def test_text_search(self, request_builder: AsyncRequestBuilder):

src/postgrest/tests/_sync/test_request_builder.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from httpx import Client, Headers, QueryParams, Request, Response
55
from yarl import URL
66

7-
from postgrest import SyncRequestBuilder, SyncSingleRequestBuilder
7+
from postgrest import (
8+
SyncMaybeSingleRequestBuilder,
9+
SyncRequestBuilder,
10+
SyncSingleRequestBuilder,
11+
)
812
from postgrest._async.request_builder import RequestConfig
913
from postgrest.base_request_builder import APIResponse, SingleAPIResponse
1014
from postgrest.types import JSON, CountMethod, ReturnMethod
@@ -140,6 +144,32 @@ def test_insert_with_select_forces_representation(
140144
"return=representation",
141145
]
142146

147+
def test_insert_with_select_single(self, request_builder: SyncRequestBuilder):
148+
builder = request_builder.insert({"key1": "val1"}).select("id").single()
149+
150+
assert isinstance(builder, SyncSingleRequestBuilder)
151+
assert builder.request.params["select"] == "id"
152+
assert builder.request.headers["accept"] == "application/vnd.pgrst.object+json"
153+
assert builder.request.headers.get_list("prefer", True) == [
154+
"return=representation"
155+
]
156+
assert builder.request.http_method == "POST"
157+
158+
def test_insert_with_select_maybe_single(self, request_builder: SyncRequestBuilder):
159+
builder = request_builder.insert({"key1": "val1"}).select("id").maybe_single()
160+
161+
assert isinstance(builder, SyncMaybeSingleRequestBuilder)
162+
assert builder.request.params["select"] == "id"
163+
assert builder.request.http_method == "POST"
164+
165+
def test_upsert_with_select_single(self, request_builder: SyncRequestBuilder):
166+
builder = request_builder.upsert({"key1": "val1"}).select("id").single()
167+
168+
assert isinstance(builder, SyncSingleRequestBuilder)
169+
assert builder.request.params["select"] == "id"
170+
assert builder.request.headers["accept"] == "application/vnd.pgrst.object+json"
171+
assert builder.request.http_method == "POST"
172+
143173
def test_bulk_upsert_with_default(self, request_builder: SyncRequestBuilder):
144174
builder = request_builder.upsert(
145175
[{"key1": "val1", "key2": "val2"}, {"key3": "val3"}], default_to_null=False
@@ -194,6 +224,21 @@ def test_update_with_select(self, request_builder: SyncRequestBuilder):
194224
assert builder.request.params["id"] == "eq.1"
195225
assert builder.request.params["select"] == "id"
196226

227+
def test_update_with_select_single(self, request_builder: SyncRequestBuilder):
228+
builder = request_builder.update({"key1": "val1"}).eq("id", 1).select().single()
229+
230+
assert isinstance(builder, SyncSingleRequestBuilder)
231+
assert builder.request.params["id"] == "eq.1"
232+
assert builder.request.headers["accept"] == "application/vnd.pgrst.object+json"
233+
assert builder.request.http_method == "PATCH"
234+
235+
def test_update_with_order_and_limit(self, request_builder: SyncRequestBuilder):
236+
builder = request_builder.update({"key1": "val1"}).order("id").limit(1)
237+
238+
assert builder.request.params["order"] == "id.asc"
239+
assert builder.request.params["limit"] == "1"
240+
assert builder.request.http_method == "PATCH"
241+
197242

198243
class TestDelete:
199244
def test_delete(self, request_builder: SyncRequestBuilder):
@@ -230,6 +275,14 @@ def test_delete_with_select(self, request_builder: SyncRequestBuilder):
230275
assert builder.request.params["id"] == "eq.1"
231276
assert builder.request.params["select"] == "id"
232277

278+
def test_delete_with_select_single(self, request_builder: SyncRequestBuilder):
279+
builder = request_builder.delete().eq("id", 1).select("id").single()
280+
281+
assert isinstance(builder, SyncSingleRequestBuilder)
282+
assert builder.request.params["id"] == "eq.1"
283+
assert builder.request.headers["accept"] == "application/vnd.pgrst.object+json"
284+
assert builder.request.http_method == "DELETE"
285+
233286

234287
class TestTextSearch:
235288
def test_text_search(self, request_builder: SyncRequestBuilder):

0 commit comments

Comments
 (0)