From adcf87f4a28d9f083030cd15e81063a829cb3acb Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 15 Oct 2026 15:13:12 +0200 Subject: [PATCH] fix(postgrest): address a hinted embed by its table name outside the select list A relation with a foreign key hint and no alias was addressed by its hinted name in `order`, embedded filter names and embedded scopes. PostgREST matches those paths against the alias or table name only, so `order=users!fkey(username)` failed with PGRST100 and `messages!fkey.limit=1` was silently ignored. The hint now only appears in the select list. Also documents what a repeated `embedded()` call does to an earlier limit and offset, has the untyped `order()` merge through `appendOrderKey`, and tidies the migration guide snippet. Refs SDK-1758 --- MIGRATION.md | 10 +++++++--- .../lib/src/postgrest_embedded_relation.dart | 7 ++++--- .../lib/src/postgrest_transform_builder.dart | 11 ++++------ .../postgrest_typed_transform_builder.dart | 3 ++- .../postgrest_embedded_relation_test.dart | 15 ++++++++++++++ packages/postgrest/test/typed_query_test.dart | 20 +++++++++++++++++++ 6 files changed, 52 insertions(+), 14 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 802716186..b6dccbf61 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2680,6 +2680,9 @@ final List titles = [ A to-one relation reads back as a nullable partial row and a to-many relation as a list of them. +When two foreign keys point at the same table, the generator adds the constraint hint and an alias, +so each embed comes back under its own key without spelling `authors!books_author_id_fkey` by hand. + `embedded()` replaces `referencedTable` on `order()`, `limit()` and `range()`. Its callback receives a scope typed against the embedded table, so ordering by a column of another table does not compile, and a scope can reach an embed nested in it: @@ -2698,7 +2701,10 @@ await supabase .table(Authors.table) .selectOnly([ Authors.name, - Authors.books.select([Books.title, Books.reviews.select([Reviews.rating])]), + Authors.books.select([ + Books.title, + Books.reviews.select([Reviews.rating]), + ]), ]) .embedded( Authors.books, @@ -2708,8 +2714,6 @@ await supabase .embedded(Books.reviews, (reviews) => reviews.limit(3)), ); ``` -When two foreign keys point at the same table, the generator adds the constraint hint and an alias, -so each embed comes back under its own key without spelling `authors!books_author_id_fkey` by hand. The generated table definition also lists `primaryKey` and `relations`. Neither changes how a request is sent; they describe the table to code that handles requests itself, such as a plugin diff --git a/packages/postgrest/lib/src/postgrest_embedded_relation.dart b/packages/postgrest/lib/src/postgrest_embedded_relation.dart index 83a6d0794..6bacff2fa 100644 --- a/packages/postgrest/lib/src/postgrest_embedded_relation.dart +++ b/packages/postgrest/lib/src/postgrest_embedded_relation.dart @@ -57,7 +57,7 @@ sealed class PostgrestRelation { }) : columns = const [], referencedColumns = const []; - /// The name PostgREST addresses the embed by: the table name, including + /// The name the embed is selected by: the table name, including /// any disambiguating foreign key hint such as /// `authors!books_author_id_fkey`, or the function name of a computed /// relationship. @@ -95,8 +95,9 @@ sealed class PostgrestRelation { /// How the embed is spelled in the `select` list. String get _selectName => alias == null ? name : '$alias:$name'; - /// How the embed is addressed outside the `select` list. - String get _reference => alias ?? name; + /// How the embed is addressed outside the `select` list, where PostgREST + /// matches the alias or the table name but not a foreign key hint. + String get _reference => key; /// The columns of this table the relation joins on; empty for a computed /// relationship. diff --git a/packages/postgrest/lib/src/postgrest_transform_builder.dart b/packages/postgrest/lib/src/postgrest_transform_builder.dart index 271071496..5f41cebf2 100644 --- a/packages/postgrest/lib/src/postgrest_transform_builder.dart +++ b/packages/postgrest/lib/src/postgrest_transform_builder.dart @@ -105,18 +105,15 @@ class PostgrestTransformBuilder extends PostgrestBuilder { bool? nullsFirst, String? referencedTable, }) { - final key = referencedTable == null ? 'order' : '$referencedTable.order'; - final existingOrder = _url.queryParameters[key]; final placement = switch (nullsFirst) { null => '', true => '.nullsfirst', false => '.nullslast', }; - final value = - '${existingOrder == null ? '' : '$existingOrder,'}$column.' - '${ascending ? 'asc' : 'desc'}$placement'; - final url = _url.overrideSearchParameters(key, value); - return PostgrestTransformBuilder(copyWithUrl(url)); + return appendOrderKey( + '$column.${ascending ? 'asc' : 'desc'}$placement', + referencedTable: referencedTable, + ); } /// Merges the rendered sort key [key] into the `order` parameter, or into diff --git a/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart b/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart index f5ab5ec2e..5d5f501ad 100644 --- a/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart +++ b/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart @@ -145,7 +145,8 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { /// ``` /// /// [relation] has to be in the `select` list. Repeated calls for the same - /// embed append their sort keys, and the last limit or range wins. See + /// embed append their sort keys, a later limit replaces the limit and a + /// later range replaces both the offset and the limit. See /// [PostgrestEmbeddedScope] for what a scope offers. PostgrestTypedTransformBuilder embedded( PostgrestToManyRelation relation, diff --git a/packages/postgrest/test/postgrest_embedded_relation_test.dart b/packages/postgrest/test/postgrest_embedded_relation_test.dart index 6c6417ef0..e951f5b39 100644 --- a/packages/postgrest/test/postgrest_embedded_relation_test.dart +++ b/packages/postgrest/test/postgrest_embedded_relation_test.dart @@ -251,6 +251,21 @@ void main() { 'parent_todo(orders(id))', ); }); + + test('addresses a hinted embed without an alias by its table name', () { + const hintedTodo = PostgrestToOneRelation( + 'todo!orders_parent_id_fkey', + columns: [Orders.parentId], + referencedTable: 'todos', + referencedColumns: [Todos.id], + ); + expect( + hintedTodo(Todos.title).expression, + 'todo!orders_parent_id_fkey(title)', + ); + expect(hintedTodo(Todos.title).desc().orderKey, 'todo(title).desc'); + expect(hintedTodo(Todos.title).embeddedFilterName, 'todo.title'); + }); }); group('select', () { diff --git a/packages/postgrest/test/typed_query_test.dart b/packages/postgrest/test/typed_query_test.dart index 145e508a4..2ee39790a 100644 --- a/packages/postgrest/test/typed_query_test.dart +++ b/packages/postgrest/test/typed_query_test.dart @@ -720,6 +720,26 @@ void main() { }); }); + test('a hinted embed without an alias is addressed by its table', () async { + const hintedBooks = PostgrestToManyRelation( + 'books!books_editor_id_fkey', + columns: [Authors.id], + referencedTable: 'books', + referencedColumns: [Books.editorId], + ); + await client + .table(Authors.table) + .selectOnly([ + hintedBooks.select([Books.title]), + ]) + .embedded(hintedBooks, (books) => books.limit(1)); + + expect(requestParameters(), { + 'select': 'books!books_editor_id_fkey(title)', + 'books.limit': '1', + }); + }); + test('repeated scopes of one embed append orderings', () async { await client .table(Authors.table)