Skip to content

feat(typegen): generate typed RPC methods from rpcFunctions - #1959

Merged
spydon merged 4 commits into
mainfrom
lukasklingsbo/sdk-2232-typed-rpc-generation
Oct 15, 2026
Merged

spydon merged 4 commits into
mainfrom
lukasklingsbo/sdk-2232-typed-rpc-generation

Conversation

@spydon

@spydon spydon commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Linear: SDK-2232. Stacked on #1958.

Why

postgrest-typegen 0.5.0 resolves which functions PostgREST exposes under /rpc, how their overloads group and what a call returns into GeneratorMetadata.rpcFunctions (supabase/sdk#271). With the PostgrestRpc runtime from #1958, supabase_typegen can now generate a typed method for every function instead of leaving rpc untyped.

What

final List<BooksRow> books = await client.rpc
    .searchBooks(query: 'dart', mood: Mood.happy)
    .where(Books.price.lt(20))
    .order(Books.title.asc());

final int? total = await client.rpc.countBooks();
  • One extension, DatabaseFunctions on PostgrestRpc, with a method per callable overload. Arguments are named parameters, optional where the function declares a default, converted to the wire the same way insert values are (enums, dates, timestamps, ranges, bytea, vectors, arrays, row values).
  • Results by shape:
    • Rows of a generated table resolve to PostgrestTypedFilterBuilder<BooksRow, List<BooksRow>>, filterable with that table's column tokens.
    • A RETURNS TABLE (…) function gets its own row type and column tokens (AuthorBookCountsRow, AuthorBookCounts.bookCount), every column nullable.
    • A single row resolves to the nullable row type: PostgREST answers a function that finds no row with an object whose every column is null, which reads as null. Scalars and sets of scalars come back converted and nullable, void to void, anything else to Object?.
  • Naming: functions outside public carry the schema as a prefix (inventoryRestock). Of overloads, the one with the fewest arguments keeps the plain name and the others spell out their arguments (booksOfWithAuthorNameAndMaxPrice). Names stay clear of PostgrestRpc's own members (call, rows, value), the core types the bodies name (int, num, …) and the codecs they call, and an overload taking the unnamed json body is named …WithBody.
  • Not generated: an overload PostgREST cannot resolve (the resolver's error), or one taking an unnamed parameter other than a sole json/jsonb one. Each gets a line in the extension's doc comment saying why, except computed fields and relationships, which stay reachable through their table.
  • Introspection moves its pin to postgrest-typegen/v0.5.0. The SQL is unchanged; rpc_functions.dart ports resolveRpcFunctions, introspect attaches its result and sortGeneratorMetadata orders it. The parser reads rpcFunctions and falls back to the port for documents that predate it. The port already carries the fixes of fix(postgrest-typegen): narrow the computed-field fallback and key RPC groups safely sdk#272 (no dot-joined keys, narrower computed-field fallback).

Deviation from the TypeScript resolver

The resolver treats a sole unnamed row parameter as callable when the function returns rows (recent_books(public.authors)), but PostgREST answers that call with PGRST202. The port keeps the upstream semantics so it stays byte-identical to 0.5.0, and the generator refuses that case itself. Upstream fix tracked in SDK-2231.

Tests

  • rpc_functions_test.dart: the Dart resolver reproduces the TypeScript rpcFunctions of the fixture exactly, plus callable filtering, input modes, RETURNS TABLE columns, setof_options, computed fields, both conflict errors, overload order and schema keys with dots.
  • Parser, generator and behavior tests for every result shape, overload naming, notes and the typed_data import; the generated methods are exercised through MockSupabaseHttpClient for the wire request and the conversions.
  • seed.sql gains functions covering each shape; the fixture is regenerated with postgrest-typegen v0.5.0 (insertions only, existing object ids unchanged) and the hostile fixture gains functions named call, rows, class and map.
  • The parity suite passes against a fresh postgres:15. The generated golden was also run end to end against a real PostgREST on the fixture database, every method included.
  • Golden diffs are insertions only; hunks the local formatter would have rewrapped keep main's text, which the SDK formatter accepts unchanged.

`PostgrestClient.rpc`, `SupabaseClient.rpc` and `SupabaseQuerySchema.rpc`
are getters returning a `PostgrestRpc`. Calling it makes the same untyped
call as before; its `rows` and `value` methods are the typed entry points
the extension methods `supabase_typegen` generates are built on. Both go
through the typed request and executor, so a function returning rows keeps
`where`, `order`, `limit`, `selectOnly`, `single()` and `count()`.

SDK-1836
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: supabase/supabase-flutter/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 38a98b13-beda-4874-b64b-23014a7d6abd
📥 Commits

Reviewing files that changed from the base of the PR and between 08392a9 and 378fc6e.

📒 Files selected for processing: 28
  • MIGRATION.md
  • packages/postgrest/lib/src/postgrest.dart
  • packages/postgrest/lib/src/postgrest_rpc.dart
  • packages/postgrest/lib/src/postgrest_table_executor.dart
  • packages/postgrest/lib/src/postgrest_table_request.dart
  • packages/postgrest/lib/src/postgrest_typed_builder.dart
  • packages/postgrest/test/typed_rpc_integration_test.dart
  • packages/postgrest/test/typed_rpc_test.dart
  • packages/supabase/lib/src/supabase_client.dart
  • packages/supabase/lib/src/supabase_query_schema.dart
  • packages/supabase_typegen/README.md
  • packages/supabase_typegen/lib/src/dart_generator.dart
  • packages/supabase_typegen/lib/src/generator_metadata_parser.dart
  • packages/supabase_typegen/lib/src/introspection/introspect.dart
  • packages/supabase_typegen/lib/src/introspection/sort.dart
  • packages/supabase_typegen/lib/src/rpc_functions.dart
  • packages/supabase_typegen/lib/src/schema_description.dart
  • packages/supabase_typegen/test/dart_generator_test.dart
  • packages/supabase_typegen/test/fixtures/generator_metadata.json
  • packages/supabase_typegen/test/fixtures/seed.sql
  • packages/supabase_typegen/test/generated_schema_behavior_test.dart
  • packages/supabase_typegen/test/generator_metadata_parser_test.dart
  • packages/supabase_typegen/test/goldens/hostile_fixture.dart
  • packages/supabase_typegen/test/goldens/hostile_schema.dart
  • packages/supabase_typegen/test/goldens/supabase_schema.dart
  • packages/supabase_typegen/test/introspection/introspect_test.dart
  • packages/supabase_typegen/test/rpc_functions_test.dart
  • sdk-compliance.yaml
+--------------------------------------------------------------+
| Your API returns 200 OK; the logic returns 404 Common Sense. |
+--------------------------------------------------------------+
  \
   \     /)/)
        ( '')
        /  |
      o(___)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

…meters

`head()` on a typed function call switched the POST to a HEAD, which carries
no body, so the arguments were dropped and PostgREST resolved another
overload or none. They are now sent as query parameters, the way an untyped
`get` call sends them.

The executor renders a function call as one more case of `_operation`, and
selects the columns of its rows through the shared mutation rule instead of
a special case of its own.

SDK-1836
Reads the `rpcFunctions` collection postgrest-typegen 0.5.0 attaches to
GeneratorMetadata and generates an extension method on `PostgrestRpc` for
every function PostgREST exposes, `client.rpc.searchBooks(query: 'dart')`.

The introspection port moves to postgrest-typegen/v0.5.0 and gains a Dart
port of `resolveRpcFunctions`, so the direct connection modes emit the
collection too and documents predating it still generate. The fixture is
regenerated with RPC functions covering every result shape.

SDK-2232
A database function named like a lowercase core type (`int`) or a codec
(`postgrest_bytea`) generated a method that shadowed it inside the
extension, and a RETURNS TABLE column named like a codec a getter that
shadowed it in the row type; both no longer compile, so those names are
reserved. An overload taking the unnamed json body is named `…WithBody`
instead of `…With$`.

A function returning a single row resolves to a nullable row: PostgREST
answers one that finds no row with an object whose every column is null,
which now reads as `null` instead of a row whose getters throw.

SDK-2232
@spydon
spydon force-pushed the lukasklingsbo/sdk-2232-typed-rpc-generation branch from 1f51636 to 378fc6e Compare October 14, 2026 15:24
spydon added a commit that referenced this pull request Oct 15, 2026
…nts (#1958)

Linear: SDK-1836

## Why

The typed surface covers tables (`client.table(Books.table)`) but not
Postgres functions: `rpc<T>()` leaves `T` free and decodes with an
unchecked cast. `supabase_typegen` can now resolve every callable
function from postgrest-typegen 0.5.0's `rpcFunctions`, so it needs a
runtime to generate against. The call site we settled on is
`client.rpc.searchBooks(query: 'dart')`, generated extension methods on
whatever `rpc` returns.

## What

- **`rpc` is a getter** on `PostgrestClient`, `SupabaseClient` and
`SupabaseQuerySchema`, returning a `PostgrestRpc`. `PostgrestRpc.call`
makes the same untyped call as before, so `client.rpc('fn', params:
{...})` and `client.rpc<int>('fn')` compile unchanged.
- **Typed entry points** the generated code builds on:
- `rows(function, arguments:, rowFromJson:, schema:)` returns a
`PostgrestTypedFilterBuilder<Row, List<Row>>`, so `where`, `order`,
`limit`, `range`, `selectOnly`, `single()` and `count()` work on the
returned rows with the column tokens of the row type.
- `value(function, arguments:, fromJson:, schema:)` returns a
`PostgrestTypedBuilder<T>` for scalars, single rows, sets of scalars and
`void`.
- **`head()`** on a typed call sends the arguments as query parameters,
since a HEAD request carries no body.
- **Schema:** a typed call addresses the schema it is given, like a
`PostgrestTable` does; a client scoped with `.schema()` wins.
- **Executor:** both go through the typed request and executor as a new
internal `PostgrestTableOperation.rpc` (plus a
`PostgrestResultShape.value`), one more case of the executor's
`_operation`, rendered through the untyped rpc builder so the wire
request matches an untyped call. Both types stay hidden from the public
API. `isMutation` is `true` for a function call, since nothing on the
client says whether it writes, so a trailing `select`/`selectOnly` goes
through the same rule as after a mutation.
- `MIGRATION.md` covers the one break: passing `rpc` itself around as a
function. `sdk-compliance.yaml` registers `PostgrestRpc` and its members
under `database.query.rpc`.

## Tests

- `typed_rpc_test.dart`: the untyped call through the getter (params,
type argument, GET, scoped schema), and `rows`/`value` with filters,
ordering, limits, `selectOnly`, `single()`, `count()`, schema precedence
and request options.
- `typed_rpc_integration_test.dart` against the local stack: `RETURNS
TABLE` rows filtered and ordered, `selectOnly` plus `single()`,
`count()`, scalars, a `NULL` result, `void`, array arguments and a
function in another schema.
- `postgrest` (613) and `supabase` (242) suites pass locally; DCM
reports no warnings; the compliance symbol, drift and schema checks pass
against the pinned capability matrix.

Follow-up: #1959, which generates the extension methods.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Database function calls now support typed row and value results,
including filtering, ordering, column selection, and schema-specific
calls.
* Function results can be decoded into application-defined types,
including scalar and nullable values.
* RPC access is available through an object that supports both regular
and typed calls.
* **Documentation**
* Added migration guidance: calling RPC access remains supported, but
passing it directly as a function no longer compiles. Wrap it in a
closure instead.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Base automatically changed from lukasklingsbo/sdk-1836-typed-rpc-entry-point to main October 15, 2026 13:05
@spydon
spydon merged commit 82e457b into main Oct 15, 2026
28 of 29 checks passed
@spydon
spydon deleted the lukasklingsbo/sdk-2232-typed-rpc-generation branch October 15, 2026 13:06
spydon added a commit that referenced this pull request Oct 16, 2026
…ity matrix to v1.16.0 (#1964)

Linear: SDK-2258

## Why

`database.generated_types.typed_queries` and
`database.generated_types.typed_rpc` were added to the capability matrix
in supabase/sdk#276, released in capability-matrix v1.16.0. Both are
already implemented here: typed tables through `table()`, and typed
function calls through `PostgrestRpc.rows`/`PostgrestRpc.value` (#1958,
generated by #1959). They show up as unregistered rather than missing.

## What

- Bumps the supabase/sdk pins in `validate-capabilities.yml` and
`sync-compliance.yml` from v1.15.0 to v1.16.0, the first tag that
defines the two IDs. The reusable workflows are unchanged between the
tags.
- Registers `database.generated_types.typed_queries` with
`PostgrestClient.table`, `SupabaseClient.table` and
`SupabaseQuerySchema.table` as entry points and the typed builders plus
`PostgrestTable` as supporting symbols.
- Registers `database.generated_types.typed_rpc` with
`PostgrestRpc.rows` and `PostgrestRpc.value` as entry points.

## Verification

Against a supabase/sdk checkout at `capability-matrix/v1.16.0`:
`validate-compliance`, `check-drift` and `check-api-symbols` all pass.
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.

1 participant