Skip to content

feat(postgrest): allow select() chaining on write builders#1383

Open
Dodothereal wants to merge 1 commit intosupabase:mainfrom
Dodothereal:codex/fix-issue-1271-postgrest-write-select
Open

feat(postgrest): allow select() chaining on write builders#1383
Dodothereal wants to merge 1 commit intosupabase:mainfrom
Dodothereal:codex/fix-issue-1271-postgrest-write-select

Conversation

@Dodothereal
Copy link

@Dodothereal Dodothereal commented Feb 8, 2026

Summary

  • add select(*columns) to sync/async query builders used by write operations
  • enable chaining after insert(), upsert(), update(), and delete()
  • when select() is used on write builders, ensure Prefer: return=representation is present without duplicating it
  • add sync/async tests covering select on write operations and return=minimal override behavior

Why

Issue #1271 requests support for resource embedding on write operations via the select query parameter. This change adds first-class builder support without requiring manual mutation of internal params.

Testing

  • uv run --package postgrest pytest tests/_sync/test_request_builder.py tests/_async/test_request_builder.py (run in src/postgrest)
  • make postgrest.mypy
  • uv run ruff check src/postgrest/src/postgrest/_sync/request_builder.py src/postgrest/src/postgrest/_async/request_builder.py src/postgrest/tests/_sync/test_request_builder.py src/postgrest/tests/_async/test_request_builder.py

Closes #1271

Summary by CodeRabbit

  • New Features

    • Added select() method to query builders, enabling users to specify which columns to retrieve from database operations.
    • The method supports method chaining and automatically manages response representation preferences in request headers.
  • Tests

    • Added comprehensive test coverage validating the new select functionality across insert, update, and delete operations, including interactions with different return preferences.

@Dodothereal
Copy link
Author

@olirice @silentworks could you please review this enhancement for #1271 when you have time? Thanks.

@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

The PR extends the fluent builder pattern in both sync and async request builders, adding a .select() method that enables resource embedding on write operations (insert, update, delete). The method manages select query parameters and ensures the Prefer header includes the representation return type, with comprehensive test coverage for the new functionality across both variants.

Changes

Cohort / File(s) Summary
Async select() Implementation
src/postgrest/src/postgrest/_async/request_builder.py
Added QueryBuilderT TypeVar bound to AsyncQueryRequestBuilder and implemented select(*columns) method that appends select parameters, manages Prefer header with return=representation, and returns self for chaining.
Sync select() Implementation
src/postgrest/src/postgrest/_sync/request_builder.py
Added QueryBuilderT TypeVar bound to SyncQueryRequestBuilder and implemented select(*columns) method with identical behavior to async variant for parameter management and header handling.
Async select() Tests
src/postgrest/tests/_async/test_request_builder.py
Added ReturnMethod import and new test cases validating select() behavior on insert, update, and delete operations, including interactions with ReturnMethod.minimal to verify correct parameter and header composition.
Sync select() Tests
src/postgrest/tests/_sync/test_request_builder.py
Added ReturnMethod import and new test cases with identical scenarios to async tests, validating select() integration across insert, update, and delete write operations with ReturnMethod combinations.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main feature addition: enabling select() method chaining on write builders (insert, update, delete operations).
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #1271: adding select() method to both sync and async write builders, ensuring Prefer headers are set correctly, and providing comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing select() chaining for write operations; no out-of-scope modifications detected in the code or tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/postgrest/src/postgrest/_async/request_builder.py`:
- Around line 36-46: In select (the async QueryBuilder.select implementation)
the code currently appends "return=representation" to the Prefer header which
can leave an existing "return=minimal" first and be ignored by PostgREST; change
the logic to parse self.request.headers.get("Prefer") and if any existing
"return=" directive exists replace that fragment with "return=representation"
(preserving other comma-separated preferences and whitespace), otherwise add
"return=representation" as before; apply the same change to the
SyncQueryRequestBuilder.select implementation so both _async/request_builder.py
and _sync/request_builder.py consistently replace any existing "return="
preference instead of appending.

Comment on lines +36 to +46
def select(self: QueryBuilderT, *columns: str) -> QueryBuilderT:
_, params, _, _ = pre_select(*columns, count=None)
self.request.params = self.request.params.add("select", params["select"])
prefer_header = self.request.headers.get("Prefer")
if not prefer_header:
self.request.headers["Prefer"] = "return=representation"
elif "return=representation" not in [
value.strip() for value in prefer_header.split(",")
]:
self.request.headers["Prefer"] = f"{prefer_header},return=representation"
return self
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

PostgREST Prefer header precedence return=minimal return=representation multiple directives

💡 Result:

In PostgREST, Prefer follows RFC 7240. PostgREST supports Prefer: return=minimal (default for writes) and Prefer: return=representation (return the mutated rows). [1][2]

If you send multiple return directives in the same request, e.g.

  • Prefer: return=minimal, return=representation (or across multiple Prefer: header lines)

then only the first occurrence is considered and later duplicates should be ignored, per RFC 7240 (“If any preference is specified more than once, only the first instance is to be considered.”). So the leftmost return=... wins. [2]

Also, RFC 7240 notes return=minimal and return=representation are mutually exclusive, so including both is generally a client bug. [2]

Sources: [1] PostgREST Prefer header docs, [2] RFC 7240.


🌐 Web query:

PostgREST RFC 7240 Prefer header handling duplicate directives

💡 Result:

RFC 7240 defines two relevant rules for Prefer:

  • Multiple Prefer header fields are allowed and are equivalent to a single header whose value is the comma-separated concatenation of all preference tokens. If an HTTP client/library “merges” multiple Prefer headers into one comma-separated header, that’s still compliant. [1]
  • Duplicate preference tokens should not be sent, but if they are, only the first occurrence is considered; later duplicates should be ignored without error (ordering becomes significant only in this duplicate case). [1]

PostgREST follows this RFC 7240 behavior when parsing request headers: if a preference is set more than once (even across multiple Prefer headers), PostgREST uses the first value and ignores subsequent duplicates. Example from PostgREST’s own fromHeaders docs: Prefer: tx=commit, tx=rollback results in tx=commit being used. [2]

Separately, PostgREST’s Prefer: handling=strict is about invalid/unrecognized/unfulfillable preferences (error vs ignore), not about duplicates—duplicates are simply handled as “first wins” per RFC 7240. [3]

Sources: RFC 7240 Prefer header specification [1]; PostgREST preference parsing (fromHeaders) behavior [2]; PostgREST Prefer header docs / handling=strict [3].

[1] https://www.rfc-editor.org/rfc/rfc7240
[2] https://hackage.haskell.org/package/postgrest/docs/PostgREST-Request-Preferences.html
[3] https://docs.postgrest.org/en/v12/references/api/preferences.html


🏁 Script executed:

# Search for SyncQueryRequestBuilder.select to verify if logic is mirrored
fd -type f -name "*.py" | xargs rg -l "SyncQueryRequestBuilder" | head -5

Repository: supabase/supabase-py

Length of output: 444


🏁 Script executed:

# Find and examine the SyncQueryRequestBuilder.select method
rg -A 15 "class SyncQueryRequestBuilder" --type py

Repository: supabase/supabase-py

Length of output: 1614


Replace return= directives in Prefer header instead of appending to avoid return=minimal overriding the intended behavior.

PostgREST follows RFC 7240, which specifies that when a preference appears multiple times, only the first occurrence is honored. If Prefer already contains return=minimal and select() appends ,return=representation, the header becomes return=minimal,return=representation—and PostgREST will use return=minimal (the first), not the intended return=representation. This breaks embedding behavior.

Replace any existing return= preference instead of appending to ensure return=representation is always used:

Required fix
-        prefer_header = self.request.headers.get("Prefer")
-        if not prefer_header:
-            self.request.headers["Prefer"] = "return=representation"
-        elif "return=representation" not in [
-            value.strip() for value in prefer_header.split(",")
-        ]:
-            self.request.headers["Prefer"] = f"{prefer_header},return=representation"
+        prefer_header = self.request.headers.get("Prefer")
+        if not prefer_header:
+            self.request.headers["Prefer"] = "return=representation"
+        else:
+            values = [v.strip() for v in prefer_header.split(",") if v.strip()]
+            values = [v for v in values if not v.lower().startswith("return=")]
+            values.append("return=representation")
+            self.request.headers["Prefer"] = ",".join(values)

Apply the same fix to SyncQueryRequestBuilder.select in src/postgrest/src/postgrest/_sync/request_builder.py to keep them synchronized.

🤖 Prompt for AI Agents
In `@src/postgrest/src/postgrest/_async/request_builder.py` around lines 36 - 46,
In select (the async QueryBuilder.select implementation) the code currently
appends "return=representation" to the Prefer header which can leave an existing
"return=minimal" first and be ignored by PostgREST; change the logic to parse
self.request.headers.get("Prefer") and if any existing "return=" directive
exists replace that fragment with "return=representation" (preserving other
comma-separated preferences and whitespace), otherwise add
"return=representation" as before; apply the same change to the
SyncQueryRequestBuilder.select implementation so both _async/request_builder.py
and _sync/request_builder.py consistently replace any existing "return="
preference instead of appending.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Support select() for Resource Embedding on Write Operations

1 participant