From 2972e00b3832639ce0cc6efd4d7d6e01f601c910 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 15 Oct 2026 13:47:01 +0200 Subject: [PATCH] feat(postgrest)!: order and limit inside a to-many embed on the typed surface Adds `embedded()` to the typed transform builder. It takes a to-many relation and a callback that receives a scope typed against the embedded table, offering `order`, `limit`, `range` and nested `embedded` calls: .embedded( Authors.books, (books) => books.order(Books.createdAt.desc()).limit(1), ) // books.order=created_at.desc&books.limit=1 The typed `limit()` and `range()` drop their untyped `referencedTable` parameter in favor of the scope. Fixes SDK-1758 --- MIGRATION.md | 36 ++++- packages/postgrest/lib/postgrest.dart | 1 - .../lib/src/postgrest_embedded_relation.dart | 3 +- .../lib/src/postgrest_embedded_scope.dart | 101 ++++++++++++++ .../lib/src/postgrest_table_executor.dart | 45 +++--- .../lib/src/postgrest_table_request.dart | 29 +--- .../lib/src/postgrest_transform_builder.dart | 17 ++- .../lib/src/postgrest_typed_builder.dart | 1 + .../postgrest_typed_transform_builder.dart | 69 ++++++---- .../test/resource_embedding_test.dart | 43 ++++++ packages/postgrest/test/typed_query_test.dart | 128 ++++++++++++++++++ sdk-compliance.yaml | 8 +- 12 files changed, 392 insertions(+), 89 deletions(-) create mode 100644 packages/postgrest/lib/src/postgrest_embedded_scope.dart diff --git a/MIGRATION.md b/MIGRATION.md index b1cf574db..fcb4fa3e2 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2485,8 +2485,9 @@ Both APIs only send what was asked for, so `.order('price', ascending: false)` m `.order(Books.price.desc())` and `.order('price', ascending: false, nullsFirst: false)` matches `.order(Books.price.desc().nullsLast())`. Repeated `order()` calls append on both APIs. -`limit()` and `range()` take the same arguments as before, including `referencedTable` for an -embedded table. `single()` and `maybeSingle()` resolve to the row type: +`limit()` and `range()` take the same arguments as before, apart from `referencedTable`: the rows +of an embed are sorted and limited with `embedded()`, see [embedded selects](#embedded-selects). +`single()` and `maybeSingle()` resolve to the row type: ```dart // Before @@ -2651,7 +2652,7 @@ final authors = await supabase Authors.name, Authors.books.select([Books.title]), ]) - .limit(3, referencedTable: 'books'); + .embedded(Authors.books, (books) => books.limit(3)); final List titles = [ for (final book in authors.first.read(Authors.books)) book.read(Books.title), @@ -2659,6 +2660,35 @@ final List titles = [ ``` A to-one relation reads back as a nullable partial row and a to-many relation as a list of them. + +`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: + +```dart +// Before +await supabase + .from('authors') + .select('name, books(title, reviews(rating))') + .order('created_at', referencedTable: 'books', ascending: false) + .limit(1, referencedTable: 'books') + .limit(3, referencedTable: 'books.reviews'); + +// After +await supabase + .table(Authors.table) + .selectOnly([ + Authors.name, + Authors.books.select([Books.title, Books.reviews.select([Reviews.rating])]), + ]) + .embedded( + Authors.books, + (books) => books + .order(Books.createdAt.desc()) + .limit(1) + .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. diff --git a/packages/postgrest/lib/postgrest.dart b/packages/postgrest/lib/postgrest.dart index 78e6a9529..ff8f6bf77 100644 --- a/packages/postgrest/lib/postgrest.dart +++ b/packages/postgrest/lib/postgrest.dart @@ -16,7 +16,6 @@ export 'src/postgrest.dart'; export 'src/postgrest_builder.dart'; export 'src/postgrest_typed_builder.dart' hide - PostgrestEmbeddedPage, PostgrestExplainOptions, PostgrestHttpTableExecutor, PostgrestRequestOptions, diff --git a/packages/postgrest/lib/src/postgrest_embedded_relation.dart b/packages/postgrest/lib/src/postgrest_embedded_relation.dart index 1c2cba8e7..83a6d0794 100644 --- a/packages/postgrest/lib/src/postgrest_embedded_relation.dart +++ b/packages/postgrest/lib/src/postgrest_embedded_relation.dart @@ -204,7 +204,8 @@ final class PostgrestToOneRelation /// whose rows are [Row], pointing at the table whose rows are [Target]. /// /// Its projections are select position only: PostgREST answers -/// `order=children(amount).desc` with PGRST118. +/// `order=children(amount).desc` with PGRST118. The embedded rows themselves +/// are sorted and limited with [PostgrestTypedTransformBuilder.embedded]. @experimental final class PostgrestToManyRelation extends PostgrestRelation diff --git a/packages/postgrest/lib/src/postgrest_embedded_scope.dart b/packages/postgrest/lib/src/postgrest_embedded_scope.dart new file mode 100644 index 000000000..c050c23f2 --- /dev/null +++ b/packages/postgrest/lib/src/postgrest_embedded_scope.dart @@ -0,0 +1,101 @@ +part of 'postgrest_typed_builder.dart'; + +/// Sort keys and a row window applied to the rows of one to-many embed, +/// built in the callback of [PostgrestTypedTransformBuilder.embedded]. +/// +/// Everything here applies per parent row and targets the embedded table, +/// whose rows are [Row], so a column of another table does not compile: +/// +/// ```dart +/// await client +/// .table(Authors.table) +/// .selectOnly([Authors.name, Authors.books.select([Books.title])]) +/// .embedded( +/// Authors.books, +/// (books) => books.order(Books.createdAt.desc()).limit(1), +/// ); +/// // books.order=created_at.desc&books.limit=1 +/// ``` +/// +/// The parameters are named after the embed, its alias when it has one, and +/// PostgREST rejects them for an embed the `select` list does not include. +@experimental +final class PostgrestEmbeddedScope { + PostgrestEmbeddedScope._( + this._relation, { + List> orderings = const [], + int? limit, + int? offset, + List> scopes = const [], + }) : _orderings = List.unmodifiable(orderings), + _limit = limit, + _offset = offset, + _scopes = List.unmodifiable(scopes); + + final PostgrestToManyRelation _relation; + final List> _orderings; + final int? _limit; + final int? _offset; + final List> _scopes; + + PostgrestEmbeddedScope _copyWith({ + List>? orderings, + int? limit, + int? offset, + List>? scopes, + }) => PostgrestEmbeddedScope._( + _relation, + orderings: orderings ?? _orderings, + limit: limit ?? _limit, + offset: offset ?? _offset, + scopes: scopes ?? _scopes, + ); + + /// Sorts the embedded rows by [ordering]. + /// + /// Repeated calls append, so the second key breaks ties in the first. + PostgrestEmbeddedScope order(PostgrestOrdering ordering) => + _copyWith(orderings: [..._orderings, ordering]); + + /// Limits the embedded rows of each parent row to [count]. + PostgrestEmbeddedScope limit(int count) => _copyWith(limit: count); + + /// Limits the embedded rows of each parent row to the ones within the + /// specified range, inclusive. + PostgrestEmbeddedScope range(int from, int to) => + _copyWith(offset: from, limit: to - from + 1); + + /// Applies [scope] to the rows of [relation], an embed nested in this one, + /// under a dotted path such as `books.reviews.limit=3`. + PostgrestEmbeddedScope embedded( + PostgrestToManyRelation relation, + PostgrestEmbeddedScope Function( + PostgrestEmbeddedScope scope, + ) + scope, + ) => _copyWith( + scopes: [..._scopes, scope(PostgrestEmbeddedScope._(relation))], + ); + + /// The order keys, limit and offset of this scope and every nested one, + /// each under the dotted path of its embed, with [prefix] in front. + Iterable<_ScopedTransforms> _flattened([String prefix = '']) sync* { + final path = '$prefix${_relation._reference}'; + yield ( + path: path, + orderKeys: [for (final ordering in _orderings) ordering.orderKey], + limit: _limit, + offset: _offset, + ); + for (final scope in _scopes) { + yield* scope._flattened('$path.'); + } + } +} + +typedef _ScopedTransforms = ({ + String path, + List orderKeys, + int? limit, + int? offset, +}); diff --git a/packages/postgrest/lib/src/postgrest_table_executor.dart b/packages/postgrest/lib/src/postgrest_table_executor.dart index a8d0990e9..ef8f43149 100644 --- a/packages/postgrest/lib/src/postgrest_table_executor.dart +++ b/packages/postgrest/lib/src/postgrest_table_executor.dart @@ -146,9 +146,8 @@ final class PostgrestHttpTableExecutor implements PostgrestTableExecutor { } } - /// Applies the filter of [request] and every offset that comes without a - /// limit, on the table or on an embedded relation, which the transforms - /// have no method for. + /// Applies the filter of [request] and an offset that comes without a + /// limit, which the transforms have no method for. static PostgrestFilterBuilder _applyFilter( PostgrestFilterBuilder builder, PostgrestTableRequest request, @@ -161,15 +160,6 @@ final class PostgrestHttpTableExecutor implements PostgrestTableExecutor { if (offset != null && request.limit == null) { filtered = filtered.appendSearchParameter('offset', '$offset'); } - for (final page in request.embeddedPages) { - final pageOffset = page.offset; - if (pageOffset != null && page.limit == null) { - filtered = filtered.appendSearchParameter( - '${page.referencedTable}.offset', - '$pageOffset', - ); - } - } return filtered; } @@ -191,20 +181,23 @@ final class PostgrestHttpTableExecutor implements PostgrestTableExecutor { } else if (limit != null) { transformed = transformed.limit(limit); } - for (final page in request.embeddedPages) { - final pageLimit = page.limit; - final pageOffset = page.offset; - if (pageOffset != null && pageLimit != null) { - transformed = transformed.range( - pageOffset, - pageOffset + pageLimit - 1, - referencedTable: page.referencedTable, - ); - } else if (pageLimit != null) { - transformed = transformed.limit( - pageLimit, - referencedTable: page.referencedTable, - ); + for (final scope in request.embeddedScopes) { + for (final transforms in scope._flattened()) { + final path = transforms.path; + for (final key in transforms.orderKeys) { + transformed = transformed.appendOrderKey(key, referencedTable: path); + } + final scopeLimit = transforms.limit; + final scopeOffset = transforms.offset; + if (scopeOffset != null && scopeLimit != null) { + transformed = transformed.range( + scopeOffset, + scopeOffset + scopeLimit - 1, + referencedTable: path, + ); + } else if (scopeLimit != null) { + transformed = transformed.limit(scopeLimit, referencedTable: path); + } } } if (request.stripNulls) transformed = transformed.stripNulls(); diff --git a/packages/postgrest/lib/src/postgrest_table_request.dart b/packages/postgrest/lib/src/postgrest_table_request.dart index b9a3ce7a6..d5a38d989 100644 --- a/packages/postgrest/lib/src/postgrest_table_request.dart +++ b/packages/postgrest/lib/src/postgrest_table_request.dart @@ -56,22 +56,6 @@ enum PostgrestResultShape { explain, } -/// A `limit` or `range` applied to an embedded relation rather than to the -/// table itself. -@experimental -final class PostgrestEmbeddedPage { - const PostgrestEmbeddedPage(this.referencedTable, {this.limit, this.offset}); - - /// The embedded relation the page applies to. - final String referencedTable; - - /// The maximum number of embedded rows. - final int? limit; - - /// The number of embedded rows to skip. - final int? offset; -} - /// The options of a [PostgrestResultShape.explain] request, see /// [PostgrestTransformBuilder.explain]. @experimental @@ -170,6 +154,7 @@ final class PostgrestRequestOptions { /// else, against a local store for example, because the filters, orderings /// and payload are still structured values rather than URL text. @experimental +@internal final class PostgrestTableRequest { PostgrestTableRequest({ required this.table, @@ -180,7 +165,7 @@ final class PostgrestTableRequest { List> orderings = const [], this.limit, this.offset, - List embeddedPages = const [], + List> embeddedScopes = const [], this.shape = PostgrestResultShape.rows, this.countOption, this.payload, @@ -196,7 +181,7 @@ final class PostgrestTableRequest { }) : options = options ?? PostgrestRequestOptions(), columns = List.unmodifiable(columns), orderings = List.unmodifiable(orderings), - embeddedPages = List.unmodifiable(embeddedPages), + embeddedScopes = List.unmodifiable(embeddedScopes), onConflict = onConflict == null ? null : List.unmodifiable(onConflict); /// The table the request addresses. @@ -227,8 +212,8 @@ final class PostgrestTableRequest { /// The number of rows to skip. final int? offset; - /// Limits and offsets applied to embedded relations. - final List embeddedPages; + /// The sort keys and row windows applied inside to-many embeds. + final List> embeddedScopes; /// The form the result is asked back in. final PostgrestResultShape shape; @@ -291,7 +276,7 @@ final class PostgrestTableRequest { List>? orderings, int? limit, int? offset, - List? embeddedPages, + List>? embeddedScopes, PostgrestResultShape? shape, CountOption? countOption, Object? payload, @@ -313,7 +298,7 @@ final class PostgrestTableRequest { orderings: orderings ?? this.orderings, limit: limit ?? this.limit, offset: offset ?? this.offset, - embeddedPages: embeddedPages ?? this.embeddedPages, + embeddedScopes: embeddedScopes ?? this.embeddedScopes, shape: shape ?? this.shape, countOption: countOption ?? this.countOption, payload: payload ?? this.payload, diff --git a/packages/postgrest/lib/src/postgrest_transform_builder.dart b/packages/postgrest/lib/src/postgrest_transform_builder.dart index 1befec4a0..271071496 100644 --- a/packages/postgrest/lib/src/postgrest_transform_builder.dart +++ b/packages/postgrest/lib/src/postgrest_transform_builder.dart @@ -119,14 +119,21 @@ class PostgrestTransformBuilder extends PostgrestBuilder { return PostgrestTransformBuilder(copyWithUrl(url)); } - /// Merges the rendered sort key [key] into the `order` parameter, which - /// PostgREST reads once, adding nothing to [key]. + /// Merges the rendered sort key [key] into the `order` parameter, or into + /// `referencedTable.order` when [referencedTable] is given, which PostgREST + /// reads once, adding nothing to [key]. @internal - PostgrestTransformBuilder appendOrderKey(String key) { - final existingOrder = _url.queryParameters['order']; + PostgrestTransformBuilder appendOrderKey( + String key, { + String? referencedTable, + }) { + final parameter = referencedTable == null + ? 'order' + : '$referencedTable.order'; + final existingOrder = _url.queryParameters[parameter]; final value = existingOrder == null ? key : '$existingOrder,$key'; return PostgrestTransformBuilder( - copyWithUrl(_url.overrideSearchParameters('order', value)), + copyWithUrl(_url.overrideSearchParameters(parameter, value)), ); } diff --git a/packages/postgrest/lib/src/postgrest_typed_builder.dart b/packages/postgrest/lib/src/postgrest_typed_builder.dart index 7763083e8..0d1090ab0 100644 --- a/packages/postgrest/lib/src/postgrest_typed_builder.dart +++ b/packages/postgrest/lib/src/postgrest_typed_builder.dart @@ -13,6 +13,7 @@ part 'postgrest_column_expression.dart'; part 'postgrest_date.dart'; part 'postgrest_derived_expression.dart'; part 'postgrest_embedded_relation.dart'; +part 'postgrest_embedded_scope.dart'; part 'postgrest_filter.dart'; part 'postgrest_filter_operators.dart'; part 'postgrest_interval.dart'; diff --git a/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart b/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart index b8affe3ca..f5ab5ec2e 100644 --- a/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart +++ b/packages/postgrest/lib/src/postgrest_typed_transform_builder.dart @@ -119,38 +119,47 @@ class PostgrestTypedTransformBuilder extends PostgrestTypedBuilder { ) => _with(_request.copyWith(orderings: [..._request.orderings, ordering])); /// Limits the result with the specified [count]. - PostgrestTypedTransformBuilder limit( - int count, { - String? referencedTable, - }) => _with( - referencedTable == null - ? _request.copyWith(limit: count) - : _request.copyWith( - embeddedPages: [ - ..._request.embeddedPages, - PostgrestEmbeddedPage(referencedTable, limit: count), - ], - ), - ); + /// + /// See [embedded] to limit the rows of an embed instead. + PostgrestTypedTransformBuilder limit(int count) => + _with(_request.copyWith(limit: count)); /// Limits the result to rows within the specified range, inclusive. - PostgrestTypedTransformBuilder range( - int from, - int to, { - String? referencedTable, - }) => _with( - referencedTable == null - ? _request.copyWith(offset: from, limit: to - from + 1) - : _request.copyWith( - embeddedPages: [ - ..._request.embeddedPages, - PostgrestEmbeddedPage( - referencedTable, - limit: to - from + 1, - offset: from, - ), - ], - ), + /// + /// See [embedded] to limit the rows of an embed instead. + PostgrestTypedTransformBuilder range(int from, int to) => + _with(_request.copyWith(offset: from, limit: to - from + 1)); + + /// Sorts and limits the rows of the to-many embed [relation] with [scope], + /// leaving the parent rows as they are. + /// + /// ```dart + /// final authors = await client + /// .table(Authors.table) + /// .selectOnly([Authors.name, Authors.books.select([Books.title])]) + /// .embedded( + /// Authors.books, + /// (books) => books.order(Books.createdAt.desc()).limit(1), + /// ); + /// // books.order=created_at.desc&books.limit=1 + /// ``` + /// + /// [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 + /// [PostgrestEmbeddedScope] for what a scope offers. + PostgrestTypedTransformBuilder embedded( + PostgrestToManyRelation relation, + PostgrestEmbeddedScope Function( + PostgrestEmbeddedScope scope, + ) + scope, + ) => _with( + _request.copyWith( + embeddedScopes: [ + ..._request.embeddedScopes, + scope(PostgrestEmbeddedScope._(relation)), + ], + ), ); /// Omits `null`-valued properties from the response objects. diff --git a/packages/postgrest/test/resource_embedding_test.dart b/packages/postgrest/test/resource_embedding_test.dart index 4c2a512f5..c160eab6c 100644 --- a/packages/postgrest/test/resource_embedding_test.dart +++ b/packages/postgrest/test/resource_embedding_test.dart @@ -35,6 +35,7 @@ class Messages { ); static const id = PostgrestColumn('id'); static const message = PostgrestColumn('message'); + static const channelId = PostgrestColumn('channel_id'); static const username = PostgrestColumn('username'); static const data = PostgrestNullableColumn('data'); static const user = PostgrestToOneRelation( @@ -225,6 +226,48 @@ void main() { expect(messages.first.toJson(), contains('inserted_at')); }); + test('embedded orders and limits the embedded rows', () async { + Future> messageIds( + PostgrestEmbeddedScope Function( + PostgrestEmbeddedScope messages, + ) + scope, + ) async { + final users = await postgrest + .table(Users.table) + .selectOnly([ + Users.username, + Users.messages(Messages.id), + ]) + .where(Users.username.eq('supabot')) + .embedded(Users.messages, scope); + return [ + for (final message in users.single.read(Users.messages)) + message.read(Messages.id), + ]; + } + + expect( + await messageIds( + (messages) => + messages.order(Messages.channelId).order(Messages.id.desc()), + ), + [3, 1, 2], + ); + expect( + await messageIds( + (messages) => messages.order(Messages.id.desc()).limit(1), + ), + [3], + ); + expect( + await messageIds( + (messages) => messages.order(Messages.id.desc()).range(1, 2), + ), + [2, 1], + ); + }); + test('a to-one embed returns one object', () async { final messages = await postgrest .table(Messages.table) diff --git a/packages/postgrest/test/typed_query_test.dart b/packages/postgrest/test/typed_query_test.dart index af2e6da9c..145e508a4 100644 --- a/packages/postgrest/test/typed_query_test.dart +++ b/packages/postgrest/test/typed_query_test.dart @@ -24,6 +24,9 @@ extension type const BookUpdate._(Map _json) extension type const Author(Map _json) implements Map {} +extension type const Review(Map _json) + implements Map {} + const bookRows = [ {'id': 1, 'title': 'a'}, {'id': 2, 'title': 'b'}, @@ -63,6 +66,17 @@ class Books { referencedColumns: [Authors.id], alias: 'editor', ); + static const reviews = PostgrestToManyRelation( + 'reviews', + columns: [id], + referencedTable: 'reviews', + referencedColumns: [Reviews.bookId], + ); +} + +class Reviews { + static const bookId = PostgrestColumn('book_id'); + static const rating = PostgrestColumn('rating'); } class Authors { @@ -80,6 +94,13 @@ class Authors { referencedTable: 'books', referencedColumns: [Books.authorId], ); + static const editedBooks = PostgrestToManyRelation( + 'books!books_editor_id_fkey', + columns: [id], + referencedTable: 'books', + referencedColumns: [Books.editorId], + alias: 'edited_books', + ); } class BookClass { @@ -617,6 +638,113 @@ void main() { }); }); + group('embedded', () { + test('orders and limits the rows of a to-many embed', () async { + await client + .table(Authors.table) + .selectOnly([ + Authors.name, + Authors.books.select([Books.title]), + ]) + .order(Authors.name) + .limit(10) + .embedded( + Authors.books, + (books) => books.order(Books.title.desc()).order(Books.id).limit(1), + ); + + expect(requestParameters(), { + 'select': 'name,books(title)', + 'order': 'name', + 'limit': '10', + 'books.order': 'title.desc,id', + 'books.limit': '1', + }); + }); + + test('range sets the offset and limit of the embed', () async { + await client + .table(Authors.table) + .selectOnly([Authors.books.select()]) + .range(0, 4) + .embedded(Authors.books, (books) => books.range(10, 19)); + + expect(requestParameters(), { + 'select': 'books(*)', + 'offset': '0', + 'limit': '5', + 'books.offset': '10', + 'books.limit': '10', + }); + }); + + test('nested scopes render dotted paths', () async { + await client + .table(Authors.table) + .selectOnly([ + Authors.books.select([Books.title, Books.reviews.select()]), + ]) + .embedded( + Authors.books, + (books) => books + .limit(2) + .embedded( + Books.reviews, + (reviews) => reviews.order(Reviews.rating.desc()).limit(3), + ), + ); + + expect(requestParameters(), { + 'select': 'books(title,reviews(*))', + 'books.limit': '2', + 'books.reviews.order': 'rating.desc', + 'books.reviews.limit': '3', + }); + }); + + test('an aliased embed is addressed by its alias', () async { + await client + .table(Authors.table) + .selectOnly([ + Authors.editedBooks.select([Books.title]), + ]) + .embedded( + Authors.editedBooks, + (books) => books.order(Books.title).limit(1), + ); + + expect(requestParameters(), { + 'select': 'edited_books:books!books_editor_id_fkey(title)', + 'edited_books.order': 'title', + 'edited_books.limit': '1', + }); + }); + + test('repeated scopes of one embed append orderings', () async { + await client + .table(Authors.table) + .selectOnly([Authors.books.select()]) + .embedded(Authors.books, (books) => books.order(Books.title).limit(5)) + .embedded(Authors.books, (books) => books.order(Books.id).limit(1)); + + expect(requestParameters()['books.order'], 'title,id'); + expect(requestParameters()['books.limit'], '1'); + }); + + test('applies to the rows a mutation returns', () async { + await client + .table(Books.table) + .insert(BookInsert(title: 'a')) + .selectOnly([Books.title, Books.reviews.select()]) + .embedded(Books.reviews, (reviews) => reviews.limit(1)); + + expect(requestParameters(), { + 'select': 'title,reviews(*)', + 'reviews.limit': '1', + }); + }); + }); + group('result modifiers', () { test('csv returns the body as text', () async { httpClient.stubText('id,title\n1,a\n', contentType: 'text/csv'); diff --git a/sdk-compliance.yaml b/sdk-compliance.yaml index 4de7a1389..c3edaedc3 100644 --- a/sdk-compliance.yaml +++ b/sdk-compliance.yaml @@ -1308,7 +1308,7 @@ features: - PostgrestTypedTransformBuilder.geojson database.using_modifiers.relationship_embed: status: implemented - note: "Two spellings. The string builder takes query-string syntax in select(), for example `author(*)`. The column-expression surface declares a relation once on the table's namespace class as a PostgrestToOneRelation or PostgrestToManyRelation and either projects a column through it, `Books.author(Authors.name)`, which is compile-time checked in select position and, for the to-one direction only, in order position (a to-many order key is PGRST118 on the server), or selects the embed as a whole with `Books.author.select([Authors.id, Authors.name])` and `Books.author.select()` for `author(*)`. Entries of one relation in a select list are merged into a single embed. Filtering inside an embed is not on the typed surface; embeddedFilterName is the form it will use." + note: "Two spellings. The string builder takes query-string syntax in select(), for example `author(*)`. The column-expression surface declares a relation once on the table's namespace class as a PostgrestToOneRelation or PostgrestToManyRelation and either projects a column through it, `Books.author(Authors.name)`, which is compile-time checked in select position and, for the to-one direction only, in order position (a to-many order key is PGRST118 on the server), or selects the embed as a whole with `Books.author.select([Authors.id, Authors.name])` and `Books.author.select()` for `author(*)`. Entries of one relation in a select list are merged into a single embed. Ordering and limiting the rows inside a to-many embed is a scope on the builder, `.embedded(Authors.books, (books) => books.order(Books.createdAt.desc()).limit(1))`, typed against the embedded table; scopes nest into dotted paths. Filtering inside an embed and the `!inner` counterpart are not on the typed surface yet; embeddedFilterName is the form filtering will use." symbols: - PostgrestTransformBuilder.select - PostgrestToOneRelation.call @@ -1316,7 +1316,13 @@ features: - PostgrestRelation.select - PostgrestToOneRelation.select - PostgrestToManyRelation.select + - PostgrestTypedTransformBuilder.embedded supporting_symbols: + - PostgrestEmbeddedScope + - PostgrestEmbeddedScope.order + - PostgrestEmbeddedScope.limit + - PostgrestEmbeddedScope.range + - PostgrestEmbeddedScope.embedded - PostgrestEmbed - PostgrestEmbed.relation - PostgrestEmbed.selections